use ciborium::value::Value;
use ring::digest;
use x509_parser::prelude::*;
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};
fn extract_x5c_array(v: &Value) -> Result<Vec<Vec<u8>>> {
let arr = match v {
Value::Array(a) => a,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"x5c must be a CBOR array".to_string(),
))
}
};
if arr.is_empty() {
return Err(WebAuthnError::InvalidAttestationObject(
"x5c must be non-empty".to_string(),
));
}
arr.iter()
.enumerate()
.map(|(i, entry)| match entry {
Value::Bytes(b) => Ok(b.clone()),
_ => Err(WebAuthnError::InvalidAttestationObject(format!(
"x5c[{i}] must be CBOR bytes"
))),
})
.collect()
}
fn verify_x5c_chain(certs: &[Vec<u8>], trust_anchors: &[Vec<u8>]) -> Result<AttestationType> {
for i in 0..certs.len().saturating_sub(1) {
let (_, subject) = X509Certificate::from_der(&certs[i]).map_err(|_| {
WebAuthnError::AttestationChainInvalid(format!(
"x5c[{i}] is not a valid DER-encoded X.509 certificate"
))
})?;
let (_, issuer) = X509Certificate::from_der(&certs[i + 1]).map_err(|_| {
WebAuthnError::AttestationChainInvalid(format!(
"x5c[{}] is not a valid DER-encoded X.509 certificate",
i + 1
))
})?;
if subject.issuer() != issuer.subject() {
return Err(WebAuthnError::AttestationChainInvalid(format!(
"x5c[{i}].issuer does not match x5c[{}].subject",
i + 1
)));
}
subject
.verify_signature(Some(issuer.public_key()))
.map_err(|_| {
WebAuthnError::AttestationChainInvalid(format!(
"x5c[{i}] is not signed by x5c[{}]",
i + 1
))
})?;
}
if trust_anchors.is_empty() {
return Ok(AttestationType::Basic);
}
let root_der = &certs[certs.len() - 1];
let (_, root) = X509Certificate::from_der(root_der).map_err(|_| {
WebAuthnError::AttestationChainInvalid("x5c root certificate is not valid DER".to_string())
})?;
for anchor_der in trust_anchors {
let Ok((_, anchor)) = X509Certificate::from_der(anchor_der) else {
continue;
};
if root.verify_signature(Some(anchor.public_key())).is_ok() {
return Ok(AttestationType::BasicVerified);
}
}
Err(WebAuthnError::AttestationRootUntrusted)
}
pub fn verify(
fmt: &str,
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
credential_id: &[u8],
trust_anchors: &[Vec<u8>],
) -> Result<AttestationType> {
match fmt {
"none" => Ok(AttestationType::None),
"packed" => verify_packed(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
trust_anchors,
),
"fido-u2f" => verify_fido_u2f(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_id,
credential_public_key,
trust_anchors,
),
"android-key" => verify_android_key(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
trust_anchors,
),
"apple" => verify_apple(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
trust_anchors,
),
"tpm" => verify_tpm(
att_stmt,
auth_data_bytes,
client_data_hash,
credential_public_key,
trust_anchors,
),
_other => Ok(AttestationType::None),
}
}
fn verify_packed(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
trust_anchors: &[Vec<u8>],
) -> 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 x5c_certs: Option<Vec<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(
"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" => {
x5c_certs = Some(extract_x5c_array(v)?);
}
_ => {}
}
}
if let Some(certs) = x5c_certs {
let alg = alg.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"packed basic-attestation attStmt missing required field: alg".to_string(),
)
})?;
let sig = sig.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"packed basic-attestation attStmt missing required field: sig".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);
match alg {
COSE_ES256 => {
let att_pk = extract_ec_p256_public_key_from_cert(&certs[0])?;
verify_es256(&att_pk, &verification_data, &sig)?;
}
COSE_RS256 => {
let att_pk = extract_rsa_public_key_der_from_cert(&certs[0])?;
verify_rs256(&att_pk, &verification_data, &sig)?;
}
other => return Err(WebAuthnError::UnsupportedAlgorithm(other)),
}
verify_x5c_chain(&certs, trust_anchors)
} else {
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,
trust_anchors: &[Vec<u8>],
) -> 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_certs: Option<Vec<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_certs = Some(extract_x5c_array(v)?);
}
_ => {}
}
}
let certs = x5c_certs.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(&certs[0])?;
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)?;
verify_x5c_chain(&certs, trust_anchors)
}
fn verify_android_key(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
trust_anchors: &[Vec<u8>],
) -> 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_certs: Option<Vec<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_certs = Some(extract_x5c_array(v)?);
}
_ => {}
}
}
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 certs = x5c_certs.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"android-key attStmt missing required field: x5c".to_string(),
)
})?;
let att_pk = extract_ec_p256_public_key_from_cert(&certs[0])?;
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)?;
verify_x5c_chain(&certs, trust_anchors)
}
fn verify_apple(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
trust_anchors: &[Vec<u8>],
) -> 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_certs: Option<Vec<Vec<u8>>> = None;
for (k, v) in stmt_map {
if let Value::Text(ref key) = k {
if key == "x5c" {
x5c_certs = Some(extract_x5c_array(v)?);
}
}
}
let certs = x5c_certs.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(&certs[0])?;
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(&certs[0])?;
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(),
));
}
verify_x5c_chain(&certs, trust_anchors)
}
fn verify_tpm(
att_stmt: &Value,
auth_data_bytes: &[u8],
client_data_hash: &[u8; 32],
credential_public_key: &PublicKey,
trust_anchors: &[Vec<u8>],
) -> Result<AttestationType> {
let stmt_map = match att_stmt {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm attStmt must be a CBOR map".to_string(),
))
}
};
let mut ver: Option<String> = None;
let mut alg: Option<i64> = None;
let mut sig: Option<Vec<u8>> = None;
let mut x5c_certs: Option<Vec<Vec<u8>>> = None;
let mut cert_info: Option<Vec<u8>> = None;
let mut pub_area: Option<Vec<u8>> = None;
for (k, v) in stmt_map {
match k {
Value::Text(ref key) if key == "ver" => {
ver = Some(match v {
Value::Text(s) => s.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm attStmt ver must be a CBOR text string".to_string(),
))
}
});
}
Value::Text(ref key) if key == "alg" => {
alg = Some(match v {
Value::Integer(i) => i64::try_from(*i).map_err(|_| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt alg value out of i64 range".to_string(),
)
})?,
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm 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(
"tpm attStmt sig must be CBOR bytes".to_string(),
))
}
});
}
Value::Text(ref key) if key == "x5c" => {
x5c_certs = Some(extract_x5c_array(v)?);
}
Value::Text(ref key) if key == "certInfo" => {
cert_info = Some(match v {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm attStmt certInfo must be CBOR bytes".to_string(),
))
}
});
}
Value::Text(ref key) if key == "pubArea" => {
pub_area = Some(match v {
Value::Bytes(b) => b.clone(),
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm attStmt pubArea must be CBOR bytes".to_string(),
))
}
});
}
_ => {}
}
}
let ver = ver.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt missing required field: ver".to_string(),
)
})?;
if ver != "2.0" {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: ver must be \"2.0\", got \"{ver}\""
)));
}
let alg = alg.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt missing required field: alg".to_string(),
)
})?;
let expected_alg = credential_public_key.algorithm();
if alg != expected_alg {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: attStmt alg ({alg}) does not match credential key algorithm ({expected_alg})"
)));
}
let certs = x5c_certs.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt missing required field: x5c (ECDAA is not supported)".to_string(),
)
})?;
let sig = sig.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt missing required field: sig".to_string(),
)
})?;
let cert_info = cert_info.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt missing required field: certInfo".to_string(),
)
})?;
let pub_area = pub_area.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm attStmt missing required field: pubArea".to_string(),
)
})?;
let cert_key_bytes = match alg {
COSE_ES256 => extract_ec_p256_public_key_from_cert(&certs[0])?,
COSE_RS256 => extract_rsa_public_key_der_from_cert(&certs[0])?,
_ => return Err(WebAuthnError::UnsupportedAlgorithm(alg)),
};
let mut att_to_be_signed = Vec::with_capacity(auth_data_bytes.len() + 32);
att_to_be_signed.extend_from_slice(auth_data_bytes);
att_to_be_signed.extend_from_slice(client_data_hash);
let expected_extra_data = crate::crypto::sha256(&att_to_be_signed);
let expected_name = compute_pub_area_name(&pub_area)?;
verify_pub_area_matches_credential(&pub_area, credential_public_key)?;
verify_cert_info(&cert_info, &expected_extra_data, &expected_name)?;
match alg {
COSE_ES256 => verify_es256(&cert_key_bytes, &cert_info, &sig)?,
COSE_RS256 => verify_rs256(&cert_key_bytes, &cert_info, &sig)?,
_ => unreachable!("alg validated earlier"),
}
verify_x5c_chain(&certs, trust_anchors)
}
fn compute_pub_area_name(pub_area: &[u8]) -> Result<Vec<u8>> {
if pub_area.len() < 4 {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea too short to read nameAlg (need at least 4 bytes)".to_string(),
));
}
let name_alg = u16::from_be_bytes([pub_area[2], pub_area[3]]);
let hash: Vec<u8> = match name_alg {
0x000B => crate::crypto::sha256(pub_area).to_vec(),
0x000C => {
digest::digest(&digest::SHA384, pub_area).as_ref().to_vec()
}
other => {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: unsupported pubArea nameAlg 0x{other:04X} \
(expected 0x000B SHA-256 or 0x000C SHA-384)"
)))
}
};
let mut name = vec![pub_area[2], pub_area[3]];
name.extend_from_slice(&hash);
Ok(name)
}
fn verify_pub_area_matches_credential(
pub_area: &[u8],
credential_public_key: &PublicKey,
) -> Result<()> {
if pub_area.len() < 10 {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea too short (need at least 10 bytes for the fixed header)".to_string(),
));
}
let key_type = u16::from_be_bytes([pub_area[0], pub_area[1]]);
let auth_policy_size = u16::from_be_bytes([pub_area[8], pub_area[9]]) as usize;
let params_start = 10_usize.checked_add(auth_policy_size).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject("tpm: pubArea authPolicy size overflow".to_string())
})?;
match key_type {
0x0023 => {
let unique_start = params_start.checked_add(10).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: pubArea ECC params offset overflow".to_string(),
)
})?;
let mut pos = unique_start;
let x = tpm_read_blob(pub_area, &mut pos)?;
let y = tpm_read_blob(pub_area, &mut pos)?;
match credential_public_key {
PublicKey::ES256 { x: cx, y: cy } => {
if x != cx.as_slice() || y != cy.as_slice() {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea ECC point does not match ES256 credential key"
.to_string(),
));
}
}
PublicKey::ES384 { x: cx, y: cy } => {
if x != cx.as_slice() || y != cy.as_slice() {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea ECC point does not match ES384 credential key"
.to_string(),
));
}
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea type is ECC (0x0023) but credential key is not an EC key"
.to_string(),
))
}
}
}
0x0001 => {
let unique_start = params_start.checked_add(12).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: pubArea RSA params offset overflow".to_string(),
)
})?;
let mut pos = unique_start;
let tpm_modulus = tpm_read_blob(pub_area, &mut pos)?;
match credential_public_key {
PublicKey::RS256 { n, .. } => {
fn strip(b: &[u8]) -> &[u8] {
let start = b.iter().position(|v| *v != 0).unwrap_or(b.len());
&b[start..]
}
if strip(tpm_modulus) != strip(n) {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea RSA modulus does not match RS256 credential key"
.to_string(),
));
}
}
_ => {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: pubArea type is RSA (0x0001) but credential key is not RS256"
.to_string(),
))
}
}
}
other => {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: unsupported pubArea key type: 0x{other:04X}"
)))
}
}
Ok(())
}
fn verify_cert_info(
cert_info: &[u8],
expected_extra_data: &[u8; 32],
expected_name: &[u8],
) -> Result<()> {
let mut pos = 0usize;
tpm_skip(cert_info, &mut pos, 2)?;
let magic = tpm_read_u32_be(cert_info, &mut pos)?;
if magic != 0xFF544347 {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: certInfo magic must be 0xFF544347 (TPM_GENERATED_VALUE), got 0x{magic:08X}"
)));
}
let attest_type = tpm_read_u16_be(cert_info, &mut pos)?;
if attest_type != 0x8017 {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: certInfo type must be 0x8017 (TPM_ST_ATTEST_CERTIFY), got 0x{attest_type:04X}"
)));
}
let _ = tpm_read_blob(cert_info, &mut pos)?;
let extra_data = tpm_read_blob(cert_info, &mut pos)?;
if extra_data != expected_extra_data.as_slice() {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: certInfo extraData does not match SHA-256(authData || clientDataHash)"
.to_string(),
));
}
tpm_skip(cert_info, &mut pos, 16)?;
let attested_name = tpm_read_blob(cert_info, &mut pos)?;
if attested_name != expected_name {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: certInfo attested.name does not match computed name(pubArea)".to_string(),
));
}
Ok(())
}
fn tpm_read_u16_be(data: &[u8], pos: &mut usize) -> Result<u16> {
let end = pos.checked_add(2).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject("tpm: position overflow reading u16".to_string())
})?;
let bytes: [u8; 2] = data
.get(*pos..end)
.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject("tpm: buffer too short reading u16".to_string())
})?
.try_into()
.expect("slice is exactly 2 bytes");
*pos = end;
Ok(u16::from_be_bytes(bytes))
}
fn tpm_read_u32_be(data: &[u8], pos: &mut usize) -> Result<u32> {
let end = pos.checked_add(4).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject("tpm: position overflow reading u32".to_string())
})?;
let bytes: [u8; 4] = data
.get(*pos..end)
.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject("tpm: buffer too short reading u32".to_string())
})?
.try_into()
.expect("slice is exactly 4 bytes");
*pos = end;
Ok(u32::from_be_bytes(bytes))
}
fn tpm_read_blob<'a>(data: &'a [u8], pos: &mut usize) -> Result<&'a [u8]> {
let len = tpm_read_u16_be(data, pos)? as usize;
let end = pos.checked_add(len).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: position overflow reading length-prefixed blob".to_string(),
)
})?;
let slice = data.get(*pos..end).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: length-prefixed field extends beyond buffer".to_string(),
)
})?;
*pos = end;
Ok(slice)
}
fn tpm_skip(data: &[u8], pos: &mut usize, n: usize) -> Result<()> {
let end = pos.checked_add(n).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(format!(
"tpm: position overflow skipping {n} bytes"
))
})?;
if end > data.len() {
return Err(WebAuthnError::InvalidAttestationObject(format!(
"tpm: buffer too short skipping {n} bytes (have {}, need {end})",
data.len()
)));
}
*pos = end;
Ok(())
}
fn extract_rsa_public_key_der_from_cert(cert_der: &[u8]) -> Result<Vec<u8>> {
const RSA_ENCRYPTION_OID: &[u8] = &[
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
];
let oid_pos = cert_der
.windows(RSA_ENCRYPTION_OID.len())
.position(|w| w == RSA_ENCRYPTION_OID)
.ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: attestation cert does not contain rsaEncryption OID".to_string(),
)
})?;
let after_oid = &cert_der[oid_pos + RSA_ENCRYPTION_OID.len()..];
let (_, _, after_params) = der_parse_tlv(after_oid).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: failed to parse AlgorithmIdentifier params after rsaEncryption OID".to_string(),
)
})?;
let (bs_tag, bs_content, _) = der_parse_tlv(after_params).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: failed to parse SubjectPublicKeyInfo BIT STRING".to_string(),
)
})?;
if bs_tag != 0x03 {
return Err(WebAuthnError::InvalidAttestationObject(
"tpm: expected BIT STRING after AlgorithmIdentifier in SubjectPublicKeyInfo"
.to_string(),
));
}
let rsa_pk = bs_content.get(1..).ok_or_else(|| {
WebAuthnError::InvalidAttestationObject(
"tpm: BIT STRING for RSA public key is too short".to_string(),
)
})?;
Ok(rsa_pk.to_vec())
}
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(
"unknown-format",
&none_att_stmt(),
&[],
&[0u8; 32],
&pk,
&[],
&[],
);
assert!(matches!(result, Ok(AttestationType::None)));
}
#[test]
fn packed_basic_attestation_valid_sig_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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let pk = dummy_es256_key();
let auth_data = b"fake-auth-data";
let client_data_hash = [0xABu8; 32];
let mut msg = Vec::new();
msg.extend_from_slice(auth_data);
msg.extend_from_slice(&client_data_hash);
let sig = kp.sign(&rng, &msg).expect("test setup");
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("packed", &stmt, auth_data, &client_data_hash, &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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
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).expect("test setup");
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let att_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, att_pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.expect("test setup");
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).expect("test setup");
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)
.expect("test setup");
let att_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, att_pkcs8.as_ref(), &rng)
.expect("test setup");
let cert = make_fake_p256_cert(att_kp.public_key().as_ref());
let cred_pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)
.expect("test setup");
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
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).expect("test setup");
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let cert_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cert_pkcs8.as_ref(), &rng)
.expect("test setup");
let cert = make_fake_p256_cert(cert_kp.public_key().as_ref());
let cred_pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)
.expect("test setup");
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let cert_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cert_pkcs8.as_ref(), &rng)
.expect("test setup");
let cert_pub = cert_kp.public_key().as_ref();
let cred_pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)
.expect("test setup");
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
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 build_tpm_ecc_pub_area(x: &[u8], y: &[u8]) -> Vec<u8> {
let mut pa = Vec::new();
pa.extend_from_slice(&[0x00, 0x23]); pa.extend_from_slice(&[0x00, 0x0B]); pa.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); pa.extend_from_slice(&[0x00, 0x00]); pa.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00]);
pa.extend_from_slice(&(x.len() as u16).to_be_bytes());
pa.extend_from_slice(x);
pa.extend_from_slice(&(y.len() as u16).to_be_bytes());
pa.extend_from_slice(y);
pa
}
fn build_tpm_cert_info(extra_data: &[u8; 32], attested_name: &[u8]) -> Vec<u8> {
let mut attest = Vec::new();
attest.extend_from_slice(&[0xFF, 0x54, 0x43, 0x47]); attest.extend_from_slice(&[0x80, 0x17]); attest.extend_from_slice(&[0x00, 0x00]); attest.extend_from_slice(&(extra_data.len() as u16).to_be_bytes());
attest.extend_from_slice(extra_data);
attest.extend_from_slice(&[0u8; 8]); attest.extend_from_slice(&[0u8; 8]); attest.extend_from_slice(&(attested_name.len() as u16).to_be_bytes());
attest.extend_from_slice(attested_name);
let mut cert_info = Vec::new();
cert_info.extend_from_slice(&(attest.len() as u16).to_be_bytes()); cert_info.extend_from_slice(&attest);
cert_info
}
#[test]
fn tpm_rejects_missing_ver() {
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; 4])]),
),
(
Value::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("ver"))
);
}
#[test]
fn tpm_rejects_wrong_ver() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("1.0".to_string()),
),
(
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; 4])]),
),
(
Value::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("2.0"))
);
}
#[test]
fn tpm_rejects_missing_alg() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(vec![0u8; 4])]),
),
(
Value::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("alg"))
);
}
#[test]
fn tpm_rejects_alg_mismatch() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-257i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(vec![0u8; 4])]),
),
(
Value::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("alg"))
);
}
#[test]
fn tpm_rejects_missing_x5c() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(
Value::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("x5c"))
);
}
#[test]
fn tpm_rejects_empty_x5c() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
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::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("non-empty"))
);
}
#[test]
fn tpm_rejects_missing_cert_info() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
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; 4])]),
),
(
Value::Text("pubArea".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("certInfo"))
);
}
#[test]
fn tpm_rejects_missing_pub_area() {
let pk = dummy_es256_key();
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
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; 4])]),
),
(
Value::Text("certInfo".to_string()),
Value::Bytes(vec![0u8; 4]),
),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("pubArea"))
);
}
#[test]
fn tpm_rejects_bad_magic_in_cert_info() {
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let cred_pub = kp.public_key().as_ref();
let pk = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let pub_area = build_tpm_ecc_pub_area(&cred_pub[1..33], &cred_pub[33..65]);
let name = compute_pub_area_name(&pub_area).expect("test setup");
let mut attest = Vec::new();
attest.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); attest.extend_from_slice(&[0x80, 0x17]);
attest.extend_from_slice(&[0x00, 0x00]); attest.extend_from_slice(&(32u16).to_be_bytes());
attest.extend_from_slice(&[0u8; 32]); attest.extend_from_slice(&[0u8; 16]); attest.extend_from_slice(&(name.len() as u16).to_be_bytes());
attest.extend_from_slice(&name);
let mut cert_info = (attest.len() as u16).to_be_bytes().to_vec();
cert_info.extend_from_slice(&attest);
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("magic"))
);
}
#[test]
fn tpm_rejects_bad_type_in_cert_info() {
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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let cred_pub = kp.public_key().as_ref();
let pk = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let pub_area = build_tpm_ecc_pub_area(&cred_pub[1..33], &cred_pub[33..65]);
let name = compute_pub_area_name(&pub_area).expect("test setup");
let mut attest = Vec::new();
attest.extend_from_slice(&[0xFF, 0x54, 0x43, 0x47]); attest.extend_from_slice(&[0x80, 0x01]); attest.extend_from_slice(&[0x00, 0x00]);
attest.extend_from_slice(&(32u16).to_be_bytes());
attest.extend_from_slice(&[0u8; 32]);
attest.extend_from_slice(&[0u8; 16]);
attest.extend_from_slice(&(name.len() as u16).to_be_bytes());
attest.extend_from_slice(&name);
let mut cert_info = (attest.len() as u16).to_be_bytes().to_vec();
cert_info.extend_from_slice(&attest);
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify("tpm", &stmt, &[], &[0u8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("type"))
);
}
#[test]
fn tpm_rejects_extra_data_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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let cred_pub = kp.public_key().as_ref();
let pk = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let pub_area = build_tpm_ecc_pub_area(&cred_pub[1..33], &cred_pub[33..65]);
let name = compute_pub_area_name(&pub_area).expect("test setup");
let cert_info = build_tpm_cert_info(&[0u8; 32], &name);
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify("tpm", &stmt, &[0xAAu8; 10], &[0xBBu8; 32], &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("extraData"))
);
}
#[test]
fn tpm_rejects_attested_name_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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let cred_pub = kp.public_key().as_ref();
let pk = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let pub_area = build_tpm_ecc_pub_area(&cred_pub[1..33], &cred_pub[33..65]);
let auth_data = b"auth-data";
let client_data_hash = [0xCCu8; 32];
let mut att_signed = auth_data.to_vec();
att_signed.extend_from_slice(&client_data_hash);
let extra_data = crate::crypto::sha256(&att_signed);
let wrong_name = vec![0u8; 34];
let cert_info = build_tpm_cert_info(&extra_data, &wrong_name);
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify("tpm", &stmt, auth_data, &client_data_hash, &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("attested.name"))
);
}
#[test]
fn tpm_rejects_pub_area_key_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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let pub_area = build_tpm_ecc_pub_area(&[0u8; 32], &[0u8; 32]);
let name = compute_pub_area_name(&pub_area).expect("test setup");
let auth_data = b"auth-data";
let client_data_hash = [0xCCu8; 32];
let mut att_signed = auth_data.to_vec();
att_signed.extend_from_slice(&client_data_hash);
let extra_data = crate::crypto::sha256(&att_signed);
let cert_info = build_tpm_cert_info(&extra_data, &name);
let cred_pub = kp.public_key().as_ref();
let pk = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(Value::Text("sig".to_string()), Value::Bytes(vec![0u8; 64])),
(Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify("tpm", &stmt, auth_data, &client_data_hash, &pk, &[], &[]);
assert!(
matches!(result, Err(WebAuthnError::InvalidAttestationObject(ref m)) if m.contains("ES256"))
);
}
#[test]
fn tpm_rejects_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)
.expect("test setup");
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = kp.public_key().as_ref();
let cert = make_fake_p256_cert(att_pub);
let cred_pub = kp.public_key().as_ref();
let pk = PublicKey::ES256 {
x: cred_pub[1..33].to_vec(),
y: cred_pub[33..65].to_vec(),
};
let pub_area = build_tpm_ecc_pub_area(&cred_pub[1..33], &cred_pub[33..65]);
let name = compute_pub_area_name(&pub_area).expect("test setup");
let auth_data = b"auth-data";
let client_data_hash = [0xCCu8; 32];
let mut att_signed = auth_data.to_vec();
att_signed.extend_from_slice(&client_data_hash);
let extra_data = crate::crypto::sha256(&att_signed);
let cert_info = build_tpm_cert_info(&extra_data, &name);
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(
Value::Text("sig".to_string()),
Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
), (Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify("tpm", &stmt, auth_data, &client_data_hash, &pk, &[], &[]);
assert!(matches!(
result,
Err(WebAuthnError::SignatureVerificationFailed)
));
}
#[test]
fn tpm_valid_es256_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)
.expect("test setup");
let att_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, att_pkcs8.as_ref(), &rng)
.expect("test setup");
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)
.expect("test setup");
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.expect("test setup");
let cred_pub = cred_kp.public_key().as_ref(); let cred_x = cred_pub[1..33].to_vec();
let cred_y = cred_pub[33..65].to_vec();
let credential_public_key = PublicKey::ES256 {
x: cred_x.clone(),
y: cred_y.clone(),
};
let auth_data = b"fake-auth-data";
let client_data_hash = [0xABu8; 32];
let pub_area = build_tpm_ecc_pub_area(&cred_x, &cred_y);
let name = compute_pub_area_name(&pub_area).expect("test setup");
let mut att_signed = auth_data.to_vec();
att_signed.extend_from_slice(&client_data_hash);
let extra_data = crate::crypto::sha256(&att_signed);
let cert_info = build_tpm_cert_info(&extra_data, &name);
let sig = att_kp.sign(&rng, &cert_info).expect("test setup");
let stmt = Value::Map(vec![
(
Value::Text("ver".to_string()),
Value::Text("2.0".to_string()),
),
(
Value::Text("alg".to_string()),
Value::Integer((-7i64).into()),
),
(
Value::Text("x5c".to_string()),
Value::Array(vec![Value::Bytes(cert)]),
),
(
Value::Text("sig".to_string()),
Value::Bytes(sig.as_ref().to_vec()),
),
(Value::Text("certInfo".to_string()), Value::Bytes(cert_info)),
(Value::Text("pubArea".to_string()), Value::Bytes(pub_area)),
]);
let result = verify(
"tpm",
&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
}
fn make_ca() -> (rcgen::KeyPair, rcgen::Certificate, Vec<u8>) {
let key = rcgen::KeyPair::generate().expect("test setup");
let mut params = rcgen::CertificateParams::default();
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let cert = params.self_signed(&key).expect("test setup");
let der = cert.der().to_vec();
(key, cert, der)
}
fn make_leaf(issuer_cert: &rcgen::Certificate, issuer_key: &rcgen::KeyPair) -> Vec<u8> {
let leaf_key = rcgen::KeyPair::generate().expect("test setup");
rcgen::CertificateParams::default()
.signed_by(&leaf_key, issuer_cert, issuer_key)
.expect("test setup")
.der()
.to_vec()
}
fn make_intermediate(
issuer_cert: &rcgen::Certificate,
issuer_key: &rcgen::KeyPair,
) -> (rcgen::KeyPair, rcgen::Certificate, Vec<u8>) {
let inter_key = rcgen::KeyPair::generate().expect("test setup");
let mut p = rcgen::CertificateParams::default();
p.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let inter_cert = p
.signed_by(&inter_key, issuer_cert, issuer_key)
.expect("test setup");
let inter_der = inter_cert.der().to_vec();
(inter_key, inter_cert, inter_der)
}
#[test]
fn chain_single_self_signed_no_anchors_returns_basic() {
let (_key, _cert, root_der) = make_ca();
let result = verify_x5c_chain(&[root_der], &[]);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
#[test]
fn chain_leaf_and_root_no_anchors_returns_basic() {
let (root_key, root_cert, root_der) = make_ca();
let leaf_der = make_leaf(&root_cert, &root_key);
let result = verify_x5c_chain(&[leaf_der, root_der], &[]);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
#[test]
fn chain_verified_to_trust_anchor_returns_basic_verified() {
let (root_key, root_cert, root_der) = make_ca();
let leaf_der = make_leaf(&root_cert, &root_key);
let result = verify_x5c_chain(&[leaf_der, root_der.clone()], &[root_der]);
assert!(matches!(result, Ok(AttestationType::BasicVerified)));
}
#[test]
fn chain_order_failure_swapped_certs_returns_chain_invalid() {
let (root_key, root_cert, root_der) = make_ca();
let leaf_der = make_leaf(&root_cert, &root_key);
let result = verify_x5c_chain(&[root_der, leaf_der], &[]);
assert!(matches!(
result,
Err(WebAuthnError::AttestationChainInvalid(_))
));
}
#[test]
fn chain_root_not_in_trust_anchors_returns_root_untrusted() {
let (root_key, root_cert, root_der) = make_ca();
let leaf_der = make_leaf(&root_cert, &root_key);
let (_other_key, _other_cert, other_root_der) = make_ca();
let result = verify_x5c_chain(&[leaf_der, root_der], &[other_root_der]);
assert!(matches!(
result,
Err(WebAuthnError::AttestationRootUntrusted)
));
}
#[test]
fn chain_leaf_intermediate_root_no_anchors_returns_basic() {
let (root_key, root_cert, root_der) = make_ca();
let (inter_key, inter_cert, inter_der) = make_intermediate(&root_cert, &root_key);
let leaf_der = make_leaf(&inter_cert, &inter_key);
let result = verify_x5c_chain(&[leaf_der, inter_der, root_der], &[]);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
#[test]
fn chain_leaf_intermediate_root_with_anchor_returns_basic_verified() {
let (root_key, root_cert, root_der) = make_ca();
let (inter_key, inter_cert, inter_der) = make_intermediate(&root_cert, &root_key);
let leaf_der = make_leaf(&inter_cert, &inter_key);
let result = verify_x5c_chain(&[leaf_der, inter_der, root_der.clone()], &[root_der]);
assert!(matches!(result, Ok(AttestationType::BasicVerified)));
}
#[test]
fn packed_basic_attestation_with_trust_anchor_returns_basic_verified() {
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)
.expect("test setup");
let att_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, att_pkcs8.as_ref(), &rng)
.expect("test setup");
let att_pub = att_kp.public_key().as_ref();
let fake_cert = make_fake_p256_cert(att_pub);
let cred_pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)
.expect("test setup");
let cred_kp =
EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, cred_pkcs8.as_ref(), &rng)
.expect("test setup");
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"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 = att_kp.sign(&rng, &verification_data).expect("test setup");
let stmt_single = 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(fake_cert)]),
),
]);
let result = verify(
"packed",
&stmt_single,
auth_data,
&client_data_hash,
&credential_public_key,
&[],
&[],
);
assert!(matches!(result, Ok(AttestationType::Basic)));
}
}