use super::auth_data::{AuthenticatorData, FLAG_AT};
use anyhow::{anyhow, Result};
use coset::{cbor::value::Value, CborSerializable, CoseKey};
#[derive(Debug, Clone)]
pub struct AttestedCredential {
pub credential_id: Vec<u8>,
pub public_key_cose: Vec<u8>,
pub aaguid: [u8; 16],
pub backup_eligible: bool,
pub backup_state: bool,
}
impl RegistrationOutputs {
#[must_use]
pub fn initial_sign_count(&self) -> u32 {
self.sign_count
}
}
pub struct RegistrationOutputs {
pub rp_id_hash: [u8; 32],
pub credential: AttestedCredential,
pub sign_count: u32,
pub user_verified: bool,
}
#[derive(Debug, Clone, Default)]
pub struct AttestationPolicy<'a> {
pub require_user_verification: bool,
pub allowed_origins: &'a [String],
}
pub fn verify_attestation_none_with_policy(
attestation_object: &[u8],
client_data: &super::client_data::ClientData,
rp_id: &str,
policy: AttestationPolicy<'_>,
) -> Result<RegistrationOutputs> {
if !policy.allowed_origins.is_empty() {
client_data.verify_expectations(
"webauthn.create",
&client_data.challenge,
policy.allowed_origins,
)?;
}
let outputs = verify_attestation_none(attestation_object, client_data, rp_id)?;
if policy.require_user_verification && !outputs.user_verified {
return Err(anyhow!("UV flag not set at registration"));
}
Ok(outputs)
}
pub fn verify_attestation_none(
attestation_object: &[u8],
_client_data: &super::client_data::ClientData,
rp_id: &str,
) -> Result<RegistrationOutputs> {
use sha2::{Digest, Sha256};
let (fmt, auth_data_bytes) = parse_attestation_object(attestation_object)?;
if fmt != "none" {
return Err(anyhow!(
"unsupported attestation format {fmt:?} (policy: none)"
));
}
let ad = AuthenticatorData::parse(&auth_data_bytes)?;
if !ad.user_present() {
return Err(anyhow!("UP flag not set at registration"));
}
let expected_hash = Sha256::digest(rp_id.as_bytes());
if ad.rp_id_hash() != expected_hash.as_slice() {
return Err(anyhow!("rpIdHash mismatch for RP {rp_id:?}"));
}
if ad.flags() & FLAG_AT == 0 || ad.attested_credential_data().is_none() {
return Err(anyhow!("AT flag not set or credential data missing"));
}
let attested = ad
.attested_credential_data()
.expect("checked directly above");
let cose = CoseKey::from_slice(attested.credential_public_key_cbor)
.map_err(|e| anyhow!("credential public key CBOR invalid: {e:?}"))?;
super::assertion_legacy::validate_supported_key(&cose)?;
let mut rp_id_hash = [0u8; 32];
rp_id_hash.copy_from_slice(ad.rp_id_hash());
Ok(RegistrationOutputs {
rp_id_hash,
sign_count: ad.sign_count(),
user_verified: ad.user_verified(),
credential: AttestedCredential {
credential_id: attested.credential_id.to_vec(),
public_key_cose: attested.credential_public_key_cbor.to_vec(),
aaguid: attested.aaguid,
backup_eligible: ad.backup_eligible(),
backup_state: ad.backup_state(),
},
})
}
fn parse_attestation_object(bytes: &[u8]) -> Result<(String, Vec<u8>)> {
let value = coset::cbor::from_reader(bytes)
.map_err(|e| anyhow!("attestationObject CBOR invalid: {e:?}"))?;
let Value::Map(map) = value else {
return Err(anyhow!("attestationObject is not a CBOR map"));
};
let mut fmt: Option<String> = None;
let mut auth_data: Option<Vec<u8>> = None;
let mut att_stmt_seen_empty = false;
for (k, v) in map {
let Some(field) = k.as_text() else {
return Err(anyhow!("non-text map key in attestationObject"));
};
match field {
"fmt" => {
let Value::Text(f) = v else {
return Err(anyhow!("fmt must be text"));
};
fmt = Some(f);
}
"attStmt" => match &v {
Value::Map(m) if m.is_empty() => att_stmt_seen_empty = true,
_ => return Err(anyhow!("attStmt must be an empty map for fmt=none")),
},
"authData" => {
let Value::Bytes(b) = v else {
return Err(anyhow!("authData must be bytes"));
};
auth_data = Some(b);
}
_ => {}
}
}
match (fmt, att_stmt_seen_empty, auth_data) {
(Some(fmt), true, Some(auth_data)) => Ok((fmt, auth_data)),
(_, false, _) => Err(anyhow!("missing or non-empty attStmt for fmt=none")),
(None, _, _) => Err(anyhow!("missing fmt")),
(_, _, None) => Err(anyhow!("missing authData")),
}
}
#[cfg(test)]
mod tests {
use super::super::client_data::ClientData;
use super::*;
#[test]
fn policy_rejects_foreign_origin_before_parsing() {
let cd = ClientData::parse(
br#"{"type":"webauthn.create","challenge":"abc","origin":"https://evil.example"}"#,
)
.expect("clientData parses");
let policy = AttestationPolicy {
require_user_verification: true,
allowed_origins: &["https://good.example".to_string()],
};
assert!(
verify_attestation_none_with_policy(b"not-even-cbor", &cd, "rp.example", policy)
.is_err()
);
}
#[test]
fn policy_rejects_wrong_ceremony_type() {
let cd = ClientData::parse(
br#"{"type":"webauthn.get","challenge":"abc","origin":"https://good.example"}"#,
)
.expect("clientData parses");
let policy = AttestationPolicy {
require_user_verification: false,
allowed_origins: &["https://good.example".to_string()],
};
assert!(
verify_attestation_none_with_policy(b"not-even-cbor", &cd, "rp.example", policy)
.is_err()
);
}
}