Skip to content

feat: user-provisioned metadata signing keys — slots 4-6 + storage v18#213

Closed
BitHighlander wants to merge 6 commits into
alphafrom
feat/user-metadata-keys
Closed

feat: user-provisioned metadata signing keys — slots 4-6 + storage v18#213
BitHighlander wants to merge 6 commits into
alphafrom
feat/user-metadata-keys

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Summary

Lets KeepKey owners add their own secp256k1 signing keys at runtime so companies / DAOs can publish their own clear-signing descriptor sets that members verify on their own device, without a firmware update.

  • 3 user slots (4..6) persisted in Storage.pub.metadata_keys
  • STORAGE_VERSION 17 → 18 with explicit V17→V18 migration that zero-inits the new key array on existing devices
  • AddMetadataKey / RemoveMetadataKey / ListMetadataKeys handlers in fsm_msg_metadata.h
    • Add: 2-step confirm (slot+label, then SHA-256 fingerprint), then PIN
    • Remove: confirm shows fingerprint of the key being removed (defends against stealth removal on a briefly-unlocked device)
    • Slots 0-3 remain firmware-baked and cannot be overwritten
  • signed_metadata.c moved from a hardcoded METADATA_PUBKEYS lookup to a unified metadata_get_pubkey() that resolves slot 0-3 from the static firmware table and slot 4-6 from storage
  • Verification confirm screen now shows the matching label: "Insight Verified" for slot 0, "<label> Verified" for user slots

Depends on

BitHighlander/device-protocol#26 — pulls submodule pin to that branch's tip.

Trust model

Slot Source Mutable?
0 Firmware-baked: "Insight" (KeepKey-managed) No
1 Firmware rotation slot No
2 Firmware reserved No
3 CI test (DEBUG_LINK only) No
4-6 User-provisioned via PIN+confirm Yes (replace/remove)

Add/remove require PIN entry + 2 confirms (1 for label/slot, 1 for fingerprint). The fingerprint is SHA-256 of the pubkey, first 4 bytes (8 hex chars) — small enough for the OLED, stable enough to verify against a company-published value out-of-band.

Storage layout

V18 uses the V17 reserved region at offset 501 in the plaintext block:

  • 4 bytes: metadata_keys_count (u32 LE)
  • 3 × 51 bytes: { used(1) + pubkey(33) + label(17 nul-padded) }
  • Total: 157 bytes within the 996-byte V17 reserved range

V17 storage on disk reads cleanly — storage_fromFlash zeros the new fields explicitly on the V17 case (memzero(dst) at entry already does it; explicit reset documents the invariant).

Test plan

  • CI builds clean (proto regen, firmware ARM build, native emulator)
  • V17 → V18 storage migration: existing-device flash dump loads correctly with empty user-key set
  • AddMetadataKey: confirm screens display label and fingerprint; PIN required
  • RemoveMetadataKey: fingerprint confirm shown for the key being removed
  • ListMetadataKeys: returns only populated user slots (0-3 firmware slots intentionally not enumerated)
  • signed_metadata flow with user-slot key: ECDSA verify succeeds, confirm shows "<label> Verified"
  • Reject: oversize label (>16 chars), uncompressed pubkey prefix, slot out of [4,6]
  • Wipe device: user keys cleared along with rest of storage

Future / not in this PR

  • Bulk-signing build for an "Insight signing device" (custom firmware that batch-signs metadata payloads without per-payload button confirm)
  • Listing firmware-baked slots 0-3 in MetadataKeysList for full visibility
  • Unit tests in unittests/firmware/signed_metadata.cpp (need the build toolchain re-installed locally — leaving for follow-up)

@BitHighlander

Copy link
Copy Markdown
Owner Author

Pre-existing CI note

include/keepkey/emulator/setup.h and include/keepkey/emulator/libkkemu.h show clang-format failures, but these files are identical to alpha (git diff alpha..feat/user-metadata-keys for them is empty) and alpha's own CI run on commit 92c8f534 (the libkkemu fix-up) failed lint for the same reason.

The format issues there are pre-existing and not introduced by this PR. Either fixing them up here as a one-line follow-on or in a separate alpha-cleanup PR is fine — happy to do whichever you prefer.

Lets KeepKey owners add their own secp256k1 signing keys at runtime so
companies / DAOs can publish their own clear-signing descriptor sets
that members verify on their own device, without firmware updates.

- 3 user slots (4..6) persisted in Storage.pub.metadata_keys, plus
  STORAGE_VERSION bump 17 -> 18 with explicit V17->V18 migration that
  zero-inits the new key array on existing devices.
- AddMetadataKey, RemoveMetadataKey, ListMetadataKeys handlers in
  fsm_msg_metadata.h. Add: 2-step confirm (slot+label, then SHA-256
  fingerprint), then PIN. Remove: confirm shows fingerprint of the key
  being removed (defends against stealth removal on a briefly-unlocked
  device). Slots 0..3 remain firmware-baked and cannot be overwritten.
- signed_metadata.c moves from a hardcoded METADATA_PUBKEYS lookup to a
  unified metadata_get_pubkey() that resolves slot 0..3 from the static
  firmware table and slot 4..6 from storage. Verification confirm screen
  now shows the matching label ("Insight Verified" for slot 0,
  "<label> Verified" for user slots).
- Pulls in device-protocol c7a824d which adds MetadataKeyType + the four
  new wire messages.
@BitHighlander
BitHighlander force-pushed the feat/user-metadata-keys branch from e0071aa to b1dbdd5 Compare April 25, 2026 03:32
…fset (1541)

The expected_flash had a hardcoded 0x11 at flash[1541] (encrypted_sec_version
LE u32 byte 0). Bumping STORAGE_VERSION 17->18 made that mismatch — fixed
by using the STORAGE_VERSION macro like the test does at flash[44] and [508],
and updated the printf substitution loop so future bumps stay green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant