Skip to main content

rust_cktap/
shared.rs

1// Copyright (c) 2025 rust-cktap contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{CardError, CkTapCard, CkTapError, SatsCard, TapSigner};
5use crate::{apdu::*, rand_nonce};
6
7use bitcoin::key::{PublicKey, rand};
8use bitcoin::secp256k1;
9use bitcoin::secp256k1::ecdh::SharedSecret;
10use bitcoin::secp256k1::ecdsa::{RecoverableSignature, RecoveryId, Signature};
11use bitcoin::secp256k1::{All, Message, Secp256k1};
12use bitcoin_hashes::sha256;
13use data_encoding::BASE32_NOPAD;
14
15use crate::error::{CertsError, ReadError, StatusError};
16use crate::sats_chip::SatsChip;
17use async_trait::async_trait;
18use bitcoin_hashes::hex::DisplayHex;
19use std::convert::TryFrom;
20use std::fmt;
21use std::fmt::Debug;
22use std::sync::Arc;
23
24/// Published Coinkite factory root keys.
25const PUB_FACTORY_ROOT_KEY: &str =
26    "03028a0e89e70d0ec0d932053a89ab1da7d9182bdc6d2f03e706ee99517d05d9e1";
27/// Obsolete dev value, but keeping for a little while longer.
28const DEV_FACTORY_ROOT_KEY: &str =
29    "027722ef208e681bac05f1b4b3cc478d6bf353ac9a09ff0c843430138f65c27bab";
30
31pub enum FactoryRootKey {
32    Pub(secp256k1::PublicKey),
33    Dev(secp256k1::PublicKey),
34}
35
36impl TryFrom<secp256k1::PublicKey> for FactoryRootKey {
37    type Error = CertsError;
38
39    fn try_from(pubkey: secp256k1::PublicKey) -> Result<Self, CertsError> {
40        match pubkey.serialize().to_lower_hex_string().as_str() {
41            PUB_FACTORY_ROOT_KEY => Ok(FactoryRootKey::Pub(pubkey)),
42            DEV_FACTORY_ROOT_KEY => Ok(FactoryRootKey::Dev(pubkey)),
43            _ => Err(CertsError::InvalidRootCert(
44                pubkey.serialize().to_lower_hex_string(),
45            )),
46        }
47    }
48}
49
50impl FactoryRootKey {
51    /// Return whether this is the production Coinkite factory root key.
52    pub const fn is_production(&self) -> bool {
53        matches!(self, Self::Pub(_))
54    }
55
56    pub fn name(&self) -> String {
57        match &self {
58            FactoryRootKey::Pub(_) => "Root Factory Certificate".to_string(),
59            FactoryRootKey::Dev(_) => "Root Factory Certificate (TESTING ONLY)".to_string(),
60        }
61    }
62}
63
64impl Debug for FactoryRootKey {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match &self {
67            FactoryRootKey::Pub(pk) => {
68                write!(f, "FactoryRootKey::Pub({pk:?})")
69            }
70            FactoryRootKey::Dev(pk) => {
71                write!(f, "FactoryRootKey::Dev({pk:?})")
72            }
73        }
74    }
75}
76
77/// Helper functions for authenticated commands.
78pub trait Authentication {
79    fn secp(&self) -> &Secp256k1<All>;
80    fn ver(&self) -> &str;
81    fn pubkey(&self) -> &PublicKey;
82    fn card_nonce(&self) -> &[u8; 16];
83    fn set_card_nonce(&mut self, new_nonce: [u8; 16]);
84    fn auth_delay(&self) -> Option<u8>;
85    fn set_auth_delay(&mut self, auth_delay: Option<u8>);
86    fn transport(&self) -> Arc<dyn CkTransport>;
87    fn card_ident(&self) -> String {
88        card_pubkey_to_ident(self.pubkey())
89    }
90
91    /// Calculate ephemeral key pair and XOR'd CVC.
92    /// ref: ["Authenticating Commands with CVC"](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#authenticating-commands-with-cvc)
93    fn calc_ekeys_xcvc(
94        &self,
95        cvc: &str,
96        command: &str,
97    ) -> (secp256k1::SecretKey, secp256k1::PublicKey, Vec<u8>) {
98        let secp = Self::secp(self);
99        let pubkey = Self::pubkey(self);
100        let nonce = Self::card_nonce(self);
101        let cvc_bytes = cvc.as_bytes();
102        let card_nonce_command = [nonce, command.as_bytes()].concat();
103        let (ephemeral_private_key, ephemeral_public_key) =
104            secp.generate_keypair(&mut rand::thread_rng());
105
106        let session_key = SharedSecret::new(&pubkey.inner, &ephemeral_private_key);
107        let md = sha256::Hash::hash(card_nonce_command.as_slice());
108        let md: &[u8; 32] = md.as_ref();
109
110        let mask: Vec<u8> = session_key
111            .as_ref()
112            .iter()
113            .zip(md)
114            .map(|(x, y)| x ^ y)
115            .take(cvc_bytes.len())
116            .collect();
117
118        let xcvc = cvc_bytes.iter().zip(mask).map(|(x, y)| x ^ y).collect();
119        (ephemeral_private_key, ephemeral_public_key, xcvc)
120    }
121}
122
123/// Convert a card's compressed pubkey into the standard public identifier.
124pub fn card_pubkey_to_ident(pubkey: &PublicKey) -> String {
125    let serialized = pubkey.inner.serialize();
126    let digest = sha256::Hash::hash(&serialized);
127    let digest_bytes = digest.to_byte_array();
128    let encoded = BASE32_NOPAD.encode(&digest_bytes[8..]);
129    let ident = &encoded[..20];
130
131    [&ident[0..5], &ident[5..10], &ident[10..15], &ident[15..20]].join("-")
132}
133
134#[cfg(test)]
135mod card_ident_tests {
136    use super::*;
137    use bitcoin::hex::FromHex;
138
139    #[test]
140    fn converts_pubkey_to_ident() {
141        let pubkey_bytes = Vec::<u8>::from_hex(
142            "0312d005ca1501b1603c3b00412eefe27c6b20a74c29377263b357b3aff12de6fa",
143        )
144        .expect("valid pubkey hex");
145        let pubkey = PublicKey::from_slice(&pubkey_bytes).expect("compressed pubkey");
146        assert_eq!(card_pubkey_to_ident(&pubkey), "IYKC5-XN6ZN-3AGAA-BWABB");
147    }
148}
149
150/// Trait for exchanging APDU data with cktap cards.
151#[async_trait]
152pub trait CkTransport: Sync + Send {
153    async fn transmit_apdu(&self, command_apdu: Vec<u8>) -> Result<Vec<u8>, CkTapError>;
154}
155
156/// Helper function for serialize APDU commands, transmit them and deserialize responses.
157pub(crate) async fn transmit<C, R>(
158    transport: Arc<dyn CkTransport>,
159    command: &C,
160) -> Result<R, CkTapError>
161where
162    C: CommandApdu + serde::Serialize + Debug + Send + Sync,
163    R: ResponseApdu + serde::de::DeserializeOwned + Debug + Send,
164{
165    let command_apdu = command.apdu_bytes();
166    let rapdu = transport.transmit_apdu(command_apdu).await?;
167    let response = R::from_cbor(rapdu.to_vec())?;
168    Ok(response)
169}
170
171pub async fn to_cktap(transport: Arc<dyn CkTransport>) -> Result<CkTapCard, StatusError> {
172    // Get status from card
173    let cmd = AppletSelect::default();
174    let status_response: StatusResponse = transmit(transport.clone(), &cmd).await?;
175
176    // Return correct card variant using status
177    match (status_response.tapsigner, status_response.satschip) {
178        (Some(true), None) => {
179            let tap_signer = TapSigner::try_from_status(transport, status_response)?;
180            Ok(CkTapCard::TapSigner(tap_signer))
181        }
182        (Some(true), Some(true)) => {
183            let sats_chip = SatsChip::try_from_status(transport, status_response)?;
184            Ok(CkTapCard::SatsChip(sats_chip))
185        }
186        (None, None) => {
187            let sats_card = SatsCard::from_status(transport, status_response)?;
188            Ok(CkTapCard::SatsCard(sats_card))
189        }
190        (_, _) => Err(StatusError::CkTap(CkTapError::UnknownCardType)),
191    }
192}
193
194/// Command to read and authenticate the cktap card's public key.
195/// SatsCard does not require a CVC but TapSigner and SatsChip do.
196/// ref: [read](https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#read)
197#[async_trait]
198pub trait Read: Authentication {
199    fn requires_auth(&self) -> bool;
200
201    fn slot(&self) -> Option<u8>;
202
203    async fn read(&mut self, cvc: Option<String>) -> Result<PublicKey, ReadError> {
204        let card_nonce = *self.card_nonce();
205        let app_nonce = rand_nonce();
206
207        // create the message digest
208        let mut message_bytes: Vec<u8> = Vec::new();
209        message_bytes.extend("OPENDIME".as_bytes());
210        message_bytes.extend(card_nonce);
211        message_bytes.extend(app_nonce);
212        if let Some(slot) = self.slot() {
213            message_bytes.push(slot);
214        } else {
215            message_bytes.push(0);
216        }
217        let hash = sha256::Hash::hash(message_bytes.as_slice());
218        let message_digest = Message::from_digest(hash.to_byte_array());
219
220        // create the read command
221        let (cmd, session_key) = if self.requires_auth() {
222            let cvc = cvc.ok_or(CkTapError::Card(CardError::NeedsAuth))?;
223            let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, ReadCommand::name());
224            (
225                ReadCommand::authenticated(app_nonce, epubkey, xcvc),
226                Some(SharedSecret::new(&self.pubkey().inner, &eprivkey)),
227            )
228        } else {
229            (ReadCommand::unauthenticated(app_nonce), None)
230        };
231
232        // send the read command to the card
233        let read_response: ReadResponse = transmit(self.transport(), &cmd).await?;
234
235        // convert the response to a PublicKey
236        let pubkey = read_response.pubkey(session_key)?;
237        self.secp().verify_ecdsa(
238            &message_digest,
239            &read_response.signature()?, // or add 'from' trait: Signature::from(response.sig: )
240            &pubkey.inner,
241        )?;
242
243        // update the card nonce
244        self.set_card_nonce(read_response.card_nonce);
245
246        Ok(pubkey)
247    }
248}
249
250#[async_trait]
251pub trait Wait: Authentication {
252    async fn wait(&mut self, cvc: Option<String>) -> Result<Option<u8>, CkTapError> {
253        let epubkey_xcvc = cvc.map(|cvc| {
254            let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, WaitCommand::name());
255            (epubkey, xcvc)
256        });
257
258        let (epubkey, xcvc) = epubkey_xcvc
259            .map(|(epubkey, xcvc)| (Some(epubkey), Some(xcvc)))
260            .unwrap_or((None, None));
261
262        let wait_command = WaitCommand::new(epubkey, xcvc);
263
264        let wait_response: WaitResponse = transmit(self.transport(), &wait_command).await?;
265        // TODO throw error if success == false
266        if wait_response.auth_delay > 0 {
267            let auth_delay = Some(wait_response.auth_delay);
268            self.set_auth_delay(auth_delay);
269            Ok(auth_delay)
270        } else {
271            self.set_auth_delay(None);
272            Ok(None)
273        }
274    }
275}
276
277#[async_trait]
278pub trait Certificate: Read {
279    /// Verify card authenticity against the production Coinkite factory root.
280    async fn check_production_certificate(&mut self) -> Result<(), CertsError> {
281        let root = self.check_certificate().await?;
282
283        if root.is_production() {
284            return Ok(());
285        }
286
287        Err(CertsError::InvalidRootCert(
288            "development factory root".to_string(),
289        ))
290    }
291
292    async fn check_certificate(&mut self) -> Result<FactoryRootKey, CertsError> {
293        let app_nonce = rand_nonce();
294        let card_nonce = *self.card_nonce();
295
296        let certs_cmd = CertsCommand::default();
297        let certs_response: CertsResponse = transmit(self.transport(), &certs_cmd).await?;
298        let cert_chain = CertificateChain::try_from(certs_response.cert_chain())?;
299
300        let check_cmd = CheckCommand::new(app_nonce);
301        let check_response: CheckResponse = transmit(self.transport(), &check_cmd).await?;
302        self.set_card_nonce(check_response.card_nonce);
303
304        let signature = parse_auth_signature(&check_response.auth_sig)?;
305
306        let slot_pubkey = self.slot_pubkey().await?;
307
308        // create message digest with slot pubkey
309        let mut message_bytes: Vec<u8> = Vec::new();
310        message_bytes.extend("OPENDIME".as_bytes());
311        message_bytes.extend(card_nonce);
312        message_bytes.extend(app_nonce);
313        if let Some(pubkey) = slot_pubkey {
314            if self.ver() != "0.9.0" {
315                let slot_pubkey_bytes = pubkey.inner.serialize();
316                message_bytes.extend(slot_pubkey_bytes);
317            }
318        }
319        let message_bytes_hash = sha256::Hash::hash(message_bytes.as_slice());
320        let message = Message::from_digest(message_bytes_hash.to_byte_array());
321
322        // verify the signature with the message and pubkey
323        self.secp()
324            .verify_ecdsa(&message, &signature, &self.pubkey().inner)?;
325
326        let root_pubkey = cert_chain.recover_root(self.secp(), *self.pubkey())?;
327        FactoryRootKey::try_from(root_pubkey.inner)
328    }
329
330    async fn slot_pubkey(&mut self) -> Result<Option<PublicKey>, ReadError>;
331}
332
333#[derive(Debug)]
334struct CertificateChain(Vec<RecoverableSignature>);
335
336impl TryFrom<Vec<Vec<u8>>> for CertificateChain {
337    type Error = CertsError;
338
339    fn try_from(certificates: Vec<Vec<u8>>) -> Result<Self, Self::Error> {
340        if certificates.is_empty() {
341            return Err(bitcoin::secp256k1::Error::InvalidSignature.into());
342        }
343
344        let signatures = certificates
345            .into_iter()
346            .map(|certificate| parse_certificate_signature(&certificate))
347            .collect::<Result<_, _>>()?;
348
349        Ok(Self(signatures))
350    }
351}
352
353impl CertificateChain {
354    fn recover_root(
355        &self,
356        secp: &Secp256k1<All>,
357        card_pubkey: PublicKey,
358    ) -> Result<PublicKey, CertsError> {
359        self.0.iter().try_fold(card_pubkey, |pubkey, signature| {
360            let pubkey_hash = sha256::Hash::hash(&pubkey.inner.serialize());
361            let message = Message::from_digest(pubkey_hash.to_byte_array());
362            let recovered = secp.recover_ecdsa(&message, signature)?;
363
364            Ok(PublicKey::new(recovered))
365        })
366    }
367}
368
369fn parse_auth_signature(signature: &[u8]) -> Result<Signature, CertsError> {
370    Ok(Signature::from_compact(signature)?)
371}
372
373fn parse_certificate_signature(certificate: &[u8]) -> Result<RecoverableSignature, CertsError> {
374    let (header, compact_signature) = certificate
375        .split_first()
376        .ok_or(bitcoin::secp256k1::Error::InvalidSignature)?;
377
378    // BIP-137: https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki
379    let recovery_id = match header {
380        27..=30 => header - 27, // P2PKH uncompressed
381        31..=34 => header - 31, // P2PKH compressed
382        35..=38 => header - 35, // Segwit P2SH
383        39..=42 => header - 39, // Segwit Bech32
384        _ => return Err(bitcoin::secp256k1::Error::InvalidRecoveryId.into()),
385    };
386    let recovery_id = RecoveryId::from_i32(i32::from(recovery_id))?;
387
388    Ok(RecoverableSignature::from_compact(
389        compact_signature,
390        recovery_id,
391    )?)
392}
393
394#[cfg(test)]
395mod certificate_tests {
396    use super::*;
397    use std::collections::VecDeque;
398    use std::sync::Mutex;
399
400    #[derive(serde::Serialize)]
401    struct CertsResponseBody {
402        cert_chain: Vec<serde_bytes::ByteBuf>,
403    }
404
405    #[derive(serde::Serialize)]
406    struct CheckResponseBody {
407        #[serde(with = "serde_bytes")]
408        auth_sig: Vec<u8>,
409        #[serde(with = "serde_bytes")]
410        card_nonce: [u8; 16],
411    }
412
413    struct QueuedTransport(Mutex<VecDeque<Vec<u8>>>);
414
415    #[async_trait]
416    impl CkTransport for QueuedTransport {
417        async fn transmit_apdu(&self, _command_apdu: Vec<u8>) -> Result<Vec<u8>, CkTapError> {
418            self.0
419                .lock()
420                .expect("response queue lock")
421                .pop_front()
422                .ok_or_else(|| CkTapError::Transport("response queue is empty".to_string()))
423        }
424    }
425
426    fn serialize_response(response: &impl serde::Serialize) -> Vec<u8> {
427        let mut bytes = Vec::new();
428        ciborium::ser::into_writer(response, &mut bytes).expect("serializable response");
429        bytes
430    }
431
432    fn certificate_signature(secp: &Secp256k1<All>) -> Vec<u8> {
433        let secret_key = secp256k1::SecretKey::from_slice(&[1; 32]).expect("valid secret key");
434        let message = Message::from_digest([2; 32]);
435        let signature = secp.sign_ecdsa_recoverable(&message, &secret_key);
436        let (recovery_id, compact_signature) = signature.serialize_compact();
437        let mut certificate = Vec::with_capacity(65);
438        certificate.push(27 + recovery_id.to_i32() as u8);
439        certificate.extend(compact_signature);
440        certificate
441    }
442
443    #[test]
444    fn rejects_invalid_auth_signature_length() {
445        let error = parse_auth_signature(&[]).unwrap_err();
446        assert!(matches!(
447            error,
448            CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidSignature)
449        ));
450    }
451
452    #[tokio::test]
453    async fn stores_response_nonce_before_parsing_auth_signature() {
454        let initial_nonce = [3; 16];
455        let response_nonce = [4; 16];
456        let secp = Secp256k1::new();
457        let secret_key = secp256k1::SecretKey::from_slice(&[1; 32]).expect("valid secret key");
458        let pubkey = PublicKey::new(secret_key.public_key(&secp));
459        let responses = VecDeque::from([
460            serialize_response(&CertsResponseBody {
461                cert_chain: vec![serde_bytes::ByteBuf::from(certificate_signature(&secp))],
462            }),
463            serialize_response(&CheckResponseBody {
464                auth_sig: Vec::new(),
465                card_nonce: response_nonce,
466            }),
467        ]);
468        let transport = Arc::new(QueuedTransport(Mutex::new(responses)));
469        let mut card = SatsCard {
470            transport,
471            secp,
472            proto: 1,
473            ver: "1.0.0".to_string(),
474            birth: 0,
475            slots: (0, 10),
476            addr: None,
477            pubkey,
478            card_nonce: initial_nonce,
479            auth_delay: None,
480        };
481
482        let error = card.check_certificate().await.unwrap_err();
483
484        assert!(matches!(
485            error,
486            CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidSignature)
487        ));
488        assert_eq!(card.card_nonce, response_nonce);
489    }
490
491    #[test]
492    fn rejects_empty_certificate_chain() {
493        let error = CertificateChain::try_from(Vec::new()).unwrap_err();
494        assert!(matches!(
495            error,
496            CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidSignature)
497        ));
498    }
499
500    #[test]
501    fn rejects_empty_certificate_signature() {
502        let error = parse_certificate_signature(&[]).unwrap_err();
503        assert!(matches!(
504            error,
505            CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidSignature)
506        ));
507    }
508
509    #[test]
510    fn rejects_invalid_certificate_header() {
511        let error = parse_certificate_signature(&[0; 65]).unwrap_err();
512        assert!(matches!(
513            error,
514            CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidRecoveryId)
515        ));
516    }
517
518    #[test]
519    fn rejects_invalid_certificate_signature_length() {
520        let error = parse_certificate_signature(&[31; 64]).unwrap_err();
521        assert!(matches!(
522            error,
523            CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidSignature)
524        ));
525    }
526}
527
528#[async_trait]
529pub trait Nfc: Authentication {
530    async fn nfc(&mut self) -> Result<String, CkTapError> {
531        let nfc_cmd = NfcCommand::default();
532        let nfc_response: NfcResponse = transmit(self.transport(), &nfc_cmd).await?;
533        Ok(nfc_response.url)
534    }
535}
536
537#[cfg(feature = "emulator")]
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use std::path::Path;
542
543    use crate::emulator::CVC;
544    use crate::emulator::find_emulator;
545    use crate::emulator::test::{CardTypeOption, EcardSubprocess};
546    use crate::rand_chaincode;
547    use crate::tap_signer::TapSignerShared;
548
549    #[tokio::test]
550    async fn test_new_command() {
551        let chain_code = rand_chaincode();
552        for card_type in CardTypeOption::values() {
553            let pipe_path = format!("/tmp/test-new-command-pipe{card_type}");
554            let pipe_path = Path::new(&pipe_path);
555            let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
556            let emulator = find_emulator(pipe_path).await.unwrap();
557            match emulator {
558                CkTapCard::SatsCard(mut sc) => {
559                    assert_eq!(card_type, CardTypeOption::SatsCard);
560                    let current_slot = sc.slots.0;
561                    let response = sc.unseal(current_slot, CVC).await;
562                    assert!(response.is_ok());
563                    let response = sc.new_slot(current_slot + 1, Some(chain_code), CVC).await;
564                    assert!(response.is_ok());
565                    assert_eq!(sc.slots.0, current_slot + 1);
566                    // test with no new chain_code
567                    let current_slot = sc.slots.0;
568                    let response = sc.unseal(current_slot, CVC).await;
569                    assert!(response.is_ok());
570                    let response = sc.new_slot(current_slot + 1, None, CVC).await;
571                    assert!(response.is_ok());
572                    assert_eq!(sc.slots.0, current_slot + 1);
573                }
574                CkTapCard::TapSigner(mut ts) => {
575                    assert_eq!(card_type, CardTypeOption::TapSigner);
576                    let response = ts.init(chain_code, CVC).await;
577                    assert!(response.is_ok())
578                }
579                CkTapCard::SatsChip(mut sc) => {
580                    assert_eq!(card_type, CardTypeOption::SatsChip);
581                    let response = sc.init(chain_code, CVC).await;
582                    assert!(response.is_ok())
583                }
584            };
585            drop(python);
586        }
587    }
588
589    #[tokio::test]
590    async fn test_cert_command() {
591        for card_type in CardTypeOption::values() {
592            let emulator_root_pubkey =
593                "0312d005ca1501b1603c3b00412eefe27c6b20a74c29377263b357b3aff12de6fa".to_string();
594            let pipe_path = format!("/tmp/test-cert-command-pipe{card_type}");
595            let pipe_path = Path::new(&pipe_path);
596            let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
597            let emulator = find_emulator(pipe_path).await.unwrap();
598            match emulator {
599                CkTapCard::SatsCard(mut sc) => {
600                    assert_eq!(card_type, CardTypeOption::SatsCard);
601                    let response = sc.check_certificate().await;
602                    assert!(response.is_err());
603                    matches!(response, Err(CertsError::InvalidRootCert(pubkey)) if pubkey == emulator_root_pubkey);
604                }
605                CkTapCard::TapSigner(mut ts) => {
606                    assert_eq!(card_type, CardTypeOption::TapSigner);
607                    let response = ts.check_certificate().await;
608                    assert!(response.is_err());
609                    matches!(response, Err(CertsError::InvalidRootCert(pubkey)) if pubkey == emulator_root_pubkey);
610                }
611                CkTapCard::SatsChip(mut sc) => {
612                    assert_eq!(card_type, CardTypeOption::SatsChip);
613                    let response = sc.check_certificate().await;
614                    assert!(response.is_err());
615                    matches!(response, Err(CertsError::InvalidRootCert(pubkey)) if pubkey == emulator_root_pubkey);
616                }
617            };
618            drop(python);
619        }
620    }
621
622    #[tokio::test]
623    async fn test_nfc_command() {
624        for card_type in CardTypeOption::values() {
625            let pipe_path = format!("/tmp/test-nfc-command-pipe{card_type}");
626            let pipe_path = Path::new(&pipe_path);
627            let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
628            let emulator = find_emulator(pipe_path).await.unwrap();
629            match emulator {
630                CkTapCard::SatsCard(mut sc) => {
631                    assert_eq!(card_type, CardTypeOption::SatsCard);
632                    let response = sc.nfc().await;
633                    assert!(response.is_ok());
634                    assert!(
635                        response
636                            .unwrap()
637                            .starts_with("https://getsatscard.com/start")
638                    )
639                }
640                CkTapCard::TapSigner(mut ts) => {
641                    assert_eq!(card_type, CardTypeOption::TapSigner);
642                    let response = ts.nfc().await;
643                    assert!(response.is_ok());
644                    assert!(response.unwrap().starts_with("https://tapsigner.com/start"))
645                }
646                CkTapCard::SatsChip(mut sc) => {
647                    assert_eq!(card_type, CardTypeOption::SatsChip);
648                    let response = sc.nfc().await;
649                    assert!(response.is_ok());
650                    assert!(response.unwrap().starts_with("https://satschip.com/start"))
651                }
652            };
653            drop(python);
654        }
655    }
656}