From 2c54cdbc9fe923a66265b93f7a14cd4ef925c647 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 15:35:07 -0500 Subject: [PATCH 1/7] feat: add 'nfc' command to lib --- README.md | 2 +- lib/src/apdu.rs | 2 +- lib/src/sats_card.rs | 5 ++++- lib/src/sats_chip.rs | 5 ++++- lib/src/shared.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ lib/src/tap_signer.rs | 5 ++++- 6 files changed, 58 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 921c7ca..7b5ccaf 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ It is up to the crate user to send and receive the raw cktap APDU messages via N - [ ] response verification - [x] [certs](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#certs) - [x] [new](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#new) -- [ ] [nfc](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#nfc) +- [x] [nfc](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#nfc) - [x] [sign](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#sign) - [x] response verification - [x] [wait](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#wait) diff --git a/lib/src/apdu.rs b/lib/src/apdu.rs index f743b69..7fd233e 100644 --- a/lib/src/apdu.rs +++ b/lib/src/apdu.rs @@ -468,7 +468,7 @@ impl CommandApdu for NfcCommand { #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct NfcResponse { /// command result - url: String, + pub url: String, } impl ResponseApdu for NfcResponse {} diff --git a/lib/src/sats_card.rs b/lib/src/sats_card.rs index c9318c5..f74b63a 100644 --- a/lib/src/sats_card.rs +++ b/lib/src/sats_card.rs @@ -8,7 +8,7 @@ use crate::apdu::{ }; use crate::error::{CardError, DeriveError, DumpError, ReadError, UnsealError}; use crate::error::{SignPsbtError, StatusError}; -use crate::shared::{Authentication, Certificate, CkTransport, Read, Wait, transmit}; +use crate::shared::{Authentication, Certificate, CkTransport, Nfc, Read, Wait, transmit}; use async_trait::async_trait; use bitcoin::bip32::{ChainCode, DerivationPath, Fingerprint, Xpub}; use bitcoin::secp256k1; @@ -422,6 +422,9 @@ impl Certificate for SatsCard { } } +#[async_trait] +impl Nfc for SatsCard {} + impl core::fmt::Debug for SatsCard { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("SatsCard") diff --git a/lib/src/sats_chip.rs b/lib/src/sats_chip.rs index 8f40f87..591f251 100644 --- a/lib/src/sats_chip.rs +++ b/lib/src/sats_chip.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use crate::apdu::StatusResponse; use crate::error::{ReadError, StatusError}; -use crate::shared::{Authentication, Certificate, CkTransport, Read, Wait}; +use crate::shared::{Authentication, Certificate, CkTransport, Nfc, Read, Wait}; use crate::tap_signer::TapSignerShared; /// - SATSCHIP model: this product variant is a TAPSIGNER in all respects, @@ -110,6 +110,9 @@ impl Certificate for SatsChip { } } +#[async_trait] +impl Nfc for SatsChip {} + impl core::fmt::Debug for SatsChip { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("SatsChip") diff --git a/lib/src/shared.rs b/lib/src/shared.rs index 8e3cd64..2267435 100644 --- a/lib/src/shared.rs +++ b/lib/src/shared.rs @@ -299,6 +299,15 @@ pub trait Certificate: Read { async fn slot_pubkey(&mut self) -> Result, ReadError>; } +#[async_trait] +pub trait Nfc: Authentication { + async fn nfc(&mut self) -> Result { + let nfc_cmd = NfcCommand::default(); + let nfc_response: NfcResponse = transmit(self.transport(), &nfc_cmd).await?; + Ok(nfc_response.url) + } +} + #[cfg(feature = "emulator")] #[cfg(test)] mod tests { @@ -383,4 +392,39 @@ mod tests { drop(python); } } + + #[tokio::test] + async fn test_nfc_command() { + for card_type in CardTypeOption::values() { + let pipe_path = format!("/tmp/test-nfc-command-pipe{card_type}"); + let pipe_path = Path::new(&pipe_path); + let python = EcardSubprocess::new(pipe_path, &card_type).unwrap(); + let emulator = find_emulator(pipe_path).await.unwrap(); + match emulator { + CkTapCard::SatsCard(mut sc) => { + assert_eq!(card_type, CardTypeOption::SatsCard); + let response = sc.nfc().await; + assert!(response.is_ok()); + assert!( + response + .unwrap() + .starts_with("https://getsatscard.com/start") + ) + } + CkTapCard::TapSigner(mut ts) => { + assert_eq!(card_type, CardTypeOption::TapSigner); + let response = ts.nfc().await; + assert!(response.is_ok()); + assert!(response.unwrap().starts_with("https://tapsigner.com/start")) + } + CkTapCard::SatsChip(mut sc) => { + assert_eq!(card_type, CardTypeOption::SatsChip); + let response = sc.nfc().await; + assert!(response.is_ok()); + assert!(response.unwrap().starts_with("https://satschip.com/start")) + } + }; + drop(python); + } + } } diff --git a/lib/src/tap_signer.rs b/lib/src/tap_signer.rs index e153101..daa07ab 100644 --- a/lib/src/tap_signer.rs +++ b/lib/src/tap_signer.rs @@ -7,7 +7,7 @@ use crate::apdu::{ tap_signer::{BackupCommand, BackupResponse, ChangeCommand, ChangeResponse}, }; use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusError}; -use crate::shared::{Authentication, Certificate, CkTransport, Read, Wait, transmit}; +use crate::shared::{Authentication, Certificate, CkTransport, Nfc, Read, Wait, transmit}; use crate::{BIP32_HARDENED_MASK, CkTapError}; use async_trait::async_trait; use bitcoin::PublicKey; @@ -316,6 +316,9 @@ pub trait TapSignerShared: Authentication { } } +#[async_trait] +impl Nfc for TapSigner {} + impl TapSignerShared for TapSigner {} impl TapSigner { From a06b87eedf8ea3388c35b8b24af55155711b86a1 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 15:43:50 -0500 Subject: [PATCH 2/7] feat: add 'nfc' command to cli --- cli/src/main.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index a265649..f57a139 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -9,7 +9,7 @@ use rust_cktap::emulator; use rust_cktap::error::{DumpError, StatusError, UnsealError}; #[cfg(not(feature = "emulator"))] use rust_cktap::pcsc; -use rust_cktap::shared::{Authentication, Read, Wait}; +use rust_cktap::shared::{Authentication, Nfc, Read, Wait}; use rust_cktap::tap_signer::TapSignerShared; use rust_cktap::{ CkTapCard, CkTapError, Psbt, PsbtParseError, SignPsbtError, rand_chaincode, shared::Certificate, @@ -74,6 +74,8 @@ enum SatsCardCommand { }, /// Call wait command until no auth delay Wait, + /// Get the card's nfc URL + Nfc, } /// TapSigner CLI @@ -110,6 +112,8 @@ enum TapSignerCommand { Sign { to_sign: String }, /// Call wait command until no auth delay Wait, + /// Get the card's nfc URL + Nfc, } /// TapSigner CLI @@ -144,6 +148,8 @@ enum SatsChipCommand { Sign { to_sign: String }, /// Call wait command until no auth delay Wait, + /// Get the card's nfc URL + Nfc, } #[tokio::main] @@ -192,6 +198,7 @@ async fn main() -> Result<(), CliError> { dbg!(response); } SatsCardCommand::Wait => wait(sc).await, + SatsCardCommand::Nfc => nfc(sc).await, } } CkTapCard::TapSigner(ts) => { @@ -230,6 +237,7 @@ async fn main() -> Result<(), CliError> { println!("{response:?}"); } TapSignerCommand::Wait => wait(ts).await, + TapSignerCommand::Nfc => nfc(ts).await, } } CkTapCard::SatsChip(sc) => { @@ -261,6 +269,7 @@ async fn main() -> Result<(), CliError> { println!("{response:?}"); } SatsChipCommand::Wait => wait(sc).await, + SatsChipCommand::Nfc => nfc(sc).await, } } } @@ -319,3 +328,11 @@ where } println!("No auth delay."); } + +async fn nfc(card: &mut C) +where + C: Nfc + Send, +{ + let nfc = card.nfc().await.expect("nfc failed"); + println!("{nfc}"); +} From cd03eee4bad4067b11131a70bb9f5a50ea42edce Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 15:55:37 -0500 Subject: [PATCH 3/7] feat: add 'nfc' command to ffi bindings --- cktap-ffi/src/sats_card.rs | 8 +++++++- cktap-ffi/src/sats_chip.rs | 8 +++++++- cktap-ffi/src/tap_signer.rs | 8 +++++++- cktap-swift/Tests/CKTapTests/CKTapTests.swift | 15 +++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/cktap-ffi/src/sats_card.rs b/cktap-ffi/src/sats_card.rs index f1b9e25..c84dda0 100644 --- a/cktap-ffi/src/sats_card.rs +++ b/cktap-ffi/src/sats_card.rs @@ -6,7 +6,7 @@ use crate::error::{ }; use crate::{ChainCode, PrivateKey, Psbt, PublicKey, check_cert, read}; use futures::lock::Mutex; -use rust_cktap::shared::{Authentication, Wait}; +use rust_cktap::shared::{Authentication, Nfc, Wait}; use std::sync::Arc; #[derive(uniffi::Object)] @@ -127,4 +127,10 @@ impl SatsCard { .map(|psbt| Psbt { inner: psbt })?; Ok(psbt) } + + pub async fn nfc(&self) -> Result { + let mut card = self.0.lock().await; + let url = card.nfc().await?; + Ok(url) + } } diff --git a/cktap-ffi/src/sats_chip.rs b/cktap-ffi/src/sats_chip.rs index ac107e6..d8ea6ae 100644 --- a/cktap-ffi/src/sats_chip.rs +++ b/cktap-ffi/src/sats_chip.rs @@ -5,7 +5,7 @@ use crate::error::{CertsError, ChangeError, CkTapError, DeriveError, ReadError, use crate::tap_signer::{change, derive, init, sign_psbt}; use crate::{ChainCode, Psbt, PublicKey, check_cert, read}; use futures::lock::Mutex; -use rust_cktap::shared::{Authentication, Wait}; +use rust_cktap::shared::{Authentication, Nfc, Wait}; use std::sync::Arc; #[derive(uniffi::Object)] @@ -79,4 +79,10 @@ impl SatsChip { change(&mut *card, new_cvc, cvc).await?; Ok(()) } + + pub async fn nfc(&self) -> Result { + let mut card = self.0.lock().await; + let url = card.nfc().await?; + Ok(url) + } } diff --git a/cktap-ffi/src/tap_signer.rs b/cktap-ffi/src/tap_signer.rs index e46a4e3..e0a33e4 100644 --- a/cktap-ffi/src/tap_signer.rs +++ b/cktap-ffi/src/tap_signer.rs @@ -4,7 +4,7 @@ use crate::error::{CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError}; use crate::{ChainCode, Psbt, PublicKey, check_cert, read}; use futures::lock::Mutex; -use rust_cktap::shared::{Authentication, Wait}; +use rust_cktap::shared::{Authentication, Nfc, Wait}; use rust_cktap::tap_signer::TapSignerShared; use std::sync::Arc; @@ -81,6 +81,12 @@ impl TapSigner { change(&mut *card, new_cvc, cvc).await?; Ok(()) } + + pub async fn nfc(&self) -> Result { + let mut card = self.0.lock().await; + let url = card.nfc().await?; + Ok(url) + } } pub async fn init( diff --git a/cktap-swift/Tests/CKTapTests/CKTapTests.swift b/cktap-swift/Tests/CKTapTests/CKTapTests.swift index 649a515..605f379 100644 --- a/cktap-swift/Tests/CKTapTests/CKTapTests.swift +++ b/cktap-swift/Tests/CKTapTests/CKTapTests.swift @@ -29,4 +29,19 @@ final class CKTapTests: XCTestCase { XCTAssertEqual(status.ver, "1.0.3") } } + func testNfcUrl() async throws { + let cardEmulator = CardEmulator() + let card = try await toCktap(transport: cardEmulator) + switch card { + case .satsCard(let satsCard): + let url: String = try await satsCard.nfc() + print("SatsCard url: \(url)") + case .tapSigner(let tapSigner): + let url: String = try await tapSigner.nfc() + print("TapSigner url: \(url)") + case .satsChip(let satsChip): + let url: String = try await satsChip.nfc() + print("SatsChip url: \(url)") + } + } } From edcae83097ca796fe44d445d19cdbc26328a984a Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 16:57:55 -0500 Subject: [PATCH 4/7] feat: add 'xpub' command to lib --- README.md | 2 +- lib/src/apdu/tap_signer.rs | 8 ++------ lib/src/error.rs | 9 ++++++++ lib/src/tap_signer.rs | 42 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7b5ccaf..0a28aee 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ It is up to the crate user to send and receive the raw cktap APDU messages via N #### TAPSIGNER-Only Commands - [x] [change](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#change) -- [ ] [xpub](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#xpub) +- [x] [xpub](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#xpub) - [x] [backup](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#backup) ### Automated and CLI Testing with Emulator diff --git a/lib/src/apdu/tap_signer.rs b/lib/src/apdu/tap_signer.rs index aecbd05..b85a9e8 100644 --- a/lib/src/apdu/tap_signer.rs +++ b/lib/src/apdu/tap_signer.rs @@ -8,7 +8,6 @@ use bitcoin::secp256k1; use bitcoin_hashes::hex::DisplayHex as _; use serde::{Deserialize, Serialize}; -// MARK: - XpubCommand /// TAPSIGNER only - Provides the current XPUB (BIP-32 serialized), either at the top level (master) /// or the derived key in use (see 'path' value in status response) #[derive(Serialize, Clone, Debug, PartialEq, Eq)] @@ -28,7 +27,6 @@ impl CommandApdu for XpubCommand { } impl XpubCommand { - #[allow(unused)] // TODO this needs to be used pub fn new(master: bool, epubkey: secp256k1::PublicKey, xcvc: Vec) -> Self { Self { cmd: Self::name(), @@ -42,9 +40,9 @@ impl XpubCommand { #[derive(Deserialize, Clone)] pub struct XpubResponse { #[serde(with = "serde_bytes")] - xpub: Vec, + pub xpub: Vec, #[serde(with = "serde_bytes")] - card_nonce: [u8; 16], + pub card_nonce: [u8; 16], } impl ResponseApdu for XpubResponse {} @@ -58,7 +56,6 @@ impl std::fmt::Debug for XpubResponse { } } -// MARK: - ChangeCommand /// TAPSIGNER only - Change the PIN (CVC) used for card authentication to a new user provided one #[derive(Serialize, Clone, Debug, PartialEq, Eq)] pub struct ChangeCommand { @@ -104,7 +101,6 @@ pub struct ChangeResponse { impl ResponseApdu for ChangeResponse {} -// MARK: - BackupCommand /// TAPSIGNER only - Get an encrypted backup of the card's private key #[derive(Serialize, Clone, Debug, PartialEq, Eq)] diff --git a/lib/src/error.rs b/lib/src/error.rs index 0ef405e..8a89ebe 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -185,6 +185,15 @@ pub enum DeriveError { InvalidChainCode(String), } +/// Errors returned by the `xpub` command. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum XpubError { + #[error(transparent)] + CkTap(#[from] CkTapError), + #[error(transparent)] + Bip32(#[from] bitcoin::bip32::Error), +} + /// Errors returned by the `unseal` command. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum UnsealError { diff --git a/lib/src/tap_signer.rs b/lib/src/tap_signer.rs index daa07ab..41080c7 100644 --- a/lib/src/tap_signer.rs +++ b/lib/src/tap_signer.rs @@ -1,17 +1,18 @@ // Copyright (c) 2025 rust-cktap contributors // SPDX-License-Identifier: MIT OR Apache-2.0 +use crate::apdu::tap_signer::{XpubCommand, XpubResponse}; use crate::apdu::{ CommandApdu as _, DeriveCommand, DeriveResponse, NewCommand, NewResponse, SignCommand, SignResponse, StatusCommand, StatusResponse, tap_signer::{BackupCommand, BackupResponse, ChangeCommand, ChangeResponse}, }; -use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusError}; +use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusError, XpubError}; use crate::shared::{Authentication, Certificate, CkTransport, Nfc, Read, Wait, transmit}; use crate::{BIP32_HARDENED_MASK, CkTapError}; use async_trait::async_trait; use bitcoin::PublicKey; -use bitcoin::bip32::ChainCode; +use bitcoin::bip32::{ChainCode, Xpub}; use bitcoin::hex::DisplayHex; use bitcoin::secp256k1::{self, All, Message, Secp256k1, ecdsa::Signature}; use bitcoin_hashes::sha256; @@ -314,6 +315,15 @@ pub trait TapSignerShared: Authentication { self.set_card_nonce(change_response.card_nonce); Ok(()) } + + async fn xpub(&mut self, cvc: &str, master: bool) -> Result { + let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, XpubCommand::name()); + let xpub_command = XpubCommand::new(master, epubkey, xcvc); + let xpub_response: XpubResponse = transmit(self.transport(), &xpub_command).await?; + self.set_card_nonce(xpub_response.card_nonce); + let xpub = Xpub::decode(xpub_response.xpub.as_slice())?; + Ok(xpub) + } } #[async_trait] @@ -390,3 +400,31 @@ impl core::fmt::Debug for TapSigner { .finish() } } + +#[cfg(feature = "emulator")] +#[cfg(test)] +mod test { + use crate::emulator::find_emulator; + use crate::emulator::test::{CardTypeOption, EcardSubprocess}; + use crate::tap_signer::TapSignerShared; + use crate::{CkTapCard, rand_chaincode}; + use std::path::Path; + + // verify the xpub command works + #[tokio::test] + async fn test_tap_signer_xpub() { + let card_type = CardTypeOption::TapSigner; + let pipe_path = "/tmp/test-tapsigner-xpub-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(); + let xpub = ts.xpub("123456", false).await.unwrap(); + assert_eq!(xpub.depth, 3); + let master_xpub = ts.xpub("123456", true).await.unwrap(); + assert_eq!(master_xpub.depth, 0); + } + drop(python); + } +} From 77e5660bf784d3482be97f24cddad26cf7ac6c80 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 17:24:23 -0500 Subject: [PATCH 5/7] feat: add 'xpub' command to cli --- cli/src/main.rs | 24 ++++++++++++++++++++++++ lib/src/tap_signer.rs | 6 +++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index f57a139..ce2b7e2 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -114,6 +114,12 @@ enum TapSignerCommand { Wait, /// Get the card's nfc URL Nfc, + /// Read the xpub (requires CVC) + Xpub { + /// Give master (`m`) XPUB, otherwise derived XPUB + #[clap(short, long)] + master: bool, + }, } /// TapSigner CLI @@ -150,6 +156,12 @@ enum SatsChipCommand { Wait, /// Get the card's nfc URL Nfc, + /// Read the xpub (requires CVC) + Xpub { + /// Give master (`m`) XPUB, otherwise derived XPUB + #[clap(short, long)] + master: bool, + }, } #[tokio::main] @@ -238,6 +250,7 @@ async fn main() -> Result<(), CliError> { } TapSignerCommand::Wait => wait(ts).await, TapSignerCommand::Nfc => nfc(ts).await, + TapSignerCommand::Xpub { master } => xpub(ts, master).await, } } CkTapCard::SatsChip(sc) => { @@ -270,6 +283,7 @@ async fn main() -> Result<(), CliError> { } SatsChipCommand::Wait => wait(sc).await, SatsChipCommand::Nfc => nfc(sc).await, + SatsChipCommand::Xpub { master } => xpub(sc, master).await, } } } @@ -336,3 +350,13 @@ where let nfc = card.nfc().await.expect("nfc failed"); println!("{nfc}"); } + +async fn xpub(card: &mut C, master: bool) +where + C: TapSignerShared + Send, +{ + dbg!(master); + let xpub = card.xpub(master, &cvc()).await.expect("xpub failed"); + dbg!(&xpub); + println!("{}", xpub.to_string()); +} diff --git a/lib/src/tap_signer.rs b/lib/src/tap_signer.rs index 41080c7..f77092a 100644 --- a/lib/src/tap_signer.rs +++ b/lib/src/tap_signer.rs @@ -316,7 +316,7 @@ pub trait TapSignerShared: Authentication { Ok(()) } - async fn xpub(&mut self, cvc: &str, master: bool) -> Result { + async fn xpub(&mut self, master: bool, cvc: &str) -> Result { let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, XpubCommand::name()); let xpub_command = XpubCommand::new(master, epubkey, xcvc); let xpub_response: XpubResponse = transmit(self.transport(), &xpub_command).await?; @@ -420,9 +420,9 @@ mod test { let emulator = find_emulator(pipe_path).await.unwrap(); if let CkTapCard::TapSigner(mut ts) = emulator { ts.init(rand_chaincode(), "123456").await.unwrap(); - let xpub = ts.xpub("123456", false).await.unwrap(); + let xpub = ts.xpub(false, "123456").await.unwrap(); assert_eq!(xpub.depth, 3); - let master_xpub = ts.xpub("123456", true).await.unwrap(); + let master_xpub = ts.xpub(true, "123456").await.unwrap(); assert_eq!(master_xpub.depth, 0); } drop(python); From 556e6d33adcd0c946d0e1b0c7929ac0c4a847824 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 18:33:13 -0500 Subject: [PATCH 6/7] feat: add 'xpub' command to ffi bindings --- Justfile | 10 ++++-- cktap-ffi/src/error.rs | 23 +++++++++++++ cktap-ffi/src/lib.rs | 20 ++++++++++- cktap-ffi/src/sats_chip.rs | 13 +++++-- cktap-ffi/src/tap_signer.rs | 12 +++++-- cktap-swift/Tests/CKTapTests/CKTapTests.swift | 34 +++++++++++++------ cli/src/main.rs | 2 +- lib/src/lib.rs | 4 +-- 8 files changed, 97 insertions(+), 21 deletions(-) diff --git a/Justfile b/Justfile index 73cd487..531b05c 100644 --- a/Justfile +++ b/Justfile @@ -28,9 +28,13 @@ help: # start the cktap emulator on /tmp/ecard-pipe start *OPTS: setup - source emulator_env/bin/activate; python3 coinkite/coinkite-tap-proto/emulator/ecard.py emulate {{OPTS}} &> emulator_env/output.log & \ - echo $! > emulator_env/ecard.pid - echo "started emulator, pid:" `cat emulator_env/ecard.pid` + if [ -f emulator_env/ecard.pid ]; then \ + echo "Emulator already running, pid:" `cat emulator_env/ecard.pid`; \ + else \ + source emulator_env/bin/activate; python3 coinkite/coinkite-tap-proto/emulator/ecard.py emulate {{OPTS}} &> emulator_env/output.log & \ + echo $! > emulator_env/ecard.pid; \ + echo "started emulator, pid:" `cat emulator_env/ecard.pid`; \ + fi # stop the cktap emulator stop: diff --git a/cktap-ffi/src/error.rs b/cktap-ffi/src/error.rs index 69a7e17..9ae46a1 100644 --- a/cktap-ffi/src/error.rs +++ b/cktap-ffi/src/error.rs @@ -376,3 +376,26 @@ impl From for ChangeError { } } } + +/// Errors returned by the `xpub` command. +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error, uniffi::Error)] +pub enum XpubError { + #[error(transparent)] + CkTap { + #[from] + err: CkTapError, + }, + #[error("BIP32 error: {msg}")] + Bip32 { msg: String }, +} + +impl From for XpubError { + fn from(value: rust_cktap::XpubError) -> Self { + match value { + rust_cktap::XpubError::CkTap(err) => XpubError::CkTap { err: err.into() }, + rust_cktap::XpubError::Bip32(err) => XpubError::Bip32 { + msg: err.to_string(), + }, + } + } +} diff --git a/cktap-ffi/src/lib.rs b/cktap-ffi/src/lib.rs index e4d699a..45cc891 100644 --- a/cktap-ffi/src/lib.rs +++ b/cktap-ffi/src/lib.rs @@ -18,7 +18,7 @@ use futures::lock::Mutex; use rust_cktap::Network; use rust_cktap::shared::FactoryRootKey; use rust_cktap::shared::{Certificate, Read}; -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; use std::sync::Arc; @@ -122,6 +122,24 @@ impl Psbt { } } +#[derive(uniffi::Object, Clone, Eq, PartialEq)] +pub struct Xpub { + inner: rust_cktap::Xpub, +} + +#[uniffi::export] +impl Xpub { + pub fn encode(&self) -> Vec { + self.inner.encode().to_vec() + } +} + +impl Display for Xpub { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + #[derive(uniffi::Enum)] pub enum CkTapCard { SatsCard(Arc), diff --git a/cktap-ffi/src/sats_chip.rs b/cktap-ffi/src/sats_chip.rs index d8ea6ae..78e59da 100644 --- a/cktap-ffi/src/sats_chip.rs +++ b/cktap-ffi/src/sats_chip.rs @@ -1,11 +1,14 @@ // Copyright (c) 2025 rust-cktap contributors // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::error::{CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError}; +use crate::error::{ + CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError, XpubError, +}; use crate::tap_signer::{change, derive, init, sign_psbt}; -use crate::{ChainCode, Psbt, PublicKey, check_cert, read}; +use crate::{ChainCode, Psbt, PublicKey, Xpub, check_cert, read}; use futures::lock::Mutex; use rust_cktap::shared::{Authentication, Nfc, Wait}; +use rust_cktap::tap_signer::TapSignerShared; use std::sync::Arc; #[derive(uniffi::Object)] @@ -85,4 +88,10 @@ impl SatsChip { let url = card.nfc().await?; Ok(url) } + + pub async fn xpub(&self, master: bool, cvc: String) -> Result { + let mut card = self.0.lock().await; + let xpub = card.xpub(master, &cvc).await?; + Ok(Xpub { inner: xpub }) + } } diff --git a/cktap-ffi/src/tap_signer.rs b/cktap-ffi/src/tap_signer.rs index e0a33e4..97828ec 100644 --- a/cktap-ffi/src/tap_signer.rs +++ b/cktap-ffi/src/tap_signer.rs @@ -1,8 +1,10 @@ // Copyright (c) 2025 rust-cktap contributors // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::error::{CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError}; -use crate::{ChainCode, Psbt, PublicKey, check_cert, read}; +use crate::error::{ + CertsError, ChangeError, CkTapError, DeriveError, ReadError, SignPsbtError, XpubError, +}; +use crate::{ChainCode, Psbt, PublicKey, Xpub, check_cert, read}; use futures::lock::Mutex; use rust_cktap::shared::{Authentication, Nfc, Wait}; use rust_cktap::tap_signer::TapSignerShared; @@ -87,6 +89,12 @@ impl TapSigner { let url = card.nfc().await?; Ok(url) } + + pub async fn xpub(&self, master: bool, cvc: String) -> Result { + let mut card = self.0.lock().await; + let xpub = card.xpub(master, &cvc).await?; + Ok(Xpub { inner: xpub }) + } } pub async fn init( diff --git a/cktap-swift/Tests/CKTapTests/CKTapTests.swift b/cktap-swift/Tests/CKTapTests/CKTapTests.swift index 605f379..4f49630 100644 --- a/cktap-swift/Tests/CKTapTests/CKTapTests.swift +++ b/cktap-swift/Tests/CKTapTests/CKTapTests.swift @@ -33,15 +33,29 @@ final class CKTapTests: XCTestCase { let cardEmulator = CardEmulator() let card = try await toCktap(transport: cardEmulator) switch card { - case .satsCard(let satsCard): - let url: String = try await satsCard.nfc() - print("SatsCard url: \(url)") - case .tapSigner(let tapSigner): - let url: String = try await tapSigner.nfc() - print("TapSigner url: \(url)") - case .satsChip(let satsChip): - let url: String = try await satsChip.nfc() - print("SatsChip url: \(url)") - } + case .satsCard(let satsCard): + let url: String = try await satsCard.nfc() + print("SatsCard url: \(url)") + case .tapSigner(let tapSigner): + let url: String = try await tapSigner.nfc() + print("TapSigner url: \(url)") + case .satsChip(let satsChip): + let url: String = try await satsChip.nfc() + print("SatsChip url: \(url)") + } + } + func testXpub() async throws { + let cardEmulator = CardEmulator() + let card = try await toCktap(transport: cardEmulator) + switch card { + case .satsCard(_): + print("SatsCard does not support he xpub command.") + case .tapSigner(let tapSigner): + let xpub: String = try await tapSigner.xpub(master: true, cvc: "123456").toString() + print("TapSigner master xpub: \(xpub)") + case .satsChip(let satsChip): + let xpub: String = try await satsChip.xpub(master: false, cvc: "123456").toString() + print("SatsChip xpub: \(xpub)") + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index ce2b7e2..38813cd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -358,5 +358,5 @@ where dbg!(master); let xpub = card.xpub(master, &cvc()).await.expect("xpub failed"); dbg!(&xpub); - println!("{}", xpub.to_string()); + println!("{xpub}"); } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a340db9..872bc35 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,12 +7,12 @@ pub use bitcoin::bip32::ChainCode; pub use bitcoin::key::FromSliceError; pub use bitcoin::psbt::{Psbt, PsbtParseError}; pub use bitcoin::secp256k1::{Error as SecpError, rand}; -pub use bitcoin::{Network, PrivateKey, PublicKey}; +pub use bitcoin::{Network, PrivateKey, PublicKey, bip32::Xpub}; pub use bitcoin_hashes::sha256::Hash; pub use error::{ CardError, CertsError, ChangeError, CkTapError, DeriveError, DumpError, ReadError, - SignPsbtError, StatusError, UnsealError, + SignPsbtError, StatusError, UnsealError, XpubError, }; pub use shared::CkTransport; From c9826bd9f2eb21c417e6f8e99d5985c842a92917 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 31 Aug 2025 18:56:24 -0500 Subject: [PATCH 7/7] feat(cli): change cli 'sign' command for TAPSIGNER+SATSCHIP to sign a PSBT --- cli/src/main.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 38813cd..5c5ecff 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -108,8 +108,8 @@ enum TapSignerCommand { Backup, /// Change the PIN (CVC) used for card authentication to a new user provided one Change { new_cvc: String }, - /// Sign a digest - Sign { to_sign: String }, + /// Sign a PSBT + Sign { psbt: String }, /// Call wait command until no auth delay Wait, /// Get the card's nfc URL @@ -150,8 +150,8 @@ enum SatsChipCommand { }, /// Change the PIN (CVC) used for card authentication to a new user provided one Change { new_cvc: String }, - /// Sign a digest - Sign { to_sign: String }, + /// Sign a PSBT + Sign { psbt: String }, /// Call wait command until no auth delay Wait, /// Get the card's nfc URL @@ -241,12 +241,10 @@ async fn main() -> Result<(), CliError> { let response = &ts.change(&new_cvc, &cvc()).await; println!("{response:?}"); } - TapSignerCommand::Sign { to_sign } => { - let digest: [u8; 32] = - rust_cktap::Hash::hash(to_sign.as_bytes()).to_byte_array(); - - let response = &ts.sign(digest, vec![], &cvc()).await; - println!("{response:?}"); + TapSignerCommand::Sign { psbt } => { + let psbt = Psbt::from_str(&psbt)?; + let signed_psbt = ts.sign_psbt(psbt, &cvc()).await?; + println!("signed_psbt: {signed_psbt}"); } TapSignerCommand::Wait => wait(ts).await, TapSignerCommand::Nfc => nfc(ts).await, @@ -274,12 +272,10 @@ async fn main() -> Result<(), CliError> { let response = &sc.change(&new_cvc, &cvc()).await; println!("{response:?}"); } - SatsChipCommand::Sign { to_sign } => { - let digest: [u8; 32] = - rust_cktap::Hash::hash(to_sign.as_bytes()).to_byte_array(); - - let response = &sc.sign(digest, vec![], &cvc()).await; - println!("{response:?}"); + SatsChipCommand::Sign { psbt } => { + let psbt = Psbt::from_str(&psbt)?; + let signed_psbt = sc.sign_psbt(psbt, &cvc()).await?; + println!("signed_psbt: {signed_psbt}"); } SatsChipCommand::Wait => wait(sc).await, SatsChipCommand::Nfc => nfc(sc).await,