Skip to main content

rust_cktap/
tap_signer.rs

1// Copyright (c) 2025 rust-cktap contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::apdu::tap_signer::{XpubCommand, XpubResponse};
5use crate::apdu::{
6    CommandApdu as _, DeriveCommand, DeriveResponse, NewCommand, NewResponse, SignCommand,
7    SignResponse, StatusCommand, StatusResponse,
8    tap_signer::{BackupCommand, BackupResponse, ChangeCommand, ChangeResponse},
9};
10use crate::error::{ChangeError, DeriveError, ReadError, SignPsbtError, StatusError, XpubError};
11use crate::shared::{
12    Authentication, Certificate, CkTransport, Nfc, Read, Wait, card_pubkey_to_ident, transmit,
13};
14use crate::{BIP32_HARDENED_MASK, CkTapError};
15use async_trait::async_trait;
16use bitcoin::PublicKey;
17use bitcoin::bip32::{ChainCode, Xpub};
18use bitcoin::hex::DisplayHex;
19use bitcoin::secp256k1::{self, All, Message, Secp256k1, ecdsa::Signature};
20use bitcoin_hashes::sha256;
21use std::sync::Arc;
22
23const BIP84_PATH_LEN: u32 = 5;
24
25// BIP84 derivation path structure, m / 84' / 0' / account' / change / address_index
26
27// Derivation sub-path indexes that must be hardened
28const BIP84_HARDENED_SUBPATH: [u32; 3] = [0, 1, 2];
29
30pub struct TapSigner {
31    pub transport: Arc<dyn CkTransport>,
32    pub secp: Secp256k1<All>,
33    pub proto: u32,
34    pub ver: String,
35    pub birth: u32,
36    pub path: Option<Vec<u32>>,
37    // [(1<<31)+84, (1<<31), (1<<31)], user-defined, will be omitted if not yet setup
38    pub num_backups: Option<u32>,
39    pub pubkey: PublicKey,
40    pub card_nonce: [u8; 16],
41    pub auth_delay: Option<u8>,
42}
43
44impl Authentication for TapSigner {
45    fn secp(&self) -> &Secp256k1<All> {
46        &self.secp
47    }
48
49    fn ver(&self) -> &str {
50        &self.ver
51    }
52
53    fn pubkey(&self) -> &PublicKey {
54        &self.pubkey
55    }
56
57    fn card_nonce(&self) -> &[u8; 16] {
58        &self.card_nonce
59    }
60
61    fn set_card_nonce(&mut self, new_nonce: [u8; 16]) {
62        self.card_nonce = new_nonce;
63    }
64
65    fn auth_delay(&self) -> Option<u8> {
66        self.auth_delay
67    }
68
69    fn set_auth_delay(&mut self, auth_delay: Option<u8>) {
70        self.auth_delay = auth_delay;
71    }
72
73    fn transport(&self) -> Arc<dyn CkTransport> {
74        self.transport.clone()
75    }
76}
77
78/// Function shared between TapSigner and SatsChip
79#[async_trait]
80pub trait TapSignerShared: Authentication {
81    /// Initialize the tap signer or sats chip, can only be done once
82    async fn init(&mut self, chain_code: ChainCode, cvc: &str) -> Result<(), CkTapError> {
83        let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, NewCommand::name());
84        let new_command = NewCommand::new(Some(0), Some(chain_code), epubkey, xcvc);
85        let new_response: NewResponse = transmit(self.transport(), &new_command).await?;
86        self.set_card_nonce(new_response.card_nonce);
87        Ok(())
88    }
89
90    /// Get the status of the tap signer or sats chip, including the current card nonce
91    async fn status(&mut self) -> Result<StatusResponse, CkTapError> {
92        let cmd = StatusCommand::default();
93        let status_response: StatusResponse = transmit(self.transport(), &cmd).await?;
94        self.set_card_nonce(status_response.card_nonce);
95        Ok(status_response)
96    }
97
98    /// Sign a message digest with the tap signer
99    async fn sign(
100        &mut self,
101        digest: [u8; 32],
102        sub_path: Vec<u32>,
103        cvc: &str,
104    ) -> Result<SignResponse, CkTapError> {
105        let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, SignCommand::name());
106
107        // Use the same session key to encrypt the new CVC
108        let session_key = secp256k1::ecdh::SharedSecret::new(&self.pubkey().inner, &eprivkey);
109
110        // encrypt the new cvc by XORing with the session key
111        let xdigest_vec: Vec<u8> = session_key
112            .as_ref()
113            .iter()
114            .zip(digest)
115            .map(|(session_key_byte, digest_byte)| session_key_byte ^ digest_byte)
116            .collect();
117
118        let xdigest: [u8; 32] = xdigest_vec.try_into().expect("input is also 32 bytes");
119
120        let sign_command =
121            SignCommand::for_tapsigner(sub_path.clone(), xdigest, epubkey, xcvc.clone());
122
123        let mut sign_response: Result<SignResponse, CkTapError> =
124            transmit(self.transport(), &sign_command).await;
125
126        let mut unlucky_number_retries = 0;
127        while let Err(CkTapError::Card(crate::CardError::UnluckyNumber)) = sign_response {
128            let sign_command =
129                SignCommand::for_tapsigner(sub_path.clone(), xdigest, epubkey, xcvc.clone());
130
131            sign_response = transmit(self.transport(), &sign_command).await;
132            unlucky_number_retries += 1;
133
134            if unlucky_number_retries > 3 {
135                break;
136            }
137        }
138
139        let sign_response = sign_response?;
140        self.set_card_nonce(sign_response.card_nonce);
141        Ok(sign_response)
142    }
143
144    /// Sign a BIP84, currently only P2WPKH (BIP84) (Native SegWit) PSBT
145    /// This function will return a signed but not finalized PSBT. You will need to finalize the
146    /// PSBT yourself before it can be broadcast.
147    async fn sign_psbt(
148        &mut self,
149        mut psbt: bitcoin::Psbt,
150        cvc: &str,
151    ) -> Result<bitcoin::Psbt, SignPsbtError> {
152        use bitcoin::{
153            secp256k1::ecdsa,
154            sighash::{EcdsaSighashType, SighashCache},
155        };
156
157        let unsigned_tx = psbt.unsigned_tx.clone();
158        let mut sighash_cache = SighashCache::new(&unsigned_tx);
159
160        for (input_index, input) in psbt.inputs.iter_mut().enumerate() {
161            // extract previous output data from the PSBT
162            let witness_utxo = input
163                .witness_utxo
164                .as_ref()
165                .ok_or(SignPsbtError::MissingUtxo(input_index as u32))?;
166
167            let amount = witness_utxo.value;
168
169            // extract the P2WPKH script from PSBT
170            let script_pubkey = &witness_utxo.script_pubkey;
171            if !script_pubkey.is_p2wpkh() {
172                return Err(SignPsbtError::InvalidScript(input_index as u32));
173            }
174
175            // get the public key from the PSBT
176            let key_pairs = &input.bip32_derivation;
177            let (psbt_pubkey, (_fingerprint, path)) = key_pairs
178                .iter()
179                .next()
180                .ok_or(SignPsbtError::MissingPubkey(input_index as u32))?;
181
182            let path = path.to_u32_vec();
183
184            if path.len() as u32 != BIP84_PATH_LEN {
185                return Err(SignPsbtError::InvalidPath(input_index as u32));
186            }
187
188            let sub_path = BIP84_HARDENED_SUBPATH.map(|i| path[i as usize]);
189            if sub_path.iter().any(|p| *p > BIP32_HARDENED_MASK) {
190                return Err(SignPsbtError::InvalidPath(input_index as u32));
191            }
192
193            // calculate sighash
194            let script = script_pubkey.as_script();
195            let sighash = sighash_cache
196                .p2wpkh_signature_hash(input_index, script, amount, EcdsaSighashType::All)
197                .map_err(|e| SignPsbtError::SighashError(e.to_string()))?;
198
199            // the digest is the sighash
200            let digest: &[u8; 32] = sighash.as_ref();
201
202            // send digest to TAPSIGNER for signing
203            let mut sign_response = self.sign(*digest, sub_path.to_vec(), cvc).await?;
204            let mut signature_raw = sign_response.sig;
205
206            // verify that TAPSIGNER used the same public key as the PSBT
207            if sign_response.pubkey != psbt_pubkey.serialize() {
208                // try deriving the TAPSIGNER and try again
209                // take the hardened path and remove the hardened bit, because `sign` hardens it
210                let path: Vec<u32> = path
211                    .into_iter()
212                    .map(|p| p ^ BIP32_HARDENED_MASK)
213                    .take(BIP84_HARDENED_SUBPATH.len())
214                    .collect();
215                let derive_response = self.derive(path, cvc).await;
216                if derive_response.is_err() {
217                    return Err(SignPsbtError::PubkeyMismatch(input_index as u32));
218                }
219
220                // update signature to the new one we just derived
221                sign_response = self.sign(*digest, sub_path.to_vec(), cvc).await?;
222                signature_raw = sign_response.sig;
223
224                // if still not matching, return error
225                if sign_response.pubkey != psbt_pubkey.serialize() {
226                    return Err(SignPsbtError::PubkeyMismatch(input_index as u32));
227                }
228            }
229
230            // update the PSBT input with the signature
231            let ecdsa_sig = ecdsa::Signature::from_compact(&signature_raw)
232                .map_err(|e| SignPsbtError::SignatureError(e.to_string()))?;
233
234            let final_sig = bitcoin::ecdsa::Signature::sighash_all(ecdsa_sig);
235            input.partial_sigs.insert((*psbt_pubkey).into(), final_sig);
236        }
237
238        Ok(psbt)
239    }
240
241    /// Derive a public key at the given hardened path.
242    ///
243    /// The derive command on the TAPSIGNER is used to perform hardened BIP-32 key derivation.
244    /// Wallets are expected to use it for deriving the BIP-44/48/84 prefix of the path; the value
245    /// is captured and stored long term. This is effectively calculating the XPUB to be used on the
246    /// mobile wallet.
247    ///
248    /// Ref: <https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#tapsigner-performs-subkey-derivation>
249    async fn derive(&mut self, path: Vec<u32>, cvc: &str) -> Result<PublicKey, DeriveError> {
250        // set most significant bit to 1 to represent hardened path steps
251        let path = path.iter().map(|p| p ^ (1 << 31)).collect::<Vec<_>>();
252        let app_nonce = crate::rand_nonce();
253        let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, DeriveCommand::name());
254        let cmd = DeriveCommand::for_tapsigner(app_nonce, path, epubkey, xcvc);
255        let derive_response: DeriveResponse = transmit(self.transport(), &cmd).await?;
256        self.set_card_nonce(derive_response.card_nonce);
257
258        let master_pubkey = PublicKey::from_slice(&derive_response.master_pubkey)?;
259        let pubkey = match &derive_response.pubkey {
260            Some(pubkey) => PublicKey::from_slice(pubkey)?,
261            None => master_pubkey,
262        };
263
264        // TODO FIX currently signature validation only works if no derivation path is used
265        if pubkey == master_pubkey {
266            let card_nonce = self.card_nonce();
267            let sig = &derive_response.sig;
268
269            let mut message_bytes: Vec<u8> = Vec::new();
270            message_bytes.extend("OPENDIME".as_bytes());
271            message_bytes.extend(card_nonce);
272            message_bytes.extend(app_nonce);
273            message_bytes.extend(&derive_response.chain_code);
274
275            let message_bytes_hash = sha256::Hash::hash(message_bytes.as_slice());
276            let message = Message::from_digest(message_bytes_hash.to_byte_array());
277
278            let signature = Signature::from_compact(sig)?;
279
280            self.secp()
281                .verify_ecdsa(&message, &signature, &master_pubkey.inner)?;
282        }
283        Ok(pubkey)
284    }
285
286    /// Change the CVC used for card authentication to a new user provided one
287    async fn change(&mut self, new_cvc: &str, cvc: &str) -> Result<(), ChangeError> {
288        if new_cvc.len() < 6 {
289            return Err(ChangeError::TooShort(new_cvc.len() as u32));
290        }
291
292        if new_cvc.len() > 32 {
293            return Err(ChangeError::TooLong(new_cvc.len() as u32));
294        }
295
296        if new_cvc == cvc {
297            return Err(ChangeError::SameAsOld);
298        }
299
300        // Create session key and encrypt current CVC
301        let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, ChangeCommand::name());
302
303        // Use the same session key to encrypt the new CVC
304        let session_key = secp256k1::ecdh::SharedSecret::new(&self.pubkey().inner, &eprivkey);
305
306        // encrypt the new cvc by XORing with the session key
307        let xnew_cvc: Vec<u8> = session_key
308            .as_ref()
309            .iter()
310            .zip(new_cvc.as_bytes().iter())
311            .map(|(session_key_byte, cvc_byte)| session_key_byte ^ cvc_byte)
312            .collect();
313
314        let change_command = ChangeCommand::new(xnew_cvc, epubkey, xcvc);
315        let change_response: ChangeResponse = transmit(self.transport(), &change_command).await?;
316
317        self.set_card_nonce(change_response.card_nonce);
318        Ok(())
319    }
320
321    async fn xpub(&mut self, master: bool, cvc: &str) -> Result<Xpub, XpubError> {
322        let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, XpubCommand::name());
323        let xpub_command = XpubCommand::new(master, epubkey, xcvc);
324        let xpub_response: XpubResponse = transmit(self.transport(), &xpub_command).await?;
325        self.set_card_nonce(xpub_response.card_nonce);
326        let xpub = Xpub::decode(xpub_response.xpub.as_slice())?;
327        Ok(xpub)
328    }
329}
330
331#[async_trait]
332impl Nfc for TapSigner {}
333
334impl TapSignerShared for TapSigner {}
335
336impl TapSigner {
337    pub fn card_ident(&self) -> String {
338        card_pubkey_to_ident(&self.pubkey)
339    }
340
341    pub fn try_from_status(
342        transport: Arc<dyn CkTransport>,
343        status_response: StatusResponse,
344    ) -> Result<Self, StatusError> {
345        let pubkey = status_response.pubkey.as_slice();
346        let pubkey = PublicKey::from_slice(pubkey)?;
347
348        Ok(TapSigner {
349            transport,
350            secp: Secp256k1::new(),
351            proto: status_response.proto,
352            ver: status_response.ver,
353            birth: status_response.birth,
354            path: status_response.path,
355            num_backups: status_response.num_backups,
356            pubkey,
357            card_nonce: status_response.card_nonce,
358            auth_delay: status_response.auth_delay,
359        })
360    }
361
362    /// Backup the current card, the backup is encrypted with the "Backup Password" on the back of the card
363    pub async fn backup(&mut self, cvc: &str) -> Result<Vec<u8>, ChangeError> {
364        let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, "backup");
365
366        let backup_command = BackupCommand::new(epubkey, xcvc);
367        let backup_response: BackupResponse = transmit(self.transport(), &backup_command).await?;
368
369        self.card_nonce = backup_response.card_nonce;
370        Ok(backup_response.data)
371    }
372}
373
374#[async_trait]
375impl Wait for TapSigner {}
376
377#[async_trait]
378impl Read for TapSigner {
379    fn requires_auth(&self) -> bool {
380        true
381    }
382
383    fn slot(&self) -> Option<u8> {
384        None
385    }
386}
387
388#[async_trait]
389impl Certificate for TapSigner {
390    async fn slot_pubkey(&mut self) -> Result<Option<PublicKey>, ReadError> {
391        Ok(None)
392    }
393}
394
395impl core::fmt::Debug for TapSigner {
396    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
397        f.debug_struct("TapSigner")
398            .field("proto", &self.proto)
399            .field("ver", &self.ver)
400            .field("birth", &self.birth)
401            .field("path", &self.path)
402            .field("num_backups", &self.num_backups)
403            .field("card_ident", &self.card_ident())
404            .field("pubkey", &self.pubkey)
405            .field("card_nonce", &self.card_nonce.to_lower_hex_string())
406            .field("auth_delay", &self.auth_delay)
407            .finish()
408    }
409}
410
411#[cfg(feature = "emulator")]
412#[cfg(test)]
413mod test {
414    use crate::emulator::find_emulator;
415    use crate::emulator::test::{CardTypeOption, EcardSubprocess};
416    use crate::tap_signer::TapSignerShared;
417    use crate::{CkTapCard, rand_chaincode};
418    use std::path::Path;
419
420    // verify the xpub command works
421    #[tokio::test]
422    async fn test_tap_signer_xpub() {
423        let card_type = CardTypeOption::TapSigner;
424        let pipe_path = "/tmp/test-tapsigner-xpub-pipe";
425        let pipe_path = Path::new(&pipe_path);
426        let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
427        let emulator = find_emulator(pipe_path).await.unwrap();
428        if let CkTapCard::TapSigner(mut ts) = emulator {
429            ts.init(rand_chaincode(), "123456").await.unwrap();
430            let xpub = ts.xpub(false, "123456").await.unwrap();
431            assert_eq!(xpub.depth, 3);
432            let master_xpub = ts.xpub(true, "123456").await.unwrap();
433            assert_eq!(master_xpub.depth, 0);
434        }
435        drop(python);
436    }
437}