diff --git a/deps/device-protocol b/deps/device-protocol index bf8646b81..c7a824d01 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit bf8646b817401d4623e0977ddb4c961b1101121f +Subproject commit c7a824d011c913aaaf7a44eee5febcfe772ebc49 diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index a33c2703b..630ad578c 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -80,6 +80,11 @@ void fsm_msgApplyPolicies(ApplyPolicies* msg); // BIP-85 (chain-agnostic, available in all builds) void fsm_msgGetBip85Mnemonic(const GetBip85Mnemonic* msg); +// Insight metadata key management (slots 4..6 user-provisioned) +void fsm_msgAddMetadataKey(const AddMetadataKey* msg); +void fsm_msgRemoveMetadataKey(const RemoveMetadataKey* msg); +void fsm_msgListMetadataKeys(const ListMetadataKeys* msg); + #ifndef BITCOIN_ONLY // ethereum void fsm_msgEthereumGetAddress(EthereumGetAddress* msg); diff --git a/include/keepkey/firmware/storage.h b/include/keepkey/firmware/storage.h index 1a57603b2..b49657da5 100644 --- a/include/keepkey/firmware/storage.h +++ b/include/keepkey/firmware/storage.h @@ -26,7 +26,14 @@ #include "keepkey/firmware/authenticator.h" #define STORAGE_VERSION \ - 17 /* Must add case fallthrough in storage_fromFlash after increment*/ + 18 /* Must add case fallthrough in storage_fromFlash after increment*/ + +/* Number of user-provisioned metadata signing key slots (slots 4..6). + * Firmware-baked slots 0..3 live in signed_metadata.c and don't count here. */ +#define METADATA_USER_KEY_COUNT 3 +#define METADATA_USER_KEY_FIRST 4 +#define METADATA_USER_KEY_LAST \ + (METADATA_USER_KEY_FIRST + METADATA_USER_KEY_COUNT - 1) #define STORAGE_RETRIES 3 #define RANDOM_SALT_LEN 32 @@ -165,6 +172,30 @@ void storage_getPolicies(PolicyType* policy_data); /// \brief Status of policy in storage bool storage_isPolicyEnabled(const char* policy_name); +typedef struct _MetadataKeyType MetadataKeyType; + +/// \brief Add or replace a user-provisioned metadata signing key. +/// \param slot Must be in [METADATA_USER_KEY_FIRST, METADATA_USER_KEY_LAST]. +/// \param pubkey 33-byte compressed secp256k1 public key. +/// \param label Null-terminated, ≤16 chars. Displayed during verification. +/// \returns true on success, false on invalid slot/pubkey/label. +bool storage_setMetadataKey(uint32_t slot, const uint8_t* pubkey, + const char* label); + +/// \brief Remove a user-provisioned metadata signing key. +/// \returns true if a key was removed, false if slot was empty/invalid. +bool storage_removeMetadataKey(uint32_t slot); + +/// \brief Look up a user-provisioned metadata signing key. +/// \param pubkey_out Receives 33 bytes on success. +/// \param label_out Receives null-terminated string (buffer must be ≥17 bytes). +/// \returns true if slot is populated. +bool storage_getMetadataKey(uint32_t slot, uint8_t pubkey_out[33], + char label_out[17]); + +/// \brief Number of populated user-provisioned metadata key slots (0..3). +uint32_t storage_getMetadataKeyCount(void); + uint32_t storage_getAutoLockDelayMs(void); void storage_setAutoLockDelayMs(uint32_t auto_lock_delay_ms); diff --git a/include/keepkey/transport/messages.options b/include/keepkey/transport/messages.options index 29a1fcea6..0ab4073ff 100644 --- a/include/keepkey/transport/messages.options +++ b/include/keepkey/transport/messages.options @@ -105,6 +105,10 @@ SignedIdentity.signature max_size:65 ApplyPolicies.policy max_count:6 +AddMetadataKey.pubkey max_size:33 +AddMetadataKey.label max_size:17 +MetadataKeysList.keys max_count:7 + # not used in firmware FirmwareUpload.payload_hash max_size:32 FirmwareUpload.payload max_size:0 diff --git a/include/keepkey/transport/types.options b/include/keepkey/transport/types.options index 307732f4b..b1a126a26 100644 --- a/include/keepkey/transport/types.options +++ b/include/keepkey/transport/types.options @@ -46,3 +46,6 @@ IdentityType.port max_size:6 IdentityType.path max_size:256 PolicyType.policy_name max_size:15 + +MetadataKeyType.pubkey max_size:33 +MetadataKeyType.label max_size:17 diff --git a/lib/firmware/fsm.c b/lib/firmware/fsm.c index 394bdbb49..31ec9ab12 100644 --- a/lib/firmware/fsm.c +++ b/lib/firmware/fsm.c @@ -294,3 +294,4 @@ void fsm_msgClearSession(ClearSession* msg) { #include "fsm_msg_tron.h" #include "fsm_msg_solana.h" #include "fsm_msg_bip85.h" +#include "fsm_msg_metadata.h" diff --git a/lib/firmware/fsm_msg_metadata.h b/lib/firmware/fsm_msg_metadata.h new file mode 100644 index 000000000..f6fd2dd12 --- /dev/null +++ b/lib/firmware/fsm_msg_metadata.h @@ -0,0 +1,165 @@ +/* + * This file is part of the KeepKey project. + * + * Copyright (C) 2026 KeepKey + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +/* User-provisioned metadata signing key management. Handlers for + * AddMetadataKey, RemoveMetadataKey, ListMetadataKeys. Slots 4..6 are + * user-writable; slots 0..3 are firmware-baked and not enumerated here. */ + +static void metadata_pubkey_fingerprint(const uint8_t pubkey[33], + char fp_out[9]) { + uint8_t digest[32]; + sha256_Raw(pubkey, 33, digest); + /* First 4 bytes → 8 hex chars. Stable, short enough for OLED. */ + for (int i = 0; i < 4; i++) { + snprintf(fp_out + i * 2, 3, "%02x", digest[i]); + } + fp_out[8] = '\0'; +} + +void fsm_msgAddMetadataKey(const AddMetadataKey *msg) { + CHECK_INITIALIZED + + if (msg->slot < METADATA_USER_KEY_FIRST || + msg->slot > METADATA_USER_KEY_LAST) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + "slot must be in user range (4..6)"); + layoutHome(); + return; + } + if (msg->pubkey.size != 33 || + (msg->pubkey.bytes[0] != 0x02 && msg->pubkey.bytes[0] != 0x03)) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + "pubkey must be 33-byte compressed secp256k1"); + layoutHome(); + return; + } + if (msg->label[0] == '\0') { + fsm_sendFailure(FailureType_Failure_SyntaxError, "label must not be empty"); + layoutHome(); + return; + } + + char fp[9]; + metadata_pubkey_fingerprint(msg->pubkey.bytes, fp); + + /* Confirm 1: slot + label. Lets user abort cheaply before PIN. */ + if (!confirm(ButtonRequestType_ButtonRequest_Other, "Add Insight Key", + "Slot %lu\n%s", (unsigned long)msg->slot, msg->label)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Add metadata key cancelled"); + layoutHome(); + return; + } + + /* Confirm 2: fingerprint. User verifies against company-published value + * out-of-band. Truncating to 8 hex chars is intentional — full SHA256 + * doesn't fit and the threat model here is mistake/typo, not collision. */ + if (!confirm(ButtonRequestType_ButtonRequest_Other, "Verify Key Fingerprint", + "fp:\n%s", fp)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Add metadata key cancelled"); + layoutHome(); + return; + } + + CHECK_PIN_UNCACHED + + if (!storage_setMetadataKey(msg->slot, msg->pubkey.bytes, msg->label)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Could not store metadata key"); + layoutHome(); + return; + } + storage_commit(); + + fsm_sendSuccess("Metadata key added"); + layoutHome(); +} + +void fsm_msgRemoveMetadataKey(const RemoveMetadataKey *msg) { + CHECK_INITIALIZED + + if (msg->slot < METADATA_USER_KEY_FIRST || + msg->slot > METADATA_USER_KEY_LAST) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + "slot must be in user range (4..6)"); + layoutHome(); + return; + } + + uint8_t pubkey[33]; + char label[17]; + if (!storage_getMetadataKey(msg->slot, pubkey, label)) { + fsm_sendFailure(FailureType_Failure_Other, "slot is empty"); + layoutHome(); + return; + } + + /* Show fingerprint of the key being removed — defends against stealth + * removal where an attacker briefly accesses an unlocked device. */ + char fp[9]; + metadata_pubkey_fingerprint(pubkey, fp); + + if (!confirm(ButtonRequestType_ButtonRequest_Other, "Remove Insight Key", + "Slot %lu: %s\nfp: %s", (unsigned long)msg->slot, label, fp)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Remove metadata key cancelled"); + layoutHome(); + return; + } + + CHECK_PIN_UNCACHED + + if (!storage_removeMetadataKey(msg->slot)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "Could not remove metadata key"); + layoutHome(); + return; + } + storage_commit(); + + fsm_sendSuccess("Metadata key removed"); + layoutHome(); +} + +void fsm_msgListMetadataKeys(const ListMetadataKeys *msg) { + (void)msg; + RESP_INIT(MetadataKeysList); + + size_t count = 0; + for (uint32_t slot = METADATA_USER_KEY_FIRST; slot <= METADATA_USER_KEY_LAST; + ++slot) { + uint8_t pubkey[33]; + char label[17]; + if (!storage_getMetadataKey(slot, pubkey, label)) continue; + + MetadataKeyType *k = &resp->keys[count++]; + memzero(k, sizeof(*k)); + k->has_slot = true; + k->slot = slot; + k->has_pubkey = true; + k->pubkey.size = 33; + memcpy(k->pubkey.bytes, pubkey, 33); + k->has_label = true; + strlcpy(k->label, label, sizeof(k->label)); + } + resp->keys_count = count; + + msg_write(MessageType_MessageType_MetadataKeysList, resp); +} diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index 39792f28e..29c953816 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -36,6 +36,10 @@ MSG_IN(MessageType_MessageType_GetBip85Mnemonic, GetBip85Mnemonic, fsm_msgGetBip85Mnemonic) + MSG_IN(MessageType_MessageType_AddMetadataKey, AddMetadataKey, fsm_msgAddMetadataKey) + MSG_IN(MessageType_MessageType_RemoveMetadataKey, RemoveMetadataKey, fsm_msgRemoveMetadataKey) + MSG_IN(MessageType_MessageType_ListMetadataKeys, ListMetadataKeys, fsm_msgListMetadataKeys) + #ifndef BITCOIN_ONLY MSG_IN(MessageType_MessageType_EthereumGetAddress, EthereumGetAddress, fsm_msgEthereumGetAddress) MSG_IN(MessageType_MessageType_EthereumSignTx, EthereumSignTx, fsm_msgEthereumSignTx) @@ -119,6 +123,8 @@ MSG_OUT(MessageType_MessageType_Bip85Mnemonic, Bip85Mnemonic, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_MetadataKeysList, MetadataKeysList, NO_PROCESS_FUNC) + #ifndef BITCOIN_ONLY MSG_OUT(MessageType_MessageType_EthereumAddress, EthereumAddress, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_EthereumTxRequest, EthereumTxRequest, NO_PROCESS_FUNC) diff --git a/lib/firmware/signed_metadata.c b/lib/firmware/signed_metadata.c index f8d4b0ad6..709d70a9a 100644 --- a/lib/firmware/signed_metadata.c +++ b/lib/firmware/signed_metadata.c @@ -3,6 +3,7 @@ #include "keepkey/board/confirm_sm.h" #include "keepkey/board/util.h" #include "keepkey/firmware/ethereum.h" +#include "keepkey/firmware/storage.h" #include "trezor/crypto/address.h" #include "trezor/crypto/bignum.h" #include "trezor/crypto/ecdsa.h" @@ -17,30 +18,33 @@ static bool metadata_available = false; static SignedMetadata stored_metadata; +static char stored_metadata_label[17]; /* - * Metadata verification public keys. - * Slot 0: active production key - * Slot 1: rotation target - * Slots 2-3: reserved + * Firmware-baked metadata verification public keys (slots 0..3). + * User-provisioned keys live in storage at slots 4..6 (see storage.h). * - * Keys are derived via KeepKey SignIdentity at keepkey.com/insight. - * Only the public key is stored here — the signing mnemonic is held - * offline and never appears in source code. + * Slot 0: active production key — the "Insight" key managed by KeepKey + * Slot 1: rotation target + * Slot 2: reserved + * Slot 3: CI test key, DEBUG_LINK only * - * To rotate: generate new key with pioneer-insight keygen, - * replace the slot below, ship firmware update. + * Slot 0 is derived via KeepKey SignIdentity at keepkey.com/insight. + * Only public keys are stored here; the signing mnemonic is held offline. + * To rotate: generate new key with pioneer-insight keygen, replace the + * slot below, ship firmware update. */ -static const uint8_t METADATA_PUBKEYS[METADATA_MAX_KEYS][33] = { - /* Key 0: production */ +static const uint8_t METADATA_FIRMWARE_PUBKEYS[METADATA_MAX_KEYS][33] = { + /* Slot 0: production */ {0x02, 0x18, 0x62, 0x1d, 0x9c, 0x14, 0x47, 0x34, 0x58, 0x71, 0x3b, 0xd3, 0xe6, 0x72, 0xe5, 0x34, 0x80, 0xaa, 0x70, 0x32, 0xca, 0x9b, 0x67, 0x35, 0x63, 0x95, 0xe8, 0x87, 0x09, 0xbb, 0x45, 0x22, 0x6a}, - /* Key 1: rotation slot */ + /* Slot 1: rotation */ {0x00}, + /* Slot 2: reserved */ {0x00}, #if DEBUG_LINK - /* Key 3: CI test key — only available in emulator/debug builds */ + /* Slot 3: CI test key — only available in emulator/debug builds */ {0x02, 0xe3, 0xb3, 0x01, 0x5c, 0x47, 0xdd, 0xca, 0xab, 0xe4, 0xf8, 0xe8, 0x72, 0xf1, 0xed, 0x8f, 0x09, 0xca, 0x14, 0x5a, 0x8d, 0x81, 0x77, 0x0d, 0x92, 0x21, 0x3d, 0x56, 0xda, 0x31, 0xab, 0x51, 0x07}, @@ -49,6 +53,33 @@ static const uint8_t METADATA_PUBKEYS[METADATA_MAX_KEYS][33] = { #endif }; +/* Display labels for firmware-baked slots. User slots get their label from + * storage. Empty string means "no label" — confirm screen falls back to a + * generic header. */ +static const char METADATA_FIRMWARE_LABELS[METADATA_MAX_KEYS][17] = { + "Insight", + "", + "", +#if DEBUG_LINK + "CI Test", +#else + "", +#endif +}; + +/* Look up a pubkey + label by slot. Slots 0..3 are firmware-baked, + * 4..6 come from storage. Returns false if the slot is unprovisioned. */ +static bool metadata_get_pubkey(uint8_t slot, uint8_t pubkey_out[33], + char label_out[17]) { + if (slot < METADATA_MAX_KEYS) { + if (METADATA_FIRMWARE_PUBKEYS[slot][0] == 0x00) return false; + memcpy(pubkey_out, METADATA_FIRMWARE_PUBKEYS[slot], 33); + strlcpy(label_out, METADATA_FIRMWARE_LABELS[slot], 17); + return true; + } + return storage_getMetadataKey(slot, pubkey_out, label_out); +} + static bool read_u8(const uint8_t **cursor, const uint8_t *end, uint8_t *out) { if ((size_t)(end - *cursor) < 1) { return false; @@ -192,6 +223,7 @@ bool signed_metadata_available(void) { return metadata_available; } void signed_metadata_clear(void) { memzero(&stored_metadata, sizeof(stored_metadata)); + memzero(stored_metadata_label, sizeof(stored_metadata_label)); metadata_available = false; } @@ -199,12 +231,14 @@ MetadataClassification signed_metadata_process(const uint8_t *payload, size_t payload_len, uint8_t key_id) { uint8_t digest[32]; + uint8_t pubkey[33]; + char label[17]; size_t signed_len; signed_metadata_clear(); - if (key_id >= METADATA_MAX_KEYS || METADATA_PUBKEYS[key_id][0] == 0x00 || - !payload || payload_len < 65) { + if (!payload || payload_len < 65 || + !metadata_get_pubkey(key_id, pubkey, label)) { return METADATA_MALFORMED; } @@ -217,12 +251,13 @@ MetadataClassification signed_metadata_process(const uint8_t *payload, signed_len = payload_len - sizeof(stored_metadata.signature) - 1; sha256_Raw(payload, signed_len, digest); - if (ecdsa_verify_digest(&secp256k1, METADATA_PUBKEYS[key_id], - stored_metadata.signature, digest) != 0) { + if (ecdsa_verify_digest(&secp256k1, pubkey, stored_metadata.signature, + digest) != 0) { signed_metadata_clear(); return METADATA_MALFORMED; } + strlcpy(stored_metadata_label, label, sizeof(stored_metadata_label)); metadata_available = true; return stored_metadata.classification; } @@ -272,12 +307,22 @@ bool signed_metadata_confirm(void) { return false; } - /* Screen 1: Verified method — use review_with_icon for trust indicator */ + /* Screen 1: Verified method — use review_with_icon for trust indicator. + * Header reflects the key source: firmware slot 0 → "Insight Verified", + * user slots → "