1use crate::CkTapError;
5use crate::apdu::{
6 AppletSelect, CommandApdu as _, DeriveCommand, DeriveResponse, DumpCommand, DumpResponse,
7 NewCommand, NewResponse, SignCommand, SignResponse, StatusResponse, UnsealCommand,
8 UnsealResponse,
9};
10use crate::error::{CardError, DeriveError, DumpError, ReadError, UnsealError};
11use crate::error::{SignPsbtError, StatusError};
12use crate::shared::{
13 Authentication, Certificate, CkTransport, Nfc, Read, Wait, card_pubkey_to_ident, transmit,
14};
15use async_trait::async_trait;
16use bitcoin::bip32::{ChainCode, DerivationPath, Fingerprint, Xpub};
17use bitcoin::secp256k1;
18use bitcoin::secp256k1::{All, Message, Secp256k1, ecdsa::Signature};
19use bitcoin::{Address, CompressedPublicKey, Network, NetworkKind, PrivateKey, PublicKey};
20use bitcoin_hashes::hex::DisplayHex;
21use bitcoin_hashes::sha256;
22use std::str::FromStr;
23use std::sync::Arc;
24
25pub struct SatsCard {
26 pub transport: Arc<dyn CkTransport>,
27 pub secp: Secp256k1<All>,
28 pub proto: u32,
29 pub ver: String,
30 pub birth: u32,
31 pub slots: (u8, u8),
32 pub addr: Option<String>,
33 pub pubkey: PublicKey,
34 pub card_nonce: [u8; 16],
35 pub auth_delay: Option<u8>,
36}
37
38impl Authentication for SatsCard {
39 fn secp(&self) -> &Secp256k1<All> {
40 &self.secp
41 }
42
43 fn ver(&self) -> &str {
44 &self.ver
45 }
46
47 fn pubkey(&self) -> &PublicKey {
48 &self.pubkey
49 }
50
51 fn card_nonce(&self) -> &[u8; 16] {
52 &self.card_nonce
53 }
54
55 fn set_card_nonce(&mut self, new_nonce: [u8; 16]) {
56 self.card_nonce = new_nonce;
57 }
58
59 fn auth_delay(&self) -> Option<u8> {
60 self.auth_delay
61 }
62
63 fn set_auth_delay(&mut self, auth_delay: Option<u8>) {
64 self.auth_delay = auth_delay;
65 }
66
67 fn transport(&self) -> Arc<dyn CkTransport> {
68 self.transport.clone()
69 }
70}
71
72impl SatsCard {
73 pub fn card_ident(&self) -> String {
74 card_pubkey_to_ident(&self.pubkey)
75 }
76
77 pub fn from_status(
78 transport: Arc<dyn CkTransport>,
79 status_response: StatusResponse,
80 ) -> Result<Self, StatusError> {
81 let pubkey = status_response.pubkey.as_slice();
82 let pubkey = PublicKey::from_slice(pubkey)?;
83
84 let slots = status_response
85 .slots
86 .ok_or_else(|| CkTapError::CborValue("Missing slots".to_string()))?;
87
88 Ok(Self {
89 transport,
90 secp: Secp256k1::new(),
91 proto: status_response.proto,
92 ver: status_response.ver,
93 birth: status_response.birth,
94 pubkey,
95 card_nonce: status_response.card_nonce,
96 auth_delay: status_response.auth_delay,
97 slots,
98 addr: status_response.addr,
99 })
100 }
101
102 pub async fn new_slot(
103 &mut self,
104 slot: u8,
105 chain_code: Option<ChainCode>,
106 cvc: &str,
107 ) -> Result<u8, CkTapError> {
108 let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, NewCommand::name());
109 let new_command = NewCommand::new(Some(slot), chain_code, epubkey, xcvc);
110 let new_response: NewResponse = transmit(self.transport(), &new_command).await?;
111 self.card_nonce = new_response.card_nonce;
112
113 let status_cmd = AppletSelect::default();
115 let status_response: StatusResponse = transmit(self.transport(), &status_cmd).await?;
116 self.slots = status_response.slots.unwrap();
117 self.addr = status_response.addr;
118 self.card_nonce = status_response.card_nonce;
119
120 Ok(new_response.slot)
121 }
122
123 pub async fn derive(&mut self) -> Result<ChainCode, DeriveError> {
133 let app_nonce = crate::rand_nonce();
134 let card_nonce = *self.card_nonce();
135
136 let cmd = DeriveCommand::for_satscard(app_nonce);
137 let derive_response: DeriveResponse = transmit(self.transport(), &cmd).await?;
138 self.set_card_nonce(derive_response.card_nonce);
139
140 let mut message_bytes: Vec<u8> = Vec::new();
142 message_bytes.extend("OPENDIME".as_bytes());
143 message_bytes.extend(card_nonce);
144 message_bytes.extend(app_nonce);
145 message_bytes.extend(derive_response.chain_code);
146
147 let message_bytes_hash = sha256::Hash::hash(message_bytes.as_slice());
148 let message = Message::from_digest(message_bytes_hash.to_byte_array());
149
150 let signature = Signature::from_compact(&derive_response.sig)?;
151
152 let master_pubkey = PublicKey::from_slice(&derive_response.master_pubkey)?;
153 self.secp()
154 .verify_ecdsa(&message, &signature, &master_pubkey.inner)?;
155
156 let response_chaincode = &derive_response.chain_code;
158
159 let card_chaincode = ChainCode::from(*response_chaincode);
164 let xpub = Xpub {
165 network: NetworkKind::Main,
166 depth: 0,
167 parent_fingerprint: Fingerprint::default(), child_number: bitcoin::bip32::ChildNumber::from_normal_idx(0).unwrap(),
169 public_key: master_pubkey.inner,
170 chain_code: card_chaincode,
171 };
172
173 let derivation_path = DerivationPath::from_str("m/0").unwrap();
175 let derived_xpub = xpub.derive_pub(self.secp(), &derivation_path).unwrap();
176
177 let bitcoin_pubkey = CompressedPublicKey(derived_xpub.public_key);
179
180 let p2wpkh_address = Address::p2wpkh(&bitcoin_pubkey, Network::Bitcoin).to_string();
182 let self_addr = self.addr.clone().unwrap();
183
184 if !(p2wpkh_address[..12] == self_addr[..12]
186 && p2wpkh_address[p2wpkh_address.len().saturating_sub(12)..]
187 == self_addr[self_addr.len().saturating_sub(12)..])
188 {
189 return Err(DeriveError::InvalidChainCode(
190 "Unable to derive card address".to_string(),
191 ));
192 }
193
194 Ok(card_chaincode)
195 }
196
197 pub async fn unseal(
201 &mut self,
202 slot: u8,
203 cvc: &str,
204 ) -> Result<(PrivateKey, PublicKey), UnsealError> {
205 let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, UnsealCommand::name());
206 let unseal_command = UnsealCommand::new(slot, epubkey, xcvc);
207 let unseal_response: UnsealResponse = transmit(self.transport(), &unseal_command).await?;
208 self.set_card_nonce(unseal_response.card_nonce);
209
210 let status_cmd = AppletSelect::default();
212 let status_response: StatusResponse = transmit(self.transport(), &status_cmd).await?;
213 self.slots = status_response.slots.unwrap();
214 self.addr = status_response.addr;
215 self.set_card_nonce(status_response.card_nonce);
216
217 let session_key = secp256k1::ecdh::SharedSecret::new(&self.pubkey().inner, &eprivkey);
220 let privkey: Vec<u8> = session_key
222 .as_ref()
223 .iter()
224 .zip(&unseal_response.privkey)
225 .map(|(session_key_byte, privkey_byte)| session_key_byte ^ privkey_byte)
226 .collect();
227 let privkey = PrivateKey::from_slice(&privkey, Network::Bitcoin)?;
228 let pubkey = PublicKey::from_slice(&unseal_response.pubkey)?;
229 Ok((privkey, pubkey))
230 }
231
232 pub async fn dump(
238 &mut self,
239 slot: u8,
240 cvc: Option<String>,
241 ) -> Result<(Option<PrivateKey>, PublicKey), DumpError> {
242 let epubkey_eprivkey_xcvc = cvc.map(|cvc| {
243 let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, DumpCommand::name());
244 (epubkey, eprivkey, xcvc)
245 });
246
247 let (epubkey, eprivkey, xcvc) = epubkey_eprivkey_xcvc
248 .map(|(epubkey, eprivkey, xcvc)| (Some(epubkey), Some(eprivkey), Some(xcvc)))
249 .unwrap_or((None, None, None));
250
251 let dump_command = DumpCommand::new(slot, epubkey, xcvc.clone());
252 let dump_response: DumpResponse = transmit(self.transport(), &dump_command).await?;
253 self.set_card_nonce(dump_response.card_nonce);
254
255 if let Some(tampered) = dump_response.tampered {
257 if tampered {
258 return Err(DumpError::SlotTampered(slot));
259 }
260 } else if let Some(sealed) = dump_response.sealed {
261 if sealed {
262 return Err(DumpError::SlotSealed(slot));
263 }
264 } else if let Some(used) = dump_response.used {
265 if !used {
266 return Err(DumpError::SlotUnused(slot));
267 }
268 }
269
270 let pubkey_bytes = match dump_response.pubkey {
272 Some(bytes) => bytes,
273 None => return Err(DumpError::SlotSealed(slot)),
274 };
275 let pubkey = PublicKey::from_slice(&pubkey_bytes)?;
276
277 let seckey = if let (Some(privkey), Some(eprivkey)) = (dump_response.privkey, eprivkey) {
280 let session_key = secp256k1::ecdh::SharedSecret::new(&self.pubkey().inner, &eprivkey);
282 let privkey: Vec<u8> = session_key
284 .as_ref()
285 .iter()
286 .zip(&privkey)
287 .map(|(session_key_byte, privkey_byte)| session_key_byte ^ privkey_byte)
288 .collect();
289 let privkey = PrivateKey::from_slice(&privkey, Network::Bitcoin)?;
290 Some(privkey)
291 } else {
292 None
293 };
294
295 Ok((seckey, pubkey))
296 }
297
298 pub async fn address(&mut self) -> Result<String, ReadError> {
299 let network = Network::Bitcoin;
300 let slot_pubkey = self.read(None).await?;
301 let slot_pubkey = CompressedPublicKey(slot_pubkey.inner);
302 let address = Address::p2wpkh(&slot_pubkey, network);
303 Ok(address.to_string())
304 }
305
306 pub async fn sign(
308 &mut self,
309 digest: [u8; 32],
310 slot: u8,
311 cvc: &str,
312 ) -> Result<SignResponse, CkTapError> {
313 let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc, SignCommand::name());
314
315 let session_key = secp256k1::ecdh::SharedSecret::new(&self.pubkey().inner, &eprivkey);
317
318 let xdigest_vec: Vec<u8> = session_key
320 .as_ref()
321 .iter()
322 .zip(digest)
323 .map(|(session_key_byte, digest_byte)| session_key_byte ^ digest_byte)
324 .collect();
325
326 let xdigest: [u8; 32] = xdigest_vec.try_into().expect("input is also 32 bytes");
327
328 let sign_command = SignCommand::for_satscard(slot, xdigest, epubkey, xcvc.clone());
329
330 let mut sign_response: Result<SignResponse, CkTapError> =
331 transmit(self.transport(), &sign_command).await;
332
333 let mut unlucky_number_retries = 0;
334 while let Err(CkTapError::Card(CardError::UnluckyNumber)) = sign_response {
335 let sign_command = SignCommand::for_satscard(slot, xdigest, epubkey, xcvc.clone());
336
337 sign_response = transmit(self.transport(), &sign_command).await;
338 unlucky_number_retries += 1;
339
340 if unlucky_number_retries > 3 {
341 break;
343 }
344 }
345
346 let sign_response = sign_response?;
347 self.set_card_nonce(sign_response.card_nonce);
348 Ok(sign_response)
349 }
350
351 pub async fn sign_psbt(
355 &mut self,
356 slot: u8,
357 mut psbt: bitcoin::Psbt,
358 cvc: &str,
359 ) -> Result<bitcoin::Psbt, SignPsbtError> {
360 use bitcoin::{
361 secp256k1::ecdsa,
362 sighash::{EcdsaSighashType, SighashCache},
363 };
364
365 type Error = SignPsbtError;
366
367 let unsigned_tx = psbt.unsigned_tx.clone();
368 let mut sighash_cache = SighashCache::new(&unsigned_tx);
369
370 for (input_index, input) in psbt.inputs.iter_mut().enumerate() {
371 let witness_utxo = input
373 .witness_utxo
374 .as_ref()
375 .ok_or(Error::MissingUtxo(input_index as u32))?;
376
377 let amount = witness_utxo.value;
378
379 let script_pubkey = &witness_utxo.script_pubkey;
381 if !script_pubkey.is_p2wpkh() {
382 return Err(Error::InvalidScript(input_index as u32));
383 }
384
385 let key_pairs = &input.bip32_derivation;
387 let (psbt_pubkey, (_fingerprint, path)) = key_pairs
388 .iter()
389 .next()
390 .ok_or(Error::MissingPubkey(input_index as u32))?;
391
392 if !path.is_empty() {
393 return Err(Error::InvalidPath(input_index as u32));
394 }
395
396 let script = script_pubkey.as_script();
398 let sighash = sighash_cache
399 .p2wpkh_signature_hash(input_index, script, amount, EcdsaSighashType::All)
400 .map_err(|e| Error::SighashError(e.to_string()))?;
401
402 let digest: &[u8; 32] = sighash.as_ref();
404
405 let sign_response = self.sign(*digest, slot, cvc).await?;
407 let signature_raw = sign_response.sig;
408
409 if sign_response.pubkey != psbt_pubkey.serialize() {
411 return Err(Error::PubkeyMismatch(input_index as u32));
412 }
413
414 let ecdsa_sig = ecdsa::Signature::from_compact(&signature_raw)
416 .map_err(|e| Error::SignatureError(e.to_string()))?;
417
418 let final_sig = bitcoin::ecdsa::Signature::sighash_all(ecdsa_sig);
419 input.partial_sigs.insert((*psbt_pubkey).into(), final_sig);
420 }
421
422 Ok(psbt)
423 }
424}
425
426#[async_trait]
427impl Wait for SatsCard {}
428
429#[async_trait]
430impl Read for SatsCard {
431 fn requires_auth(&self) -> bool {
432 false
433 }
434 fn slot(&self) -> Option<u8> {
435 Some(self.slots.0)
436 }
437}
438
439#[async_trait]
440impl Certificate for SatsCard {
441 async fn slot_pubkey(&mut self) -> Result<Option<PublicKey>, ReadError> {
442 let pubkey = self.read(None).await?;
443 Ok(Some(pubkey))
444 }
445}
446
447#[async_trait]
448impl Nfc for SatsCard {}
449
450impl core::fmt::Debug for SatsCard {
451 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
452 f.debug_struct("SatsCard")
453 .field("proto", &self.proto)
454 .field("ver", &self.ver)
455 .field("birth", &self.birth)
456 .field("slots", &self.slots)
457 .field("addr", &self.addr)
458 .field("card_ident", &self.card_ident())
459 .field("pubkey", &self.pubkey)
460 .field("card_nonce", &self.card_nonce.to_lower_hex_string())
461 .field("auth_delay", &self.auth_delay)
462 .finish()
463 }
464}
465
466#[cfg(feature = "emulator")]
467#[cfg(test)]
468mod test {
469 #![allow(deprecated)] use crate::CkTapCard;
471 use crate::emulator::find_emulator;
472 use crate::emulator::test::{CardTypeOption, EcardSubprocess};
473 use crate::error::DumpError;
474 use crate::shared::Certificate;
475 use bdk_wallet::chain::{BlockId, ConfirmationBlockTime};
476 use bdk_wallet::template::P2Wpkh;
477 use bdk_wallet::test_utils::{insert_anchor, insert_checkpoint, insert_tx, new_tx};
478 use bdk_wallet::{KeychainKind, SignOptions, Wallet};
479 use bitcoin::Network::Testnet;
480 use bitcoin::hashes::Hash;
481 use bitcoin::{Address, Amount, BlockHash, FeeRate, Network, Transaction, TxOut};
482 use std::path::Path;
483 use std::str::FromStr;
484
485 #[tokio::test]
487 async fn test_satscard_sign_psbt() {
488 let card_type = CardTypeOption::SatsCard;
489 let pipe_path = "/tmp/test-satscard-sign-psbt-pipe";
490 let pipe_path = Path::new(&pipe_path);
491 let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
492 let emulator = find_emulator(pipe_path).await.unwrap();
493 if let CkTapCard::SatsCard(mut sc) = emulator {
494 let slot_pubkey = sc.slot_pubkey().await.unwrap().unwrap();
495 let card_address = sc.address().await.unwrap();
496 let (_seckey, pubkey) = sc.unseal(0, "123456").await.unwrap();
497 assert_eq!(pubkey, slot_pubkey);
498
499 let descriptor = P2Wpkh(pubkey);
500 let mut wallet = Wallet::create_single(descriptor)
501 .network(Network::Bitcoin)
502 .create_wallet_no_persist()
503 .unwrap();
504 let wallet_address = wallet.reveal_next_address(KeychainKind::External).address;
505 assert_eq!(&wallet_address.to_string(), &card_address);
506
507 let tx0 = Transaction {
508 output: vec![TxOut {
509 value: Amount::from_sat(76_000),
510 script_pubkey: wallet_address.script_pubkey(),
511 }],
512 ..new_tx(0)
513 };
514
515 insert_checkpoint(
516 &mut wallet,
517 BlockId {
518 height: 1_000,
519 hash: BlockHash::all_zeros(),
520 },
521 );
522 insert_checkpoint(
523 &mut wallet,
524 BlockId {
525 height: 2_000,
526 hash: BlockHash::all_zeros(),
527 },
528 );
529 insert_tx(&mut wallet, tx0.clone());
530 insert_anchor(
531 &mut wallet,
532 tx0.compute_txid(),
533 ConfirmationBlockTime {
534 block_id: BlockId {
535 height: 1_000,
536 hash: BlockHash::all_zeros(),
537 },
538 confirmation_time: 100,
539 },
540 );
541 let balance = wallet.balance();
542 assert_eq!(balance.confirmed, Amount::from_sat(76_000));
543 let mut builder = wallet.build_tx();
544 builder
545 .add_recipient(
546 Address::from_str("tb1qlvuuza7al8k6shl67qv00g8va3eepy70a42nxd")
547 .unwrap()
548 .require_network(Testnet)
549 .unwrap(),
550 Amount::from_sat(1000),
551 )
552 .fee_rate(FeeRate::from_sat_per_vb(2).unwrap());
553 let psbt = builder.finish().unwrap();
554 let mut signed_psbt = sc.sign_psbt(0, psbt, "123456").await.unwrap();
555 let finalized = wallet
556 .finalize_psbt(&mut signed_psbt, SignOptions::default())
557 .unwrap();
558 assert!(finalized);
559 } else {
560 panic!("Expected SatsCard");
561 }
562
563 drop(python);
564 }
565
566 #[tokio::test]
568 async fn test_satscard_dump() {
569 let card_type = CardTypeOption::SatsCard;
570 let pipe_path = "/tmp/test-satscard-dump-pipe";
571 let pipe_path = Path::new(&pipe_path);
572 let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
573 let emulator = find_emulator(pipe_path).await.unwrap();
574 if let CkTapCard::SatsCard(mut sc) = emulator {
575 let slot_keys = sc.dump(0, Some("123456".to_string())).await;
577 assert!(matches!(slot_keys, Err(DumpError::SlotSealed(slot)) if slot == 0));
578 let slot_keys = sc.dump(0, None).await;
580 assert!(matches!(slot_keys, Err(DumpError::SlotSealed(slot)) if slot == 0));
581 sc.unseal(0, "123456").await.unwrap();
583 let slot_keys = sc.dump(0, Some("123456".to_string())).await;
585 assert!(slot_keys.is_ok());
586 assert!(matches!(slot_keys, Ok((Some(_), _))));
587 let slot_keys = slot_keys.unwrap();
588 let pubkey_from_privkey = slot_keys.0.unwrap().public_key(&sc.secp);
590 assert_eq!(pubkey_from_privkey, slot_keys.1);
591 let slot_keys = sc.dump(0, None).await;
593 assert!(slot_keys.is_ok());
594 assert!(matches!(slot_keys, Ok((None, _))));
595 let dump_response = sc.dump(1, Some("123456".to_string())).await;
597 assert!(matches!(dump_response, Err(DumpError::SlotUnused(slot)) if slot == 1));
598 let dump_response = sc.dump(1, None).await;
600 assert!(matches!(dump_response, Err(DumpError::SlotUnused(slot)) if slot == 1));
601 }
602 drop(python);
603 }
604}