Skip to content

fix: verify derive signature for all derivation paths and update Swift bindings#67

Draft
r1b2ns wants to merge 3 commits intobitcoindevkit:masterfrom
r1b2ns:fix/signature-derivation-path
Draft

fix: verify derive signature for all derivation paths and update Swift bindings#67
r1b2ns wants to merge 3 commits intobitcoindevkit:masterfrom
r1b2ns:fix/signature-derivation-path

Conversation

@r1b2ns
Copy link
Copy Markdown
Contributor

@r1b2ns r1b2ns commented Apr 13, 2026

Description

  • Fix critical security bug where TapSigner derive response signature was not verified when a derivation path was provided
  • Fix incorrect card_nonce usage — was using the post-command nonce instead of the pre-command nonce for verification
  • Regenerate Swift FFI bindings with latest uniffi-rs

Notes to the reviewers

Signature verification (security fix)

In TapSignerShared::derive(), the response signature was only verified when
pubkey == master_pubkey (i.e., no derivation path). When a path was provided,
the card returns a derived pubkey different from master, causing the if guard
to skip verification entirely.

Per the Coinkite Tap Protocol
and the reference Python implementation (verify_master_pubkey in cktap/utils.py),
the card always signs with the master private key, regardless of whether a
derivation path was used. The verification is now unconditional.

Nonce ordering fix

The card_nonce used in message construction was captured after set_card_nonce()
updated it with the response nonce (intended for the next command). The card signs
with the nonce that existed before the command. This matches the pattern already
used correctly in SatsCard::derive().

Tests report

Screenshot 2026-04-14 at 10 46 22
  • Additionally, I’ve run tests using cktap-swift in SatsBuddy, and it works as expected

Issues

#23 Fix verifying the sig when a derivation path is used

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

r1b2ns added 2 commits April 13, 2026 11:02
The derive response signature was only verified when no derivation path
was used (pubkey == master_pubkey). Per the Coinkite protocol, the card
always signs with the master private key regardless of path, so the
verification must run unconditionally.

Also fixes a bug where card_nonce was captured AFTER transmit instead of
BEFORE — the card signs with the pre-command nonce, but the code was
using the new nonce from the response.
@r1b2ns r1b2ns marked this pull request as ready for review April 14, 2026 11:46
@r1b2ns r1b2ns changed the title [draft] fix: verify derive signature for all derivation paths and update Swift bindings fix: verify derive signature for all derivation paths and update Swift bindings Apr 14, 2026
@notmandatory
Copy link
Copy Markdown
Member

Does this fix #23? If so please reference that in the description. Also can you add a unit test to verify the behavior with and without derivation paths?

@notmandatory notmandatory added the bug Something isn't working label Apr 20, 2026
@notmandatory notmandatory added this to the Release 0.3.0 milestone Apr 20, 2026
@notmandatory
Copy link
Copy Markdown
Member

This looks like the right way to fix it but I think it still doesn't work with real TAPSIGNER cards. I just tried it with one of mine and it says the signature fails. Do you have a real card to test with?

$ just run derive -p 84,0,0

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/cktap-cli derive -p 84,0,0`
Enter cvc: 
[cli/src/main.rs:233:21] &ts.derive(path.unwrap_or_default(), &cvc()).await = Err(
    Secp256k1(
        IncorrectSignature,
    ),
)

$ just run derive          

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/cktap-cli derive`
Enter cvc: 
[cli/src/main.rs:233:21] &ts.derive(path.unwrap_or_default(), &cvc()).await = Ok(
    PublicKey {
        compressed: true,
        inner: PublicKey(
            f96ecf9fecd902bf724692ba340dc59200d45717552f94d62f8749f27b0922dd2f72c81368cc3f8c9e258a6a660a89d2f90825acc5ef91ff0938224d28dd6334,
        ),
    },
)

@notmandatory notmandatory removed this from the Release 0.3.0 milestone Apr 20, 2026
@r1b2ns
Copy link
Copy Markdown
Contributor Author

r1b2ns commented Apr 22, 2026

This looks like the right way to fix it but I think it still doesn't work with real TAPSIGNER cards. I just tried it with one of mine and it says the signature fails. Do you have a real card to test with?

$ just run derive -p 84,0,0

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/cktap-cli derive -p 84,0,0`
Enter cvc: 
[cli/src/main.rs:233:21] &ts.derive(path.unwrap_or_default(), &cvc()).await = Err(
    Secp256k1(
        IncorrectSignature,
    ),
)

$ just run derive          

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/cktap-cli derive`
Enter cvc: 
[cli/src/main.rs:233:21] &ts.derive(path.unwrap_or_default(), &cvc()).await = Ok(
    PublicKey {
        compressed: true,
        inner: PublicKey(
            f96ecf9fecd902bf724692ba340dc59200d45717552f94d62f8749f27b0922dd2f72c81368cc3f8c9e258a6a660a89d2f90825acc5ef91ff0938224d28dd6334,
        ),
    },
)

Unfortunately, I haven’t, but I’m looking for a card to test it better. I’ll keep it as a draft until I can test it properly. I’ve only tested it with a Satscard by compiling the library to Swift and testing it on iOS.

@r1b2ns r1b2ns marked this pull request as draft April 22, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants