Area
Backup / lock / audit / attestation
The problem
RS-Key can back up its master seed. It cannot export a single credential.
Everything not derived from the seed — resident passkeys, OpenPGP private keys, PIV
private keys, OATH secrets, OTP slots — is sealed to the source chip, so a dead board
takes them with it. The documented answer today is "keep two keys and enrol both
everywhere" (backup-key.md),
which is redundancy rather than a backup.
This is by explicit decision, not omission:
The seed backup covers the deterministic identity only. Non-resident
credentials (ssh ed25519-sk, most 2FA registrations) derive from the master seed
and survive a restore onto a new board. Not covered: resident passkeys (stored
records, not derivable), OpenPGP private keys, PIV private keys, OATH secrets, OTP
slots, all sealed to the source chip. A board swap means re-enrolling those.
Status: by design; a full at-rest export would gut the at-rest story.
— limitations.md, "Backup & migration"
I agree with the reasoning behind that line. The question is whether a narrow
export can exist without emptying it.
Three concrete consequences today:
- A failed board is an account-recovery event, not a restore. Every account
holding only a resident passkey needs its own recovery flow.
- A fleet cannot be rotated.
rsk inventory list and rsk offboard cover the
asset lifecycle (fleet.md),
but nothing moves a credential set from a retired key to its replacement.
- The host has no way to ask.
enumerateCredentials returns metadata only, and
no host-side codec exists that could carry a credential even if one were moved.
Absence of any export path is verifiable: the credential-management surface in
crates/rsk-fido/src/credmgmt.rs implements exactly seven subcommands, none of which
returns stored material, and there is no export/import/dump codec in tools/rsk.
Proposed shape
Add an authenticated, device-bound credential export/import path, reusing the
existing 0x41 vendor surface. The design to copy is Pico Vault (PKV1), which
solves exactly this problem and publishes its wire format:
The shape that matters:
- A 32-byte
Kvault root secret, generated on the device, never exported in
cleartext — stored wrapped under the device key.
- A public
VaultID = SHA-256("PicoKeys Vault ID v1" || Kvault) — a domain
identifier explicitly not a secret and not a key.
- Per-credential layer keys:
K_i = HKDF-SHA-256(salt = VaultID, IKM = Kvault, info = "PicoKeys Vault enrollment v1" || CredentialHash || algorithm || i)
- A salted, authenticated envelope: an 86-byte header (magic
PKV\x01, VaultID,
CredentialHash, source serial, algorithm id), with the full header as AEAD
associated data for every layer, one or two nested AEAD layers
(ChaCha20-Poly1305 and/or AES-256-GCM) each with an independent 12-byte nonce.
- Portability is bound to
Kvault equality, not to the board serial. The serial
is authenticated provenance; import does not require a match, and same-board-only
import is left as an optional policy.
- Every command except Status requires a
pinUvAuthToken with the acfg
permission, over the same MAC preimage shape RS-Key already uses for
CONFIG_WRITE.
Why this is not the export limitations.md rejects
|
Full at-rest export (rejected) |
Authenticated, device-bound export (proposed) |
| What the host receives |
readable key material |
an AEAD envelope bound to Kvault |
| Usable on another board |
yes, trivially |
only within the same vault |
| Usable without the PIN |
yes |
no — acfg token required |
| Usable without physical presence |
yes |
no — touch required |
| Usable if the file leaks |
yes |
no — Kvault never leaves the device |
| Bypasses secure boot / OTP root |
yes |
no — Kvault is wrapped under the device key |
| Detectable afterwards |
no |
yes — audit journal entry |
The property that makes it defensible: stealing the envelope is not enough. An
attacker needs the envelope and an unlocked enrolled board and the PIN. That is
closer to the existing soft-lock composition ("identity becomes device + words",
soft-lock.md)
than to a dump.
It does widen the story in one honest way: a board that is powered on, unlocked and
PIN-authorized can now be asked to reveal one credential at a time, where before it
could reveal only the seed, once. That is why the gates below are proposed as
mandatory rather than optional. The reference design has the same property.
Subcommands
0x01–0x0E are taken (consts.rs),
so the vault arm starts at 0x0F:
| Sub |
Name |
Params (key 2) |
Response |
Gate |
0x0F |
CRED_EXPORT |
{1: credentialId} |
{1: envelope, 2: metadata} |
MSE + touch + PIN-token (acfg) |
0x10 |
CRED_IMPORT |
{1: envelope} |
{} |
MSE + touch + PIN-token (acfg) |
0x11 |
VAULT_STATE |
— |
{1: vault_id, 2: present, 3: sealed, 4: exported_count, 5: remaining} |
ungated |
0x12 |
VAULT_FINALIZE |
— |
— |
touch + PIN-token when a PIN is set |
This mirrors §9's existing conventions: the one-shot MSE channel (0x41 / 0x01) is
already there, BACKUP_STATE is already the ungated "is this armed" probe, and
BACKUP_FINALIZE is already the seal-the-window shape. §11's guidance that unknown
subcommands be treated as skippable keeps it additive for hosts that predate it.
Decisions worth making explicitly:
- Export scope. The reference covers resident FIDO credentials only. Extending
to OpenPGP/PIV/OATH is where RS-Key would go beyond it, and where risk
concentrates, because those applets have different PIN models.
- One-time window or standing capability? Seed export is once-ever by design. A
backup re-exported for each new device cannot work that way, so this is the crux.
VAULT_FINALIZE above assumes a sealable window by analogy; a fleet use case
plausibly wants a re-armable ceremony instead.
fips-profile. fips.md
refuses seed export while still allowing BACKUP_LOAD ("keys may migrate into a
profile device, never out"). The same asymmetry is the obvious default.
- Record the operation in the audit journal, so an export is visible afterwards.
The reference design has no such record.
- Optional destination binding. The draft deliberately does not require a serial
match on import. An optional allowed_destinations list on the envelope would be a
cheap addition for fleet operators who want it.
Primitives: most of it is already in the tree
| Primitive |
Needed for |
RS-Key status |
| SHA-256 |
VaultID, CredentialHash |
present (rsk-crypto) |
| HKDF-SHA-256 |
layer key derivation |
present (rsk-crypto) |
| ChaCha20-Poly1305 |
AEAD profile 1 / 3 |
present (rsk-crypto) |
| AES-256-GCM |
AEAD profile 2 / 4 |
present (rsk-crypto) |
| X448 |
enrollment only (HPKE DHKEM(X448, HKDF-SHA512), legacy channel) |
absent (rsk-ec has X25519, not X448) |
Worth calling out because it is encouraging: X448 is only needed to accept a
Kvault from an external enroller. A self-contained RS-Key ↔ RS-Key export/import
has the device generate and wrap its own Kvault, and that path uses only
primitives RS-Key already ships. Export/import/Status can land without X448;
cross-vendor enrollment is the part that needs it.
One difficulty specific to this tree: the resident credential's private key is
never materialized today — the stored box is a ChaCha20-Poly1305 box keyed from
the device seed. Exporting means decrypting it inside the gate and re-wrapping under
the layer keys, which is a new place plaintext key material exists in RAM. That
needs a scrub on every path, and probably a note in
threat-model.md.
Interop footguns
If Pico Vault wire compatibility is a goal, two details are easy to get wrong and
both were fixed late upstream:
- Do not re-derive the resident/client ID on import. Store the transported
resident_id verbatim. The upstream derivation mixed the board serial into its
HMAC, which made cross-board import silently produce a credential no connector
would match — fixed in
b8e5e48.
- Import must be atomic. Upstream publishes credential, RP hash, public key,
private key, metadata and state in one container generation. A partial publish
leaves a credential that is assertable but undiscoverable — a failure mode RS-Key
already documents as reachable via an EF_RP strand
(threat-model.md).
Also worth acknowledging: the reference design has no freshness counter, no expiry,
no revocation and no replay database, and import is not consuming — a re-imported
envelope is governed only by storage semantics. If this lands, the audit journal is
the natural place to record exported_count and give an operator something the
reference lacks.
Multi-device backup and PicoForge
The motivating use case is several keys: back up each Pico 2, restore onto a
replacement, keep a mirror in sync.
rsk vault init # arm the vault on a device, generate Kvault
rsk vault export --all --out backup/<serial>.pkv
rsk vault export --credential <id> --out backup/<serial>/<id>.pkv
rsk vault restore --from backup/<serial>.pkv
rsk vault sync --from <primary> --to <backup> # mirror, both live
rsk vault status --json # per-device: sealed, exported_count, coverage
Note rsk vault sync is not rsk pair. pair deliberately sets up two
independent identities and warns when a seed restore would turn them into clones. A
vault sync deliberately is a shared identity across boards. Both can coexist, but
the UX must make them hard to confuse, because they sit on opposite sides of the "one
secret in two places" trade-off.
Questions I would want answered before implementation:
- Is a narrow, authenticated credential export in scope at all, or is the seed-only
boundary considered settled (close as wontfix)?
- If in scope, is
Kvault-style device-bound binding the right model, or would you
prefer an explicit destination allow-list from the start?
- Keep the one-time-window model (safer, incompatible with fleet rotation), or relax
to a re-armable ceremony?
- Is Pico Vault wire compatibility a goal, or only design inspiration? It decides
whether the header constants must be byte-exact.
- OpenPGP/PIV/OATH in scope, or resident passkeys only to start?
Related: a companion issue covers making the vendor command set machine-discoverable,
which a host needs in order to drive this safely —
#122.
References
Alternatives considered
- Keep the seed-only boundary and document "buy a second key" (status quo). Cheap
and safe. It does not cover board failure after the fact, and cannot rotate a fleet —
the two problems above.
- Full at-rest export of the KV store. Rejected in
limitations.md and I agree:
it would gut the at-rest story, and it fails every row of the comparison table.
- Host-side escrow of derived material. Would require the host to hold key
material permanently. Strictly worse than a device-bound envelope, since the
envelope alone is useless.
- A pure Pico Vault fork, no RS-Key-specific additions. Viable and cheapest to
interop-test, but it inherits the missing freshness/revocation semantics and the
absence of an audit trail, both of which this project is in a position to do better.
Area
Backup / lock / audit / attestation
The problem
RS-Key can back up its master seed. It cannot export a single credential.
Everything not derived from the seed — resident passkeys, OpenPGP private keys, PIV
private keys, OATH secrets, OTP slots — is sealed to the source chip, so a dead board
takes them with it. The documented answer today is "keep two keys and enrol both
everywhere" (backup-key.md),
which is redundancy rather than a backup.
This is by explicit decision, not omission:
I agree with the reasoning behind that line. The question is whether a narrow
export can exist without emptying it.
Three concrete consequences today:
holding only a resident passkey needs its own recovery flow.
rsk inventory listandrsk offboardcover theasset lifecycle (fleet.md),
but nothing moves a credential set from a retired key to its replacement.
enumerateCredentialsreturns metadata only, andno host-side codec exists that could carry a credential even if one were moved.
Absence of any export path is verifiable: the credential-management surface in
crates/rsk-fido/src/credmgmt.rsimplements exactly seven subcommands, none of whichreturns stored material, and there is no export/import/dump codec in
tools/rsk.Proposed shape
Add an authenticated, device-bound credential export/import path, reusing the
existing
0x41vendor surface. The design to copy is Pico Vault (PKV1), whichsolves exactly this problem and publishes its wire format:
Credential Export and Import
docs/vault/draft-picokeys-vaulted-passkeys-00.txt(normative-style wire spec)
src/fido/fido_vault.c,reference host tests in
tests/pico-fido/test_080_vault.pyThe shape that matters:
Kvaultroot secret, generated on the device, never exported incleartext — stored wrapped under the device key.
VaultID = SHA-256("PicoKeys Vault ID v1" || Kvault)— a domainidentifier explicitly not a secret and not a key.
K_i = HKDF-SHA-256(salt = VaultID, IKM = Kvault, info = "PicoKeys Vault enrollment v1" || CredentialHash || algorithm || i)PKV\x01, VaultID,CredentialHash, source serial, algorithm id), with the full header as AEAD
associated data for every layer, one or two nested AEAD layers
(ChaCha20-Poly1305 and/or AES-256-GCM) each with an independent 12-byte nonce.
Kvaultequality, not to the board serial. The serialis authenticated provenance; import does not require a match, and same-board-only
import is left as an optional policy.
pinUvAuthTokenwith theacfgpermission, over the same MAC preimage shape RS-Key already uses for
CONFIG_WRITE.Why this is not the export
limitations.mdrejectsKvaultacfgtoken requiredKvaultnever leaves the deviceKvaultis wrapped under the device keyThe property that makes it defensible: stealing the envelope is not enough. An
attacker needs the envelope and an unlocked enrolled board and the PIN. That is
closer to the existing soft-lock composition ("identity becomes device + words",
soft-lock.md)
than to a dump.
It does widen the story in one honest way: a board that is powered on, unlocked and
PIN-authorized can now be asked to reveal one credential at a time, where before it
could reveal only the seed, once. That is why the gates below are proposed as
mandatory rather than optional. The reference design has the same property.
Subcommands
0x01–0x0Eare taken (consts.rs),so the vault arm starts at
0x0F:0x0FCRED_EXPORT{1: credentialId}{1: envelope, 2: metadata}acfg)0x10CRED_IMPORT{1: envelope}{}acfg)0x11VAULT_STATE{1: vault_id, 2: present, 3: sealed, 4: exported_count, 5: remaining}0x12VAULT_FINALIZEThis mirrors §9's existing conventions: the one-shot MSE channel (
0x41 / 0x01) isalready there,
BACKUP_STATEis already the ungated "is this armed" probe, andBACKUP_FINALIZEis already the seal-the-window shape. §11's guidance that unknownsubcommands be treated as skippable keeps it additive for hosts that predate it.
Decisions worth making explicitly:
to OpenPGP/PIV/OATH is where RS-Key would go beyond it, and where risk
concentrates, because those applets have different PIN models.
backup re-exported for each new device cannot work that way, so this is the crux.
VAULT_FINALIZEabove assumes a sealable window by analogy; a fleet use caseplausibly wants a re-armable ceremony instead.
fips-profile. fips.mdrefuses seed export while still allowing
BACKUP_LOAD("keys may migrate into aprofile device, never out"). The same asymmetry is the obvious default.
The reference design has no such record.
match on import. An optional
allowed_destinationslist on the envelope would be acheap addition for fleet operators who want it.
Primitives: most of it is already in the tree
rsk-crypto)rsk-crypto)rsk-crypto)rsk-crypto)DHKEM(X448, HKDF-SHA512), legacy channel)rsk-echas X25519, not X448)Worth calling out because it is encouraging: X448 is only needed to accept a
Kvaultfrom an external enroller. A self-contained RS-Key ↔ RS-Key export/importhas the device generate and wrap its own
Kvault, and that path uses onlyprimitives RS-Key already ships. Export/import/Status can land without X448;
cross-vendor enrollment is the part that needs it.
One difficulty specific to this tree: the resident credential's private key is
never materialized today — the stored box is a ChaCha20-Poly1305 box keyed from
the device seed. Exporting means decrypting it inside the gate and re-wrapping under
the layer keys, which is a new place plaintext key material exists in RAM. That
needs a scrub on every path, and probably a note in
threat-model.md.
Interop footguns
If Pico Vault wire compatibility is a goal, two details are easy to get wrong and
both were fixed late upstream:
resident_idverbatim. The upstream derivation mixed the board serial into itsHMAC, which made cross-board import silently produce a credential no connector
would match — fixed in
b8e5e48.private key, metadata and state in one container generation. A partial publish
leaves a credential that is assertable but undiscoverable — a failure mode RS-Key
already documents as reachable via an
EF_RPstrand(threat-model.md).
Also worth acknowledging: the reference design has no freshness counter, no expiry,
no revocation and no replay database, and import is not consuming — a re-imported
envelope is governed only by storage semantics. If this lands, the audit journal is
the natural place to record
exported_countand give an operator something thereference lacks.
Multi-device backup and PicoForge
The motivating use case is several keys: back up each Pico 2, restore onto a
replacement, keep a mirror in sync.
Note
rsk vault syncis notrsk pair.pairdeliberately sets up twoindependent identities and warns when a seed restore would turn them into clones. A
vault sync deliberately is a shared identity across boards. Both can coexist, but
the UX must make them hard to confuse, because they sit on opposite sides of the "one
secret in two places" trade-off.
Questions I would want answered before implementation:
boundary considered settled (close as
wontfix)?Kvault-style device-bound binding the right model, or would youprefer an explicit destination allow-list from the start?
to a re-armable ceremony?
whether the header constants must be byte-exact.
Related: a companion issue covers making the vendor command set machine-discoverable,
which a host needs in order to drive this safely —
#122.
References
authenticatorVendorbackup-key.md ·
soft-lock.md
Alternatives considered
and safe. It does not cover board failure after the fact, and cannot rotate a fleet —
the two problems above.
limitations.mdand I agree:it would gut the at-rest story, and it fails every row of the comparison table.
material permanently. Strictly worse than a device-bound envelope, since the
envelope alone is useless.
interop-test, but it inherits the missing freshness/revocation semantics and the
absence of an audit trail, both of which this project is in a position to do better.