@@ -264,8 +264,9 @@ pub trait TapSignerShared: Authentication {
264264 None => master_pubkey,
265265 } ;
266266
267- // Verify signature: per the Coinkite protocol the card always signs with
268- // the master private key, regardless of whether a derivation path was used.
267+ // Verify signature: the TAPSIGNER signs with the DERIVED private key
268+ // (or the master private key when the path is empty, in which case
269+ // `pubkey` falls back to `master_pubkey` above).
269270 // Message: "OPENDIME" || card_nonce (pre-command) || app_nonce || chain_code
270271 let sig = & derive_response. sig ;
271272
@@ -281,7 +282,7 @@ pub trait TapSignerShared: Authentication {
281282 let signature = Signature :: from_compact ( sig) ?;
282283
283284 self . secp ( )
284- . verify_ecdsa ( & message, & signature, & master_pubkey . inner ) ?;
285+ . verify_ecdsa ( & message, & signature, & pubkey . inner ) ?;
285286
286287 Ok ( pubkey)
287288 }
@@ -437,4 +438,46 @@ mod test {
437438 }
438439 drop ( python) ;
439440 }
441+
442+ // Regression test for the signature verification fix:
443+ // `derive` with a non-empty path must still cryptographically verify the
444+ // response using the master pubkey and the card_nonce captured BEFORE the
445+ // transmit. If either bug regressed, this call would fail with
446+ // DeriveError::Secp (signature verification failed).
447+ #[ tokio:: test]
448+ async fn test_tap_signer_derive_with_path ( ) {
449+ let card_type = CardTypeOption :: TapSigner ;
450+ let pipe_path = "/tmp/test-tapsigner-derive-path-pipe" ;
451+ let pipe_path = Path :: new ( & pipe_path) ;
452+ let python = EcardSubprocess :: new ( pipe_path, & card_type) . unwrap ( ) ;
453+ let emulator = find_emulator ( pipe_path) . await . unwrap ( ) ;
454+ if let CkTapCard :: TapSigner ( mut ts) = emulator {
455+ ts. init ( rand_chaincode ( ) , "123456" ) . await . unwrap ( ) ;
456+ // BIP84 prefix m/84'/0'/0' — non-empty path so the card returns a
457+ // derived pubkey different from the master pubkey. Pre-fix, this
458+ // branch skipped verification entirely.
459+ let derived_pubkey = ts. derive ( vec ! [ 84 , 0 , 0 ] , "123456" ) . await . unwrap ( ) ;
460+ // Sanity check: derivation succeeded and returned a valid pubkey
461+ assert_eq ! ( derived_pubkey. inner. serialize( ) . len( ) , 33 ) ;
462+ }
463+ drop ( python) ;
464+ }
465+
466+ // Regression test ensuring the empty-path case (pubkey == master_pubkey)
467+ // also still works after removing the `if pubkey == master_pubkey` guard.
468+ #[ tokio:: test]
469+ async fn test_tap_signer_derive_empty_path ( ) {
470+ let card_type = CardTypeOption :: TapSigner ;
471+ let pipe_path = "/tmp/test-tapsigner-derive-empty-pipe" ;
472+ let pipe_path = Path :: new ( & pipe_path) ;
473+ let python = EcardSubprocess :: new ( pipe_path, & card_type) . unwrap ( ) ;
474+ let emulator = find_emulator ( pipe_path) . await . unwrap ( ) ;
475+ if let CkTapCard :: TapSigner ( mut ts) = emulator {
476+ ts. init ( rand_chaincode ( ) , "123456" ) . await . unwrap ( ) ;
477+ // Empty path — card returns pubkey == master_pubkey (None in response)
478+ let master_pubkey = ts. derive ( vec ! [ ] , "123456" ) . await . unwrap ( ) ;
479+ assert_eq ! ( master_pubkey. inner. serialize( ) . len( ) , 33 ) ;
480+ }
481+ drop ( python) ;
482+ }
440483}
0 commit comments