Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 19 additions & 37 deletions cktap-swift/Sources/CKTap/cktap_ffi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1780,8 +1780,7 @@ public func FfiConverterTypeTapSignerStatus_lower(_ value: TapSignerStatus) -> R
/**
* Errors returned by the CkTap card.
*/
public
enum CardError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum CardError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -1914,8 +1913,7 @@ public func FfiConverterTypeCardError_lower(_ value: CardError) -> RustBuffer {
/**
* Errors returned by the `certs` command.
*/
public
enum CertsError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum CertsError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2012,8 +2010,7 @@ public func FfiConverterTypeCertsError_lower(_ value: CertsError) -> RustBuffer
/**
* Errors returned by the `change` command.
*/
public
enum ChangeError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum ChangeError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2112,7 +2109,8 @@ public func FfiConverterTypeChangeError_lower(_ value: ChangeError) -> RustBuffe
return FfiConverterTypeChangeError.lower(value)
}


// Note that we don't yet support `indirect` for enums.
// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.

public enum CkTapCard {

Expand Down Expand Up @@ -2198,8 +2196,7 @@ public func FfiConverterTypeCkTapCard_lower(_ value: CkTapCard) -> RustBuffer {
/**
* Errors returned by the card, CBOR deserialization or value encoding, or the APDU transport.
*/
public
enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum CkTapError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2312,8 +2309,7 @@ public func FfiConverterTypeCkTapError_lower(_ value: CkTapError) -> RustBuffer
/**
* Errors returned by the `derive` command.
*/
public
enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum DeriveError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2410,8 +2406,7 @@ public func FfiConverterTypeDeriveError_lower(_ value: DeriveError) -> RustBuffe
/**
* Errors returned by the `dump` command.
*/
public
enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum DumpError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2530,8 +2525,7 @@ public func FfiConverterTypeDumpError_lower(_ value: DumpError) -> RustBuffer {
}


public
enum KeyError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum KeyError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2618,8 +2612,7 @@ public func FfiConverterTypeKeyError_lower(_ value: KeyError) -> RustBuffer {
/**
* Errors returned by the `read` command.
*/
public
enum ReadError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum ReadError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2703,8 +2696,7 @@ public func FfiConverterTypeReadError_lower(_ value: ReadError) -> RustBuffer {
}


public
enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum SignPsbtError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2891,8 +2883,7 @@ public func FfiConverterTypeSignPsbtError_lower(_ value: SignPsbtError) -> RustB
/**
* Errors returned by the `status` command.
*/
public
enum StatusError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum StatusError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -2979,8 +2970,7 @@ public func FfiConverterTypeStatusError_lower(_ value: StatusError) -> RustBuffe
/**
* Errors returned by the `unseal` command.
*/
public
enum UnsealError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum UnsealError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -3067,8 +3057,7 @@ public func FfiConverterTypeUnsealError_lower(_ value: UnsealError) -> RustBuffe
/**
* Errors returned by the `xpub` command.
*/
public
enum XpubError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
public enum XpubError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {



Expand Down Expand Up @@ -3167,8 +3156,9 @@ fileprivate struct UniffiCallbackInterfaceCkTransport {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
//
// Store the vtable directly.
static let vtable: UniffiVTableCallbackInterfaceCkTransport = UniffiVTableCallbackInterfaceCkTransport(
// This creates 1-element array, since this seems to be the only way to construct a const
// pointer that we can pass to the Rust code.
static let vtable: [UniffiVTableCallbackInterfaceCkTransport] = [UniffiVTableCallbackInterfaceCkTransport(
uniffiFree: { (uniffiHandle: UInt64) -> () in
do {
try FfiConverterCallbackInterfaceCkTransport.handleMap.remove(handle: uniffiHandle)
Expand Down Expand Up @@ -3226,19 +3216,11 @@ fileprivate struct UniffiCallbackInterfaceCkTransport {
droppedCallback: uniffiOutDroppedCallback
)
}
)

// Rust stores this pointer for future callback invocations, so it must live
// for the process lifetime (not just for the init function call).
static let vtablePtr: UnsafePointer<UniffiVTableCallbackInterfaceCkTransport> = {
let ptr = UnsafeMutablePointer<UniffiVTableCallbackInterfaceCkTransport>.allocate(capacity: 1)
ptr.initialize(to: vtable)
return UnsafePointer(ptr)
}()
)]
}

private func uniffiCallbackInitCkTransport() {
uniffi_cktap_ffi_fn_init_callback_vtable_cktransport(UniffiCallbackInterfaceCkTransport.vtablePtr)
uniffi_cktap_ffi_fn_init_callback_vtable_cktransport(UniffiCallbackInterfaceCkTransport.vtable)
}

// FfiConverter protocol for callback interfaces
Expand Down
76 changes: 61 additions & 15 deletions lib/src/tap_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ pub trait TapSignerShared: Authentication {
// set most significant bit to 1 to represent hardened path steps
let path = path.iter().map(|p| p ^ (1 << 31)).collect::<Vec<_>>();
let app_nonce = crate::rand_nonce();
// capture card_nonce BEFORE transmit — the card signs with this nonce,
// and the response contains a NEW nonce for the next command
let card_nonce = *self.card_nonce();
let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, DeriveCommand::name());
let cmd = DeriveCommand::for_tapsigner(app_nonce, path, epubkey, xcvc);
let derive_response: DeriveResponse = transmit(self.transport(), &cmd).await?;
Expand All @@ -261,25 +264,26 @@ pub trait TapSignerShared: Authentication {
None => master_pubkey,
};

// TODO FIX currently signature validation only works if no derivation path is used
if pubkey == master_pubkey {
let card_nonce = self.card_nonce();
let sig = &derive_response.sig;
// Verify signature: the TAPSIGNER signs with the DERIVED private key
// (or the master private key when the path is empty, in which case
// `pubkey` falls back to `master_pubkey` above).
// Message: "OPENDIME" || card_nonce (pre-command) || app_nonce || chain_code
let sig = &derive_response.sig;

let mut message_bytes: Vec<u8> = Vec::new();
message_bytes.extend("OPENDIME".as_bytes());
message_bytes.extend(card_nonce);
message_bytes.extend(app_nonce);
message_bytes.extend(&derive_response.chain_code);
let mut message_bytes: Vec<u8> = Vec::new();
message_bytes.extend("OPENDIME".as_bytes());
message_bytes.extend(card_nonce);
message_bytes.extend(app_nonce);
message_bytes.extend(&derive_response.chain_code);

let message_bytes_hash = sha256::Hash::hash(message_bytes.as_slice());
let message = Message::from_digest(message_bytes_hash.to_byte_array());
let message_bytes_hash = sha256::Hash::hash(message_bytes.as_slice());
let message = Message::from_digest(message_bytes_hash.to_byte_array());

let signature = Signature::from_compact(sig)?;
let signature = Signature::from_compact(sig)?;

self.secp()
.verify_ecdsa(&message, &signature, &pubkey.inner)?;

self.secp()
.verify_ecdsa(&message, &signature, &master_pubkey.inner)?;
}
Ok(pubkey)
}

Expand Down Expand Up @@ -434,4 +438,46 @@ mod test {
}
drop(python);
}

// Regression test for the signature verification fix:
// `derive` with a non-empty path must still cryptographically verify the
// response using the master pubkey and the card_nonce captured BEFORE the
// transmit. If either bug regressed, this call would fail with
// DeriveError::Secp (signature verification failed).
#[tokio::test]
async fn test_tap_signer_derive_with_path() {
let card_type = CardTypeOption::TapSigner;
let pipe_path = "/tmp/test-tapsigner-derive-path-pipe";
let pipe_path = Path::new(&pipe_path);
let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
let emulator = find_emulator(pipe_path).await.unwrap();
if let CkTapCard::TapSigner(mut ts) = emulator {
ts.init(rand_chaincode(), "123456").await.unwrap();
// BIP84 prefix m/84'/0'/0' — non-empty path so the card returns a
// derived pubkey different from the master pubkey. Pre-fix, this
// branch skipped verification entirely.
let derived_pubkey = ts.derive(vec![84, 0, 0], "123456").await.unwrap();
// Sanity check: derivation succeeded and returned a valid pubkey
assert_eq!(derived_pubkey.inner.serialize().len(), 33);
}
drop(python);
}

// Regression test ensuring the empty-path case (pubkey == master_pubkey)
// also still works after removing the `if pubkey == master_pubkey` guard.
#[tokio::test]
async fn test_tap_signer_derive_empty_path() {
let card_type = CardTypeOption::TapSigner;
let pipe_path = "/tmp/test-tapsigner-derive-empty-pipe";
let pipe_path = Path::new(&pipe_path);
let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
let emulator = find_emulator(pipe_path).await.unwrap();
if let CkTapCard::TapSigner(mut ts) = emulator {
ts.init(rand_chaincode(), "123456").await.unwrap();
// Empty path — card returns pubkey == master_pubkey (None in response)
let master_pubkey = ts.derive(vec![], "123456").await.unwrap();
assert_eq!(master_pubkey.inner.serialize().len(), 33);
}
drop(python);
}
}
Loading