diff --git a/cktap-swift/Sources/CKTap/cktap_ffi.swift b/cktap-swift/Sources/CKTap/cktap_ffi.swift index f117c34..588dca4 100644 --- a/cktap-swift/Sources/CKTap/cktap_ffi.swift +++ b/cktap-swift/Sources/CKTap/cktap_ffi.swift @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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) @@ -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 = { - let ptr = UnsafeMutablePointer.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 diff --git a/lib/src/tap_signer.rs b/lib/src/tap_signer.rs index 2ad50d9..5c7ce14 100644 --- a/lib/src/tap_signer.rs +++ b/lib/src/tap_signer.rs @@ -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::>(); 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?; @@ -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 = 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 = 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) } @@ -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); + } }