From 09271b68b429ad57b35dd08ff0e3bbcd933d9d6c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 26 Jun 2024 19:00:59 +0200 Subject: [PATCH] Add tests for attestation formats preference --- tests/basic.rs | 115 ++++++++++++++++++++++++++++++++++++++---- tests/webauthn/mod.rs | 62 +++++++++++++++++------ 2 files changed, 151 insertions(+), 26 deletions(-) diff --git a/tests/basic.rs b/tests/basic.rs index 7e6a898..3a85f4b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -10,8 +10,8 @@ use hex_literal::hex; use virt::{Ctap2, Ctap2Error}; use webauthn::{ - ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, GetAssertion, - GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, + AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, + GetAssertion, GetInfo, KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor, PubKeyCredParam, PublicKey, Rp, SharedSecret, User, }; @@ -33,6 +33,10 @@ fn test_get_info() { &hex!("8BC5496807B14D5FB249607F5D527DA2") ); assert_eq!(reply.pin_protocols, Some(vec![2, 1])); + assert_eq!( + reply.attestation_formats, + Some(vec!["packed".to_owned(), "none".to_owned()]) + ); }); } @@ -109,14 +113,87 @@ struct RequestPinToken { rp_id: Option, } +#[derive(Clone, Copy, Debug)] +enum AttestationFormatsPreference { + Empty, + None, + Packed, + NonePacked, + PackedNone, + OtherNonePacked, + MultiOtherNonePacked, +} + +impl AttestationFormatsPreference { + const ALL: &'static [Self] = &[ + Self::Empty, + Self::None, + Self::Packed, + Self::NonePacked, + Self::PackedNone, + Self::OtherNonePacked, + Self::MultiOtherNonePacked, + ]; + + fn format(&self) -> Option { + match self { + Self::Empty | Self::Packed | Self::PackedNone => Some(AttStmtFormat::Packed), + Self::NonePacked | Self::OtherNonePacked | Self::MultiOtherNonePacked => { + Some(AttStmtFormat::None) + } + Self::None => None, + } + } +} + +impl From for Vec<&'static str> { + fn from(preference: AttestationFormatsPreference) -> Self { + let mut vec = Vec::new(); + match preference { + AttestationFormatsPreference::Empty => {} + AttestationFormatsPreference::None => { + vec.push("none"); + } + AttestationFormatsPreference::Packed => { + vec.push("packed"); + } + AttestationFormatsPreference::NonePacked => { + vec.push("none"); + vec.push("packed"); + } + AttestationFormatsPreference::PackedNone => { + vec.push("packed"); + vec.push("none"); + } + AttestationFormatsPreference::OtherNonePacked => { + vec.push("tpm"); + vec.push("none"); + vec.push("packed"); + } + AttestationFormatsPreference::MultiOtherNonePacked => { + vec.resize(100, "tpm"); + vec.push("none"); + vec.push("packed"); + } + } + vec + } +} + #[derive(Debug)] struct TestMakeCredential { pin_token: Option, pub_key_alg: i32, + attestation_formats_preference: Option, } impl TestMakeCredential { fn run(&self) { + println!("{}", "=".repeat(80)); + println!("Running test:"); + println!("{self:#?}"); + println!(); + let key_agreement_key = KeyAgreementKey::generate(); let pin = b"123456"; let rp_id = "example.com"; @@ -148,6 +225,8 @@ impl TestMakeCredential { request.pin_auth = Some(pin_auth); request.pin_protocol = Some(2); } + request.attestation_formats_preference = + self.attestation_formats_preference.map(From::from); let result = device.exec(request); if let Some(error) = self.expected_error() { @@ -155,8 +234,17 @@ impl TestMakeCredential { } else { let reply = result.unwrap(); assert!(reply.auth_data.credential.is_some()); - assert_eq!(reply.fmt, "packed"); - reply.att_stmt.unwrap().validate(&reply.auth_data); + let format = self + .attestation_formats_preference + .unwrap_or(AttestationFormatsPreference::Packed) + .format(); + if let Some(format) = format { + assert_eq!(reply.fmt, format.as_str()); + reply.att_stmt.unwrap().validate(format, &reply.auth_data); + } else { + assert_eq!(reply.fmt, AttStmtFormat::None.as_str()); + assert!(reply.att_stmt.is_none()); + } } }); } @@ -202,15 +290,20 @@ fn test_make_credential() { ]; for pin_token in pin_tokens { for pub_key_alg in [-7, -11] { - let test = TestMakeCredential { + TestMakeCredential { pin_token: pin_token.clone(), pub_key_alg, - }; - println!("{}", "=".repeat(80)); - println!("Running test:"); - println!("{test:#?}"); - println!(); - test.run(); + attestation_formats_preference: None, + } + .run(); + for attestation_formats_preference in AttestationFormatsPreference::ALL { + TestMakeCredential { + pin_token: pin_token.clone(), + pub_key_alg, + attestation_formats_preference: Some(*attestation_formats_preference), + } + .run(); + } } } } diff --git a/tests/webauthn/mod.rs b/tests/webauthn/mod.rs index b642195..0698522 100644 --- a/tests/webauthn/mod.rs +++ b/tests/webauthn/mod.rs @@ -305,6 +305,7 @@ pub struct MakeCredential { pub options: Option, pub pin_auth: Option<[u8; 32]>, pub pin_protocol: Option, + pub attestation_formats_preference: Option>, } impl MakeCredential { @@ -323,6 +324,7 @@ impl MakeCredential { options: None, pin_auth: None, pin_protocol: None, + attestation_formats_preference: None, } } } @@ -353,6 +355,13 @@ impl From for Value { if let Some(pin_protocol) = request.pin_protocol { map.push(9, pin_protocol); } + if let Some(attestation_formats_preference) = request.attestation_formats_preference { + let preference: Vec<_> = attestation_formats_preference + .into_iter() + .map(Value::from) + .collect(); + map.push(0x0b, preference); + } map.into() } } @@ -418,26 +427,47 @@ impl Request for MakeCredential { type Reply = MakeCredentialReply; } +pub enum AttStmtFormat { + None, + Packed, +} + +impl AttStmtFormat { + pub fn as_str(&self) -> &'static str { + match self { + Self::None => "none", + Self::Packed => "packed", + } + } +} + #[derive(Debug, PartialEq, Deserialize)] pub struct AttStmt(BTreeMap); impl AttStmt { - pub fn validate(&self, auth_data: &AuthData) { - let alg = self.0.get("alg").unwrap(); - let x5c = self.0.get("x5c").unwrap().as_array().unwrap(); - let cert = x5c.first().unwrap().as_bytes().unwrap(); - let sig = self.0.get("sig").unwrap().as_bytes().unwrap(); - assert_eq!(alg, &Value::from(-7)); + pub fn validate(&self, format: AttStmtFormat, auth_data: &AuthData) { + match format { + AttStmtFormat::Packed => { + let alg = self.0.get("alg").unwrap(); + let x5c = self.0.get("x5c").unwrap().as_array().unwrap(); + let cert = x5c.first().unwrap().as_bytes().unwrap(); + let sig = self.0.get("sig").unwrap().as_bytes().unwrap(); + assert_eq!(alg, &Value::from(-7)); - let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap(); - assert!(rest.is_empty()); - let signature = DerSignature::from_bytes(sig).unwrap(); - let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); - let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else { - panic!("unexpected public key in attestation certificate"); - }; - let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); - public_key.verify(&auth_data.bytes, &signature).unwrap(); + let (rest, cert) = x509_parser::parse_x509_certificate(cert).unwrap(); + assert!(rest.is_empty()); + let signature = DerSignature::from_bytes(sig).unwrap(); + let public_key = cert.tbs_certificate.subject_pki.parsed().unwrap(); + let x509_parser::public_key::PublicKey::EC(ec_point) = public_key else { + panic!("unexpected public key in attestation certificate"); + }; + let public_key = VerifyingKey::from_sec1_bytes(ec_point.data()).unwrap(); + public_key.verify(&auth_data.bytes, &signature).unwrap(); + } + AttStmtFormat::None => { + assert!(self.0.is_empty()); + } + } } } @@ -667,6 +697,7 @@ pub struct GetInfoReply { pub versions: Vec, pub aaguid: Value, pub pin_protocols: Option>, + pub attestation_formats: Option>, } impl From for GetInfoReply { @@ -676,6 +707,7 @@ impl From for GetInfoReply { versions: map.remove(&1).unwrap().deserialized().unwrap(), aaguid: map.remove(&3).unwrap().deserialized().unwrap(), pin_protocols: map.remove(&6).map(|value| value.deserialized().unwrap()), + attestation_formats: map.remove(&0x16).map(|value| value.deserialized().unwrap()), } } }