1use 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
24const PUB_FACTORY_ROOT_KEY: &str =
26 "03028a0e89e70d0ec0d932053a89ab1da7d9182bdc6d2f03e706ee99517d05d9e1";
27const 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 pub fn name(&self) -> String {
52 match &self {
53 FactoryRootKey::Pub(_) => "Root Factory Certificate".to_string(),
54 FactoryRootKey::Dev(_) => "Root Factory Certificate (TESTING ONLY)".to_string(),
55 }
56 }
57}
58
59impl Debug for FactoryRootKey {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match &self {
62 FactoryRootKey::Pub(pk) => {
63 write!(f, "FactoryRootKey::Pub({pk:?})")
64 }
65 FactoryRootKey::Dev(pk) => {
66 write!(f, "FactoryRootKey::Dev({pk:?})")
67 }
68 }
69 }
70}
71
72pub trait Authentication {
74 fn secp(&self) -> &Secp256k1<All>;
75 fn ver(&self) -> &str;
76 fn pubkey(&self) -> &PublicKey;
77 fn card_nonce(&self) -> &[u8; 16];
78 fn set_card_nonce(&mut self, new_nonce: [u8; 16]);
79 fn auth_delay(&self) -> &Option<usize>;
80 fn set_auth_delay(&mut self, auth_delay: Option<usize>);
81 fn transport(&self) -> Arc<dyn CkTransport>;
82 fn card_ident(&self) -> String {
83 card_pubkey_to_ident(self.pubkey())
84 }
85
86 fn calc_ekeys_xcvc(
89 &self,
90 cvc: &str,
91 command: &str,
92 ) -> (secp256k1::SecretKey, secp256k1::PublicKey, Vec<u8>) {
93 let secp = Self::secp(self);
94 let pubkey = Self::pubkey(self);
95 let nonce = Self::card_nonce(self);
96 let cvc_bytes = cvc.as_bytes();
97 let card_nonce_command = [nonce, command.as_bytes()].concat();
98 let (ephemeral_private_key, ephemeral_public_key) =
99 secp.generate_keypair(&mut rand::thread_rng());
100
101 let session_key = SharedSecret::new(&pubkey.inner, &ephemeral_private_key);
102 let md = sha256::Hash::hash(card_nonce_command.as_slice());
103 let md: &[u8; 32] = md.as_ref();
104
105 let mask: Vec<u8> = session_key
106 .as_ref()
107 .iter()
108 .zip(md)
109 .map(|(x, y)| x ^ y)
110 .take(cvc_bytes.len())
111 .collect();
112
113 let xcvc = cvc_bytes.iter().zip(mask).map(|(x, y)| x ^ y).collect();
114 (ephemeral_private_key, ephemeral_public_key, xcvc)
115 }
116}
117
118pub fn card_pubkey_to_ident(pubkey: &PublicKey) -> String {
120 let serialized = pubkey.inner.serialize();
121 let digest = sha256::Hash::hash(&serialized);
122 let digest_bytes = digest.to_byte_array();
123 let encoded = BASE32_NOPAD.encode(&digest_bytes[8..]);
124 let ident = &encoded[..20];
125
126 [&ident[0..5], &ident[5..10], &ident[10..15], &ident[15..20]].join("-")
127}
128
129#[cfg(test)]
130mod card_ident_tests {
131 use super::*;
132 use bitcoin::hex::FromHex;
133
134 #[test]
135 fn converts_pubkey_to_ident() {
136 let pubkey_bytes = Vec::<u8>::from_hex(
137 "0312d005ca1501b1603c3b00412eefe27c6b20a74c29377263b357b3aff12de6fa",
138 )
139 .expect("valid pubkey hex");
140 let pubkey = PublicKey::from_slice(&pubkey_bytes).expect("compressed pubkey");
141 assert_eq!(card_pubkey_to_ident(&pubkey), "IYKC5-XN6ZN-3AGAA-BWABB");
142 }
143}
144
145#[async_trait]
147pub trait CkTransport: Sync + Send {
148 async fn transmit_apdu(&self, command_apdu: Vec<u8>) -> Result<Vec<u8>, CkTapError>;
149}
150
151pub(crate) async fn transmit<C, R>(
153 transport: Arc<dyn CkTransport>,
154 command: &C,
155) -> Result<R, CkTapError>
156where
157 C: CommandApdu + serde::Serialize + Debug + Send + Sync,
158 R: ResponseApdu + serde::de::DeserializeOwned + Debug + Send,
159{
160 let command_apdu = command.apdu_bytes();
161 let rapdu = transport.transmit_apdu(command_apdu).await?;
162 let response = R::from_cbor(rapdu.to_vec())?;
163 Ok(response)
164}
165
166pub async fn to_cktap(transport: Arc<dyn CkTransport>) -> Result<CkTapCard, StatusError> {
167 let cmd = AppletSelect::default();
169 let status_response: StatusResponse = transmit(transport.clone(), &cmd).await?;
170
171 match (status_response.tapsigner, status_response.satschip) {
173 (Some(true), None) => {
174 let tap_signer = TapSigner::try_from_status(transport, status_response)?;
175 Ok(CkTapCard::TapSigner(tap_signer))
176 }
177 (Some(true), Some(true)) => {
178 let sats_chip = SatsChip::try_from_status(transport, status_response)?;
179 Ok(CkTapCard::SatsChip(sats_chip))
180 }
181 (None, None) => {
182 let sats_card = SatsCard::from_status(transport, status_response)?;
183 Ok(CkTapCard::SatsCard(sats_card))
184 }
185 (_, _) => Err(StatusError::CkTap(CkTapError::UnknownCardType)),
186 }
187}
188
189#[async_trait]
193pub trait Read: Authentication {
194 fn requires_auth(&self) -> bool;
195
196 fn slot(&self) -> Option<u8>;
197
198 async fn read(&mut self, cvc: Option<String>) -> Result<PublicKey, ReadError> {
199 let card_nonce = *self.card_nonce();
200 let app_nonce = rand_nonce();
201
202 let mut message_bytes: Vec<u8> = Vec::new();
204 message_bytes.extend("OPENDIME".as_bytes());
205 message_bytes.extend(card_nonce);
206 message_bytes.extend(app_nonce);
207 if let Some(slot) = self.slot() {
208 message_bytes.push(slot);
209 } else {
210 message_bytes.push(0);
211 }
212 let hash = sha256::Hash::hash(message_bytes.as_slice());
213 let message_digest = Message::from_digest(hash.to_byte_array());
214
215 let (cmd, session_key) = if self.requires_auth() {
217 let cvc = cvc.ok_or(CkTapError::Card(CardError::NeedsAuth))?;
218 let (eprivkey, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, ReadCommand::name());
219 (
220 ReadCommand::authenticated(app_nonce, epubkey, xcvc),
221 Some(SharedSecret::new(&self.pubkey().inner, &eprivkey)),
222 )
223 } else {
224 (ReadCommand::unauthenticated(app_nonce), None)
225 };
226
227 let read_response: ReadResponse = transmit(self.transport(), &cmd).await?;
229
230 let pubkey = read_response.pubkey(session_key)?;
232 self.secp().verify_ecdsa(
233 &message_digest,
234 &read_response.signature()?, &pubkey.inner,
236 )?;
237
238 self.set_card_nonce(read_response.card_nonce);
240
241 Ok(pubkey)
242 }
243}
244
245#[async_trait]
246pub trait Wait: Authentication {
247 async fn wait(&mut self, cvc: Option<String>) -> Result<Option<usize>, CkTapError> {
248 let epubkey_xcvc = cvc.map(|cvc| {
249 let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(&cvc, WaitCommand::name());
250 (epubkey, xcvc)
251 });
252
253 let (epubkey, xcvc) = epubkey_xcvc
254 .map(|(epubkey, xcvc)| (Some(epubkey), Some(xcvc)))
255 .unwrap_or((None, None));
256
257 let wait_command = WaitCommand::new(epubkey, xcvc);
258
259 let wait_response: WaitResponse = transmit(self.transport(), &wait_command).await?;
260 if wait_response.auth_delay > 0 {
262 let auth_delay = Some(wait_response.auth_delay);
263 self.set_auth_delay(auth_delay);
264 Ok(auth_delay)
265 } else {
266 self.set_auth_delay(None);
267 Ok(None)
268 }
269 }
270}
271
272#[async_trait]
273pub trait Certificate: Read {
274 async fn check_certificate(&mut self) -> Result<FactoryRootKey, CertsError> {
275 let app_nonce = rand_nonce();
276 let card_nonce = *self.card_nonce();
277
278 let certs_cmd = CertsCommand::default();
279 let certs_response: CertsResponse = transmit(self.transport(), &certs_cmd).await?;
280
281 let check_cmd = CheckCommand::new(app_nonce);
282 let check_response: CheckResponse = transmit(self.transport(), &check_cmd).await?;
283 self.set_card_nonce(check_response.card_nonce);
284
285 let slot_pubkey = self.slot_pubkey().await?;
286
287 let mut message_bytes: Vec<u8> = Vec::new();
289 message_bytes.extend("OPENDIME".as_bytes());
290 message_bytes.extend(card_nonce);
291 message_bytes.extend(app_nonce);
292 if let Some(pubkey) = slot_pubkey {
293 if self.ver() != "0.9.0" {
294 let slot_pubkey_bytes = pubkey.inner.serialize();
295 message_bytes.extend(slot_pubkey_bytes);
296 }
297 }
298 let message_bytes_hash = sha256::Hash::hash(message_bytes.as_slice());
299 let message = Message::from_digest(message_bytes_hash.to_byte_array());
300
301 let signature = Signature::from_compact(check_response.auth_sig.as_slice())
303 .expect("Failed to construct ECDSA signature from check response");
304 self.secp()
305 .verify_ecdsa(&message, &signature, &self.pubkey().inner)?;
306
307 let mut pubkey = *self.pubkey();
308 for sig in &certs_response.cert_chain() {
309 let subtract_by = match sig[0] {
311 27..=30 => 27, 31..=34 => 31, 35..=38 => 35, 39..=42 => 39, _ => panic!("Unrecognized BIP-137 address"),
316 };
317 let rec_id = RecoveryId::from_i32((sig[0] as i32) - subtract_by)?;
318 let (_, sig) = sig.split_at(1);
319 let result = RecoverableSignature::from_compact(sig, rec_id);
320 let rec_sig = result?;
321 let pubkey_hash = sha256::Hash::hash(&pubkey.inner.serialize());
322 let md = Message::from_digest(pubkey_hash.to_byte_array());
323 let result = self.secp().recover_ecdsa(&md, &rec_sig);
324 pubkey = PublicKey::new(result?);
325 }
326
327 FactoryRootKey::try_from(pubkey.inner)
328 }
329
330 async fn slot_pubkey(&mut self) -> Result<Option<PublicKey>, ReadError>;
331}
332
333#[async_trait]
334pub trait Nfc: Authentication {
335 async fn nfc(&mut self) -> Result<String, CkTapError> {
336 let nfc_cmd = NfcCommand::default();
337 let nfc_response: NfcResponse = transmit(self.transport(), &nfc_cmd).await?;
338 Ok(nfc_response.url)
339 }
340}
341
342#[cfg(feature = "emulator")]
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use std::path::Path;
347
348 use crate::emulator::CVC;
349 use crate::emulator::find_emulator;
350 use crate::emulator::test::{CardTypeOption, EcardSubprocess};
351 use crate::rand_chaincode;
352 use crate::tap_signer::TapSignerShared;
353
354 #[tokio::test]
355 async fn test_new_command() {
356 let chain_code = rand_chaincode();
357 for card_type in CardTypeOption::values() {
358 let pipe_path = format!("/tmp/test-new-command-pipe{card_type}");
359 let pipe_path = Path::new(&pipe_path);
360 let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
361 let emulator = find_emulator(pipe_path).await.unwrap();
362 match emulator {
363 CkTapCard::SatsCard(mut sc) => {
364 assert_eq!(card_type, CardTypeOption::SatsCard);
365 let current_slot = sc.slots.0;
366 let response = sc.unseal(current_slot, CVC).await;
367 assert!(response.is_ok());
368 let response = sc.new_slot(current_slot + 1, Some(chain_code), CVC).await;
369 assert!(response.is_ok());
370 assert_eq!(sc.slots.0, current_slot + 1);
371 let current_slot = sc.slots.0;
373 let response = sc.unseal(current_slot, CVC).await;
374 assert!(response.is_ok());
375 let response = sc.new_slot(current_slot + 1, None, CVC).await;
376 assert!(response.is_ok());
377 assert_eq!(sc.slots.0, current_slot + 1);
378 }
379 CkTapCard::TapSigner(mut ts) => {
380 assert_eq!(card_type, CardTypeOption::TapSigner);
381 let response = ts.init(chain_code, CVC).await;
382 assert!(response.is_ok())
383 }
384 CkTapCard::SatsChip(mut sc) => {
385 assert_eq!(card_type, CardTypeOption::SatsChip);
386 let response = sc.init(chain_code, CVC).await;
387 assert!(response.is_ok())
388 }
389 };
390 drop(python);
391 }
392 }
393
394 #[tokio::test]
395 async fn test_cert_command() {
396 for card_type in CardTypeOption::values() {
397 let emulator_root_pubkey =
398 "0312d005ca1501b1603c3b00412eefe27c6b20a74c29377263b357b3aff12de6fa".to_string();
399 let pipe_path = format!("/tmp/test-cert-command-pipe{card_type}");
400 let pipe_path = Path::new(&pipe_path);
401 let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
402 let emulator = find_emulator(pipe_path).await.unwrap();
403 match emulator {
404 CkTapCard::SatsCard(mut sc) => {
405 assert_eq!(card_type, CardTypeOption::SatsCard);
406 let response = sc.check_certificate().await;
407 assert!(response.is_err());
408 matches!(response, Err(CertsError::InvalidRootCert(pubkey)) if pubkey == emulator_root_pubkey);
409 }
410 CkTapCard::TapSigner(mut ts) => {
411 assert_eq!(card_type, CardTypeOption::TapSigner);
412 let response = ts.check_certificate().await;
413 assert!(response.is_err());
414 matches!(response, Err(CertsError::InvalidRootCert(pubkey)) if pubkey == emulator_root_pubkey);
415 }
416 CkTapCard::SatsChip(mut sc) => {
417 assert_eq!(card_type, CardTypeOption::SatsChip);
418 let response = sc.check_certificate().await;
419 assert!(response.is_err());
420 matches!(response, Err(CertsError::InvalidRootCert(pubkey)) if pubkey == emulator_root_pubkey);
421 }
422 };
423 drop(python);
424 }
425 }
426
427 #[tokio::test]
428 async fn test_nfc_command() {
429 for card_type in CardTypeOption::values() {
430 let pipe_path = format!("/tmp/test-nfc-command-pipe{card_type}");
431 let pipe_path = Path::new(&pipe_path);
432 let python = EcardSubprocess::new(pipe_path, &card_type).unwrap();
433 let emulator = find_emulator(pipe_path).await.unwrap();
434 match emulator {
435 CkTapCard::SatsCard(mut sc) => {
436 assert_eq!(card_type, CardTypeOption::SatsCard);
437 let response = sc.nfc().await;
438 assert!(response.is_ok());
439 assert!(
440 response
441 .unwrap()
442 .starts_with("https://getsatscard.com/start")
443 )
444 }
445 CkTapCard::TapSigner(mut ts) => {
446 assert_eq!(card_type, CardTypeOption::TapSigner);
447 let response = ts.nfc().await;
448 assert!(response.is_ok());
449 assert!(response.unwrap().starts_with("https://tapsigner.com/start"))
450 }
451 CkTapCard::SatsChip(mut sc) => {
452 assert_eq!(card_type, CardTypeOption::SatsChip);
453 let response = sc.nfc().await;
454 assert!(response.is_ok());
455 assert!(response.unwrap().starts_with("https://satschip.com/start"))
456 }
457 };
458 drop(python);
459 }
460 }
461}