diff --git a/include/keepkey/firmware/app_layout.h b/include/keepkey/firmware/app_layout.h index 7699c582e..f4d0e7bca 100644 --- a/include/keepkey/firmware/app_layout.h +++ b/include/keepkey/firmware/app_layout.h @@ -124,7 +124,8 @@ void layout_zcash_address_text_notification(const char* desc, const char* address, NotificationType type); void layout_pin(const char* str, char* pin); -void layout_cipher(const char* current_word, const char* cipher); +void layout_cipher(const char* current_word, const char* cipher, + const char* prev_word_info); void layout_address(const char* address, QRSize qr_size); void set_leaving_handler(leaving_handler_t leaving_func); diff --git a/include/keepkey/firmware/bip85.h b/include/keepkey/firmware/bip85.h new file mode 100644 index 000000000..73c197995 --- /dev/null +++ b/include/keepkey/firmware/bip85.h @@ -0,0 +1,22 @@ +#ifndef BIP85_H +#define BIP85_H + +#include +#include +#include + +/** + * Derive a child BIP-39 mnemonic via BIP-85. + * + * Path: m/83696968'/39'/0'/'/' + * + * @param word_count Number of words: 12, 18, or 24. + * @param index Child index (0-based). + * @param mnemonic Output buffer (must be at least 241 bytes). + * @param mnemonic_len Size of the output buffer. + * @return true on success, false on error. + */ +bool bip85_derive_mnemonic(uint32_t word_count, uint32_t index, char *mnemonic, + size_t mnemonic_len); + +#endif diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index 6245adeee..0ce246965 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -156,4 +156,6 @@ void fsm_msgFlashWrite(FlashWrite* msg); void fsm_msgFlashHash(FlashHash* msg); void fsm_msgSoftReset(SoftReset* msg); +void fsm_msgGetBip85Mnemonic(const GetBip85Mnemonic* msg); + #endif diff --git a/lib/board/signatures.c b/lib/board/signatures.c index f4be6bf82..9bf2107fb 100644 --- a/lib/board/signatures.c +++ b/lib/board/signatures.c @@ -20,7 +20,9 @@ #include "trezor/crypto/sha2.h" #include "trezor/crypto/ecdsa.h" #include "trezor/crypto/secp256k1.h" +#include "trezor/crypto/memzero.h" #include "keepkey/board/memory.h" +#include "keepkey/board/memcmp_s.h" #include "keepkey/board/signatures.h" #include "keepkey/board/pubkeys.h" @@ -68,23 +70,51 @@ int signatures_ok(void) { return KEY_EXPIRED; } /* Expired signing key */ + /* F3 hardening: double-compute SHA-256, compare in constant time */ + uint8_t firmware_fingerprint2[32]; sha256_Raw((uint8_t*)FLASH_APP_START, codelen, firmware_fingerprint); + asm volatile("" ::: "memory"); + sha256_Raw((uint8_t*)FLASH_APP_START, codelen, firmware_fingerprint2); - if (ecdsa_verify_digest(&secp256k1, pubkey[sigindex1 - 1], - (uint8_t*)FLASH_META_SIG1, - firmware_fingerprint) != 0) { /* Failure */ + if (memcmp_s(firmware_fingerprint, firmware_fingerprint2, 32) != 0) { + memzero(firmware_fingerprint, sizeof(firmware_fingerprint)); + memzero(firmware_fingerprint2, sizeof(firmware_fingerprint2)); return SIG_FAIL; } + memzero(firmware_fingerprint2, sizeof(firmware_fingerprint2)); - if (ecdsa_verify_digest(&secp256k1, pubkey[sigindex2 - 1], - (uint8_t*)FLASH_META_SIG2, - firmware_fingerprint) != 0) { /* Failure */ + /* F3 hardening: infective aggregation — accumulate all three ECDSA + * results instead of early-returning on each. Forces attacker to + * corrupt all three verify calls, not just skip one branch. */ + volatile int verify_acc = 0; + volatile int verify_sentinel = 0; + + verify_acc |= + ecdsa_verify_digest(&secp256k1, pubkey[sigindex1 - 1], + (uint8_t*)FLASH_META_SIG1, firmware_fingerprint); + verify_sentinel++; + asm volatile("" ::: "memory"); + + verify_acc |= + ecdsa_verify_digest(&secp256k1, pubkey[sigindex2 - 1], + (uint8_t*)FLASH_META_SIG2, firmware_fingerprint); + verify_sentinel++; + asm volatile("" ::: "memory"); + + verify_acc |= + ecdsa_verify_digest(&secp256k1, pubkey[sigindex3 - 1], + (uint8_t*)FLASH_META_SIG3, firmware_fingerprint); + verify_sentinel++; + asm volatile("" ::: "memory"); + + memzero(firmware_fingerprint, sizeof(firmware_fingerprint)); + + /* All three verifies must have executed and all must have passed */ + if (verify_sentinel != 3) { return SIG_FAIL; } - if (ecdsa_verify_digest(&secp256k1, pubkey[sigindex3 - 1], - (uint8_t*)FLASH_META_SIG3, - firmware_fingerprint) != 0) { /* Failure */ + if (verify_acc != 0) { return SIG_FAIL; } diff --git a/lib/firmware/CMakeLists.txt b/lib/firmware/CMakeLists.txt index 1d034be87..c819e805a 100644 --- a/lib/firmware/CMakeLists.txt +++ b/lib/firmware/CMakeLists.txt @@ -2,6 +2,7 @@ set(sources app_confirm.c app_layout.c authenticator.c + bip85.c binance.c coins.c crypto.c diff --git a/lib/firmware/app_layout.c b/lib/firmware/app_layout.c index 022adbc4d..85d33a26c 100644 --- a/lib/firmware/app_layout.c +++ b/lib/firmware/app_layout.c @@ -766,7 +766,8 @@ void layout_pin(const char* str, char pin[]) { * OUTPUT * none */ -void layout_cipher(const char* current_word, const char* cipher) { +void layout_cipher(const char* current_word, const char* cipher, + const char* prev_word_info) { DrawableParams sp; const Font* title_font = get_body_font(); Canvas* canvas = layout_get_canvas(); @@ -774,8 +775,18 @@ void layout_cipher(const char* current_word, const char* cipher) { call_leaving_handler(); layout_clear(); - /* Draw prompt */ - sp.y = 11; + /* Draw previous word info at top-left -- must be x < 76 to avoid + * being wiped by cipher animation which clears x >= CIPHER_START_X */ + if (prev_word_info && prev_word_info[0]) { + sp.y = 2; + sp.x = 4; + sp.color = CIPHER_FONT_COLOR; /* gray -- less prominent than current word */ + draw_string(canvas, title_font, prev_word_info, &sp, 68, + font_height(title_font)); + } + + /* Draw prompt -- push down when prev word is shown */ + sp.y = (prev_word_info && prev_word_info[0]) ? 14 : 11; sp.x = 4; sp.color = BODY_COLOR; draw_string(canvas, title_font, "Recovery Cipher:", &sp, 58, diff --git a/lib/firmware/bip85.c b/lib/firmware/bip85.c new file mode 100644 index 000000000..7d2ef6c81 --- /dev/null +++ b/lib/firmware/bip85.c @@ -0,0 +1,105 @@ +#include "keepkey/firmware/bip85.h" +#include "keepkey/firmware/storage.h" +#include "trezor/crypto/bip32.h" +#include "trezor/crypto/bip39.h" +#include "trezor/crypto/curves.h" +#include "trezor/crypto/hmac.h" +#include "trezor/crypto/memzero.h" + +#include + +/* + * BIP-85: Deterministic Entropy From BIP32 Keychains + * + * For BIP-39 mnemonic derivation: + * path = m / 83696968' / 39' / 0' / ' / ' + * k = derived_node.private_key (32 bytes) + * hmac = HMAC-SHA512(key="bip-entropy-from-k", msg=k) + * entropy = hmac[0 : entropy_bytes] + * 12 words -> 16 bytes, 18 words -> 24 bytes, 24 words -> 32 bytes + * mnemonic = bip39_from_entropy(entropy) + */ + +/* BIP-85 application number for deriving entropy from a key */ +static const uint8_t BIP85_HMAC_KEY[] = "bip-entropy-from-k"; +#define BIP85_HMAC_KEY_LEN 18 + +bool bip85_derive_mnemonic(uint32_t word_count, uint32_t index, char *mnemonic, + size_t mnemonic_len) { + /* Reject index >= 0x80000000 to avoid hardened-bit collision */ + if (index & 0x80000000) { + return false; + } + + /* Validate word count and compute entropy length */ + int entropy_bytes; + switch (word_count) { + case 12: + entropy_bytes = 16; + break; + case 18: + entropy_bytes = 24; + break; + case 24: + entropy_bytes = 32; + break; + default: + return false; + } + + /* BIP-85 derivation path: m/83696968'/39'/0'/'/' */ + uint32_t address_n[5]; + address_n[0] = 0x80000000 | 83696968; /* purpose (hardened) */ + address_n[1] = 0x80000000 | 39; /* BIP-39 app (hardened) */ + address_n[2] = 0x80000000; /* English language 0 (hardened) */ + address_n[3] = 0x80000000 | word_count; /* word count (hardened) */ + address_n[4] = 0x80000000 | index; /* child index (hardened) */ + + /* Get the master node from storage (respects passphrase) */ + static CONFIDENTIAL HDNode node; + if (!storage_getRootNode(SECP256K1_NAME, true, &node)) { + memzero(&node, sizeof(node)); + return false; + } + + /* Derive to the BIP-85 path */ + for (int i = 0; i < 5; i++) { + if (hdnode_private_ckd(&node, address_n[i]) == 0) { + memzero(&node, sizeof(node)); + return false; + } + } + + /* HMAC-SHA512(key="bip-entropy-from-k", msg=private_key) */ + static CONFIDENTIAL uint8_t hmac_out[64]; + hmac_sha512(BIP85_HMAC_KEY, BIP85_HMAC_KEY_LEN, node.private_key, 32, + hmac_out); + + /* We no longer need the derived node */ + memzero(&node, sizeof(node)); + + /* Truncate HMAC output to the required entropy length */ + static CONFIDENTIAL uint8_t entropy[32]; + memcpy(entropy, hmac_out, entropy_bytes); + memzero(hmac_out, sizeof(hmac_out)); + + /* Convert entropy to BIP-39 mnemonic */ + const char *words = mnemonic_from_data(entropy, entropy_bytes); + memzero(entropy, sizeof(entropy)); + + if (!words) { + return false; + } + + /* Copy to output buffer */ + size_t words_len = strlen(words); + if (words_len >= mnemonic_len) { + mnemonic_clear(); + return false; + } + + memcpy(mnemonic, words, words_len + 1); + mnemonic_clear(); + + return true; +} diff --git a/lib/firmware/fsm.c b/lib/firmware/fsm.c index ac007845f..3259c7ad6 100644 --- a/lib/firmware/fsm.c +++ b/lib/firmware/fsm.c @@ -35,6 +35,7 @@ #include "keepkey/firmware/app_confirm.h" #include "keepkey/firmware/app_layout.h" #include "keepkey/firmware/authenticator.h" +#include "keepkey/firmware/bip85.h" #include "keepkey/firmware/coins.h" #include "keepkey/firmware/cosmos.h" #include "keepkey/firmware/binance.h" @@ -301,3 +302,4 @@ void fsm_msgClearSession(ClearSession* msg) { #include "fsm_msg_solana.h" #include "fsm_msg_hive.h" #include "fsm_msg_zcash.h" +#include "fsm_msg_bip85.h" diff --git a/lib/firmware/fsm_msg_bip85.h b/lib/firmware/fsm_msg_bip85.h new file mode 100644 index 000000000..420d4ba55 --- /dev/null +++ b/lib/firmware/fsm_msg_bip85.h @@ -0,0 +1,143 @@ +void fsm_msgGetBip85Mnemonic(const GetBip85Mnemonic *msg) { + CHECK_INITIALIZED + + /* Validate word count (required field, always present in nanopb) */ + if (msg->word_count != 12 && msg->word_count != 18 && msg->word_count != 24) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + "word_count must be 12, 18, or 24"); + layoutHome(); + return; + } + + /* Reject index >= 0x80000000 (hardened-bit collision) */ + if (msg->index & 0x80000000) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + "index must be less than 2147483648"); + layoutHome(); + return; + } + + CHECK_PIN + + /* User confirmation */ + char desc[80]; + snprintf(desc, sizeof(desc), "Derive %lu-word child seed at index %lu?", + (unsigned long)msg->word_count, (unsigned long)msg->index); + + if (!confirm(ButtonRequestType_ButtonRequest_Other, "BIP-85 Derive Seed", + "%s", desc)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "BIP-85 derivation cancelled"); + layoutHome(); + return; + } + + layout_simple_message("Deriving child seed..."); + + /* Derive the mnemonic */ + static CONFIDENTIAL char mnemonic_buf[241]; + if (!bip85_derive_mnemonic(msg->word_count, msg->index, mnemonic_buf, + sizeof(mnemonic_buf))) { + memzero(mnemonic_buf, sizeof(mnemonic_buf)); + fsm_sendFailure(FailureType_Failure_Other, "BIP-85 derivation failed"); + layoutHome(); + return; + } + + /* + * Display mnemonic on device screen only — never send over USB. + * Uses the same paginated display as the backup flow in reset.c. + */ + uint32_t word_count = 0, page_count = 0; + static char CONFIDENTIAL tokened_mnemonic[TOKENED_MNEMONIC_BUF]; + static char CONFIDENTIAL + formatted_mnemonic[MAX_PAGES][FORMATTED_MNEMONIC_BUF]; + static char CONFIDENTIAL mnemonic_display[FORMATTED_MNEMONIC_BUF]; + static char CONFIDENTIAL formatted_word[MAX_WORD_LEN + ADDITIONAL_WORD_PAD]; + + strlcpy(tokened_mnemonic, mnemonic_buf, TOKENED_MNEMONIC_BUF); + memzero(mnemonic_buf, sizeof(mnemonic_buf)); + + /* Clear formatting buffers */ + memzero(formatted_mnemonic, sizeof(formatted_mnemonic)); + memzero(mnemonic_display, sizeof(mnemonic_display)); + + const char *tok = strtok(tokened_mnemonic, " "); + + while (tok) { + snprintf(formatted_word, MAX_WORD_LEN + ADDITIONAL_WORD_PAD, + (word_count & 1) ? "%lu.%s\n" : "%lu.%s", + (unsigned long)(word_count + 1), tok); + + /* Check that we have enough room on display to show word */ + snprintf(mnemonic_display, FORMATTED_MNEMONIC_BUF, "%s %s", + formatted_mnemonic[page_count], formatted_word); + + if (calc_str_line(get_body_font(), mnemonic_display, BODY_WIDTH) > 3) { + page_count++; + + if (MAX_PAGES <= page_count) { + memzero(tokened_mnemonic, sizeof(tokened_mnemonic)); + memzero(formatted_mnemonic, sizeof(formatted_mnemonic)); + memzero(mnemonic_display, sizeof(mnemonic_display)); + memzero(formatted_word, sizeof(formatted_word)); + fsm_sendFailure(FailureType_Failure_Other, + "Too many pages of mnemonic words"); + layoutHome(); + return; + } + + snprintf(mnemonic_display, FORMATTED_MNEMONIC_BUF, "%s %s", + formatted_mnemonic[page_count], formatted_word); + } + + strlcpy(formatted_mnemonic[page_count], mnemonic_display, + FORMATTED_MNEMONIC_BUF); + + tok = strtok(NULL, " "); + word_count++; + } + + /* Switch from 0-indexing to 1-indexing */ + page_count++; + + display_constant_power(true); + + /* Show each page of the mnemonic on screen */ + for (uint32_t current_page = 0; current_page < page_count; current_page++) { + char title[MEDIUM_STR_BUF]; + + if (page_count > 1) { + snprintf(title, MEDIUM_STR_BUF, "BIP-85 Seed %" PRIu32 "/%" PRIu32, + current_page + 1, page_count); + } else { + snprintf(title, MEDIUM_STR_BUF, "BIP-85 Seed"); + } + + if (!confirm_constant_power(ButtonRequestType_ButtonRequest_ConfirmWord, + title, "%s", + formatted_mnemonic[current_page])) { + memzero(tokened_mnemonic, sizeof(tokened_mnemonic)); + memzero(formatted_mnemonic, sizeof(formatted_mnemonic)); + memzero(mnemonic_display, sizeof(mnemonic_display)); + memzero(formatted_word, sizeof(formatted_word)); + display_constant_power(false); + fsm_sendFailure(FailureType_Failure_ActionCancelled, + "BIP-85 display cancelled"); + layoutHome(); + return; + } + } + + display_constant_power(false); + + /* Wipe all sensitive buffers */ + memzero(tokened_mnemonic, sizeof(tokened_mnemonic)); + memzero(formatted_mnemonic, sizeof(formatted_mnemonic)); + memzero(mnemonic_display, sizeof(mnemonic_display)); + memzero(formatted_word, sizeof(formatted_word)); + + /* Send success — mnemonic is NOT sent over the wire */ + fsm_sendSuccess("BIP-85 seed displayed on device"); + layoutHome(); +} diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index b6328a5e6..277f5cca4 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -73,6 +73,9 @@ MSG_IN(MessageType_MessageType_MayachainSignTx, MayachainSignTx, fsm_msgMayachainSignTx) MSG_IN(MessageType_MessageType_MayachainMsgAck, MayachainMsgAck, fsm_msgMayachainMsgAck) + MSG_IN(MessageType_MessageType_GetBip85Mnemonic, GetBip85Mnemonic, fsm_msgGetBip85Mnemonic) + MSG_OUT(MessageType_MessageType_Bip85Mnemonic, Bip85Mnemonic, NO_PROCESS_FUNC) + /* Normal Out Messages */ MSG_OUT(MessageType_MessageType_Success, Success, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_Failure, Failure, NO_PROCESS_FUNC) diff --git a/lib/firmware/recovery_cipher.c b/lib/firmware/recovery_cipher.c index 0db3d1616..1eee063b6 100644 --- a/lib/firmware/recovery_cipher.c +++ b/lib/firmware/recovery_cipher.c @@ -371,13 +371,29 @@ void next_character(void) { } #endif + /* Save word so we can show it as "prev" on next word. + * When auto-complete fires, current_word holds the full expanded word + * (e.g. "alcohol" not "alc"). Otherwise save the typed prefix. */ + static char CONFIDENTIAL last_completed_word[12]; + if (strlen(current_word) > 0) { + strlcpy(last_completed_word, current_word, sizeof(last_completed_word)); + } + /* Format current word and display it along with cipher */ static char CONFIDENTIAL formatted_word[CURRENT_WORD_BUF + 10]; format_current_word(word_pos, current_word, auto_completed, &formatted_word); memzero(current_word, sizeof(current_word)); + /* Format previous word indicator (e.g. "(1.alcohol)" when entering word 2) */ + static char prev_info[32]; + prev_info[0] = '\0'; + if (word_pos > 0 && last_completed_word[0]) { + snprintf(prev_info, sizeof(prev_info), "(%" PRIu32 ".%s)", word_pos, + last_completed_word); + } + /* Show cipher and partial word */ - layout_cipher(formatted_word, cipher); + layout_cipher(formatted_word, cipher, prev_info); memzero(formatted_word, sizeof(formatted_word)); } @@ -462,6 +478,24 @@ void recovery_character(const char* character) { } } } else { + /* Per-word BIP39 validation: reject immediately if the decoded word + * doesn't match any entry in the wordlist. */ + if (enforce_wordlist && strlen(decoded_word) > 0) { + static CONFIDENTIAL char check_word[CURRENT_WORD_BUF]; + strlcpy(check_word, decoded_word, sizeof(check_word)); + bool valid = attempt_auto_complete(check_word); + memzero(check_word, sizeof(check_word)); + if (!valid) { + memzero(coded_word, sizeof(coded_word)); + memzero(decoded_word, sizeof(decoded_word)); + recovery_cipher_abort(); + fsm_sendFailure(FailureType_Failure_SyntaxError, + "Word not found in BIP39 wordlist"); + layout_warning_static("Word not in wordlist"); + return; + } + } + memzero(coded_word, sizeof(coded_word)); memzero(decoded_word, sizeof(decoded_word)); @@ -575,7 +609,7 @@ void recovery_cipher_finalize(void) { } memzero(temp_word, sizeof(temp_word)); - if (!auto_completed && !enforce_wordlist) { + if (!auto_completed && enforce_wordlist) { if (!dry_run) { storage_reset(); } diff --git a/tools/firmware/keepkey_main.c b/tools/firmware/keepkey_main.c index fcf4aa9cb..6954f3cf2 100644 --- a/tools/firmware/keepkey_main.c +++ b/tools/firmware/keepkey_main.c @@ -73,16 +73,15 @@ void memory_getDeviceLabel(char *str, size_t len) { bool inPrivilegedMode(void) { // Check to see if we are in priv mode. If so, return true to drop privs. uint32_t creg = 0xffff; - // CONTROL register nPRIV,bit[0]: + // CONTROL register nPRIV,bit[0]: // 0 Thread mode has privileged access - // 1 Thread mode has unprivileged access. + // 1 Thread mode has unprivileged access. // Note: In Handler mode, execution is always privileged fi_defense_delay(creg); // vary access time - __asm__ volatile( - "mrs %0, control" : "=r" (creg)); + __asm__ volatile("mrs %0, control" : "=r"(creg)); fi_defense_delay(creg); // vary test time - if (creg & 0x0001) - return false; // can't drop privs + if (creg & 0x0001) + return false; // can't drop privs else return true; @@ -173,9 +172,23 @@ int main(void) { _timerusr_isr = (void *)&timerisr_usr; _mmhusr_isr = (void *)&mmhisr; - { // limit sigRet lifetime to this block + { // limit sigRet lifetime to this block + /* F5 hardening: replace full signatures_ok() (~1 sec crypto) with fast + * metadata presence check. The bootloader has already performed the + * authoritative signature verification before jumping here. We only + * need to know whether the bootloader considered us signed, which is + * indicated by valid signature indices in flash metadata. */ int sigRet = SIG_FAIL; - sigRet = signatures_ok(); + + volatile uint8_t si1 = *((volatile uint8_t *)FLASH_META_SIGINDEX1); + volatile uint8_t si2 = *((volatile uint8_t *)FLASH_META_SIGINDEX2); + volatile uint8_t si3 = *((volatile uint8_t *)FLASH_META_SIGINDEX3); + + if (si1 >= 1 && si1 <= PUBKEYS && si2 >= 1 && si2 <= PUBKEYS && si3 >= 1 && + si3 <= PUBKEYS && si1 != si2 && si1 != si3 && si2 != si3) { + sigRet = SIG_OK; + } + flash_collectHWEntropy(SIG_OK == sigRet); /* Drop privileges */ @@ -194,8 +207,9 @@ int main(void) { drbg_init(); - /* Bootloader Verification. Only check if valid signed firmware on-board, allow any other firmware - to run with any bootloader. This allows unsigned release firmware to run after warning */ + /* Bootloader Verification. Only check if valid signed firmware on-board, + allow any other firmware to run with any bootloader. This allows unsigned + release firmware to run after warning */ if (SIG_OK == sigRet) { check_bootloader(); }