use anyhow::{anyhow, Result};
pub const FLAG_UP: u8 = 0x01;
pub const FLAG_UV: u8 = 0x04;
pub const FLAG_BE: u8 = 0x08;
pub const FLAG_BS: u8 = 0x10;
pub const FLAG_AT: u8 = 0x40;
pub const FLAG_ED: u8 = 0x80;
const HEADER_LEN: usize = 37;
const AAGUID_LEN: usize = 16;
const MAX_CREDENTIAL_ID_LEN: usize = 2048;
#[derive(Debug, Clone)]
pub struct AuthenticatorData<'a> {
raw: &'a [u8],
rp_id_hash: &'a [u8],
flags: u8,
sign_count: u32,
attested: Option<AttestedCredentialData<'a>>,
}
#[derive(Debug, Clone)]
pub struct AttestedCredentialData<'a> {
pub aaguid: [u8; AAGUID_LEN],
pub credential_id: &'a [u8],
pub credential_public_key_cbor: &'a [u8],
}
impl<'a> AuthenticatorData<'a> {
pub fn parse(raw: &'a [u8]) -> Result<Self> {
if raw.len() < HEADER_LEN {
return Err(anyhow!(
"authenticatorData too short: {} < {HEADER_LEN}",
raw.len()
));
}
let flags = raw[32];
let sign_count = u32::from_be_bytes([raw[33], raw[34], raw[35], raw[36]]);
let mut cursor = HEADER_LEN;
let attested = if flags & FLAG_AT != 0 {
if raw.len() < cursor + AAGUID_LEN + 2 {
return Err(anyhow!("attested credential data truncated at AAGUID"));
}
let mut aaguid = [0u8; AAGUID_LEN];
aaguid.copy_from_slice(&raw[cursor..cursor + AAGUID_LEN]);
cursor += AAGUID_LEN;
let cred_len = usize::from(u16::from_be_bytes([raw[cursor], raw[cursor + 1]]));
if cred_len == 0 {
return Err(anyhow!("credential id must not be empty"));
}
if cred_len > MAX_CREDENTIAL_ID_LEN {
return Err(anyhow!("credential id length {cred_len} exceeds limit"));
}
cursor += 2;
let credential_id = raw
.get(cursor..cursor + cred_len)
.ok_or_else(|| anyhow!("attested credential id truncated"))?;
cursor += cred_len;
let key_start = cursor;
let key_end = cbor_item_len(&raw[cursor..])
.ok_or_else(|| anyhow!("credential public key CBOR truncated"))?
+ cursor;
let key_slice = raw
.get(key_start..key_end)
.ok_or_else(|| anyhow!("credential public key CBOR overruns buffer"))?;
cursor = key_end;
Some(AttestedCredentialData {
aaguid,
credential_id,
credential_public_key_cbor: key_slice,
})
} else {
None
};
if flags & FLAG_ED != 0 {
cursor += cbor_item_len(&raw[cursor..])
.ok_or_else(|| anyhow!("extension data CBOR truncated"))?;
}
if cursor != raw.len() {
return Err(anyhow!(
"trailing bytes after authenticatorData: {} extra",
raw.len() - cursor
));
}
Ok(Self {
rp_id_hash: &raw[..32],
flags,
sign_count,
attested,
raw,
})
}
#[must_use]
pub fn as_raw(&self) -> &'a [u8] {
self.raw
}
#[must_use]
pub fn rp_id_hash(&self) -> &'a [u8] {
self.rp_id_hash
}
#[must_use]
pub fn flags(&self) -> u8 {
self.flags
}
#[must_use]
pub fn user_present(&self) -> bool {
self.flags & FLAG_UP != 0
}
#[must_use]
pub fn user_verified(&self) -> bool {
self.flags & FLAG_UV != 0
}
#[must_use]
pub fn backup_eligible(&self) -> bool {
self.flags & FLAG_BE != 0
}
#[must_use]
pub fn backup_state(&self) -> bool {
self.flags & FLAG_BS != 0
}
#[must_use]
pub fn sign_count(&self) -> u32 {
self.sign_count
}
#[must_use]
pub fn attested_credential_data(&self) -> Option<&AttestedCredentialData<'a>> {
self.attested.as_ref()
}
}
const MAX_CBOR_DEPTH: usize = 32;
pub(crate) fn cbor_item_len(input: &[u8]) -> Option<usize> {
cbor_item_len_depth(input, 0)
}
fn cbor_item_len_depth(input: &[u8], depth: usize) -> Option<usize> {
if depth > MAX_CBOR_DEPTH {
return None;
}
let (&first, rest) = input.split_first()?;
let major = first >> 5;
let info = first & 0b0001_1111;
let arg = |info: u8, rest: &[u8]| -> Option<(usize, u64)> {
match info {
0..=23 => Some((0, u64::from(info))),
24 => Some((1, u64::from(*rest.first()?))),
25 => Some((
2,
u16::from_be_bytes(rest.get(..2)?.try_into().ok()?).into(),
)),
26 => Some((
4,
u32::from_be_bytes(rest.get(..4)?.try_into().ok()?).into(),
)),
27 => Some((8, u64::from_be_bytes(rest.get(..8)?.try_into().ok()?))),
_ => None, }
};
let (hdr_extra, value) = arg(info, rest)?;
let payload_start = 1usize + hdr_extra;
if value > input.len() as u64 && matches!(major, 2 | 3) {
return None;
}
let total = match major {
0 | 1 | 7 => payload_start, 2 | 3 => payload_start.checked_add(value as usize)?, 4 => {
let mut consumed = payload_start;
for _ in 0..value {
consumed += cbor_item_len_depth(input.get(consumed..)?, depth + 1)?;
}
consumed
}
6 => {
payload_start + cbor_item_len_depth(rest.get(hdr_extra..)?, depth + 1)?
}
5 => {
let mut consumed = payload_start;
for _ in 0..value {
consumed += cbor_item_len_depth(input.get(consumed..)?, depth + 1)?;
consumed += cbor_item_len_depth(input.get(consumed..)?, depth + 1)?;
}
consumed
}
_ => return None,
};
if total > input.len() {
return None;
}
Some(total)
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn header_bytes(flags: u8, count: u32, rp_id: &str) -> Vec<u8> {
let mut v = Sha256::digest(rp_id.as_bytes()).to_vec();
v.push(flags);
v.extend_from_slice(&count.to_be_bytes());
v
}
#[test]
fn rejects_short_buffer() {
assert!(AuthenticatorData::parse(&[0u8; 36]).is_err());
}
#[test]
fn parses_header_fields() {
let raw = header_bytes(FLAG_UP, 7, "example.com");
let ad = AuthenticatorData::parse(&raw).unwrap();
assert_eq!(ad.rp_id_hash(), Sha256::digest(b"example.com").as_slice());
assert!(ad.user_present());
assert!(!ad.user_verified());
assert_eq!(ad.sign_count(), 7);
assert!(ad.attested_credential_data().is_none());
}
#[test]
fn at_flag_without_payload_is_rejected() {
let raw = header_bytes(FLAG_UP | FLAG_AT, 0, "celestia.world");
assert!(AuthenticatorData::parse(&raw).is_err());
}
#[test]
fn ed_flag_without_payload_is_rejected() {
let raw = header_bytes(FLAG_UP | FLAG_ED, 3, "celestia.world");
assert!(AuthenticatorData::parse(&raw).is_err());
}
#[test]
fn cbor_len_measures_nested_structures() {
let bytes = [0xa2, 0x01, 0x02, 0x27, 0x42, 0xaa, 0xbb];
assert_eq!(cbor_item_len(&bytes), Some(bytes.len()));
}
#[test]
fn cbor_len_rejects_indefinite() {
assert_eq!(cbor_item_len(&[0xbf]), None);
}
#[test]
fn cbor_len_survives_deep_nesting() {
let mut deep = vec![0x81u8; 100_000];
deep.push(0x00);
assert_eq!(cbor_item_len(&deep), None);
}
#[test]
fn cbor_len_rejects_overruning_string_lengths() {
assert_eq!(cbor_item_len(&[0x5a, 0xff, 0xff, 0xff, 0xff, 0x00]), None);
}
#[test]
fn zero_length_credential_id_rejected() {
let mut raw = header_bytes(FLAG_UP | FLAG_AT, 0, "celestia.world");
raw.extend_from_slice(&[0u8; 16]); raw.extend_from_slice(&0u16.to_be_bytes()); assert!(AuthenticatorData::parse(&raw).is_err());
}
}