use ciborium::value::Value;
use crate::algorithm::{COSE_EDDSA, COSE_ES256, COSE_RS256};
use crate::credential::{AttestationType, PublicKey};
use crate::crypto::{verify_eddsa, verify_es256, verify_rs256};
use crate::der::rsa_components_to_der;
use crate::error::{Result, WebAuthnError};
pub fn verify(
fmt: &str,
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
credential_id: &[u8],
) -> Result<AttestationType> {
match fmt {
"none" => Ok(AttestationType::None),
"packed" => verify_packed(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
),
"fido-u2f" => verify_fido_u2f(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_id,
credential_public_key,
),
"android-key" => verify_android_key(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
),
"apple" => verify_apple(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
),
_other => Ok(AttestationType::None),
}
}
fn verify_packed(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
) -> Result<AttestationType> {
let stmt_map = match att_stmt {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"packed attStmt must be a CBOR map".to_string(),
))
}
};
let mut alg: Option<i64> = None;
let mut sig: Option<Vec<u8>> = None;
let mut has_x5c = false;
for (k, v) in stmt_map {
match k {
Value::Text(ref key) if key == "alg" => {
alg = Some(match v {
Value::Integer(i) => i64::try_from(*i).map_err(|_| {
WebAuthnError::InvalidAttestationObject(
"packed attStmt alg value out of i64 range".to_string(),
)
})?,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"packed attStmt alg must be a CBOR integer".to_string(),
))
}
});
}
Value::Text(ref key) if key == "sig" => {
sig = Some(match v {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"packed attStmt sig must be CBOR bytes".to_string(),
))
}
});
}
Value::Text(ref key) if key == "x5c" => {
has_x5c = true;
}
_ => {}
}
}
if has_x5c {
return Ok(AttestationType::Basic);
}
let alg = alg.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"packed self-attestation attStmt missing required field: alg".to_string(),
)
})?;
let sig = sig.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"packed self-attestation attStmt missing required field: sig".to_string(),
)
})?;
let expected_alg = credential_public_key.algorithm();
if alg != expected_alg {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"packed self-attestation alg ({alg}) does not match credential key algorithm ({expected_alg})"
)));
}
let mut verification_data = Vec::with_capacity(auth_data_bytes.len() + 32);
verification_data.extend_from_slice(auth_data_bytes);
verification_data.extend_from_slice(client_data_hash);
match credential_public_key {
PublicKey::ES256 { x, y } if alg == COSE_ES256 => {
let mut uncompressed = Vec::with_capacity(65);
uncompressed.push(0x04);
uncompressed.extend_from_slice(x);
uncompressed.extend_from_slice(y);
verify_es256(&uncompressed, &verification_data, &sig)?;
}
PublicKey::EdDSA(pk) if alg == COSE_EDDSA => {
verify_eddsa(pk, &verification_data, &sig)?;
}
PublicKey::RS256 { n, e } if alg == COSE_RS256 => {
let der = rsa_components_to_der(n, e)?;
verify_rs256(&der, &verification_data, &sig)?;
}
_ => {
return Err(WebAuthnError::UnsupportedAlgorithm(alg));
}
}
Ok(AttestationType::SelfAttestation)
}
fn verify_fido_u2f(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_id: &[u8],
credential_public_key: &PublicKey,
) -> Result<AttestationType> {
let stmt_map = match att_stmt {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f attStmt must be a CBOR map".to_string(),
))
}
};
let mut sig: Option<Vec<u8>> = None;
let mut x5c_first_cert: Option<Vec<u8>> = None;
for (k, v) in stmt_map {
match k {
Value::Text(ref key) if key == "sig" => {
sig = Some(match v {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f attStmt sig must be CBOR bytes".to_string(),
))
}
});
}
Value::Text(ref key) if key == "x5c" => {
x5c_first_cert = Some(match v {
Value::Array(certs) if !certs.is_empty() => match &certs[0] {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f x5c[0] must be CBOR bytes".to_string(),
))
}
},
Value::Array(_) => {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f x5c must be non-empty".to_string(),
))
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f x5c must be a CBOR array".to_string(),
))
}
});
}
_ => {}
}
}
let cert_der = x5c_first_cert.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"fido-u2f attStmt missing required field: x5c".to_string(),
)
})?;
let sig = sig.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"fido-u2f attStmt missing required field: sig".to_string(),
)
})?;
let att_pk = extract_ec_p256_public_key_from_cert(&cert_der)?;
let cred_pk_bytes = match credential_public_key {
PublicKey::ES256 { x, y } => {
let mut pk = Vec::with_capacity(65);
pk.push(0x04);
pk.extend_from_slice(x);
pk.extend_from_slice(y);
pk
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f attestation requires an EC P-256 credential key".to_string(),
))
}
};
if auth_data_bytes.len() < 32 {
return Err(WebAuthnError::InvalidAttestationObject(
"fido-u2f: authenticator data too short to contain rpIdHash".to_string(),
));
}
let rp_id_hash = &auth_data_bytes[..32];
let mut verification_data =
Vec::with_capacity(1 + 32 + 32 + credential_id.len() + cred_pk_bytes.len());
verification_data.push(0x00);
verification_data.extend_from_slice(rp_id_hash);
verification_data.extend_from_slice(client_data_hash);
verification_data.extend_from_slice(credential_id);
verification_data.extend_from_slice(&cred_pk_bytes);
verify_es256(&att_pk, &verification_data, &sig)?;
Ok(AttestationType::Basic)
}
fn verify_android_key(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
) -> Result<AttestationType> {
let stmt_map = match att_stmt {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key attStmt must be a CBOR map".to_string(),
))
}
};
let mut alg: Option<i64> = None;
let mut sig: Option<Vec<u8>> = None;
let mut x5c_first_cert: Option<Vec<u8>> = None;
for (k, v) in stmt_map {
match k {
Value::Text(ref key) if key == "alg" => {
alg = Some(match v {
Value::Integer(i) => i64::try_from(*i).map_err(|_| {
WebAuthnError::InvalidAttestationObject(
"android-key attStmt alg value out of i64 range".to_string(),
)
})?,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key attStmt alg must be a CBOR integer".to_string(),
))
}
});
}
Value::Text(ref key) if key == "sig" => {
sig = Some(match v {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key attStmt sig must be CBOR bytes".to_string(),
))
}
});
}
Value::Text(ref key) if key == "x5c" => {
x5c_first_cert = Some(match v {
Value::Array(certs) if !certs.is_empty() => match &certs[0] {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key x5c[0] must be CBOR bytes".to_string(),
))
}
},
Value::Array(_) => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key x5c must be non-empty".to_string(),
))
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key x5c must be a CBOR array".to_string(),
))
}
});
}
_ => {}
}
}
alg.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"android-key attStmt missing required field: alg".to_string(),
)
})?;
let sig = sig.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"android-key attStmt missing required field: sig".to_string(),
)
})?;
let cert_der = x5c_first_cert.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"android-key attStmt missing required field: x5c".to_string(),
)
})?;
let att_pk = extract_ec_p256_public_key_from_cert(&cert_der)?;
let cred_pk_uncompressed = match credential_public_key {
PublicKey::ES256 { x, y } => {
let mut pk = Vec::with_capacity(65);
pk.push(0x04);
pk.extend_from_slice(x);
pk.extend_from_slice(y);
pk
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key: attestation requires an ES256 (P-256) credential key".to_string(),
))
}
};
if att_pk != cred_pk_uncompressed {
return Err(WebAuthnError::InvalidAttestationObject(
"android-key: credential public key does not match attestation certificate key"
.to_string(),
));
}
let mut verification_data = Vec::with_capacity(auth_data_bytes.len() + 32);
verification_data.extend_from_slice(auth_data_bytes);
verification_data.extend_from_slice(client_data_hash);
verify_es256(&att_pk, &verification_data, &sig)?;
Ok(AttestationType::Basic)
}
fn verify_apple(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
) -> Result<AttestationType> {
let stmt_map = match att_stmt {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"apple attStmt must be a CBOR map".to_string(),
))
}
};
let mut x5c_first_cert: Option<Vec<u8>> = None;
for (k, v) in stmt_map {
if let Value::Text(ref key) = k {
if key == "x5c" {
x5c_first_cert = Some(match v {
Value::Array(certs) if !certs.is_empty() => match &certs[0] {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"apple x5c[0] must be CBOR bytes".to_string(),
))
}
},
Value::Array(_) => {
return Err(WebAuthnError::InvalidAttestationObject(
"apple x5c must be non-empty".to_string(),
))
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"apple x5c must be a CBOR array".to_string(),
))
}
});
}
}
}
let cert_der = x5c_first_cert.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"apple attStmt missing required field: x5c".to_string(),
)
})?;
let mut nonce_input = Vec::with_capacity(auth_data_bytes.len() + 32);
nonce_input.extend_from_slice(auth_data_bytes);
nonce_input.extend_from_slice(client_data_hash);
let expected_nonce = crate::crypto::sha256(&nonce_input);
let cert_nonce = extract_apple_nonce_from_cert(&cert_der)?;
if cert_nonce != expected_nonce {
return Err(WebAuthnError::InvalidAttestationObject(
"apple: certificate nonce does not match SHA-256(authData || clientDataHash)"
.to_string(),
));
}
let cert_pk = extract_ec_p256_public_key_from_cert(&cert_der)?;
let cred_pk_uncompressed = match credential_public_key {
PublicKey::ES256 { x, y } => {
let mut pk = Vec::with_capacity(65);
pk.push(0x04);
pk.extend_from_slice(x);
pk.extend_from_slice(y);
pk
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"apple: attestation requires an ES256 (P-256) credential key".to_string(),
))
}
};
if cert_pk != cred_pk_uncompressed {
return Err(WebAuthnError::InvalidAttestationObject(
"apple: credential public key does not match attestation certificate key".to_string(),
));
}
Ok(AttestationType::Basic)
}
fn extract_apple_nonce_from_cert(cert_der: &[u8]) -> Result<[u8; 32]> {
const APPLE_NONCE_OID: &[u8] = &[
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x63, 0x64, 0x08, 0x02,
];
let oid_pos = cert_der
.windows(APPLE_NONCE_OID.len())
.position(|w| w == APPLE_NONCE_OID)
.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"apple: credential certificate missing Apple nonce extension \
(OID 1.2.840.113635.100.8.2)"
.to_string(),
)
})?;
let after_oid = &cert_der[oid_pos + APPLE_NONCE_OID.len()..];
let ext_value_bytes = if after_oid.first() == Some(&0x01) {
after_oid.get(3..).unwrap_or(&[])
} else {
after_oid
};
let ext_content = der_unwrap_octet_string(ext_value_bytes).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"apple: failed to parse extension extnValue OCTET STRING".to_string(),
)
})?;
let outer_seq = der_unwrap_sequence(ext_content).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"apple: failed to parse extension value outer SEQUENCE".to_string(),
)
})?;
let inner_seq = der_unwrap_sequence(outer_seq).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"apple: failed to parse extension value inner SEQUENCE".to_string(),
)
})?;
let nonce_bytes = der_unwrap_octet_string(inner_seq).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"apple: failed to parse nonce OCTET STRING in extension".to_string(),
)
})?;
if nonce_bytes.len() != 32 {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"apple: extension nonce must be 32 bytes, got {}",
nonce_bytes.len()
)));
}
let mut nonce = [0u8; 32];
nonce.copy_from_slice(nonce_bytes);
Ok(nonce)
}
fn der_parse_tlv(data: &[u8]) -> Option<(u8, &[u8], &[u8])> {
let tag = *data.first()?;
if data.len() < 2 {
return None;
}
let (len, header_len) = if data[1] & 0x80 == 0 {
(data[1] as usize, 2)
} else {
let num_bytes = (data[1] & 0x7f) as usize;
if num_bytes == 0 || data.len() < 2 + num_bytes {
return None;
}
let mut len = 0usize;
for &b in &data[2..2 + num_bytes] {
len = (len << 8) | b as usize;
}
(len, 2 + num_bytes)
};
let end = header_len.checked_add(len)?;
if data.len() < end {
return None;
}
Some((tag, &data[header_len..end], &data[end..]))
}
fn der_unwrap_sequence(data: &[u8]) -> Option<&[u8]> {
let (tag, contents, _) = der_parse_tlv(data)?;
if tag == 0x30 {
Some(contents)
} else {
None
}
}
fn der_unwrap_octet_string(data: &[u8]) -> Option<&[u8]> {
let (tag, contents, _) = der_parse_tlv(data)?;
if tag == 0x04 {
Some(contents)
} else {
None
}
}
fn extract_ec_p256_public_key_from_cert(cert_der: &[u8]) -> Result<Vec<u8>> {
const SPKI_PREFIX: &[u8] = &[
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08,
0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04,
];
cert_der
.windows(SPKI_PREFIX.len())
.position(|w| w == SPKI_PREFIX)
.and_then(|pos| {
let key_start = pos + SPKI_PREFIX.len() - 1; cert_der.get(key_start..key_start + 65)
})
.map(|key| key.to_vec())
.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"fido-u2f: certificate does not contain a P-256 EC public key".to_string(),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn none_att_stmt() -> Value {
Value::Map(vec![])
}
#[test]
fn accepts_none_format() {
let pk = dummy_es256_key();
let result = verify("none", &none_att_stmt(), &[], &[0u8; 32], &pk, &[]);
assert!(matches!(result, Ok(AttestationType::None)));
}
#[test]
fn accepts_unknown_format_as_none() {
let pk = dummy_es256_key();
let result = verify("tpm", &none_att_stmt(), &[], &[0u8; 32], &pk, &[]);
assert!(matches!(result, Ok(AttestationType::None)));
}
#[test]
fn packed_basic_attestation_detected_when_x5c_present() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(vec![0u8; 10])]),
),
]);
let result = verify("packed", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
#[test]
fn packed_rejects_non_map_att_stmt() {
let pk = dummy_es256_key();
let result = verify(
"packed",
&Value::Text("bad".to_string()),
&[],
&[0u8; 32],
&pk,
&[],
);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAttestationObject(_))
));
}
#[test]
fn packed_rejects_missing_alg() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![(
Value::Text("sig".to_string()),
Value::Bytes(vec![0u8; 64]),
)]);
let result = verify("packed", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("alg")
));
}
#[test]
fn packed_rejects_missing_sig() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
)]);
let result = verify("packed", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("sig")
));
}
#[test]
fn packed_rejects_algorithm_mismatch() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-257i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
]);
let result = verify("packed", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("alg")
));
}
#[test]
fn packed_self_attestation_es256_valid_signature() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pub_bytes = kp.public_key().as_ref();
let x = pub_bytes[1..33].to_vec();
let y = pub_bytes[33..65].to_vec();
let pk = PublicKey::ES256 { x, y };
let auth_data = b"fake-auth-data";
let client_data_hash = [0xABu8; 32];
let mut verification_data = Vec::new();
verification_data.extend_from_slice(auth_data);
verification_data.extend_from_slice(&client_data_hash);
let sig = kp.sign(&rng, &verification_data).unwrap();
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("sig".to_string()),
Value::Bytes(sig.as_ref().to_vec()),
),
]);
let result = verify("packed", &stmt, auth_data, &client_data_hash, &pk, &[]);
assert!(matches!(result, Ok(AttestationType::SelfAttestation)));
}
#[test]
fn packed_self_attestation_es256_bad_signature() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pub_bytes = kp.public_key().as_ref();
let x = pub_bytes[1..33].to_vec();
let y = pub_bytes[33..65].to_vec();
let pk = PublicKey::ES256 { x, y };
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("sig".to_string()),
Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
),
]);
let result = verify("packed", &stmt, b"auth", &[0u8; 32], &pk, &[]);
assert!(matches!(
result,
Err(WebAuthnError::SignatureVerificationFailed)
));
}
#[test]
fn fido_u2f_rejects_missing_x5c() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![(
Value::Text("sig".to_string()),
Value::Bytes(vec![0u8; 64]),
)]);
let result = verify("fido-u2f", &stmt, &[0u8; 37], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("x5c"))
);
}
#[test]
fn fido_u2f_rejects_missing_sig() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(vec![0u8; 10])]),
)]);
let result = verify("fido-u2f", &stmt, &[0u8; 37], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("sig"))
);
}
#[test]
fn fido_u2f_rejects_empty_x5c_array() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![]), ),
]);
let result = verify("fido-u2f", &stmt, &[0u8; 37], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("non-empty"))
);
}
#[test]
fn fido_u2f_rejects_non_es256_credential_key() {
let pk = PublicKey::EdDSA(vec![0u8; 32]);
let stmt = Value::Map(vec![
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(make_fake_p256_cert(&[0x04; 65]))]),
),
]);
let result = verify("fido-u2f", &stmt, &[0u8; 37], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("P-256"))
);
}
#[test]
fn fido_u2f_valid_signature_returns_basic() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let att_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let att_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, att_pkcs8.as_ref(), &rng)
.unwrap();
let att_pub = att_kp.public_key().as_ref(); let cert = make_fake_p256_cert(att_pub);
let cred_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.unwrap();
let cred_pub = cred_kp.public_key().as_ref();
let credential_public_key = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let credential_id = b"test-cred-id";
let rp_id_hash = [0x77u8; 32];
let client_data_hash = [0x88u8; 32];
let mut auth_data = rp_id_hash.to_vec();
auth_data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00]);
let mut verification_data = vec![0x00u8];
verification_data.extend_from_slice(&rp_id_hash);
verification_data.extend_from_slice(&client_data_hash);
verification_data.extend_from_slice(credential_id);
verification_data.extend_from_slice(cred_pub);
let sig = att_kp.sign(&rng, &verification_data).unwrap();
let stmt = Value::Map(vec![
(
Value::Text("sig".to_string()),
Value::Bytes(sig.as_ref().to_vec()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
]);
let result = verify(
"fido-u2f",
&stmt,
&auth_data,
&client_data_hash,
&credential_public_key,
credential_id,
);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
#[test]
fn fido_u2f_bad_signature_rejected() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let att_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let att_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, att_pkcs8.as_ref(), &rng)
.unwrap();
let cert = make_fake_p256_cert(att_kp.public_key().as_ref());
let cred_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.unwrap();
let cred_pub = cred_kp.public_key().as_ref();
let credential_public_key = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let mut auth_data = [0x77u8; 32].to_vec();
auth_data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00]);
let stmt = Value::Map(vec![
(
Value::Text("sig".to_string()),
Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]), ),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
]);
let result = verify(
"fido-u2f",
&stmt,
&auth_data,
&[0x88u8; 32],
&credential_public_key,
b"cred-id",
);
assert!(matches!(
result,
Err(WebAuthnError::SignatureVerificationFailed)
));
}
#[test]
fn android_key_valid_signature_returns_basic() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pub_bytes = kp.public_key().as_ref(); let cert = make_fake_p256_cert(pub_bytes);
let credential_public_key = PublicKey::ES256 {
x: pub_bytes[1..33].to_vec(),
y: pub_bytes[33..65].to_vec(),
};
let auth_data = b"fake-auth-data";
let client_data_hash = [0xABu8; 32];
let mut verification_data = Vec::new();
verification_data.extend_from_slice(auth_data);
verification_data.extend_from_slice(&client_data_hash);
let sig = kp.sign(&rng, &verification_data).unwrap();
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("sig".to_string()),
Value::Bytes(sig.as_ref().to_vec()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
]);
let result = verify(
"android-key",
&stmt,
auth_data,
&client_data_hash,
&credential_public_key,
&[],
);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
#[test]
fn android_key_bad_signature_rejected() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pub_bytes = kp.public_key().as_ref();
let cert = make_fake_p256_cert(pub_bytes);
let credential_public_key = PublicKey::ES256 {
x: pub_bytes[1..33].to_vec(),
y: pub_bytes[33..65].to_vec(),
};
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("sig".to_string()),
Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]), ),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
]);
let result = verify(
"android-key",
&stmt,
b"auth-data",
&[0u8; 32],
&credential_public_key,
&[],
);
assert!(matches!(
result,
Err(WebAuthnError::SignatureVerificationFailed)
));
}
#[test]
fn android_key_rejects_missing_x5c() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
]);
let result = verify("android-key", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("x5c"))
);
}
#[test]
fn android_key_rejects_missing_sig() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(make_fake_p256_cert(&[0x04; 65]))]),
),
]);
let result = verify("android-key", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("sig"))
);
}
#[test]
fn android_key_rejects_key_mismatch() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let cert_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let cert_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cert_pkcs8.as_ref(), &rng)
.unwrap();
let cert = make_fake_p256_cert(cert_kp.public_key().as_ref());
let cred_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.unwrap();
let cred_pub = cred_kp.public_key().as_ref();
let credential_public_key = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
]);
let result = verify(
"android-key",
&stmt,
&[],
&[0u8; 32],
&credential_public_key,
&[],
);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("does not match"))
);
}
#[test]
fn android_key_rejects_non_es256_credential_key() {
let pk = PublicKey::EdDSA(vec![0u8; 32]);
let stmt = Value::Map(vec![
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(make_fake_p256_cert(&[0x04; 65]))]),
),
]);
let result = verify("android-key", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("ES256"))
);
}
#[test]
fn apple_rejects_missing_x5c() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![]);
let result = verify("apple", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("x5c"))
);
}
#[test]
fn apple_rejects_empty_x5c() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![(Value::Text("x5c".to_string()), Value::Array(vec![]))]);
let result = verify("apple", &stmt, &[], &[0u8; 32], &pk, &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("non-empty"))
);
}
#[test]
fn apple_rejects_nonce_mismatch() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pub_bytes = kp.public_key().as_ref();
let credential_public_key = PublicKey::ES256 {
x: pub_bytes[1..33].to_vec(),
y: pub_bytes[33..65].to_vec(),
};
let wrong_nonce = [0u8; 32];
let cert = make_fake_apple_cert(pub_bytes, &wrong_nonce);
let stmt = Value::Map(vec![(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
)]);
let result = verify(
"apple",
&stmt,
b"auth-data",
&[0xABu8; 32],
&credential_public_key,
&[],
);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("nonce"))
);
}
#[test]
fn apple_rejects_key_mismatch() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let cert_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let cert_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cert_pkcs8.as_ref(), &rng)
.unwrap();
let cert_pub = cert_kp.public_key().as_ref();
let cred_pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.unwrap();
let cred_pub = cred_kp.public_key().as_ref();
let credential_public_key = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let auth_data = b"auth-data";
let client_data_hash = [0xABu8; 32];
let mut nonce_input = Vec::new();
nonce_input.extend_from_slice(auth_data);
nonce_input.extend_from_slice(&client_data_hash);
let nonce = crate::crypto::sha256(&nonce_input);
let cert = make_fake_apple_cert(cert_pub, &nonce);
let stmt = Value::Map(vec![(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
)]);
let result = verify(
"apple",
&stmt,
auth_data,
&client_data_hash,
&credential_public_key,
&[],
);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("does not match"))
);
}
#[test]
fn apple_valid_returns_basic() {
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
let rng = SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.unwrap();
let pub_bytes = kp.public_key().as_ref(); let credential_public_key = PublicKey::ES256 {
x: pub_bytes[1..33].to_vec(),
y: pub_bytes[33..65].to_vec(),
};
let auth_data = b"fake-auth-data";
let client_data_hash = [0xABu8; 32];
let mut nonce_input = Vec::new();
nonce_input.extend_from_slice(auth_data);
nonce_input.extend_from_slice(&client_data_hash);
let nonce = crate::crypto::sha256(&nonce_input);
let cert = make_fake_apple_cert(pub_bytes, &nonce);
let stmt = Value::Map(vec![(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
)]);
let result = verify(
"apple",
&stmt,
auth_data,
&client_data_hash,
&credential_public_key,
&[],
);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
fn dummy_es256_key() -> PublicKey {
PublicKey::ES256 {
x: vec![0u8; 32],
y: vec![0u8; 32],
}
}
fn make_fake_p256_cert(pub_key_uncompressed: &[u8]) -> Vec<u8> {
let spki_prefix: &[u8] = &[
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06,
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00,
];
let mut cert = vec![0x30u8, 0x82, 0x01, 0x00]; cert.extend_from_slice(spki_prefix);
cert.extend_from_slice(pub_key_uncompressed); cert
}
fn make_fake_apple_cert(pub_key_uncompressed: &[u8], nonce: &[u8; 32]) -> Vec<u8> {
let spki_prefix: &[u8] = &[
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06,
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00,
];
let apple_oid: &[u8] = &[
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x63, 0x64, 0x08, 0x02,
];
let mut cert = vec![0x30u8, 0x82, 0x01, 0x00]; cert.extend_from_slice(spki_prefix);
cert.extend_from_slice(pub_key_uncompressed); cert.extend_from_slice(apple_oid);
cert.extend_from_slice(&[0x04, 0x26, 0x30, 0x24, 0x30, 0x22, 0x04, 0x20]);
cert.extend_from_slice(nonce);
cert
}
}