Add tests for attestation formats preference

This commit is contained in:
Robin Krahl
2024-06-27 12:50:58 +02:00
parent 8025fd9d52
commit 09271b68b4
2 changed files with 151 additions and 26 deletions
+104 -11
View File
@@ -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<String>,
}
#[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<AttStmtFormat> {
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<AttestationFormatsPreference> 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<RequestPinToken>,
pub_key_alg: i32,
attestation_formats_preference: Option<AttestationFormatsPreference>,
}
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();
}
}
}
}
+47 -15
View File
@@ -305,6 +305,7 @@ pub struct MakeCredential {
pub options: Option<MakeCredentialOptions>,
pub pin_auth: Option<[u8; 32]>,
pub pin_protocol: Option<u8>,
pub attestation_formats_preference: Option<Vec<&'static str>>,
}
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<MakeCredential> 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<String, Value>);
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<String>,
pub aaguid: Value,
pub pin_protocols: Option<Vec<u8>>,
pub attestation_formats: Option<Vec<String>>,
}
impl From<Value> for GetInfoReply {
@@ -676,6 +707,7 @@ impl From<Value> 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()),
}
}
}