1- # Zcash seed_fingerprint binding tests ( ZIP-32 §6.1) .
1+ # Device-backed tests for ZIP-32 §6.1 seed_fingerprint binding .
22#
3- # Covers:
4- # - calculate_seed_fingerprint() matches the Keystone3 reference vector
5- # (cross-checked against keystone3-firmware
6- # rust/keystore/src/algorithms/zcash/mod.rs::test_keystore_derive_zcash_ufvk).
7- # - ZcashGetOrchardFVK returns the seed_fingerprint.
8- # - The fingerprint is consistent across messages on the same device/seed
9- # (FVK response, ZcashAddress response).
10- # - expected_seed_fingerprint passes when matching, fails when wrong.
11- # - Backward compat: omitting expected_seed_fingerprint still works.
3+ # Pure-Python helper tests live in test_zcash_seed_fingerprint_helper.py
4+ # (no common.KeepKeyTest dependency — runs offline).
125
136import unittest
147import pytest
2417
2518
2619class TestMsgZcashSeedFingerprint (common .KeepKeyTest ):
20+ """Binding behavior on a real device. Wipes/initializes the device."""
2721
2822 def setUp (self ):
2923 super ().setUp ()
3024 self .requires_firmware ("7.15.0" )
3125 self .requires_message ("ZcashGetOrchardFVK" )
3226
33- # ── Pure helper: no device ────────────────────────────────────────
34-
35- def test_helper_reference_vector (self ):
36- """calculate_seed_fingerprint matches the keystone3-firmware vector.
37-
38- seed = 000102...1f, fingerprint =
39- deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3
40- """
41- seed = bytes (range (32 ))
42- fp = calculate_seed_fingerprint (seed )
43- self .assertEqual (
44- fp .hex (),
45- "deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3" ,
46- )
47-
48- def test_helper_rejects_trivial_seeds (self ):
49- with pytest .raises (ValueError ):
50- calculate_seed_fingerprint (b"\x00 " * 32 )
51- with pytest .raises (ValueError ):
52- calculate_seed_fingerprint (b"\xff " * 32 )
53-
54- def test_helper_rejects_out_of_range (self ):
55- with pytest .raises (ValueError ):
56- calculate_seed_fingerprint (b"\x01 " * 31 ) # too short
57- with pytest .raises (ValueError ):
58- calculate_seed_fingerprint (b"\x01 " * 253 ) # too long
59-
60- # ── Device-backed tests ───────────────────────────────────────────
61-
6227 def test_get_orchard_fvk_returns_seed_fingerprint (self ):
6328 """ZcashGetOrchardFVK response now includes a 32-byte seed_fingerprint."""
6429 self .setup_mnemonic_allallall ()
@@ -69,7 +34,7 @@ def test_get_orchard_fvk_returns_seed_fingerprint(self):
6934 )
7035 self .assertTrue (fvk .HasField ("seed_fingerprint" ))
7136 self .assertEqual (len (fvk .seed_fingerprint ), 32 )
72- # Not all zero (defensive: would mean BLAKE2b returned junk)
37+ # Defensive: BLAKE2b should never produce all-zero output for a real seed
7338 self .assertNotEqual (fvk .seed_fingerprint , b"\x00 " * 32 )
7439
7540 def test_fingerprint_stable_across_accounts (self ):
@@ -82,99 +47,104 @@ def test_fingerprint_stable_across_accounts(self):
8247 address_n = [H + 32 , H + 133 , H + 1 ], account = 1 )
8348 self .assertEqual (fvk0 .seed_fingerprint , fvk1 .seed_fingerprint )
8449
85- # ── ZcashDisplayAddress: expected_seed_fingerprint binding ────────
50+ # ── ZcashDisplayAddress: through client.zcash_display_address(...) ──
51+ # These tests exercise the new expected_seed_fingerprint kwarg on the
52+ # public client helper, not just raw protobuf.
8653
87- def test_display_address_accepts_matching_fingerprint (self ):
88- """DisplayAddress with the device's own fingerprint succeeds."""
54+ def test_display_address_helper_accepts_matching_fingerprint (self ):
55+ """Helper passes expected_seed_fingerprint through; matching fp succeeds."""
8956 self .setup_mnemonic_allallall ()
9057
9158 fvk = self .client .zcash_get_orchard_fvk (
9259 address_n = [H + 32 , H + 133 , H + 0 ], account = 0 )
9360
94- resp = self .client .call (
95- zcash_proto .ZcashDisplayAddress (
96- address_n = [H + 32 , H + 133 , H + 0 ],
97- account = 0 ,
98- address = "u1placeholder" ,
99- ak = fvk .ak ,
100- nk = fvk .nk ,
101- rivk = fvk .rivk ,
102- expected_seed_fingerprint = fvk .seed_fingerprint ,
103- )
61+ resp = self .client .zcash_display_address (
62+ address_n = [H + 32 , H + 133 , H + 0 ],
63+ address = "u1placeholder" ,
64+ ak = fvk .ak ,
65+ nk = fvk .nk ,
66+ rivk = fvk .rivk ,
67+ account = 0 ,
68+ expected_seed_fingerprint = fvk .seed_fingerprint ,
10469 )
10570 self .assertIsInstance (resp , zcash_proto .ZcashAddress )
106- # Response also returns the device's seed_fingerprint
10771 self .assertTrue (resp .HasField ("seed_fingerprint" ))
10872 self .assertEqual (resp .seed_fingerprint , fvk .seed_fingerprint )
10973
110- def test_display_address_rejects_wrong_fingerprint (self ):
111- """DisplayAddress with a wrong fingerprint is rejected before display ."""
74+ def test_display_address_helper_rejects_wrong_fingerprint (self ):
75+ """Helper passes expected_seed_fingerprint through; wrong fp rejected."""
11276 self .setup_mnemonic_allallall ()
11377
11478 fvk = self .client .zcash_get_orchard_fvk (
11579 address_n = [H + 32 , H + 133 , H + 0 ], account = 0 )
11680
117- # Flip one byte to fabricate a non-matching fingerprint
11881 bad = bytearray (fvk .seed_fingerprint )
11982 bad [0 ] ^= 0xFF
12083
12184 with pytest .raises (CallException ):
122- self .client .call (
123- zcash_proto .ZcashDisplayAddress (
124- address_n = [H + 32 , H + 133 , H + 0 ],
125- account = 0 ,
126- address = "u1placeholder" ,
127- ak = fvk .ak ,
128- nk = fvk .nk ,
129- rivk = fvk .rivk ,
130- expected_seed_fingerprint = bytes (bad ),
131- )
85+ self .client .zcash_display_address (
86+ address_n = [H + 32 , H + 133 , H + 0 ],
87+ address = "u1placeholder" ,
88+ ak = fvk .ak ,
89+ nk = fvk .nk ,
90+ rivk = fvk .rivk ,
91+ account = 0 ,
92+ expected_seed_fingerprint = bytes (bad ),
13293 )
13394
134- def test_display_address_backward_compat_no_fingerprint (self ):
135- """Omitting expected_seed_fingerprint still works (existing flow)."""
95+ def test_display_address_helper_backward_compat (self ):
96+ """Helper without expected_seed_fingerprint still works (existing flow)."""
13697 self .setup_mnemonic_allallall ()
13798
13899 fvk = self .client .zcash_get_orchard_fvk (
139100 address_n = [H + 32 , H + 133 , H + 0 ], account = 0 )
140101
141- resp = self .client .call (
142- zcash_proto .ZcashDisplayAddress (
143- address_n = [H + 32 , H + 133 , H + 0 ],
144- account = 0 ,
145- address = "u1placeholder" ,
146- ak = fvk .ak ,
147- nk = fvk .nk ,
148- rivk = fvk .rivk ,
149- )
102+ resp = self .client .zcash_display_address (
103+ address_n = [H + 32 , H + 133 , H + 0 ],
104+ address = "u1placeholder" ,
105+ ak = fvk .ak ,
106+ nk = fvk .nk ,
107+ rivk = fvk .rivk ,
108+ account = 0 ,
150109 )
151110 self .assertIsInstance (resp , zcash_proto .ZcashAddress )
152- # Device still populates seed_fingerprint on responses regardless
111+ # Device populates seed_fingerprint on responses regardless of request
153112 self .assertTrue (resp .HasField ("seed_fingerprint" ))
154113 self .assertEqual (resp .seed_fingerprint , fvk .seed_fingerprint )
155114
156- # ── ZcashSignPCZT: expected_seed_fingerprint binding ──────────────
115+ def test_device_fingerprint_matches_python_helper (self ):
116+ """Cross-check: device-derived fingerprint == calculate_seed_fingerprint(seed)
117+ for the all-allallall mnemonic seed. Ties firmware C and python-keepkey
118+ helper to the same byte-for-byte output."""
119+ self .setup_mnemonic_allallall ()
120+
121+ fvk = self .client .zcash_get_orchard_fvk (
122+ address_n = [H + 32 , H + 133 , H + 0 ], account = 0 )
157123
158- def test_sign_pczt_rejects_wrong_fingerprint (self ):
159- """SignPCZT with wrong fingerprint is rejected before any signing."""
124+ # all-all-all mnemonic, empty passphrase, BIP-39 seed
125+ from mnemonic import Mnemonic
126+ seed = Mnemonic .to_seed ("all all all all all all all all all all all all" , "" )
127+ expected_fp = calculate_seed_fingerprint (seed )
128+ self .assertEqual (fvk .seed_fingerprint , expected_fp )
129+
130+ # ── ZcashSignPCZT: through client.zcash_sign_pczt(...) ──────────────
131+
132+ def test_sign_pczt_helper_rejects_wrong_fingerprint (self ):
133+ """Helper passes expected_seed_fingerprint through; wrong fp rejected
134+ before any signing crypto runs."""
160135 self .setup_mnemonic_allallall ()
161136
162- # Fabricate a fingerprint that's clearly not this seed's.
163137 wrong_fp = b"\x01 " * 32
164138
165- # Minimal action — won't actually sign because we expect rejection
166- # at the seed-fingerprint check before any key derivation.
167139 with pytest .raises (CallException ):
168- self .client .call (
169- zcash_proto .ZcashSignPCZT (
170- address_n = [H + 32 , H + 133 , H + 0 ],
171- account = 0 ,
172- n_actions = 1 ,
173- total_amount = 100000 ,
174- fee = 10000 ,
175- branch_id = 0x37519621 ,
176- expected_seed_fingerprint = wrong_fp ,
177- )
140+ self .client .zcash_sign_pczt (
141+ address_n = [H + 32 , H + 133 , H + 0 ],
142+ actions = [{}], # placeholder — won't be reached past the fp check
143+ account = 0 ,
144+ total_amount = 100000 ,
145+ fee = 10000 ,
146+ branch_id = 0x37519621 ,
147+ expected_seed_fingerprint = wrong_fp ,
178148 )
179149
180150
0 commit comments