Skip to content

RFC: credential-level export/import for backup, restore, and device migration #121

Description

@bashful-jacob

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:

  1. A failed board is an account-recovery event, not a restore. Every account
    holding only a resident passkey needs its own recovery flow.
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. 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.
  4. Record the operation in the audit journal, so an export is visible afterwards.
    The reference design has no such record.
  5. 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:

  1. 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.
  2. 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:

  1. Is a narrow, authenticated credential export in scope at all, or is the seed-only
    boundary considered settled (close as wontfix)?
  2. If in scope, is Kvault-style device-bound binding the right model, or would you
    prefer an explicit destination allow-list from the start?
  3. Keep the one-time-window model (safer, incompatible with fleet rotation), or relax
    to a re-armable ceremony?
  4. Is Pico Vault wire compatibility a goal, or only design inspiration? It decides
    whether the header constants must be byte-exact.
  5. 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.

Activity

  1. TheMaxMur commented on Sep 22, 2026

    @TheMaxMur
    Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions