Skip to content

Commit edcae83

Browse files
committed
feat: add 'xpub' command to lib
1 parent cd03eee commit edcae83

4 files changed

Lines changed: 52 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ It is up to the crate user to send and receive the raw cktap APDU messages via N
3939
#### TAPSIGNER-Only Commands
4040

4141
- [x] [change](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#change)
42-
- [ ] [xpub](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#xpub)
42+
- [x] [xpub](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#xpub)
4343
- [x] [backup](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#backup)
4444

4545
### Automated and CLI Testing with Emulator

lib/src/apdu/tap_signer.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use bitcoin::secp256k1;
88
use bitcoin_hashes::hex::DisplayHex as _;
99
use serde::{Deserialize, Serialize};
1010

11-
// MARK: - XpubCommand
1211
/// TAPSIGNER only - Provides the current XPUB (BIP-32 serialized), either at the top level (master)
1312
/// or the derived key in use (see 'path' value in status response)
1413
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
@@ -28,7 +27,6 @@ impl CommandApdu for XpubCommand {
2827
}
2928

3029
impl XpubCommand {
31-
#[allow(unused)] // TODO this needs to be used
3230
pub fn new(master: bool, epubkey: secp256k1::PublicKey, xcvc: Vec<u8>) -> Self {
3331
Self {
3432
cmd: Self::name(),
@@ -42,9 +40,9 @@ impl XpubCommand {
4240
#[derive(Deserialize, Clone)]
4341
pub struct XpubResponse {
4442
#[serde(with = "serde_bytes")]
45-
xpub: Vec<u8>,
43+
pub xpub: Vec<u8>,
4644
#[serde(with = "serde_bytes")]
47-
card_nonce: [u8; 16],
45+
pub card_nonce: [u8; 16],
4846
}
4947

5048
impl ResponseApdu for XpubResponse {}
@@ -58,7 +56,6 @@ impl std::fmt::Debug for XpubResponse {
5856
}
5957
}
6058

61-
// MARK: - ChangeCommand
6259
/// TAPSIGNER only - Change the PIN (CVC) used for card authentication to a new user provided one
6360
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
6461
pub struct ChangeCommand {
@@ -104,7 +101,6 @@ pub struct ChangeResponse {
104101

105102
impl ResponseApdu for ChangeResponse {}
106103

107-
// MARK: - BackupCommand
108104
/// TAPSIGNER only - Get an encrypted backup of the card's private key
109105
110106
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]

lib/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ pub enum DeriveError {
185185
InvalidChainCode(String),
186186
}
187187

188+
/// Errors returned by the `xpub` command.
189+
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
190+
pub enum XpubError {
191+
#[error(transparent)]
192+
CkTap(#[from] CkTapError),
193+
#[error(transparent)]
194+
Bip32(#[from] bitcoin::bip32::Error),
195+
}
196+
188197
/// Errors returned by the `unseal` command.
189198
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
190199
pub enum UnsealError {

lib/src/tap_signer.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
// Copyright (c) 2025 rust-cktap contributors
22
// SPDX-License-Identifier: MIT OR Apache-2.0
33

4+
use crate::apdu::tap_signer::{XpubCommand, XpubResponse};
45
use crate::apdu::{
56
CommandApdu as _, DeriveCommand, DeriveResponse, NewCommand, NewResponse, SignCommand,
67
SignResponse, StatusCommand, StatusResponse,
78
tap_signer::{BackupCommand, BackupResponse, ChangeCommand, ChangeResponse},
89
};
9-
use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusError};
10+
use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusError, XpubError};
1011
use crate::shared::{Authentication, Certificate, CkTransport, Nfc, Read, Wait, transmit};
1112
use crate::{BIP32_HARDENED_MASK, CkTapError};
1213
use async_trait::async_trait;
1314
use bitcoin::PublicKey;
14-
use bitcoin::bip32::ChainCode;
15+
use bitcoin::bip32::{ChainCode, Xpub};
1516
use bitcoin::hex::DisplayHex;
1617
use bitcoin::secp256k1::{self, All, Message, Secp256k1, ecdsa::Signature};
1718
use bitcoin_hashes::sha256;
@@ -314,6 +315,15 @@ pub trait TapSignerShared: Authentication {
314315
self.set_card_nonce(change_response.card_nonce);
315316
Ok(())
316317
}
318+
319+
async fn xpub(&mut self, cvc: &str, master: bool) -> Result<Xpub, XpubError> {
320+
let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, XpubCommand::name());
321+
let xpub_command = XpubCommand::new(master, epubkey, xcvc);
322+
let xpub_response: XpubResponse = transmit(self.transport(), &xpub_command).await?;
323+
self.set_card_nonce(xpub_response.card_nonce);
324+
let xpub = Xpub::decode(xpub_response.xpub.as_slice())?;
325+
Ok(xpub)
326+
}
317327
}
318328

319329
#[async_trait]
@@ -390,3 +400,31 @@ impl core::fmt::Debug for TapSigner {
390400
.finish()
391401
}
392402
}
403+
404+
#[cfg(feature = "emulator")]
405+
#[cfg(test)]
406+
mod test {
407+
use crate::emulator::find_emulator;
408+
use crate::emulator::test::{CardTypeOption, EcardSubprocess};
409+
use crate::tap_signer::TapSignerShared;
410+
use crate::{CkTapCard, rand_chaincode};
411+
use std::path::Path;
412+
413+
// verify the xpub command works
414+
#[tokio::test]
415+
async fn test_tap_signer_xpub() {
416+
let card_type = CardTypeOption::TapSigner;
417+
let pipe_path = "/tmp/test-tapsigner-xpub-pipe";
418+
let pipe_path = Path::new(&pipe_path);
419+
let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
420+
let emulator = find_emulator(pipe_path).await.unwrap();
421+
if let CkTapCard::TapSigner(mut ts) = emulator {
422+
ts.init(rand_chaincode(), "123456").await.unwrap();
423+
let xpub = ts.xpub("123456", false).await.unwrap();
424+
assert_eq!(xpub.depth, 3);
425+
let master_xpub = ts.xpub("123456", true).await.unwrap();
426+
assert_eq!(master_xpub.depth, 0);
427+
}
428+
drop(python);
429+
}
430+
}

0 commit comments

Comments
 (0)