diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95f04a30c..97e396b66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -777,7 +777,7 @@ jobs: if: always() && env.DYLIB_PATH != '' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: libkkemu-${{ github.sha }} + name: libkkemu-${{ github.event.pull_request.head.sha || github.sha }} path: ${{ env.DYLIB_PATH }} retention-days: 30 if-no-files-found: error diff --git a/deps/crypto/CMakeLists.txt b/deps/crypto/CMakeLists.txt index 758a019fd..4a1d9d880 100644 --- a/deps/crypto/CMakeLists.txt +++ b/deps/crypto/CMakeLists.txt @@ -32,6 +32,7 @@ set(sources #trezor-firmware/crypto/tests/test_openssl.c #trezor-firmware/crypto/tests/test_speed.c trezor-firmware/crypto/secp256k1.c + trezor-firmware/crypto/bip340.c trezor-firmware/crypto/bignum.c trezor-firmware/crypto/segwit_addr.c trezor-firmware/crypto/ripemd160.c diff --git a/deps/crypto/trezor-firmware b/deps/crypto/trezor-firmware index 62230ee62..9c882485e 160000 --- a/deps/crypto/trezor-firmware +++ b/deps/crypto/trezor-firmware @@ -1 +1 @@ -Subproject commit 62230ee620fa9ea2bdd2c8067be7fde3f9362248 +Subproject commit 9c882485e6bed1dae7538ec4d43a4f9a12c428aa diff --git a/deps/device-protocol b/deps/device-protocol index f2246cebe..674777f6d 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit f2246cebea8f96fcd7ec2883588a784a60b430ae +Subproject commit 674777f6d4dd16e2b8c4c2df10608976375ee879 diff --git a/deps/python-keepkey b/deps/python-keepkey index d88a073a5..84b4a2503 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit d88a073a5af2d83e0f1f19574665ca1a44789414 +Subproject commit 84b4a25030763bbe8f84175b12e7a8a245bbb6d2 diff --git a/include/keepkey/firmware/transaction.h b/include/keepkey/firmware/transaction.h index fd699554c..5b3525a17 100644 --- a/include/keepkey/firmware/transaction.h +++ b/include/keepkey/firmware/transaction.h @@ -78,6 +78,15 @@ uint32_t serialize_script_multisig(const CoinType* coin, int compile_output(const CoinType* coin, const HDNode* root, TxOutputType* in, TxOutputBinType* out, bool needs_confirm); +bool address_to_script_pubkey(const CoinType* coin, const char* address, + uint8_t* script_pubkey, size_t* script_pubkey_len, + size_t script_pubkey_size); + +bool fill_input_script_pubkey(const CoinType* coin, const HDNode* root, + const TxInputType* in, uint8_t* script_pubkey, + size_t* script_pubkey_len, + size_t script_pubkey_size); + uint32_t tx_prevout_hash(Hasher* hasher, const TxInputType* input); uint32_t tx_script_hash(Hasher* hasher, uint32_t size, const uint8_t* data); uint32_t tx_sequence_hash(Hasher* hasher, const TxInputType* input); diff --git a/include/keepkey/transport/messages-solana.options b/include/keepkey/transport/messages-solana.options index 4b08e771f..0886fb61b 100644 --- a/include/keepkey/transport/messages-solana.options +++ b/include/keepkey/transport/messages-solana.options @@ -13,6 +13,8 @@ SolanaSignTx.raw_tx max_size:2048 SolanaSignTx.token_info max_count:4 SolanaSignTx.schema_payload max_size:256 SolanaSignTx.schema_signature max_size:64 +SolanaSignTx.token_recipient_owner max_count:4 +SolanaSignTx.token_recipient_owner max_size:32 SolanaSignedTx.signature max_size:64 diff --git a/lib/firmware/app_layout.c b/lib/firmware/app_layout.c index 2499916be..82bff62b3 100644 --- a/lib/firmware/app_layout.c +++ b/lib/firmware/app_layout.c @@ -703,8 +703,35 @@ void layout_address_notification(const char* desc, const char* address, sp.y += font_height(address_font) + ADDRESS_TOP_MARGIN; sp.x = LEFT_MARGIN; sp.color = BODY_COLOR; + + /* Bech32 addresses longer than one line (p2wsh and p2tr are both 62 chars) + did not fit: draw_string() stops at the bottom of the canvas and drops the + remainder SILENTLY, so the user verified a prefix while the QR beside it + encoded the whole address. + Close the padding between lines rather than moving the block up -- the QR + is drawn last and would overwrite the start of a raised first line. */ + uint16_t address_line_height = + font_height(address_font) + BODY_FONT_LINE_PADDING; + { + const uint32_t lines = + calc_str_line(address_font, address, TRANSACTION_WIDTH); + if (lines > ONE_LINE) { + /* Close the inter-line padding first: raising the block is what collides + with the QR, which is drawn afterwards and would overwrite the start of + the first line. */ + address_line_height = font_height(address_font); + const uint16_t bottom = + sp.y + (lines - 1) * address_line_height + font_height(address_font); + if (bottom > KEEPKEY_DISPLAY_HEIGHT) { + /* Still short: raise by the minimum that fits, no more. */ + const uint16_t overflow = bottom - KEEPKEY_DISPLAY_HEIGHT; + sp.y = (sp.y > overflow) ? sp.y - overflow : 0; + } + } + } + draw_string(canvas, address_font, address, &sp, TRANSACTION_WIDTH, - font_height(address_font) + BODY_FONT_LINE_PADDING); + address_line_height); /* Draw description */ if (strcmp(desc, "") != 0) { diff --git a/lib/firmware/coins.c b/lib/firmware/coins.c index a231bc41a..964b9cf52 100644 --- a/lib/firmware/coins.c +++ b/lib/firmware/coins.c @@ -228,6 +228,22 @@ static bool path_mismatched(const CoinType* coin, const uint32_t* address_n, return mismatch; } + // m/86' : BIP86 Taproot + // m / purpose' / bip44_account_path' / account' / change / address_index + if (address_n[0] == (0x80000000 + 86)) { + mismatch |= !coin->has_segwit || !coin->segwit; + mismatch |= !coin->has_bech32_prefix; + mismatch |= !coin->has_taproot || !coin->taproot; + mismatch |= (address_n_count != (whole_account ? 3 : 5)); + mismatch |= (address_n[1] != coin->bip44_account_path); + mismatch |= (address_n[2] & 0x80000000) == 0; + if (!whole_account) { + mismatch |= (address_n[3] & 0x80000000) == 0x80000000; + mismatch |= (address_n[4] & 0x80000000) == 0x80000000; + } + return mismatch; + } + return false; } diff --git a/lib/firmware/fsm_msg_coin.h b/lib/firmware/fsm_msg_coin.h index 978c80712..d1b002a1d 100644 --- a/lib/firmware/fsm_msg_coin.h +++ b/lib/firmware/fsm_msg_coin.h @@ -188,6 +188,21 @@ static bool path_mismatched(const CoinType* coin, const GetAddress* msg) { return mismatch; } + // m/86' : BIP86 Taproot + // m / purpose' / bip44_account_path' / account' / change / address_index + if (msg->address_n[0] == (0x80000000 + 86)) { + mismatch |= (msg->script_type != InputScriptType_SPENDTAPROOT); + mismatch |= !coin->has_segwit || !coin->segwit; + mismatch |= !coin->has_bech32_prefix; + mismatch |= !coin->has_taproot || !coin->taproot; + mismatch |= (msg->address_n_count != 5); + mismatch |= (msg->address_n[1] != coin->bip44_account_path); + mismatch |= (msg->address_n[2] & 0x80000000) == 0; + mismatch |= (msg->address_n[3] & 0x80000000) == 0x80000000; + mismatch |= (msg->address_n[4] & 0x80000000) == 0x80000000; + return mismatch; + } + return false; } diff --git a/lib/firmware/fsm_msg_common.h b/lib/firmware/fsm_msg_common.h index 1547486a9..82c9ec4d4 100644 --- a/lib/firmware/fsm_msg_common.h +++ b/lib/firmware/fsm_msg_common.h @@ -41,6 +41,12 @@ void fsm_msgGetFeatures(GetFeatures* msg) { resp->has_model = true; strlcpy(resp->model, model(), sizeof(resp->model)); + /* Taproot capability. Reported directly so a host does not have to infer + P2TR support from a firmware version -- that inference breaks whenever the + feature is retargeted to a different release. */ + resp->has_supports_taproot = true; + resp->supports_taproot = true; + /* Variant Name */ resp->has_firmware_variant = true; #if BITCOIN_ONLY diff --git a/lib/firmware/signing.c b/lib/firmware/signing.c index a7b367e0f..d0b11c5fb 100644 --- a/lib/firmware/signing.c +++ b/lib/firmware/signing.c @@ -26,13 +26,13 @@ #include "keepkey/firmware/app_confirm.h" #include "keepkey/firmware/coins.h" #include "keepkey/firmware/crypto.h" -#include "keepkey/firmware/crypto.h" #include "keepkey/firmware/fsm.h" #include "keepkey/firmware/home_sm.h" #include "keepkey/firmware/policy.h" #include "keepkey/firmware/signing.h" #include "keepkey/firmware/txin_check.h" #include "keepkey/firmware/transaction.h" +#include "trezor/crypto/bip340.h" #include "trezor/crypto/ecdsa.h" #include "trezor/crypto/memzero.h" #include "trezor/crypto/secp256k1.h" @@ -82,12 +82,33 @@ static TxInputType input; static TxOutputBinType bin_output; static TxStruct to, tp, ti; static Hasher hasher_prevouts, hasher_sequence, hasher_outputs, hasher_check; +/* BIP-341 commits to every input's amount and scriptPubKey, which BIP-143 + does not, so taproot needs two accumulators segwit never required. These + are SHA256_CTX rather than Hasher because BIP-341 fixes them to plain + SHA256: a Hasher carries a union sized by GROESTL512_CTX and would cost + ~1.2 KB of SRAM here for no benefit. */ +static SHA256_CTX ctx_amounts, ctx_scriptpubkeys; +/* BIP-143 hashes prevouts/sequences/outputs with DOUBLE sha256 (hasher_sign is + HASHER_SHA2D for Bitcoin); BIP-341 specifies SINGLE sha256. The BIP-143 + accumulators therefore cannot be reused -- doing so yields a valid signature + over the wrong commitment. Hence a parallel set. */ +static SHA256_CTX ctx_prevouts_tr, ctx_sequences_tr, ctx_outputs_tr; static uint8_t CONFIDENTIAL privkey[32]; static uint8_t pubkey[33], sig[64]; static uint8_t hash_prevouts[32], hash_sequence[32], hash_outputs[32]; +static uint8_t hash_amounts[32], hash_scriptpubkeys[32]; +static uint8_t hash_prevouts_tr[32], hash_sequences_tr[32], hash_outputs_tr[32]; static uint8_t hash_prefix[32]; static uint8_t hash_check[32]; static uint64_t to_spend, authorized_bip143_in, spending, change_spend; +static bool has_taproot_input, missing_bip341_input_amount; +/* + * Taproot signatures must never be reachable before phase 1 has completed + * the physical transaction-summary confirmation. Keep this as independent + * state instead of inferring it from signing_stage so a malformed or + * corrupted stage transition fails closed at the Schnorr signing boundary. + */ +static bool taproot_transaction_confirmed; static uint32_t version = 1; static uint32_t lock_time = 0; static uint32_t expiry = 0; @@ -113,6 +134,10 @@ static uint32_t tx_weight; use and still allow to quickly brute-force the correct bip32 path. */ #define BIP32_MAX_LAST_ELEMENT 1000000 +/* BIP-341 SIGHASH_DEFAULT: commits to all outputs, and unlike SIGHASH_ALL + its byte is omitted from the witness entirely. */ +#define SIGHASH_DEFAULT_TAPROOT 0 + /* transaction header size: 4 byte version */ #define TXSIZE_HEADER 4 /* transaction footer size: 4 byte lock time */ @@ -408,6 +433,18 @@ void phase1_request_next_input(void) { // compute segwit hashPrevouts & hashSequence hasher_Final(&hasher_prevouts, hash_prevouts); hasher_Final(&hasher_sequence, hash_sequence); + if (has_taproot_input && missing_bip341_input_amount) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Taproot transaction input without amount")); + signing_abort(); + return; + } + if (coin->has_taproot && coin->taproot) { + sha256_Final(&ctx_amounts, hash_amounts); + sha256_Final(&ctx_scriptpubkeys, hash_scriptpubkeys); + sha256_Final(&ctx_prevouts_tr, hash_prevouts_tr); + sha256_Final(&ctx_sequences_tr, hash_sequences_tr); + } hasher_Final(&hasher_check, hash_check); // init hashOutputs hasher_Reset(&hasher_outputs); @@ -564,7 +601,12 @@ bool check_change_bip32_path(const TxOutputType* toutput) { toutput->address_n[count - 1] <= BIP32_MAX_LAST_ELEMENT); } -bool compile_input_script_sig(TxInputType* tinput) { +/* Re-validates the input against what phase 1 saw, then derives its node into + `node`. Split out of compile_input_script_sig() so taproot can reuse the + checks and the derivation without building a scriptSig -- a taproot input + has an empty one, and skipping this would also skip the guard that the + host has not swapped address_n between phases. */ +static bool prepare_input_node(TxInputType* tinput) { if (!multisig_fp_mismatch) { // check that this is still multisig uint8_t h[32]; @@ -594,6 +636,13 @@ bool compile_input_script_sig(TxInputType* tinput) { return false; } hdnode_fill_public_key(&node); + return true; +} + +bool compile_input_script_sig(TxInputType* tinput) { + if (!prepare_input_node(tinput)) { + return false; + } if (tinput->has_multisig) { tinput->script_sig.size = compile_script_multisig(coin, &(tinput->multisig), tinput->script_sig.bytes); @@ -646,6 +695,9 @@ void signing_init(const SignTx* msg, const CoinType* _coin, spending = 0; change_spend = 0; authorized_bip143_in = 0; + has_taproot_input = false; + missing_bip341_input_amount = false; + taproot_transaction_confirmed = false; memset(&input, 0, sizeof(TxInputType)); memset(&resp, 0, sizeof(TxRequest)); @@ -690,6 +742,15 @@ void signing_init(const SignTx* msg, const CoinType* _coin, hasher_Init(&hasher_sequence, curve->hasher_sign); hasher_Init(&hasher_outputs, curve->hasher_sign); hasher_Init(&hasher_check, curve->hasher_sign); + if (coin->has_taproot && coin->taproot) { + /* BIP-341 fixes these as plain SHA256, independent of the coin's + signing hasher, so they are initialised separately on purpose. */ + sha256_Init(&ctx_amounts); + sha256_Init(&ctx_scriptpubkeys); + sha256_Init(&ctx_prevouts_tr); + sha256_Init(&ctx_sequences_tr); + sha256_Init(&ctx_outputs_tr); + } } layoutProgressSwipe(_("Signing transaction"), 0); @@ -719,7 +780,8 @@ static bool is_internal_input_script_type(const TxInputType* txinput) { if (txinput->script_type == InputScriptType_SPENDADDRESS || txinput->script_type == InputScriptType_SPENDMULTISIG || txinput->script_type == InputScriptType_SPENDP2SHWITNESS || - txinput->script_type == InputScriptType_SPENDWITNESS) { + txinput->script_type == InputScriptType_SPENDWITNESS || + txinput->script_type == InputScriptType_SPENDTAPROOT) { return true; } return false; @@ -729,7 +791,8 @@ static bool is_change_output_script_type(const TxOutputType* txoutput) { if (txoutput->script_type == OutputScriptType_PAYTOADDRESS || txoutput->script_type == OutputScriptType_PAYTOMULTISIG || txoutput->script_type == OutputScriptType_PAYTOP2SHWITNESS || - txoutput->script_type == OutputScriptType_PAYTOWITNESS) { + txoutput->script_type == OutputScriptType_PAYTOWITNESS || + txoutput->script_type == OutputScriptType_PAYTOTAPROOT) { return true; } return false; @@ -737,7 +800,8 @@ static bool is_change_output_script_type(const TxOutputType* txoutput) { static bool is_segwit_input_script_type(const TxInputType* txinput) { if (txinput->script_type == InputScriptType_SPENDP2SHWITNESS || - txinput->script_type == InputScriptType_SPENDWITNESS) { + txinput->script_type == InputScriptType_SPENDWITNESS || + txinput->script_type == InputScriptType_SPENDTAPROOT) { return true; } return false; @@ -779,6 +843,13 @@ static bool signing_validate_input(const TxInputType* txinput) { } } + if (txinput->script_type == InputScriptType_SPENDTAPROOT && + (!coin->has_taproot || !coin->taproot)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Taproot not enabled on this coin.")); + signing_abort(); + return false; + } if (is_segwit_input_script_type(txinput)) { if (!coin->has_segwit) { fsm_sendFailure(FailureType_Failure_Other, @@ -813,6 +884,16 @@ static bool signing_validate_output(const TxOutputType* txoutput) { return false; } + // has_taproot is the nanopb presence flag, not the value. Every coin in + // coins.def sets it, so both fields must be checked. + if (txoutput->script_type == OutputScriptType_PAYTOTAPROOT && + (!coin->has_taproot || !coin->taproot)) { + fsm_sendFailure(FailureType_Failure_Other, + _("Taproot not enabled on this coin.")); + signing_abort(); + return false; + } + if (txoutput->script_type == OutputScriptType_PAYTOOPRETURN) { if (txoutput->has_address || (txoutput->address_n_count > 0) || txoutput->has_multisig) { @@ -822,14 +903,6 @@ static bool signing_validate_output(const TxOutputType* txoutput) { return false; } - if (txoutput->script_type == OutputScriptType_PAYTOTAPROOT && - !coin->has_taproot) { - fsm_sendFailure(FailureType_Failure_Other, - _("Taproot not enabled on this coin.")); - signing_abort(); - return false; - } - if (txoutput->amount != 0) { fsm_sendFailure(FailureType_Failure_Other, _("OP_RETURN output with non-zero amount")); @@ -898,6 +971,36 @@ static bool signing_check_input(TxInputType* txinput) { // compute segwit hashPrevouts & hashSequence tx_prevout_hash(&hasher_prevouts, txinput); tx_sequence_hash(&hasher_sequence, txinput); + // BIP-341 commits to the amount and scriptPubKey of EVERY input, not just + // the taproot ones, so these must be accumulated for all of them. Only + // coins with taproot enabled pay the per-input derivation, and + // hdnode_private_ckd_cached keeps repeated derivations along one account + // path cheap. + if (coin->has_taproot && coin->taproot) { + has_taproot_input |= txinput->script_type == InputScriptType_SPENDTAPROOT; + missing_bip341_input_amount |= !txinput->has_amount; + uint8_t script_pubkey[64]; + size_t script_pubkey_len = 0; + if (!fill_input_script_pubkey(coin, root, txinput, script_pubkey, + &script_pubkey_len, sizeof(script_pubkey))) { + fsm_sendFailure(FailureType_Failure_Other, + _("Failed to derive input scriptPubKey")); + signing_abort(); + return false; + } + /* outpoint: prev_hash is carried display-order and goes out reversed, + matching tx_prevout_hash() */ + for (int i = 0; i < 32; i++) { + sha256_Update(&ctx_prevouts_tr, &txinput->prev_hash.bytes[31 - i], 1); + } + sha256_Update(&ctx_prevouts_tr, (const uint8_t*)&txinput->prev_index, 4); + sha256_Update(&ctx_sequences_tr, (const uint8_t*)&txinput->sequence, 4); + sha256_Update(&ctx_amounts, (const uint8_t*)&txinput->amount, 8); + uint8_t lenbuf[5]; + uint32_t lenlen = ser_length(script_pubkey_len, lenbuf); + sha256_Update(&ctx_scriptpubkeys, lenbuf, lenlen); + sha256_Update(&ctx_scriptpubkeys, script_pubkey, script_pubkey_len); + } if (coin->decred) { if (txinput->decred_script_version > 0) { fsm_sendFailure(FailureType_Failure_SyntaxError, @@ -1002,6 +1105,15 @@ static bool signing_check_output(TxOutputType* txoutput) { } // compute segwit hashOuts tx_output_hash(&hasher_outputs, &bin_output, coin->decred); + /* BIP-341's sha_outputs: single sha256 over amount || ser_script */ + if (coin->has_taproot && coin->taproot) { + sha256_Update(&ctx_outputs_tr, (const uint8_t*)&bin_output.amount, 8); + uint8_t lenbuf[5]; + uint32_t lenlen = ser_length(bin_output.script_pubkey.size, lenbuf); + sha256_Update(&ctx_outputs_tr, lenbuf, lenlen); + sha256_Update(&ctx_outputs_tr, bin_output.script_pubkey.bytes, + bin_output.script_pubkey.size); + } return true; } @@ -1038,6 +1150,9 @@ static bool signing_check_fee(void) { signing_abort(); return false; } + if (has_taproot_input) { + taproot_transaction_confirmed = true; + } return true; } @@ -1061,6 +1176,9 @@ static void phase1_request_next_output(void) { tx_hash_final(&ti, hash_prefix, false); } hasher_Final(&hasher_outputs, hash_outputs); + if (coin->has_taproot && coin->taproot) { + sha256_Final(&ctx_outputs_tr, hash_outputs_tr); + } if (!signing_check_fee()) { return; } @@ -1077,6 +1195,14 @@ static void phase1_request_next_output(void) { } } +/* BIP-341 key-path sighash. The assembly itself lives in bip341_sighash() + so the field ordering is unit-testable against the published vectors. */ +static void signing_hash_bip341(uint32_t input_index, uint8_t* hash) { + bip341_sighash(SIGHASH_DEFAULT_TAPROOT, version, lock_time, hash_prevouts_tr, + hash_amounts, hash_scriptpubkeys, hash_sequences_tr, + hash_outputs_tr, input_index, hash); +} + static void signing_hash_bip143(const TxInputType* txinput, uint8_t* hash) { uint32_t hash_type = signing_hash_type(); Hasher hasher_preimage; @@ -1250,7 +1376,73 @@ static bool signing_sign_input(void) { static bool signing_sign_segwit_input(TxInputType* txinput) { // idx1: index to sign - if (is_segwit_input_script_type(txinput)) { + if (txinput->script_type == InputScriptType_SPENDTAPROOT) { + if (!taproot_transaction_confirmed) { + fsm_sendFailure(FailureType_Failure_Other, + _("Taproot transaction was not confirmed")); + signing_abort(); + return false; + } + /* No scriptSig to build, but the same re-validation and derivation the + other input types get. */ + if (!prepare_input_node(txinput)) { + fsm_sendFailure(FailureType_Failure_Other, _("Failed to compile input")); + signing_abort(); + return false; + } + if (txinput->amount > authorized_bip143_in) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Transaction has changed during signing")); + signing_abort(); + return false; + } + authorized_bip143_in -= txinput->amount; + + uint8_t hash[32]; + signing_hash_bip341(idx1, hash); + + /* Sign with the BIP-86 tweaked key, so the signature verifies against the + output key committed to in the scriptPubKey rather than the internal + key. */ + static CONFIDENTIAL uint8_t tweaked[32]; + if (bip340_tweak_seckey(curve->params, node.private_key, + /*merkle_root=*/NULL, tweaked) != 0) { + memzero(tweaked, sizeof(tweaked)); + fsm_sendFailure(FailureType_Failure_Other, _("Failed to tweak key")); + signing_abort(); + return false; + } + /* aux = NULL means an all-zero aux_rand, i.e. deterministic signing. That + is spec-permitted and matches this firmware's ECDSA, which is RFC6979 + deterministic; the nonce still depends on the private key and the + message, so it is never reused across different transactions. Fresh + randomness would only add side-channel hardening, at the cost of making + signatures unreproducible and so untestable against a published + vector. */ + int sign_ret = bip340_sign(curve->params, tweaked, hash, sizeof(hash), + /*aux=*/NULL, sig); + memzero(tweaked, sizeof(tweaked)); + if (sign_ret != 0) { + fsm_sendFailure(FailureType_Failure_Other, _("Signing failed")); + signing_abort(); + return false; + } + + resp.has_serialized = true; + resp.serialized.has_signature_index = true; + resp.serialized.signature_index = idx1; + resp.serialized.has_signature = true; + resp.serialized.signature.size = 64; + memcpy(resp.serialized.signature.bytes, sig, 64); + + /* Witness is a single 64-byte element. SIGHASH_DEFAULT omits the trailing + sighash byte entirely -- appending 0x00 would be a different, invalid + signature. */ + uint32_t r = 0; + r += ser_length(1, resp.serialized.serialized_tx.bytes + r); + r += tx_serialize_script(64, sig, resp.serialized.serialized_tx.bytes + r); + resp.serialized.serialized_tx.size = r; + } else if (is_segwit_input_script_type(txinput)) { if (!compile_input_script_sig(txinput)) { fsm_sendFailure(FailureType_Failure_Other, _("Failed to compile input")); signing_abort(); @@ -1403,6 +1595,7 @@ void signing_txack(TransactionType* tx) { send_req_2_prev_meta(); } } else if (tx->inputs[0].script_type == InputScriptType_SPENDWITNESS || + tx->inputs[0].script_type == InputScriptType_SPENDTAPROOT || tx->inputs[0].script_type == InputScriptType_SPENDP2SHWITNESS) { if (coin->decred) { @@ -1535,6 +1728,26 @@ void signing_txack(TransactionType* tx) { signing_abort(); return; } + /* BIP-341 commits to every input's amount and scriptPubKey. For a + mixed legacy+Taproot transaction the host must provide the legacy + amount, and it must describe the actual prevout rather than an + invented commitment that would produce an invalid signature. */ + if (coin->has_taproot && coin->taproot && input.has_amount) { + uint8_t expected_script[64]; + size_t expected_script_len = 0; + if (input.amount != tx->bin_outputs[0].amount || + !fill_input_script_pubkey(coin, root, &input, expected_script, + &expected_script_len, + sizeof(expected_script)) || + expected_script_len != tx->bin_outputs[0].script_pubkey.size || + memcmp(expected_script, tx->bin_outputs[0].script_pubkey.bytes, + expected_script_len) != 0) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Input amount or script does not match prevout")); + signing_abort(); + return; + } + } to_spend += tx->bin_outputs[0].amount; } if (idx2 < tp.outputs_len - 1) { @@ -1902,4 +2115,5 @@ void signing_abort(void) { } memzero(&root, sizeof(root)); memzero(&node, sizeof(node)); + taproot_transaction_confirmed = false; } diff --git a/lib/firmware/transaction.c b/lib/firmware/transaction.c index 9d30f1acd..8022ae603 100644 --- a/lib/firmware/transaction.c +++ b/lib/firmware/transaction.c @@ -32,6 +32,7 @@ #include "keepkey/transport/interface.h" #include "trezor/crypto/address.h" #include "trezor/crypto/base58.h" +#include "trezor/crypto/bip340.h" #include "trezor/crypto/cash_addr.h" #include "trezor/crypto/ecdsa.h" #include "trezor/crypto/memzero.h" @@ -43,6 +44,7 @@ #define _(X) (X) #define SEGWIT_VERSION_0 0 +#define SEGWIT_VERSION_1 1 #define CASHADDR_P2KH (0) #define CASHADDR_P2SH (8) @@ -113,6 +115,95 @@ uint32_t op_push(uint32_t i, uint8_t* out) { return 5; } +bool address_to_script_pubkey(const CoinType* coin, const char* address, + uint8_t* script_pubkey, size_t* script_pubkey_len, + size_t script_pubkey_size) { + uint8_t addr_raw[MAX_ADDR_RAW_SIZE]; + size_t addr_raw_len; + int witver; + + const curve_info* curve = get_curve_by_name(coin->curve_name); + if (!curve) return false; + + // Deliberately narrower than compile_output's output-side decoding: this is + // only reached for inputs of a coin with taproot enabled, i.e. Bitcoin and + // Testnet, so cashaddr and the BCH burn warning do not apply. Kept separate + // rather than factored out of compile_output because that block interleaves + // confirm-and-cancel UX with the script building. + if (coin->has_bech32_prefix && + segwit_addr_decode(&witver, addr_raw, &addr_raw_len, coin->bech32_prefix, + address)) { + // push witness version (OP_0 = 0, OP_i = 80 + i), then the program + if (addr_raw_len + 2 > script_pubkey_size) { + return false; + } + script_pubkey[0] = witver == 0 ? 0 : 80 + witver; + script_pubkey[1] = addr_raw_len; + memcpy(script_pubkey + 2, addr_raw, addr_raw_len); + *script_pubkey_len = addr_raw_len + 2; + return true; + } + + addr_raw_len = base58_decode_check(address, curve->hasher_base58, addr_raw, + MAX_ADDR_RAW_SIZE); + + if (coin->has_address_type && + addr_raw_len == 20 + address_prefix_bytes_len(coin->address_type) && + address_check_prefix(addr_raw, coin->address_type)) { + if (25 > script_pubkey_size) { + return false; + } + script_pubkey[0] = 0x76; // OP_DUP + script_pubkey[1] = 0xA9; // OP_HASH_160 + script_pubkey[2] = 0x14; // pushing 20 bytes + memcpy(script_pubkey + 3, + addr_raw + address_prefix_bytes_len(coin->address_type), 20); + script_pubkey[23] = 0x88; // OP_EQUALVERIFY + script_pubkey[24] = 0xAC; // OP_CHECKSIG + *script_pubkey_len = 25; + return true; + } + + if (coin->has_address_type_p2sh && + addr_raw_len == 20 + address_prefix_bytes_len(coin->address_type_p2sh) && + address_check_prefix(addr_raw, coin->address_type_p2sh)) { + if (23 > script_pubkey_size) { + return false; + } + script_pubkey[0] = 0xA9; // OP_HASH_160 + script_pubkey[1] = 0x14; // pushing 20 bytes + memcpy(script_pubkey + 2, + addr_raw + address_prefix_bytes_len(coin->address_type_p2sh), 20); + script_pubkey[22] = 0x87; // OP_EQUAL + *script_pubkey_len = 23; + return true; + } + + return false; +} + +bool fill_input_script_pubkey(const CoinType* coin, const HDNode* root, + const TxInputType* in, uint8_t* script_pubkey, + size_t* script_pubkey_len, + size_t script_pubkey_size) { + static CONFIDENTIAL HDNode node; + char address[MAX_ADDR_SIZE] = {0}; + bool res; + + memcpy(&node, root, sizeof(HDNode)); + res = hdnode_private_ckd_cached(&node, in->address_n, in->address_n_count, + NULL) != 0; + if (res) { + hdnode_fill_public_key(&node); // returns void in this tree + } + res = res && compute_address(coin, in->script_type, &node, in->has_multisig, + &in->multisig, address); + memzero(&node, sizeof(node)); + + return res && address_to_script_pubkey(coin, address, script_pubkey, + script_pubkey_len, script_pubkey_size); +} + bool compute_address(const CoinType* coin, InputScriptType script_type, const HDNode* node, bool has_multisig, const MultisigRedeemScriptType* multisig, @@ -125,6 +216,11 @@ bool compute_address(const CoinType* coin, InputScriptType script_type, if (has_multisig) { size_t prelen; + // No taproot multisig. Without this the request would fall through to + // the p2sh branch below and hand back a p2sh address for a taproot ask. + if (script_type == InputScriptType_SPENDTAPROOT) { + return 0; + } if (cryptoMultisigPubkeyIndex(coin, multisig, node->public_key) < 0) { return 0; } @@ -186,9 +282,28 @@ bool compute_address(const CoinType* coin, InputScriptType script_type, return 0; } } else if (script_type == InputScriptType_SPENDTAPROOT) { - // we don't handle spendtaproot input types - return 0; - + // p2tr: the witness program is the BIP-86 tweaked output key, bech32m + // encoded at witness version 1. + if ((!coin->has_segwit || !coin->segwit) || !coin->has_bech32_prefix) { + return 0; + } + if (!coin->has_taproot || !coin->taproot) { + return 0; + } + uint8_t output_key[32]; + // node->public_key is compressed; bytes 1..33 are the x-only internal key. + // BIP-341 defines the internal key as x-only, so the odd-y case resolves + // to its even-y counterpart here and in the signer alike. + if (bip340_tweak_pubkey(curve->params, node->public_key + 1, + /*merkle_root=*/NULL, output_key) != 0) { + return 0; + } + // Exactly 32 bytes: segwit_addr_encode only length-checks the witness + // program for version 0, so a wrong length would encode silently. + if (!segwit_addr_encode(address, coin->bech32_prefix, SEGWIT_VERSION_1, + output_key, sizeof(output_key))) { + return 0; + } } else if (script_type == InputScriptType_SPENDP2SHWITNESS) { // segwit p2wpkh embedded in p2sh if (!coin->has_segwit || !coin->segwit) { @@ -285,6 +400,9 @@ int compile_output(const CoinType* coin, const HDNode* root, TxOutputType* in, case OutputScriptType_PAYTOP2SHWITNESS: input_script_type = InputScriptType_SPENDP2SHWITNESS; break; + case OutputScriptType_PAYTOTAPROOT: + input_script_type = InputScriptType_SPENDTAPROOT; + break; default: return 0; // failed to compile output } @@ -969,6 +1087,9 @@ uint32_t tx_input_weight(const CoinType* coin, const TxInputType* txinput) { weight += 4; // empty input script } weight += input_script_size; // discounted witness + } else if (txinput->script_type == InputScriptType_SPENDTAPROOT) { + weight += 4; // empty scriptSig length in the non-witness serialization + weight += 2 + TXSIZE_SCHNORR_SIGNATURE; // stack count, item length, sig } return weight; } diff --git a/scripts/emulator/python-keepkey-tests.sh b/scripts/emulator/python-keepkey-tests.sh index e23497084..905496dd7 100755 --- a/scripts/emulator/python-keepkey-tests.sh +++ b/scripts/emulator/python-keepkey-tests.sh @@ -93,19 +93,43 @@ fi # Tests that skip via requires_message/requires_firmware are OK. # Tests that fail or are missing from JUnit = CI failure. echo "=== Phase 2: Full test suite ===" +set +e KK_EXPECT_PERSIST_REJECTED=1 \ KK_TRANSPORT_MAIN=kkemu:11044 \ KK_TRANSPORT_DEBUG=kkemu:11045 \ pytest -v --junitxml=/kkemu/test-reports/python-keepkey/junit.xml PYTEST_RC=$? +echo "=== Phase 2: Validate report catalog ===" +python3 ../scripts/generate-test-report.py \ + --junit=/kkemu/test-reports/python-keepkey/junit.xml \ + ${FW_VERSION:+--fw-version=$FW_VERSION} \ + --validate-junit +CATALOG_RC=$? + echo "=== Phase 2: Generate test report ===" python3 ../scripts/generate-test-report.py \ --junit=/kkemu/test-reports/python-keepkey/junit.xml \ - ${FW_VERSION:+--fw-version=$FW_VERSION} || true + ${FW_VERSION:+--fw-version=$FW_VERSION} \ + --screenshots=/kkemu/test-reports/screenshots \ + --output=/kkemu/test-reports/test-report.pdf +REPORT_RC=$? +set -e -echo "$PYTEST_RC" > /kkemu/test-reports/python-keepkey/status +if [ "$PYTEST_RC" -eq 0 ] && [ "$CATALOG_RC" -eq 0 ] && [ "$REPORT_RC" -eq 0 ]; then + echo "0" > /kkemu/test-reports/python-keepkey/status +else + echo "1" > /kkemu/test-reports/python-keepkey/status +fi if [ "$PYTEST_RC" -ne 0 ]; then echo "pytest failed with exit code $PYTEST_RC" exit "$PYTEST_RC" fi +if [ "$CATALOG_RC" -ne 0 ]; then + echo "report catalog validation failed with exit code $CATALOG_RC" + exit "$CATALOG_RC" +fi +if [ "$REPORT_RC" -ne 0 ]; then + echo "test report generation failed with exit code $REPORT_RC" + exit "$REPORT_RC" +fi diff --git a/unittests/crypto/CMakeLists.txt b/unittests/crypto/CMakeLists.txt index 6b4cbf8d6..6b2ace1ec 100644 --- a/unittests/crypto/CMakeLists.txt +++ b/unittests/crypto/CMakeLists.txt @@ -1,4 +1,5 @@ set(sources + bip340.cpp rand.cpp vuln1845.cpp) diff --git a/unittests/crypto/bip340.cpp b/unittests/crypto/bip340.cpp new file mode 100644 index 000000000..3dbb363c4 --- /dev/null +++ b/unittests/crypto/bip340.cpp @@ -0,0 +1,516 @@ +// Official BIP-340 test vectors, verbatim from +// https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv +// +// Vectors with a secret key are signed and the signature compared byte for +// byte (BIP-340 signing is deterministic given aux_rand). Every vector, +// with or without a secret key, is run through verification. + +#include + +extern "C" { +#include "trezor/crypto/bip32.h" +#include "trezor/crypto/bip340.h" +#include "trezor/crypto/bip39.h" +#include "trezor/crypto/curves.h" +#include "trezor/crypto/ecdsa.h" +#include "trezor/crypto/secp256k1.h" +#include "trezor/crypto/segwit_addr.h" +} + +#include "gtest/gtest.h" + +#include +#include +#include +#include + +namespace { + +std::vector unhex(const std::string &s) { + std::vector out; + out.reserve(s.size() / 2); + for (size_t i = 0; i + 1 < s.size(); i += 2) { + out.push_back((uint8_t)std::stoul(s.substr(i, 2), nullptr, 16)); + } + return out; +} + +std::string hex(const uint8_t *p, size_t len) { + static const char *digits = "0123456789ABCDEF"; + std::string out; + for (size_t i = 0; i < len; i++) { + out += digits[p[i] >> 4]; + out += digits[p[i] & 0x0f]; + } + return out; +} + +struct Vector { + int index; + const char *seckey; // empty when the vector is verify-only + const char *pubkey; + const char *aux; + const char *msg; + const char *sig; + bool valid; + const char *comment; +}; + +// 100 bytes of 0x99, the message of vector 18. +const char *kMsg100 = + "9999999999999999999999999999999999999999999999999999999999999999" + "9999999999999999999999999999999999999999999999999999999999999999" + "9999999999999999999999999999999999999999999999999999999999999999" + "99999999"; + +const Vector kVectors[] = { + {0, + "0000000000000000000000000000000000000000000000000000000000000003", + "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215" + "25F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0", + true, ""}, + {1, + "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "0000000000000000000000000000000000000000000000000000000000000001", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE3341" + "8906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A", + true, ""}, + {2, + "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9", + "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906", + "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C", + "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1B" + "AB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7", + true, ""}, + {3, + "0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710", + "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC" + "97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3", + true, "test fails if msg is reduced modulo p or n"}, + {4, "", + "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", "", + "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703", + "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C63" + "76AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4", + true, ""}, + {5, "", + "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + false, "public key not on the curve"}, + {6, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A1460297556" + "3CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2", + false, "has_even_y(R) is false"}, + {7, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F" + "28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD", + false, "negated message"}, + {8, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6", + false, "negated s value"}, + {9, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "0000000000000000000000000000000000000000000000000000000000000000" + "123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051", + false, "sG - eP is infinite, x(inf) as 0"}, + {10, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "0000000000000000000000000000000000000000000000000000000000000001" + "7615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197", + false, "sG - eP is infinite, x(inf) as 1"}, + {11, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + false, "sig[0:32] is not an X coordinate on the curve"}, + {12, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + false, "sig[0:32] is equal to field size"}, + {13, "", + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + false, "sig[32:64] is equal to curve order"}, + {14, "", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", "", + "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769" + "69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + false, "public key exceeds the field size"}, + {15, + "0340034003400340034003400340034003400340034003400340034003400340", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "0000000000000000000000000000000000000000000000000000000000000000", "", + "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF" + "6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63", + true, "message of size 0"}, + {16, + "0340034003400340034003400340034003400340034003400340034003400340", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "0000000000000000000000000000000000000000000000000000000000000000", "11", + "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303" + "EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF", + true, "message of size 1"}, + {17, + "0340034003400340034003400340034003400340034003400340034003400340", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "0000000000000000000000000000000000000000000000000000000000000000", + "0102030405060708090A0B0C0D0E0F1011", + "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370" + "C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5", + true, "message of size 17"}, + {18, + "0340034003400340034003400340034003400340034003400340034003400340", + "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + "0000000000000000000000000000000000000000000000000000000000000000", + kMsg100, + "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8" + "585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367", + true, "message of size 100"}, +}; + +} // namespace + +// Official BIP-86 test vectors, from +// https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki +// (mnemonic "abandon abandon ... about", account m/86'/0'/0'). +// HD derivation is covered at the firmware level; what is pinned here is the +// tweak and the bech32m encoding that turn an internal key into an address. +TEST(BIP340, BIP86Vectors) { + const struct { + const char *path; + const char *internal_key; + const char *output_key; + const char *address; + } vectors[] = { + {"m/86'/0'/0'/0/0", + "cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115", + "a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c", + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"}, + {"m/86'/0'/0'/0/1", + "83dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145", + "a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb", + "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"}, + {"m/86'/0'/0'/1/0", + "399f1b2f4393f29a18c937859c5dd8a77350103157eb880f02e8c08214277cef", + "882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc", + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"}, + }; + + for (const auto &v : vectors) { + std::vector internal = unhex(v.internal_key); + uint8_t out[BIP340_XONLY_LENGTH] = {0}; + + ASSERT_EQ(0, bip340_tweak_pubkey(&secp256k1, internal.data(), nullptr, out)) + << v.path; + + std::string got = hex(out, sizeof(out)); + std::transform(got.begin(), got.end(), got.begin(), ::tolower); + ASSERT_EQ(std::string(v.output_key), got) << v.path; + + // Witness version 1 + 32 bytes must come out bech32m, i.e. a bc1p address. + char address[MAX_ADDR_SIZE] = {0}; + ASSERT_EQ(1, segwit_addr_encode(address, "bc", 1, out, sizeof(out))) + << v.path; + ASSERT_EQ(std::string(v.address), std::string(address)) << v.path; + } +} + +// The same BIP-86 vectors driven from the mnemonic, so HD derivation and the +// x-only convention are covered too. compute_address() feeds +// node->public_key + 1 to bip340_tweak_pubkey(); this pins that the byte after +// the compressed prefix really is the internal key BIP-86 expects, which an +// off-by-one would otherwise turn into a valid-looking wrong address. +TEST(BIP340, BIP86FromMnemonic) { + const char *mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon about"; + const struct { + uint32_t change; + uint32_t index; + const char *address; + } vectors[] = { + {0, 0, "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"}, + {0, 1, "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"}, + {1, 0, "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"}, + }; + + uint8_t seed[64] = {0}; + mnemonic_to_seed(mnemonic, "", seed, nullptr); + + for (const auto &v : vectors) { + HDNode node = {0}; + ASSERT_EQ(1, hdnode_from_seed(seed, sizeof(seed), SECP256K1_NAME, &node)); + // m/86'/0'/0'/change/index + ASSERT_EQ(1, hdnode_private_ckd(&node, 0x80000000 + 86)); + ASSERT_EQ(1, hdnode_private_ckd(&node, 0x80000000 + 0)); + ASSERT_EQ(1, hdnode_private_ckd(&node, 0x80000000 + 0)); + ASSERT_EQ(1, hdnode_private_ckd(&node, v.change)); + ASSERT_EQ(1, hdnode_private_ckd(&node, v.index)); + hdnode_fill_public_key(&node); + + uint8_t out[BIP340_XONLY_LENGTH] = {0}; + ASSERT_EQ(0, bip340_tweak_pubkey(&secp256k1, node.public_key + 1, nullptr, out)); + + char address[MAX_ADDR_SIZE] = {0}; + ASSERT_EQ(1, segwit_addr_encode(address, "bc", 1, out, sizeof(out))); + ASSERT_EQ(std::string(v.address), std::string(address)) + << "change=" << v.change << " index=" << v.index; + } +} + +// Official BIP-341 key-path spending vector, input index 4, from +// https://github.com/bitcoin/bips/blob/master/bip-0341/wallet-test-vectors.json +// +// This is the only published input that uses SIGHASH_DEFAULT (hashType 0), so +// it is the one that pins our signing configuration end to end. It carries a +// merkle root, which is why bip340_tweak_seckey/pubkey take one -- without it +// there is no published witness to check the sigmsg field ordering against, +// and a transposed field yields a perfectly valid signature over the wrong +// transaction. +namespace bip341 { +const char *kInternalPrivkey = + "f36bb07a11e469ce941d16b63b11b9b9120a84d9d87cff2c84a8d4affb438f4e"; +const char *kInternalPubkey = + "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f"; +const char *kMerkleRoot = + "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2"; +const char *kTweakedPrivkey = + "a8e7aa924f0d58854185a490e6c41f6efb7b675c0f3331b7f14b549400b4d501"; +const char *kSigHash = + "4f900a0bae3f1446fd48490c2958b5a023228f01661cda3496a11da502a7f7ef"; +const char *kWitness = + "b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669d" + "e185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f"; +const char *kHashPrevouts = + "e3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f"; +const char *kHashAmounts = + "58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde6"; +const char *kHashScriptPubkeys = + "23ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e21"; +const char *kHashSequences = + "18959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e"; +const char *kHashOutputs = + "a2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc5"; +const uint32_t kVersion = 2; +const uint32_t kLockTime = 500000000; +const uint32_t kInputIndex = 4; + +std::string lower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + return s; +} +} // namespace bip341 + +TEST(BIP341, TweakSeckey) { + std::vector sk = unhex(bip341::kInternalPrivkey); + std::vector root = unhex(bip341::kMerkleRoot); + uint8_t pk[BIP340_XONLY_LENGTH] = {0}; + uint8_t tweaked[32] = {0}; + + ASSERT_EQ(0, bip340_get_xonly_pubkey(&secp256k1, sk.data(), pk)); + ASSERT_EQ(std::string(bip341::kInternalPubkey), + bip341::lower(hex(pk, sizeof(pk)))); + + ASSERT_EQ(0, bip340_tweak_seckey(&secp256k1, sk.data(), root.data(), + tweaked)); + ASSERT_EQ(std::string(bip341::kTweakedPrivkey), + bip341::lower(hex(tweaked, sizeof(tweaked)))); + + // The tweaked private key must correspond to the tweaked public key, or the + // signature verifies under a key that does not own the output. + uint8_t from_seckey[BIP340_XONLY_LENGTH] = {0}; + uint8_t from_pubkey[BIP340_XONLY_LENGTH] = {0}; + ASSERT_EQ(0, bip340_get_xonly_pubkey(&secp256k1, tweaked, from_seckey)); + ASSERT_EQ(0, bip340_tweak_pubkey(&secp256k1, pk, root.data(), from_pubkey)); + ASSERT_EQ(0, memcmp(from_seckey, from_pubkey, BIP340_XONLY_LENGTH)); +} + +TEST(BIP341, Sighash) { + std::vector prevouts = unhex(bip341::kHashPrevouts); + std::vector amounts = unhex(bip341::kHashAmounts); + std::vector spks = unhex(bip341::kHashScriptPubkeys); + std::vector seqs = unhex(bip341::kHashSequences); + std::vector outs = unhex(bip341::kHashOutputs); + uint8_t hash[SHA256_DIGEST_LENGTH] = {0}; + + bip341_sighash(/*hash_type=*/0, bip341::kVersion, bip341::kLockTime, + prevouts.data(), amounts.data(), spks.data(), seqs.data(), + outs.data(), bip341::kInputIndex, hash); + + ASSERT_EQ(std::string(bip341::kSigHash), bip341::lower(hex(hash, sizeof(hash)))); +} + +TEST(BIP341, KeyPathSignatureMatchesPublishedWitness) { + std::vector sk = unhex(bip341::kInternalPrivkey); + std::vector root = unhex(bip341::kMerkleRoot); + std::vector sighash = unhex(bip341::kSigHash); + uint8_t tweaked[32] = {0}; + uint8_t sig[BIP340_SIG_LENGTH] = {0}; + + ASSERT_EQ(0, bip340_tweak_seckey(&secp256k1, sk.data(), root.data(), + tweaked)); + // BIP-341's vectors are generated with an all-zero aux_rand. + ASSERT_EQ(0, bip340_sign(&secp256k1, tweaked, sighash.data(), sighash.size(), + nullptr, sig)); + ASSERT_EQ(std::string(bip341::kWitness), bip341::lower(hex(sig, sizeof(sig)))); +} + +TEST(BIP340, TweakRejectsInvalidInternalKey) { + // Vector 5's x coordinate, which is not on the curve. + std::vector bad = + unhex("EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34"); + // And an x coordinate past the field size (vector 14). + std::vector too_big = + unhex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30"); + uint8_t out[BIP340_XONLY_LENGTH] = {0}; + + ASSERT_NE(0, bip340_tweak_pubkey(&secp256k1, bad.data(), nullptr, out)); + ASSERT_NE(0, bip340_tweak_pubkey(&secp256k1, too_big.data(), nullptr, out)); +} + +TEST(BIP340, TaggedHash) { + // tagged_hash("BIP0340/challenge", "") == SHA256(h || h) where + // h = SHA256("BIP0340/challenge"). Pins the double-tag construction. + uint8_t out[SHA256_DIGEST_LENGTH] = {0}; + uint8_t tag_hash[SHA256_DIGEST_LENGTH] = {0}; + uint8_t expected[SHA256_DIGEST_LENGTH] = {0}; + uint8_t doubled[2 * SHA256_DIGEST_LENGTH] = {0}; + + bip340_tagged_hash("BIP0340/challenge", nullptr, 0, out); + + sha256_Raw((const uint8_t *)"BIP0340/challenge", 17, tag_hash); + memcpy(doubled, tag_hash, sizeof(tag_hash)); + memcpy(doubled + sizeof(tag_hash), tag_hash, sizeof(tag_hash)); + sha256_Raw(doubled, sizeof(doubled), expected); + + ASSERT_EQ(0, memcmp(out, expected, sizeof(expected))); +} + +TEST(BIP340, XOnlyPubkey) { + for (const auto &v : kVectors) { + if (v.seckey[0] == '\0') continue; + + std::vector sk = unhex(v.seckey); + uint8_t pk[BIP340_XONLY_LENGTH] = {0}; + + ASSERT_EQ(0, bip340_get_xonly_pubkey(&secp256k1, sk.data(), pk)) + << "vector " << v.index; + ASSERT_EQ(std::string(v.pubkey), hex(pk, sizeof(pk))) + << "vector " << v.index; + } +} + +TEST(BIP340, Sign) { + for (const auto &v : kVectors) { + if (v.seckey[0] == '\0') continue; + + std::vector sk = unhex(v.seckey); + std::vector aux = unhex(v.aux); + std::vector msg = unhex(v.msg); + uint8_t sig[BIP340_SIG_LENGTH] = {0}; + + ASSERT_EQ(0, bip340_sign(&secp256k1, sk.data(), msg.data(), msg.size(), + aux.data(), sig)) + << "vector " << v.index << ": " << v.comment; + ASSERT_EQ(std::string(v.sig), hex(sig, sizeof(sig))) + << "vector " << v.index << ": " << v.comment; + } +} + +TEST(BIP340, Verify) { + for (const auto &v : kVectors) { + std::vector pk = unhex(v.pubkey); + std::vector msg = unhex(v.msg); + std::vector sig = unhex(v.sig); + + int ret = bip340_verify(&secp256k1, pk.data(), msg.data(), msg.size(), + sig.data()); + if (v.valid) { + ASSERT_EQ(0, ret) << "vector " << v.index << ": " << v.comment; + } else { + ASSERT_NE(0, ret) << "vector " << v.index << ": " << v.comment; + } + } +} + +TEST(BIP340, SignRejectsOutOfRangeKeys) { + const uint8_t zero[32] = {0}; + // n, the curve order -- the first scalar that is out of range. + const uint8_t order[32] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41}; + const uint8_t msg[32] = {0}; + uint8_t sig[BIP340_SIG_LENGTH] = {0}; + + // Pre-fill, so the zero-on-failure check below cannot pass vacuously. + memset(sig, 0xFF, sizeof(sig)); + + ASSERT_NE(0, bip340_sign(&secp256k1, zero, msg, sizeof(msg), nullptr, sig)); + ASSERT_NE(0, bip340_sign(&secp256k1, order, msg, sizeof(msg), nullptr, sig)); + + // A rejected signing attempt must not leave anything in the output buffer. + uint8_t empty[BIP340_SIG_LENGTH] = {0}; + ASSERT_EQ(0, memcmp(sig, empty, sizeof(sig))); +} + +TEST(BIP340, XOnlyPubkeyZeroesOnFailure) { + const uint8_t zero[32] = {0}; + uint8_t pk[BIP340_XONLY_LENGTH]; + + memset(pk, 0xFF, sizeof(pk)); + ASSERT_NE(0, bip340_get_xonly_pubkey(&secp256k1, zero, pk)); + + uint8_t empty[BIP340_XONLY_LENGTH] = {0}; + ASSERT_EQ(0, memcmp(pk, empty, sizeof(pk))); +} + +TEST(BIP340, ZeroSTakesTheSpecPath) { + // s == 0 is in range per BIP-340 and carries no special guard: verification + // must compute R = -eP and reject on the x-coordinate comparison, not bail + // out early. Pins the absence of a guard that would deviate from the spec. + std::vector pk = unhex(kVectors[1].pubkey); + std::vector msg = unhex(kVectors[1].msg); + std::vector sig = unhex(kVectors[1].sig); + memset(sig.data() + 32, 0, 32); + + ASSERT_NE(0, bip340_verify(&secp256k1, pk.data(), msg.data(), msg.size(), + sig.data())); +} + +TEST(BIP340, NullAuxMatchesZeroAux) { + std::vector sk = unhex(kVectors[0].seckey); + std::vector msg = unhex(kVectors[0].msg); + uint8_t with_null[BIP340_SIG_LENGTH] = {0}; + + ASSERT_EQ(0, bip340_sign(&secp256k1, sk.data(), msg.data(), msg.size(), + nullptr, with_null)); + // Vector 0 uses an all-zero aux_rand, so NULL must reproduce it exactly. + ASSERT_EQ(std::string(kVectors[0].sig), hex(with_null, sizeof(with_null))); +} diff --git a/unittests/firmware/CMakeLists.txt b/unittests/firmware/CMakeLists.txt index 667911741..f9fdf7064 100644 --- a/unittests/firmware/CMakeLists.txt +++ b/unittests/firmware/CMakeLists.txt @@ -2,6 +2,7 @@ set(sources app_confirm.cpp recovery.cpp storage.cpp + transaction.cpp usb_rx.cpp u2f.cpp) diff --git a/unittests/firmware/transaction.cpp b/unittests/firmware/transaction.cpp new file mode 100644 index 000000000..1946e3694 --- /dev/null +++ b/unittests/firmware/transaction.cpp @@ -0,0 +1,15 @@ +extern "C" { +#include "keepkey/firmware/transaction.h" +} + +#include "gtest/gtest.h" + +TEST(Transaction, TaprootInputWeightIncludesWitness) { + CoinType coin = CoinType_init_zero; + TxInputType input = TxInputType_init_zero; + input.script_type = InputScriptType_SPENDTAPROOT; + + // 41 non-witness bytes * 4 plus a one-item witness containing the fixed + // 64-byte SIGHASH_DEFAULT Schnorr signature. + ASSERT_EQ(230U, tx_input_weight(&coin, &input)); +}