From 3bbdf971338510dde1a63fbbc76459db5fd77c3f Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 30 Jul 2026 15:32:13 -0300 Subject: [PATCH] feat(zcash): sign NU6.3 Ironwood transactions --- deps/device-protocol | 2 +- deps/python-keepkey | 2 +- include/keepkey/firmware/zcash.h | 22 +++ .../keepkey/transport/messages-zcash.options | 2 +- lib/firmware/fsm_msg_zcash.h | 150 ++++++++++++------ lib/firmware/zcash.c | 112 ++++++++++--- tools/check_pallas_api_boundary.py | 12 +- unittests/firmware/zcash.cpp | 66 ++++++++ 8 files changed, 297 insertions(+), 71 deletions(-) diff --git a/deps/device-protocol b/deps/device-protocol index 4cc8b7175..f2246cebe 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit 4cc8b717517c79ee3ac436161141dd033db286fd +Subproject commit f2246cebea8f96fcd7ec2883588a784a60b430ae diff --git a/deps/python-keepkey b/deps/python-keepkey index c406a1ba9..d88a073a5 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit c406a1ba9120da410c356dbff7f4d4bd1e1758fa +Subproject commit d88a073a5af2d83e0f1f19574665ca1a44789414 diff --git a/include/keepkey/firmware/zcash.h b/include/keepkey/firmware/zcash.h index 26d6a7501..b331f52b1 100644 --- a/include/keepkey/firmware/zcash.h +++ b/include/keepkey/firmware/zcash.h @@ -55,6 +55,9 @@ typedef struct { size_t sapling_digest_size; bool has_orchard_digest; size_t orchard_digest_size; + bool is_ironwood; + bool has_ironwood_digest; + size_t ironwood_digest_size; bool has_orchard_flags; uint32_t orchard_flags; bool has_orchard_value_balance; @@ -153,6 +156,15 @@ bool zcash_compute_shielded_sighash(const uint8_t header_digest[32], uint32_t branch_id, uint8_t sighash_out[32]); +/** Compute the five-component ZIP-229 transaction-v6 sighash. */ +bool zcash_compute_v6_shielded_sighash(const uint8_t header_digest[32], + const uint8_t transparent_digest[32], + const uint8_t sapling_digest[32], + const uint8_t orchard_digest[32], + const uint8_t ironwood_digest[32], + uint32_t branch_id, + uint8_t sighash_out[32]); + /** * Compute ZIP-244 T.1 header_digest from plaintext transaction header fields. */ @@ -218,6 +230,16 @@ bool zcash_orchard_compute_cmx( const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32]); +/** ZIP-2005 V3 note commitment used by the Ironwood pool. */ +bool zcash_ironwood_compute_cmx( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32]); + +bool zcash_ironwood_compute_cmx_with_progress( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32], + ZcashOrchardProgressCallback progress, void* progress_context); + /** * Progress-reporting note-commitment verification for interactive PCZT flows. * The callback exposes only the public Sinsemilla word index and count. diff --git a/include/keepkey/transport/messages-zcash.options b/include/keepkey/transport/messages-zcash.options index f7776b869..63d1035b4 100644 --- a/include/keepkey/transport/messages-zcash.options +++ b/include/keepkey/transport/messages-zcash.options @@ -6,6 +6,7 @@ ZcashSignPCZT.header_digest max_size:32 ZcashSignPCZT.transparent_digest max_size:32 ZcashSignPCZT.sapling_digest max_size:32 ZcashSignPCZT.orchard_digest max_size:32 +ZcashSignPCZT.ironwood_digest max_size:32 ZcashSignPCZT.orchard_value_balance int_size:IS_64 ZcashSignPCZT.orchard_anchor max_size:32 ZcashSignPCZT.expected_seed_fingerprint max_size:32 @@ -44,7 +45,6 @@ ZcashTransparentInput.amount int_size:IS_64 ZcashTransparentInput.prevout_txid max_size:32 ZcashTransparentInput.script_pubkey max_size:128 -ZcashTransparentSig.signature max_size:73 ZcashTransparentSigned.signatures max_count:8, max_size:73 ZcashDisplayAddress.address_n max_count:8 diff --git a/lib/firmware/fsm_msg_zcash.h b/lib/firmware/fsm_msg_zcash.h index c1bfec87f..9616354c8 100644 --- a/lib/firmware/fsm_msg_zcash.h +++ b/lib/firmware/fsm_msg_zcash.h @@ -76,6 +76,10 @@ static struct { ZcashOrchardKeys keys; uint8_t header_digest[32]; uint8_t sighash[32]; + bool transaction_v6; + bool is_ironwood; + uint8_t orchard_component_digest[32]; + uint8_t ironwood_component_digest[32]; /* Phase 2a: on-device sighash computation */ bool has_device_sighash; /* Phase 2b: incremental orchard digest verification */ @@ -236,13 +240,18 @@ static bool zcash_verify_and_confirm_orchard_output( } uint8_t computed_cmx[32]; - if (!zcash_orchard_compute_cmx_with_progress( - msg->recipient.bytes, msg->value, msg->nullifier.bytes, - msg->rseed.bytes, computed_cmx, progress, progress_context) || - memcmp(computed_cmx, msg->cmx.bytes, 32) != 0) { + bool cmx_ok = + zcash_signing.is_ironwood + ? zcash_ironwood_compute_cmx_with_progress( + msg->recipient.bytes, msg->value, msg->nullifier.bytes, + msg->rseed.bytes, computed_cmx, progress, progress_context) + : zcash_orchard_compute_cmx_with_progress( + msg->recipient.bytes, msg->value, msg->nullifier.bytes, + msg->rseed.bytes, computed_cmx, progress, progress_context); + if (!cmx_ok || memcmp(computed_cmx, msg->cmx.bytes, 32) != 0) { memzero(computed_cmx, sizeof(computed_cmx)); fsm_sendFailure(FailureType_Failure_Other, - _("Orchard note commitment mismatch")); + _("Shielded note commitment mismatch")); return false; } memzero(computed_cmx, sizeof(computed_cmx)); @@ -414,6 +423,20 @@ static bool zcash_build_transparent_digest_info( return true; } +static bool zcash_compute_active_sighash(const uint8_t transparent_digest[32], + uint8_t sighash[32]) { + if (zcash_signing.transaction_v6) { + return zcash_compute_v6_shielded_sighash( + zcash_signing.header_digest, transparent_digest, EMPTY_SAPLING_DIGEST, + zcash_signing.orchard_component_digest, + zcash_signing.ironwood_component_digest, zcash_signing.branch_id, + sighash); + } + return zcash_compute_shielded_sighash( + zcash_signing.header_digest, transparent_digest, EMPTY_SAPLING_DIGEST, + zcash_signing.orchard_component_digest, zcash_signing.branch_id, sighash); +} + static bool zcash_finalize_transparent_digest(void) { if (!zcash_signing.has_expected_transparent_digest) return false; @@ -439,10 +462,13 @@ static bool zcash_finalize_transparent_digest(void) { return false; } - zcash_compute_shielded_sighash( - zcash_signing.header_digest, transparent_digest, EMPTY_SAPLING_DIGEST, - zcash_signing.expected_orchard_digest, zcash_signing.branch_id, - zcash_signing.sighash); + if (!zcash_compute_active_sighash(transparent_digest, + zcash_signing.sighash)) { + memzero(transparent_digest, sizeof(transparent_digest)); + memzero(inputs, sizeof(inputs)); + memzero(outputs, sizeof(outputs)); + return false; + } zcash_signing.has_device_sighash = true; zcash_signing.transparent_digest_verified = true; @@ -487,25 +513,21 @@ static bool zcash_sign_transparent_inputs(bool* cancelled) { stored->address_n_count, NULL); if (!node) goto cleanup; - /* ZIP-244 §4.4: signature_digest = ZcashTxHash_( - * header_digest || transparent_sig_digest || sapling_digest || - * orchard_digest) Binding the transparent ECDSA sig to all four components - * ensures it cannot be replayed in a transaction with different - * Orchard/header data. */ + /* ZIP-244/229: bind the transparent ECDSA signature to every transaction + * component, including Ironwood for transaction v6. */ uint8_t t_sig_digest[32] = {0}; uint8_t full_sighash[32] = {0}; uint8_t sig[64] = {0}; uint8_t der_sig[73] = {0}; - bool sign_ok = - zcash_compute_transparent_sighash_digest( - inputs, zcash_signing.n_transparent_inputs, outputs, - zcash_signing.n_transparent_outputs, i, 0x01, t_sig_digest) && - zcash_compute_shielded_sighash(zcash_signing.header_digest, - t_sig_digest, EMPTY_SAPLING_DIGEST, - zcash_signing.expected_orchard_digest, - zcash_signing.branch_id, full_sighash) && - hdnode_sign_digest(node, full_sighash, sig, NULL, NULL) == 0; + bool sign_ok = zcash_compute_transparent_sighash_digest( + inputs, zcash_signing.n_transparent_inputs, outputs, + zcash_signing.n_transparent_outputs, i, 0x01, t_sig_digest); + if (sign_ok) { + sign_ok = zcash_compute_active_sighash(t_sig_digest, full_sighash); + } + sign_ok = + sign_ok && hdnode_sign_digest(node, full_sighash, sig, NULL, NULL) == 0; memzero(node, sizeof(*node)); memzero(t_sig_digest, sizeof(t_sig_digest)); @@ -581,6 +603,26 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { } uint32_t branch_id = msg->has_branch_id ? msg->branch_id : 0; + bool is_ironwood = + msg->has_shielded_pool && + msg->shielded_pool == ZcashShieldedPool_ZCASH_SHIELDED_POOL_IRONWOOD; + if (msg->has_shielded_pool && + msg->shielded_pool != ZcashShieldedPool_ZCASH_SHIELDED_POOL_ORCHARD && + msg->shielded_pool != ZcashShieldedPool_ZCASH_SHIELDED_POOL_IRONWOOD) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Unknown shielded pool")); + layoutHome(); + return; + } + if (is_ironwood && + (!msg->has_tx_version || msg->tx_version != 6 || + !msg->has_version_group_id || msg->version_group_id != 0xD884B698 || + branch_id != 0x37A5165B)) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid Ironwood transaction")); + layoutHome(); + return; + } ZcashPCZTSigningRequestMeta signing_meta = {0}; signing_meta.has_header_digest = msg->has_header_digest; @@ -591,6 +633,9 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { signing_meta.sapling_digest_size = msg->sapling_digest.size; signing_meta.has_orchard_digest = msg->has_orchard_digest; signing_meta.orchard_digest_size = msg->orchard_digest.size; + signing_meta.is_ironwood = is_ironwood; + signing_meta.has_ironwood_digest = msg->has_ironwood_digest; + signing_meta.ironwood_digest_size = msg->ironwood_digest.size; signing_meta.has_orchard_flags = msg->has_orchard_flags; signing_meta.orchard_flags = msg->orchard_flags; signing_meta.has_orchard_value_balance = msg->has_orchard_value_balance; @@ -652,7 +697,7 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { /* Display confirmation — different text for shielded-only vs hybrid */ if (n_tinputs > 0) { if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Zcash Shield", - "Shield transparent ZEC to Orchard?\n" + "Shield transparent ZEC?\n" "Amount: %s\nFee: %s\nInputs: %lu\nOutputs: %lu\nActions: %lu", amount_str, fee_str, (unsigned long)n_tinputs, (unsigned long)n_toutputs, (unsigned long)msg->n_actions)) { @@ -716,7 +761,14 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { zcash_signing.total_amount = total; zcash_signing.fee = fee; zcash_signing.branch_id = branch_id; + zcash_signing.transaction_v6 = msg->tx_version == 6; + zcash_signing.is_ironwood = is_ironwood; memcpy(zcash_signing.header_digest, header_digest, 32); + memcpy(zcash_signing.orchard_component_digest, msg->orchard_digest.bytes, 32); + if (is_ironwood) { + memcpy(zcash_signing.ironwood_component_digest, msg->ironwood_digest.bytes, + 32); + } zcash_signing.has_device_sighash = false; zcash_signing.verify_orchard_digest = false; zcash_signing.n_transparent_outputs = @@ -733,7 +785,8 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { * TRUST MODEL: * * What the device verifies: - * - Orchard digest: recomputed from streamed action data (Phase 2b) + * - Active shielded-pool digest: recomputed from streamed action data + * (Phase 2b) * covering nullifiers, commitments, ephemeral keys, ciphertexts, * value commitments, randomized keys, flags, value balance, anchor. * - Orchard outputs: each displayed receiver/value is bound to cmx by @@ -742,7 +795,7 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { * - Transaction fee: computed from streamed transparent totals plus * orchard_value_balance and compared to the requested fee before final * user confirmation. - * - Sighash: assembled on-device from the 4 sub-digests. + * - Sighash: assembled on-device from all v5 or v6 sub-digests. * - transparent_digest: recomputed from streamed transparent outputs and * inputs before any transparent or Orchard signature is emitted. * - header_digest: recomputed from plaintext transaction header fields @@ -762,15 +815,11 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { * For mixed transactions: * transparent_digest is mandatory and verified against plaintext * transparent metadata before local sighash derivation. */ - uint8_t t_digest[32], s_digest[32]; + uint8_t t_digest[32]; if (n_tinputs == 0 && n_toutputs == 0) { memcpy(t_digest, EMPTY_TRANSPARENT_DIGEST, 32); - memcpy(s_digest, EMPTY_SAPLING_DIGEST, 32); - - zcash_compute_shielded_sighash( - header_digest, t_digest, s_digest, msg->orchard_digest.bytes, - zcash_signing.branch_id, zcash_signing.sighash); + zcash_compute_active_sighash(t_digest, zcash_signing.sighash); zcash_signing.has_device_sighash = true; zcash_signing.transparent_digest_verified = true; } else { @@ -779,19 +828,25 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { zcash_signing.has_expected_transparent_digest = true; } memzero(t_digest, sizeof(t_digest)); - memzero(s_digest, sizeof(s_digest)); - /* Phase 2b: Orchard digest verification is mandatory for signing. + /* Phase 2b: the active shielded-pool digest is mandatory for signing. * The device incrementally hashes each action's data and verifies the - * computed orchard_digest matches the one used for sighash. */ - memcpy(zcash_signing.expected_orchard_digest, msg->orchard_digest.bytes, 32); + * computed digest matches the one used for sighash. */ + memcpy(zcash_signing.expected_orchard_digest, + is_ironwood ? msg->ironwood_digest.bytes : msg->orchard_digest.bytes, + 32); zcash_signing.orchard_flags = (uint8_t)msg->orchard_flags; zcash_signing.orchard_value_balance = msg->orchard_value_balance; memcpy(zcash_signing.orchard_anchor, msg->orchard_anchor.bytes, 32); - blake2b_InitPersonal(&zcash_signing.compact_ctx, 32, "ZTxIdOrcActCHash", 16); - blake2b_InitPersonal(&zcash_signing.memos_ctx, 32, "ZTxIdOrcActMHash", 16); - blake2b_InitPersonal(&zcash_signing.noncompact_ctx, 32, "ZTxIdOrcActNHash", + blake2b_InitPersonal(&zcash_signing.compact_ctx, 32, + is_ironwood ? "ZTxIdIrnActCH_v6" : "ZTxIdOrcActCHash", + 16); + blake2b_InitPersonal(&zcash_signing.memos_ctx, 32, + is_ironwood ? "ZTxIdIrnActMH_v6" : "ZTxIdOrcActMHash", + 16); + blake2b_InitPersonal(&zcash_signing.noncompact_ctx, 32, + is_ironwood ? "ZTxIdIrnActNH_v6" : "ZTxIdOrcActNHash", 16); zcash_signing.verify_orchard_digest = true; @@ -1112,18 +1167,23 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { blake2b_Final(&zcash_signing.memos_ctx, memos_hash, 32); blake2b_Final(&zcash_signing.noncompact_ctx, noncompact_hash, 32); - /* Compute orchard_digest = BLAKE2b("ZTxIdOrchardHash", - * compact_hash || memos_hash || noncompact_hash || - * flags(1) || value_balance(8) || anchor(32)) */ + /* V5 Orchard commits the anchor in the txid component. Transaction-v6 + * Ironwood moves the anchor to the authorizing-data digest (ZIP-229), + * so the device deliberately omits it here. */ BLAKE2B_CTX orchard_ctx; - blake2b_InitPersonal(&orchard_ctx, 32, "ZTxIdOrchardHash", 16); + blake2b_InitPersonal( + &orchard_ctx, 32, + zcash_signing.is_ironwood ? "ZTxIdIronwd_H_v6" : "ZTxIdOrchardHash", + 16); blake2b_Update(&orchard_ctx, compact_hash, 32); blake2b_Update(&orchard_ctx, memos_hash, 32); blake2b_Update(&orchard_ctx, noncompact_hash, 32); blake2b_Update(&orchard_ctx, &zcash_signing.orchard_flags, 1); blake2b_Update(&orchard_ctx, (const uint8_t*)&zcash_signing.orchard_value_balance, 8); - blake2b_Update(&orchard_ctx, zcash_signing.orchard_anchor, 32); + if (!zcash_signing.transaction_v6) { + blake2b_Update(&orchard_ctx, zcash_signing.orchard_anchor, 32); + } uint8_t computed_orchard_digest[32]; blake2b_Final(&orchard_ctx, computed_orchard_digest, 32); @@ -1132,7 +1192,7 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { if (memcmp(computed_orchard_digest, zcash_signing.expected_orchard_digest, 32) != 0) { fsm_sendFailure(FailureType_Failure_Other, - _("Orchard digest mismatch: transaction data " + _("Shielded digest mismatch: transaction data " "does not match sighash")); zcash_signing_abort(); layoutHome(); diff --git a/lib/firmware/zcash.c b/lib/firmware/zcash.c index 17124ca7c..b9379d040 100644 --- a/lib/firmware/zcash.c +++ b/lib/firmware/zcash.c @@ -513,30 +513,45 @@ static bool zcash_pack_orchard_note_commit_msg(const uint8_t receiver[43], return true; } -bool zcash_orchard_compute_cmx_with_progress( +static bool zcash_orchard_family_compute_cmx_with_progress( const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32], - ZcashOrchardProgressCallback progress, void* progress_context) { + bool ironwood, ZcashOrchardProgressCallback progress, + void* progress_context) { if (!receiver || !rho || !rseed || !cmx_out) return false; uint8_t msg[136]; - uint8_t prf_in[33]; + uint8_t prf_in[137]; uint8_t prf_out[64]; uint8_t rcm[32]; uint8_t psi[32]; - curve_point q, r; + curve_point gd, q, r; bool ok = false; + /* psi is unchanged between V2 (Orchard) and V3 (Ironwood) notes. */ memcpy(prf_in + 1, rho, 32); - - prf_in[0] = 0x05; - prf_expand(rseed, prf_in, sizeof(prf_in), prf_out); - to_scalar(prf_out, rcm); - prf_in[0] = 0x09; - prf_expand(rseed, prf_in, sizeof(prf_in), prf_out); + prf_expand(rseed, prf_in, 33, prf_out); to_base(prf_out, psi); + if (ironwood) { + /* ZIP-2005 H_rcm binds V3 randomness to every note field. */ + if (!orchard_diversify_point(receiver, &gd)) goto cleanup; + prf_in[0] = 0x0B; + pallas_point_encode(&gd, prf_in + 1); + memcpy(prf_in + 33, receiver + 11, 32); + for (size_t i = 0; i < 8; i++) { + prf_in[65 + i] = (uint8_t)((value >> (8 * i)) & 0xff); + } + memcpy(prf_in + 73, rho, 32); + memcpy(prf_in + 105, psi, 32); + prf_expand(rseed, prf_in, sizeof(prf_in), prf_out); + } else { + prf_in[0] = 0x05; + prf_expand(rseed, prf_in, 33, prf_out); + } + to_scalar(prf_out, rcm); + ok = zcash_pack_orchard_note_commit_msg(receiver, value, rho, psi, msg) && pallas_group_hash("z.cash:SinsemillaQ", (const uint8_t*)"z.cash:Orchard-NoteCommit-M", @@ -545,20 +560,27 @@ bool zcash_orchard_compute_cmx_with_progress( pallas_sinsemilla_short_commit_progress(&q, &r, msg, 1086, rcm, cmx_out, progress, progress_context) == 0; - if (!ok) { - memzero(cmx_out, 32); - } - +cleanup: + if (!ok) memzero(cmx_out, 32); memzero(msg, sizeof(msg)); memzero(prf_in, sizeof(prf_in)); memzero(prf_out, sizeof(prf_out)); memzero(rcm, sizeof(rcm)); memzero(psi, sizeof(psi)); + memzero(&gd, sizeof(gd)); memzero(&q, sizeof(q)); memzero(&r, sizeof(r)); return ok; } +bool zcash_orchard_compute_cmx_with_progress( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32], + ZcashOrchardProgressCallback progress, void* progress_context) { + return zcash_orchard_family_compute_cmx_with_progress( + receiver, value, rho, rseed, cmx_out, false, progress, progress_context); +} + bool zcash_orchard_compute_cmx( const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32]) { @@ -566,6 +588,21 @@ bool zcash_orchard_compute_cmx( cmx_out, NULL, NULL); } +bool zcash_ironwood_compute_cmx_with_progress( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32], + ZcashOrchardProgressCallback progress, void* progress_context) { + return zcash_orchard_family_compute_cmx_with_progress( + receiver, value, rho, rseed, cmx_out, true, progress, progress_context); +} + +bool zcash_ironwood_compute_cmx( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32]) { + return zcash_ironwood_compute_cmx_with_progress(receiver, value, rho, rseed, + cmx_out, NULL, NULL); +} + bool zcash_derive_orchard_keys_with_progress( const uint8_t* seed, uint32_t seed_len, uint32_t account, ZcashOrchardKeys* keys, ZcashOrchardProgressCallback progress, @@ -696,12 +733,15 @@ bool zcash_derive_orchard_keys(const uint8_t* seed, uint32_t seed_len, NULL, NULL); } -bool zcash_compute_shielded_sighash(const uint8_t header_digest[32], - const uint8_t transparent_digest[32], - const uint8_t sapling_digest[32], - const uint8_t orchard_digest[32], - uint32_t branch_id, - uint8_t sighash_out[32]) { +static bool zcash_compute_shielded_sighash_inner( + const uint8_t header_digest[32], const uint8_t transparent_digest[32], + const uint8_t sapling_digest[32], const uint8_t orchard_digest[32], + const uint8_t* ironwood_digest, uint32_t branch_id, + uint8_t sighash_out[32]) { + if (!header_digest || !transparent_digest || !sapling_digest || + !orchard_digest || !sighash_out) { + return false; + } Hasher h; uint8_t personal[16]; @@ -713,11 +753,36 @@ bool zcash_compute_shielded_sighash(const uint8_t header_digest[32], hasher_Update(&h, transparent_digest, 32); hasher_Update(&h, sapling_digest, 32); hasher_Update(&h, orchard_digest, 32); + if (ironwood_digest) hasher_Update(&h, ironwood_digest, 32); hasher_Final(&h, sighash_out); - + memzero(personal, sizeof(personal)); return true; } +bool zcash_compute_shielded_sighash(const uint8_t header_digest[32], + const uint8_t transparent_digest[32], + const uint8_t sapling_digest[32], + const uint8_t orchard_digest[32], + uint32_t branch_id, + uint8_t sighash_out[32]) { + return zcash_compute_shielded_sighash_inner(header_digest, transparent_digest, + sapling_digest, orchard_digest, + NULL, branch_id, sighash_out); +} + +bool zcash_compute_v6_shielded_sighash(const uint8_t header_digest[32], + const uint8_t transparent_digest[32], + const uint8_t sapling_digest[32], + const uint8_t orchard_digest[32], + const uint8_t ironwood_digest[32], + uint32_t branch_id, + uint8_t sighash_out[32]) { + if (!ironwood_digest) return false; + return zcash_compute_shielded_sighash_inner( + header_digest, transparent_digest, sapling_digest, orchard_digest, + ironwood_digest, branch_id, sighash_out); +} + static void zcash_write_u32_le(uint32_t value, uint8_t out[4]) { out[0] = (uint8_t)(value & 0xff); out[1] = (uint8_t)((value >> 8) & 0xff); @@ -1077,6 +1142,11 @@ ZcashPCZTSigningRequestStatus zcash_pczt_signing_request_status( return ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE; } + if (meta->is_ironwood && + (!meta->has_ironwood_digest || meta->ironwood_digest_size != 32)) { + return ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS; + } + if (meta->has_transparent_digest && meta->transparent_digest_size != 32) { return ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE; } diff --git a/tools/check_pallas_api_boundary.py b/tools/check_pallas_api_boundary.py index d30c4b2ad..db54c6d07 100644 --- a/tools/check_pallas_api_boundary.py +++ b/tools/check_pallas_api_boundary.py @@ -153,10 +153,18 @@ def main(): "interactive Orchard note verification") forbid(output_verification, "zcash_orchard_compute_cmx(", "interactive Orchard note verification") + # Orchard V2 and Ironwood V3 share the public Sinsemilla commitment path; + # only their rcm derivation differs. Keep the expensive implementation in + # one helper, and ensure both interactive wrappers route through it. note_commitment = code_only(function_body( - zcash, "zcash_orchard_compute_cmx_with_progress")) + zcash, "zcash_orchard_family_compute_cmx_with_progress")) require(note_commitment, "pallas_sinsemilla_short_commit_progress", - "Orchard note verification progress") + "Orchard-family note verification progress") + for name in ("zcash_orchard_compute_cmx_with_progress", + "zcash_ironwood_compute_cmx_with_progress"): + wrapper = code_only(function_body(zcash, name)) + require(wrapper, "zcash_orchard_family_compute_cmx_with_progress", + name) derive_rk = code_only(function_body(redpallas, "redpallas_derive_rk")) require(derive_rk, "pallas_ct_add_mod_q", "redpallas_derive_rk") diff --git a/unittests/firmware/zcash.cpp b/unittests/firmware/zcash.cpp index f536554c4..e77b2abc8 100644 --- a/unittests/firmware/zcash.cpp +++ b/unittests/firmware/zcash.cpp @@ -983,6 +983,35 @@ TEST(Zcash, OrchardNoteCommitment_KnownVectorAndProgress) { memzero(tampered, sizeof(tampered)); } +TEST(Zcash, IronwoodNoteCommitment_V3KnownVector) { + const uint8_t recipient[ZCASH_ORCHARD_RAW_RECEIVER_SIZE] = { + 0x3c, 0x15, 0x0e, 0x60, 0x98, 0xb8, 0x61, 0x71, 0x6c, 0xc7, 0xf6, + 0x28, 0x35, 0xf6, 0x9f, 0xeb, 0x30, 0x21, 0x93, 0xc9, 0x26, 0x60, + 0x44, 0x4f, 0x26, 0x62, 0x4f, 0xd1, 0x3e, 0x00, 0xea, 0x7a, 0xc7, + 0x74, 0xcd, 0x55, 0x07, 0x4d, 0x63, 0x67, 0xef, 0xef, 0x37}; + const uint8_t rho[32] = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00}; + const uint8_t rseed[32] = { + 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}; + const uint8_t expected_cmx[32] = { + 0x89, 0x6e, 0xe3, 0x45, 0xd8, 0xb0, 0x40, 0x98, + 0x72, 0x17, 0x25, 0x37, 0x66, 0x6a, 0x48, 0x24, + 0x09, 0x66, 0x1a, 0x22, 0xad, 0x77, 0xc0, 0x98, + 0x96, 0xa3, 0xe7, 0x17, 0x65, 0xf1, 0x86, 0x33}; + + uint8_t cmx[32] = {0}; + ASSERT_TRUE( + zcash_ironwood_compute_cmx(recipient, 12345678, rho, rseed, cmx)); + EXPECT_TRUE(memcmp(cmx, expected_cmx, sizeof(cmx)) == 0); + memzero(cmx, sizeof(cmx)); +} + TEST(Zcash, OrchardReceiverToUnifiedAddress_KnownVector) { const uint8_t recipient[ZCASH_ORCHARD_RAW_RECEIVER_SIZE] = { 0x3c, 0x15, 0x0e, 0x60, 0x98, 0xb8, 0x61, 0x71, 0x6c, 0xc7, 0xf6, @@ -1198,6 +1227,23 @@ TEST(Zcash, PCZTSigningPolicy_RejectsMissingTransactionDigests) { EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); } +TEST(Zcash, PCZTSigningPolicy_RequiresIronwoodDigestForV6Pool) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + meta.is_ironwood = true; + + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS); + + meta.has_ironwood_digest = true; + meta.ironwood_digest_size = 32; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_OK); + + meta.ironwood_digest_size = 31; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS); +} + TEST(Zcash, PCZTSigningPolicy_RejectsMissingPlaintextHeaderFields) { ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); @@ -1447,6 +1493,26 @@ TEST(Zcash, ComputeShieldedSighash_Deterministic) { << "Sighash must be deterministic"; } +TEST(Zcash, ComputeV6ShieldedSighash_KnownVector) { + uint8_t header[32], transparent[32], sapling[32], orchard[32], ironwood[32]; + memset(header, 0x11, sizeof(header)); + memset(transparent, 0x22, sizeof(transparent)); + memset(sapling, 0x33, sizeof(sapling)); + memset(orchard, 0x44, sizeof(orchard)); + memset(ironwood, 0x55, sizeof(ironwood)); + const uint8_t expected[32] = { + 0xdc, 0x07, 0x66, 0x98, 0xdb, 0xe0, 0x8b, 0x6d, + 0xcd, 0x23, 0xf5, 0xa1, 0xb6, 0xbb, 0xae, 0x41, + 0xf7, 0xb1, 0x23, 0xd8, 0xb2, 0x47, 0xf3, 0x88, + 0x7f, 0x7c, 0xa2, 0xbb, 0x68, 0xb5, 0xdc, 0xaa}; + + uint8_t sighash[32] = {0}; + ASSERT_TRUE(zcash_compute_v6_shielded_sighash( + header, transparent, sapling, orchard, ironwood, 0x37a5165b, + sighash)); + EXPECT_TRUE(memcmp(sighash, expected, sizeof(sighash)) == 0); +} + TEST(Zcash, ComputeShieldedSighash_DifferentInputs) { uint8_t header[32], transparent[32], sapling[32], orchard[32]; memset(header, 0x01, 32);