use ciborium::value::Value as CborValue;
use webpki::EndEntityCert;
use x509_parser::{certificate::X509Certificate, prelude::*};
use crate::passkey::errors::PasskeyError;
use super::utils::extract_public_key_coords;
pub(super) fn verify_u2f_attestation(
auth_data: &[u8],
client_data_hash: &[u8],
att_stmt: &Vec<(CborValue, CborValue)>,
) -> Result<(), PasskeyError> {
tracing::debug!("Verifying FIDO-U2F attestation");
for (i, (k, v)) in att_stmt.iter().enumerate() {
tracing::debug!("U2F att_stmt[{}]: key={:?}, value={:?}", i, k, v);
}
let mut sig: Option<Vec<u8>> = None;
let mut x5c_opt: Option<Vec<Vec<u8>>> = None;
for (k, v) in att_stmt {
if let CborValue::Text(key_str) = k {
match key_str.as_str() {
"sig" => {
if let CborValue::Bytes(s) = v {
sig = Some(s.clone());
tracing::debug!("Found sig: {} bytes", s.len());
}
}
"x5c" => {
if let CborValue::Array(certs) = v {
let mut cert_chain = Vec::new();
for cert in certs {
if let CborValue::Bytes(cert_bytes) = cert {
cert_chain.push(cert_bytes.clone());
}
}
if !cert_chain.is_empty() {
x5c_opt = Some(cert_chain.clone());
tracing::debug!("Found x5c with {} certificates", cert_chain.len());
}
}
}
_ => {
tracing::debug!("Unexpected key in U2F attestation: {}", key_str);
}
}
}
}
let sig = sig.ok_or_else(|| {
PasskeyError::Verification("Missing signature in FIDO-U2F attestation".to_string())
})?;
let x5c = x5c_opt.ok_or_else(|| {
PasskeyError::Verification("Missing x5c in FIDO-U2F attestation".to_string())
})?;
if x5c.is_empty() {
return Err(PasskeyError::Verification(
"Empty x5c in FIDO-U2F attestation".to_string(),
));
}
let attestn_cert_bytes = &x5c[0];
let attestn_cert = EndEntityCert::try_from(attestn_cert_bytes.as_ref()).map_err(|e| {
PasskeyError::Verification(format!(
"Failed to parse U2F attestation certificate: {e:?}"
))
})?;
let (_, x509_cert) = X509Certificate::from_der(attestn_cert_bytes).map_err(|e| {
PasskeyError::Verification(format!("Failed to parse X509 certificate: {e}"))
})?;
if let Some(basic_constraints) = x509_cert
.extensions()
.iter()
.find(|ext| ext.oid.as_bytes() == oid_registry::OID_X509_EXT_BASIC_CONSTRAINTS.as_bytes())
&& basic_constraints.value.contains(&0x01)
{
return Err(PasskeyError::Verification(
"U2F certificate must not be a CA certificate".to_string(),
));
}
if auth_data.len() < 55 {
return Err(PasskeyError::Verification(
"auth_data too short to contain credential ID length".to_string(),
));
}
let credential_id_length = ((auth_data[53] as u16) << 8) | (auth_data[54] as u16);
let credential_id_end = 55 + credential_id_length as usize;
if auth_data.len() <= credential_id_end {
return Err(PasskeyError::Verification(
"Invalid auth_data length".to_string(),
));
}
let mut verification_data = Vec::new();
verification_data.push(0x00);
verification_data.extend_from_slice(&auth_data[0..32]);
verification_data.extend_from_slice(client_data_hash);
let credential_id = &auth_data[55..credential_id_end];
verification_data.extend_from_slice(credential_id);
let public_key_cbor = ciborium::from_reader(&auth_data[credential_id_end..])
.map_err(|e| PasskeyError::Format(format!("Failed to parse public key CBOR: {e}")))?;
let (x_coord, y_coord) = extract_public_key_coords(&public_key_cbor)?;
verification_data.push(0x04); verification_data.extend_from_slice(&x_coord);
verification_data.extend_from_slice(&y_coord);
attestn_cert
.verify_signature(&webpki::ECDSA_P256_SHA256, &verification_data, &sig)
.map_err(|_| PasskeyError::Verification("U2F attestation signature invalid".to_string()))?;
tracing::debug!("FIDO-U2F attestation verification successful");
Ok(())
}
#[cfg(test)]
mod tests;