From 1e1d0bc3b0c5cce7e65030cc441a7f2b5765c2ab Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Mon, 10 Aug 2026 15:02:00 -0500 Subject: [PATCH] Preserve card nonce after invalid signature Store the response nonce before parsing the certificate check signature so later authenticated commands use the current card nonce. Add a transport-level test for malformed signatures. --- lib/src/shared.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/lib/src/shared.rs b/lib/src/shared.rs index 3eebe6d..3a286f0 100644 --- a/lib/src/shared.rs +++ b/lib/src/shared.rs @@ -299,10 +299,10 @@ pub trait Certificate: Read { let check_cmd = CheckCommand::new(app_nonce); let check_response: CheckResponse = transmit(self.transport(), &check_cmd).await?; - let signature = parse_auth_signature(&check_response.auth_sig)?; - self.set_card_nonce(check_response.card_nonce); + let signature = parse_auth_signature(&check_response.auth_sig)?; + let slot_pubkey = self.slot_pubkey().await?; // create message digest with slot pubkey @@ -394,6 +394,51 @@ fn parse_certificate_signature(certificate: &[u8]) -> Result, + } + + #[derive(serde::Serialize)] + struct CheckResponseBody { + #[serde(with = "serde_bytes")] + auth_sig: Vec, + #[serde(with = "serde_bytes")] + card_nonce: [u8; 16], + } + + struct QueuedTransport(Mutex>>); + + #[async_trait] + impl CkTransport for QueuedTransport { + async fn transmit_apdu(&self, _command_apdu: Vec) -> Result, CkTapError> { + self.0 + .lock() + .expect("response queue lock") + .pop_front() + .ok_or_else(|| CkTapError::Transport("response queue is empty".to_string())) + } + } + + fn serialize_response(response: &impl serde::Serialize) -> Vec { + let mut bytes = Vec::new(); + ciborium::ser::into_writer(response, &mut bytes).expect("serializable response"); + bytes + } + + fn certificate_signature(secp: &Secp256k1) -> Vec { + let secret_key = secp256k1::SecretKey::from_slice(&[1; 32]).expect("valid secret key"); + let message = Message::from_digest([2; 32]); + let signature = secp.sign_ecdsa_recoverable(&message, &secret_key); + let (recovery_id, compact_signature) = signature.serialize_compact(); + let mut certificate = Vec::with_capacity(65); + certificate.push(27 + recovery_id.to_i32() as u8); + certificate.extend(compact_signature); + certificate + } #[test] fn rejects_invalid_auth_signature_length() { @@ -404,6 +449,45 @@ mod certificate_tests { )); } + #[tokio::test] + async fn stores_response_nonce_before_parsing_auth_signature() { + let initial_nonce = [3; 16]; + let response_nonce = [4; 16]; + let secp = Secp256k1::new(); + let secret_key = secp256k1::SecretKey::from_slice(&[1; 32]).expect("valid secret key"); + let pubkey = PublicKey::new(secret_key.public_key(&secp)); + let responses = VecDeque::from([ + serialize_response(&CertsResponseBody { + cert_chain: vec![serde_bytes::ByteBuf::from(certificate_signature(&secp))], + }), + serialize_response(&CheckResponseBody { + auth_sig: Vec::new(), + card_nonce: response_nonce, + }), + ]); + let transport = Arc::new(QueuedTransport(Mutex::new(responses))); + let mut card = SatsCard { + transport, + secp, + proto: 1, + ver: "1.0.0".to_string(), + birth: 0, + slots: (0, 10), + addr: None, + pubkey, + card_nonce: initial_nonce, + auth_delay: None, + }; + + let error = card.check_certificate().await.unwrap_err(); + + assert!(matches!( + error, + CertsError::Secp256k1(bitcoin::secp256k1::Error::InvalidSignature) + )); + assert_eq!(card.card_nonce, response_nonce); + } + #[test] fn rejects_empty_certificate_chain() { let error = CertificateChain::try_from(Vec::new()).unwrap_err();