Skip to content

Commit 8324597

Browse files
committed
docs: large-blob storage roadmap (legibility → encrypted notes → SSH-cert interop)
Capture the design brainstormed for the FIDO2 CTAP 2.1 large-blob array: the user deliberately chooses what their key's small key-global blob is for, with keyroost making the space legible first (A), plain-text notes already shipped (B), a portable then optionally device-bound encrypted note for break-glass secrets (C1/C2), and real largeBlobKey SSH-cert interop last (D). Grounded on the distinction that SSH auth uses the resident-credential store, not the large blob, and that first-party GUI tooling for this space is thin now that the YubiKey Manager GUI is end-of-life. Molto2 title, OpenPGP DOs, and PIV objects are recorded as separate out-of-scope tracks; the Molto2 title also lands as its own TODO entry.
1 parent c351cb8 commit 8324597

2 files changed

Lines changed: 227 additions & 0 deletions

File tree

TODO-v0.7.5.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ Captured here so they don't get lost. Unchecked = not started.
3535
whether egui-winit's glue changes incidentally resolve it on Fedora-44 KWin
3636
while we're here.
3737

38+
## Molto2 — surface the per-profile title (≤12 bytes), per slot
39+
40+
- [ ] The `set_title` command already exists at the `keyroost-proto` layer
41+
(INS `0xD5`, SM4-ECB of ≤12 UTF-8 bytes, per docs/PROTOCOL.md) but is
42+
**write-only and not wired to CLI or GUI**. Surface it as a per-slot
43+
editable title in the Molto2 view.
44+
- [ ] Read-back gap: no known plain command returns a stored title, so the
45+
editor is write-only — decide how to present that (optimistic local echo,
46+
or "set-only" affordance). Needs hardware confirmation.
47+
- [ ] Independent of the FIDO large-blob "storage purpose" work — this is the
48+
Molto2 device's own label field, not the CTAP large-blob array.
49+
3850
## GUI — Text-size control polish ([#42](https://github.com/framefilter/keyroost/issues/42), @token2)
3951

4052
- [ ] Add discrete **"−" / "+" buttons** on the ends of the zoom slider; mouse
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# FIDO2 large-blob storage — roadmap & design
2+
3+
- **Date:** 2026-06-30
4+
- **Status:** Design approved in brainstorming; roadmap captured for review. Not
5+
scheduled to a specific release — tiers land as they're built, around whatever
6+
v0.7.3 turns up in the field.
7+
- **Scope:** The CTAP 2.1 `authenticatorLargeBlobs` array that is common across
8+
modern FIDO2 keys (YubiKey 5.5.1+, Token2, Solo/Nitrokey, etc.). Not the
9+
Molto2 device (see *Out of scope*).
10+
11+
## The one idea everything hangs on
12+
13+
**The user decides what their large blob is for.**
14+
15+
The large blob is a single small (~1 KB), key-global side area — separate from
16+
FIDO credentials, separate from PIV/OpenPGP. keyroost's job is to make that
17+
space *legible* and then let the person *deliberately claim it* for a purpose:
18+
19+
- leave it alone (just inspect what's there),
20+
- keep a plain-text note (world-readable),
21+
- lock a few secrets into an **encrypted note**, or
22+
- hold an **SSH certificate** the way OpenSSH/Yubico intend.
23+
24+
That "choose how to use it" is the top-level UX. Newcomers pick a **purpose
25+
preset**; power users get a **capacity meter + raw hex/ASCII view** underneath.
26+
One space, one deliberate choice, clearly surfaced.
27+
28+
## Why keyroost is the right home for this
29+
30+
keyroost is a friendly, cross-platform, cross-vendor management layer for
31+
hardware security keys. First-party tooling for the large blob is thin and
32+
getting thinner:
33+
34+
- The only established large-blob tooling is `fido2-token`/libfido2 —
35+
developer-grade, Linux-centric (`/dev/hidraw0` device paths), no GUI
36+
([Yubico: Storing SSH Certificates](https://developers.yubico.com/SSH/Storing_SSH_Certificates.html)).
37+
- **YubiKey Manager GUI reached end-of-life on 2026-02-19**; Yubico now points
38+
users to *Yubico Authenticator*, which does **not** manage large blobs, PIV
39+
data objects, or SSH-cert-on-key. The friendly-GUI surface for this work just
40+
disappeared.
41+
42+
keyroost already implements the hard part — a checksum-safe, structural
43+
large-blob read/write layer (`keyroost-ctap/src/large_blobs.rs`) with a
44+
plain-text note layer, exposed via `keyroostctl fido large-blob …` and the GUI
45+
Storage sub-view. This roadmap builds *purpose* on top of that foundation.
46+
47+
## What a large blob actually is (and isn't)
48+
49+
Grounding, because it's widely conflated with SSH auth:
50+
51+
- **SSH auth on a security key** uses a FIDO2 **discoverable/resident
52+
credential** (`ssh-keygen -t ed25519-sk -O resident`). The private seed never
53+
leaves the key; `ssh-keygen -K` regenerates the stub files anywhere. This has
54+
**nothing to do with the large blob** — it's the credential store, which
55+
keyroost already manages (creds list/delete).
56+
- **The large blob** is a separate ~1 KB scratch area. Its only spec-blessed SSH
57+
use is holding the **CA-signed SSH certificate** (`-cert.pub`) so the cert
58+
travels with the resident credential. So the blob adds value for SSH **only if
59+
you use SSH certificates** — an org/CA/fleet workflow, not plain sk-key auth.
60+
61+
Consequence for the roadmap: the blob is not a storage product. It's a
62+
*legibility problem* plus a *small pool of leftable space*. The tiers below are
63+
ordered by honest value, not novelty.
64+
65+
## The tiers
66+
67+
| Tier | Purpose | Audience | New crypto/transport | Status |
68+
|---|---|---|---|---|
69+
| **A** | **Legibility** — decode & export everything in the blob | anyone curious about their key | none | to build (raw hex exists) |
70+
| **B** | **Plain-text note** | casual | none | **shipped** |
71+
| **C1** | **Encrypted note (passphrase)** — portable | recovery codes, wallet seeds | KDF + AEAD | to build |
72+
| **C2** | **Encrypted note (device-bound)** — hardware-hardened | offline-attack-averse | `hmac-secret` | to build (additive on C1) |
73+
| **D** | **SSH-cert companion** — real interop | SSH-CA / fleet users | `largeBlobKey` AEAD | to build |
74+
75+
Sequencing is **pure engineering layering**, not demand-gated. We build C1, then
76+
layer C2 and D as they come. No tier waits on a usage signal to justify the
77+
next.
78+
79+
### A — Legibility (the foundation)
80+
81+
Make the blob's contents readable and exportable, whatever put them there.
82+
83+
- **Entry recognition.** Classify each entry in the array:
84+
- keyroost plain-text note (existing magic prefix),
85+
- keyroost encrypted note (new container, see C),
86+
- opaque relying-party / AEAD data (read-only; keyroost never rewrites it),
87+
- **SSH certificate** — recognize the OpenSSH cert format and decode the
88+
human-relevant fields (type, key-id, principals, valid-from/valid-to,
89+
critical options).
90+
- **Views.** Per-entry hex + ASCII (exists), plus a parsed/structured view for
91+
recognized types. A **capacity meter**: total / used / free bytes and entry
92+
count, so scarcity is visible before the user commits space.
93+
- **Export.** Save any entry's bytes to a file (e.g. dump an SSH cert back to
94+
`-cert.pub`).
95+
- Everything here is read-only inspection + export; no new crypto, low risk.
96+
This is keyroost's defensible core and the immediate ykman-GUI-gap filler.
97+
98+
### C — Encrypted note
99+
100+
The value center. AEAD-encrypt small, high-value "break-glass" data that should
101+
live physically on the key: Signal/session recovery codes, a wallet seed, key
102+
info. Stored as a keyroost-authored entry alongside (not replacing) RP data.
103+
104+
**On-blob container format (designed once, in C1, to carry both modes):**
105+
106+
```
107+
magic keyroost encrypted-note tag (distinct from the plain-note magic)
108+
version u8 format version
109+
protection_mode u8 0 = passphrase (C1), 1 = device-bound, 2 = passphrase+device (C2)
110+
kdf_id u8 e.g. Argon2id / scrypt identifier
111+
kdf_params … cost params + salt (passphrase modes)
112+
hmac_cred_ref … credential/salt reference (device-bound modes; absent in C1)
113+
nonce … AEAD nonce
114+
ciphertext+tag … AEAD(plaintext)
115+
```
116+
117+
The `version` + `protection_mode` fields are the whole point of doing the format
118+
work in C1: C2 becomes purely additive — new mode value, no migration, existing
119+
notes keep decrypting.
120+
121+
**Capacity reality.** ~1 KB total blob, minus existing entries, minus container
122+
overhead (salt + nonce + tag + header ≈ 60–90 bytes). This holds a handful of
123+
short codes, not a vault. The UI states this plainly and the capacity meter
124+
enforces the expectation.
125+
126+
#### C1 — passphrase mode (portable)
127+
128+
- Key = strong KDF(passphrase, salt): **Argon2id** (preferred) or scrypt, tuned
129+
for offline-attack resistance since the blob is world-readable.
130+
- AEAD = **AES-256-GCM** or **ChaCha20-Poly1305** over the note text.
131+
- **Portable by design:** the encrypted entry can be exported to a file, backed
132+
up, and imported into any key/device; the same passphrase decrypts. Matches
133+
the "travels between devices / lives on the hardware" use cases.
134+
- **Threat note:** world-readable blob ⇒ a stolen key permits an *offline*
135+
brute-force. Passphrase strength and KDF cost are the only defense; the UI
136+
must say so and default the KDF cost high.
137+
138+
#### C2 — device-bound mode (additive)
139+
140+
- Fold in the FIDO2 **`hmac-secret`** extension (this key + PIN + touch) so
141+
decryption requires the physical key — removing the offline-attack path.
142+
- **Composition rule (to finalize):** default to *passphrase AND device* — both
143+
required — so a stolen blob is uncrackable without the key, and a note is not
144+
silently single-factor. (Alternative "passphrase OR device" is more convenient
145+
but weakest-link; decision deferred to C2 design, but the container reserves
146+
space for either.)
147+
- **Capability handling:** detect `hmac-secret` support; hide/disable the bind
148+
toggle where unsupported. keyroost already recognizes `hmac-secret`.
149+
- **Tradeoff surfaced in UI:** device-bound notes are *not* portable and die
150+
with the key — correct for break-glass copies, wrong for sole-copy secrets.
151+
152+
### D — SSH-cert companion (finish what has no GUI)
153+
154+
Genuine interop with the Yubico/OpenSSH large-blob SSH-cert flow, cross-platform
155+
for any key with a large blob.
156+
157+
- Implement CTAP **`largeBlobKey`** + per-credential **AES-256-GCM** so keyroost
158+
can *author and retrieve* SSH-cert entries that `fido2-token`/OpenSSH
159+
round-trip — not just display them (A already displays them).
160+
- Store a CA-signed cert against its resident credential; retrieve/export it on a
161+
fresh machine. This is the "as Yubico intends" path, minus the Linux-only
162+
`fido2-token` friction.
163+
- Smallest audience, largest build — hence last. A depends on none of this; D
164+
builds on A's SSH-cert recognition.
165+
166+
## Dependencies & conventions
167+
168+
- **New crypto dependency — needs the standard "vendor over depend" discussion.**
169+
C needs a KDF (Argon2id/scrypt) + an AEAD (AES-256-GCM / ChaCha20-Poly1305);
170+
D needs AES-256-GCM. Precedent exists: `keyroost-token2otp` already carries a
171+
scoped RustCrypto exception (`aes`, `cbc`, `sha2`, `p256`, `zeroize`). Options
172+
to decide at C1 design time: (a) extend that RustCrypto exception, (b) vendor
173+
the primitives in-tree as with SM4/SHA-1. `zeroize` for plaintext/secret
174+
buffers either way.
175+
- **Writes stay checksum-safe and structural** via the existing
176+
`large_blobs.rs` path (re-read array, apply, recompute checksum, PIN-authed
177+
write). Encrypted/SSH entries are just new entry *types* in that same array.
178+
- **Honesty in the UI is a feature.** Plain notes: "world-readable, not for
179+
secrets." Encrypted notes: state the offline-attack caveat (C1) and the
180+
not-portable/dies-with-key caveat (C2). Capacity meter always visible.
181+
182+
## Out of scope / separate work
183+
184+
- **Molto2 per-profile title (≤12 chars).** A field on the Molto2 device, *not*
185+
a FIDO2 large blob. Tracked separately in `TODO-v0.7.5.md`.
186+
- **OpenPGP private DOs (0101–0104) and login-data (005E).** The OpenPGP-applet
187+
analog of arbitrary storage; smaller, PW-gated, gpg-ecosystem-specific. A
188+
possible future "device storage" story, not part of this large-blob roadmap.
189+
- **PIV data objects (Printed Info / Discovery).** No natural free-text fit;
190+
everything PIV does is keys/certs. Skip.
191+
- **Storing the SSH pubkey next to the resident key.** Neat but redundant —
192+
regenerable via `ssh-keygen -K`. Not worth a tier.
193+
- **Nitrokey product integration.** A separate future track; noted so the
194+
cross-vendor direction is on record.
195+
196+
## Testing
197+
198+
- **A:** fixtures for each entry type (plain note, RP/AEAD opaque, OpenSSH cert);
199+
assert correct classification + field decode; export byte-exactness.
200+
- **C:** known-answer vectors for the container (KDF + AEAD) so encryption is
201+
reproducible; round-trip encrypt→store→read→decrypt; wrong-passphrase and
202+
tamper (bad tag) rejection; capacity-exceeded handling.
203+
- **D:** interop vectors validating a keyroost-written SSH-cert entry is
204+
retrievable by `fido2-token`/OpenSSH, and vice-versa.
205+
- Any change to command/APDU construction keeps the existing known-answer suites
206+
green (project convention).
207+
208+
## Open decisions (to settle at each tier's design time)
209+
210+
1. KDF choice + cost defaults (Argon2id vs scrypt) and AEAD choice
211+
(AES-256-GCM vs ChaCha20-Poly1305) — C1.
212+
2. Where the crypto lives: extend the RustCrypto exception vs vendor in-tree — C1.
213+
3. C2 composition rule: passphrase-AND-device vs passphrase-OR-device.
214+
4. Whether the "purpose preset" picker is per-key state keyroost remembers, or
215+
purely a per-action framing at add-time — A/C UX.

0 commit comments

Comments
 (0)