use std::collections::HashMap;
use std::io::Cursor;
use ciborium::value::Value;
use crate::algorithm::{
COSE_CRV_ED25519, COSE_CRV_P256, COSE_CRV_P384, COSE_EDDSA, COSE_ES256, COSE_ES384,
COSE_KTY_EC2, COSE_KTY_OKP, COSE_KTY_RSA, COSE_RS256,
};
use crate::error::{Result, WebAuthnError};
const FLAG_UP: u8 = 0x01;
const FLAG_UV: u8 = 0x04;
const FLAG_BE: u8 = 0x08;
const FLAG_BS: u8 = 0x10;
const FLAG_AT: u8 = 0x40;
const FLAG_ED: u8 = 0x80;
const MAX_CREDENTIAL_ID_LEN: usize = 1023;
const MIN_RSA_N_LEN: usize = 256;
#[derive(Debug, Clone, Copy)]
pub struct AuthenticatorFlags {
pub user_present: bool,
pub user_verified: bool,
pub backup_eligible: bool,
pub backup_state: bool,
pub attested_credential_data: bool,
pub extension_data: bool,
}
#[derive(Debug, Clone)]
pub enum CoseKey {
EC2 {
alg: i64,
crv: i64,
x: Vec<u8>,
y: Vec<u8>,
},
OKP {
alg: i64,
x: Vec<u8>,
},
RSA {
alg: i64,
n: Vec<u8>,
e: Vec<u8>,
},
}
impl CoseKey {
pub fn alg(&self) -> i64 {
match self {
CoseKey::EC2 { alg, .. } => *alg,
CoseKey::OKP { alg, .. } => *alg,
CoseKey::RSA { alg, .. } => *alg,
}
}
}
#[derive(Debug)]
pub struct AttestedCredentialData {
pub aaguid: [u8; 16],
pub credential_id: Vec<u8>,
pub public_key: CoseKey,
}
#[derive(Debug)]
pub struct AuthenticatorData {
pub rp_id_hash: [u8; 32],
pub flags: AuthenticatorFlags,
pub sign_count: u32,
pub attested_credential_data: Option<AttestedCredentialData>,
pub extensions: Option<HashMap<String, Value>>,
pub raw: Vec<u8>,
}
pub fn parse_authenticator_data(data: &[u8]) -> Result<AuthenticatorData> {
if data.len() < 37 {
return Err(WebAuthnError::InvalidAuthenticatorData(format!(
"too short: expected at least 37 bytes, got {}",
data.len()
)));
}
let rp_id_hash: [u8; 32] = data[0..32]
.try_into()
.expect("slice of exactly 32 bytes always converts");
let flags_byte = data[32];
let flags = AuthenticatorFlags {
user_present: flags_byte & FLAG_UP != 0,
user_verified: flags_byte & FLAG_UV != 0,
backup_eligible: flags_byte & FLAG_BE != 0,
backup_state: flags_byte & FLAG_BS != 0,
attested_credential_data: flags_byte & FLAG_AT != 0,
extension_data: flags_byte & FLAG_ED != 0,
};
if flags.backup_state && !flags.backup_eligible {
return Err(WebAuthnError::InvalidAuthenticatorData(
"BS flag (backup state) is set but BE flag (backup eligibility) is not — invalid per §6.1".to_string(),
));
}
let sign_count = u32::from_be_bytes(
data[33..37]
.try_into()
.expect("slice of exactly 4 bytes always converts"),
);
let (attested_credential_data, at_bytes_consumed) = if flags.attested_credential_data {
let (cred_data, n) = parse_attested_credential_data(&data[37..])?;
(Some(cred_data), n)
} else {
(None, 0)
};
let extensions = if flags.extension_data {
let ext_start = 37 + at_bytes_consumed;
Some(parse_extension_map(&data[ext_start..])?)
} else {
None
};
Ok(AuthenticatorData {
rp_id_hash,
flags,
sign_count,
attested_credential_data,
extensions,
raw: data.to_vec(),
})
}
pub fn parse_cose_key(data: &[u8]) -> Result<CoseKey> {
let value: Value = ciborium::from_reader(data)
.map_err(|e| WebAuthnError::CborDecodeError(format!("COSE key: {e}")))?;
let map = match value {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidPublicKey(
"COSE key must be a CBOR map".to_string(),
))
}
};
{
let mut seen: Vec<i64> = Vec::new();
for (k, _) in &map {
if let Value::Integer(ki) = k {
if let Ok(n) = i64::try_from(*ki) {
if seen.contains(&n) {
return Err(WebAuthnError::InvalidPublicKey(format!(
"duplicate COSE key: {n}"
)));
}
seen.push(n);
}
}
}
}
let get_int = |key: i64| -> Option<i64> {
map.iter().find_map(|(k, v)| {
if let (Value::Integer(ki), Value::Integer(vi)) = (k, v) {
if i64::try_from(*ki).ok()? == key {
return i64::try_from(*vi).ok();
}
}
None
})
};
let get_bytes = |key: i64| -> Option<Vec<u8>> {
map.iter().find_map(|(k, v)| {
if let Value::Integer(ki) = k {
if i64::try_from(*ki).ok()? == key {
if let Value::Bytes(b) = v {
return Some(b.clone());
}
}
}
None
})
};
let kty = get_int(1).ok_or_else(|| {
WebAuthnError::InvalidPublicKey("missing required field: kty".to_string())
})?;
if kty == COSE_KTY_OKP {
parse_okp_key(&get_int, &get_bytes)
} else if kty == COSE_KTY_EC2 {
parse_ec2_key(&get_int, &get_bytes)
} else if kty == COSE_KTY_RSA {
parse_rsa_key(&get_int, &get_bytes)
} else {
Err(WebAuthnError::InvalidPublicKey(format!(
"unsupported key type: {kty} (supported: OKP=1, EC2=2, RSA=3)"
)))
}
}
fn parse_okp_key(
get_int: &impl Fn(i64) -> Option<i64>,
get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
) -> Result<CoseKey> {
let alg = get_int(3).ok_or_else(|| {
WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
})?;
if alg != COSE_EDDSA {
return Err(WebAuthnError::UnsupportedAlgorithm(alg));
}
let crv = get_int(-1).ok_or_else(|| {
WebAuthnError::InvalidPublicKey("missing required field: crv".to_string())
})?;
if crv != COSE_CRV_ED25519 {
return Err(WebAuthnError::InvalidPublicKey(format!(
"unsupported OKP curve: {crv} (only Ed25519 / crv=6 is supported)"
)));
}
let x = get_bytes(-2)
.ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: x".to_string()))?;
if x.len() != 32 {
return Err(WebAuthnError::InvalidPublicKey(format!(
"Ed25519 public key must be 32 bytes, got {}",
x.len()
)));
}
Ok(CoseKey::OKP { alg, x })
}
fn parse_ec2_key(
get_int: &impl Fn(i64) -> Option<i64>,
get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
) -> Result<CoseKey> {
let alg = get_int(3).ok_or_else(|| {
WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
})?;
let crv = get_int(-1).ok_or_else(|| {
WebAuthnError::InvalidPublicKey("missing required field: crv".to_string())
})?;
let coord_size: usize = match alg {
COSE_ES256 => {
if crv != COSE_CRV_P256 {
return Err(WebAuthnError::InvalidPublicKey(format!(
"ES256 requires curve P-256 (crv=1), got {crv}"
)));
}
32
}
COSE_ES384 => {
if crv != COSE_CRV_P384 {
return Err(WebAuthnError::InvalidPublicKey(format!(
"ES384 requires curve P-384 (crv=2), got {crv}"
)));
}
48
}
_ => return Err(WebAuthnError::UnsupportedAlgorithm(alg)),
};
let x = get_bytes(-2)
.ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: x".to_string()))?;
let y = get_bytes(-3)
.ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: y".to_string()))?;
if x.len() != coord_size {
return Err(WebAuthnError::InvalidPublicKey(format!(
"x coordinate must be {coord_size} bytes, got {}",
x.len()
)));
}
if y.len() != coord_size {
return Err(WebAuthnError::InvalidPublicKey(format!(
"y coordinate must be {coord_size} bytes, got {}",
y.len()
)));
}
Ok(CoseKey::EC2 { alg, crv, x, y })
}
fn parse_rsa_key(
get_int: &impl Fn(i64) -> Option<i64>,
get_bytes: &impl Fn(i64) -> Option<Vec<u8>>,
) -> Result<CoseKey> {
let alg = get_int(3).ok_or_else(|| {
WebAuthnError::InvalidPublicKey("missing required field: alg".to_string())
})?;
if alg != COSE_RS256 {
return Err(WebAuthnError::UnsupportedAlgorithm(alg));
}
let n = get_bytes(-1)
.ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: n".to_string()))?;
if n.len() < MIN_RSA_N_LEN {
return Err(WebAuthnError::InvalidPublicKey(format!(
"RSA modulus must be at least {} bytes (2048-bit), got {}",
MIN_RSA_N_LEN,
n.len()
)));
}
let e = get_bytes(-2)
.ok_or_else(|| WebAuthnError::InvalidPublicKey("missing required field: e".to_string()))?;
if e.is_empty() {
return Err(WebAuthnError::InvalidPublicKey(
"RSA exponent (e) must not be empty".to_string(),
));
}
Ok(CoseKey::RSA { alg, n, e })
}
fn parse_attested_credential_data(data: &[u8]) -> Result<(AttestedCredentialData, usize)> {
if data.len() < 18 {
return Err(WebAuthnError::InvalidAuthenticatorData(format!(
"attested credential data too short: expected at least 18 bytes after flags/counter, got {}",
data.len()
)));
}
let mut offset = 0usize;
let aaguid: [u8; 16] = data
.get(offset..offset + 16)
.ok_or_else(|| {
WebAuthnError::InvalidAuthenticatorData("truncated before aaguid".to_string())
})?
.try_into()
.expect("slice of exactly 16 bytes always converts");
offset += 16;
let cred_id_len = u16::from_be_bytes([
*data.get(offset).ok_or_else(|| {
WebAuthnError::InvalidAuthenticatorData(
"truncated before credentialIdLength high byte".to_string(),
)
})?,
*data.get(offset + 1).ok_or_else(|| {
WebAuthnError::InvalidAuthenticatorData(
"truncated before credentialIdLength low byte".to_string(),
)
})?,
]) as usize;
offset += 2;
if cred_id_len == 0 {
return Err(WebAuthnError::InvalidAuthenticatorData(
"credentialIdLength is 0 — empty credential ID is not valid".to_string(),
));
}
if cred_id_len > MAX_CREDENTIAL_ID_LEN {
return Err(WebAuthnError::InvalidAuthenticatorData(format!(
"credentialIdLength {cred_id_len} exceeds maximum {MAX_CREDENTIAL_ID_LEN} — likely corrupt data"
)));
}
let credential_id = data
.get(offset..offset + cred_id_len)
.ok_or_else(|| {
WebAuthnError::InvalidAuthenticatorData(format!(
"credentialIdLength ({cred_id_len}) extends past end of buffer"
))
})?
.to_vec();
offset += cred_id_len;
let remaining = data.get(offset..).ok_or_else(|| {
WebAuthnError::InvalidAuthenticatorData("truncated before public key CBOR".to_string())
})?;
if remaining.is_empty() {
return Err(WebAuthnError::InvalidAuthenticatorData(
"no bytes remaining for credentialPublicKey CBOR".to_string(),
));
}
let mut cursor = Cursor::new(remaining);
let _: Value = ciborium::from_reader(&mut cursor)
.map_err(|e| WebAuthnError::CborDecodeError(format!("COSE key: {e}")))?;
let cbor_len = cursor.position() as usize;
let public_key = parse_cose_key(&remaining[..cbor_len])?;
Ok((
AttestedCredentialData {
aaguid,
credential_id,
public_key,
},
offset + cbor_len,
))
}
fn parse_extension_map(data: &[u8]) -> Result<HashMap<String, Value>> {
if data.is_empty() {
return Err(WebAuthnError::InvalidAuthenticatorData(
"ED flag is set but no extension bytes are present".to_string(),
));
}
let value: Value = ciborium::from_reader(data)
.map_err(|e| WebAuthnError::CborDecodeError(format!("extension map: {e}")))?;
let map = match value {
Value::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidAuthenticatorData(
"extension data must be a CBOR map".to_string(),
))
}
};
let mut extensions = HashMap::new();
for (k, v) in map {
if let Value::Text(key) = k {
extensions.insert(key, v);
}
}
Ok(extensions)
}
#[cfg(test)]
mod tests {
use super::*;
use ciborium::value::Value;
pub fn make_auth_data(
rp_id_hash: &[u8; 32],
flags: u8,
sign_count: u32,
cred_data: Option<&[u8]>,
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(rp_id_hash);
out.push(flags);
out.extend_from_slice(&sign_count.to_be_bytes());
if let Some(cd) = cred_data {
out.extend_from_slice(cd);
}
out
}
fn make_cose_key_cbor(x: &[u8], y: &[u8]) -> Vec<u8> {
make_cose_key_cbor_with_alg_crv(x, y, -7, 1)
}
fn make_cose_key_cbor_with_alg_crv(x: &[u8], y: &[u8], alg: i64, crv: i64) -> Vec<u8> {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(2i64.into())),
(Value::Integer(3i64.into()), Value::Integer(alg.into())),
(Value::Integer((-1i64).into()), Value::Integer(crv.into())),
(Value::Integer((-2i64).into()), Value::Bytes(x.to_vec())),
(Value::Integer((-3i64).into()), Value::Bytes(y.to_vec())),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
buf
}
fn make_rsa_cose_key_cbor(n: &[u8], e: &[u8]) -> Vec<u8> {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(3i64.into())), (
Value::Integer(3i64.into()),
Value::Integer((-257i64).into()),
), (Value::Integer((-1i64).into()), Value::Bytes(n.to_vec())), (Value::Integer((-2i64).into()), Value::Bytes(e.to_vec())), ]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
buf
}
fn make_attested_cred_data(cred_id: &[u8], pk_cbor: &[u8]) -> Vec<u8> {
let mut out = vec![0u8; 16]; out.extend_from_slice(&(cred_id.len() as u16).to_be_bytes());
out.extend_from_slice(cred_id);
out.extend_from_slice(pk_cbor);
out
}
#[test]
fn parses_be_flag() {
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BE, 0, None);
let parsed = parse_authenticator_data(&data).expect("test setup");
assert!(parsed.flags.backup_eligible);
assert!(!parsed.flags.backup_state);
}
#[test]
fn parses_be_and_bs_flags() {
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BE | FLAG_BS, 0, None);
let parsed = parse_authenticator_data(&data).expect("test setup");
assert!(parsed.flags.backup_eligible);
assert!(parsed.flags.backup_state);
}
#[test]
fn rejects_bs_without_be() {
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_BS, 0, None);
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
assert!(err.to_string().contains("BS"));
}
#[test]
fn rejects_too_short() {
let result = parse_authenticator_data(&[0u8; 10]);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAuthenticatorData(_))
));
}
#[test]
fn rejects_exactly_36_bytes() {
let result = parse_authenticator_data(&[0u8; 36]);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAuthenticatorData(_))
));
}
#[test]
fn rejects_empty() {
let result = parse_authenticator_data(&[]);
assert!(matches!(
result,
Err(WebAuthnError::InvalidAuthenticatorData(_))
));
}
#[test]
fn parses_minimal_auth_data() {
let rp_hash = [0xABu8; 32];
let data = make_auth_data(&rp_hash, FLAG_UP, 42, None);
let parsed = parse_authenticator_data(&data).expect("test setup");
assert_eq!(parsed.rp_id_hash, rp_hash);
assert!(parsed.flags.user_present);
assert!(!parsed.flags.user_verified);
assert_eq!(parsed.sign_count, 42);
assert!(parsed.attested_credential_data.is_none());
assert_eq!(parsed.raw, data);
}
#[test]
fn parses_up_and_uv_flags() {
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_UV, 0, None);
let parsed = parse_authenticator_data(&data).expect("test setup");
assert!(parsed.flags.user_present);
assert!(parsed.flags.user_verified);
}
#[test]
fn raw_field_equals_input_bytes() {
let data = make_auth_data(&[0u8; 32], FLAG_UP, 7, None);
let parsed = parse_authenticator_data(&data).expect("test setup");
assert_eq!(parsed.raw, data);
}
#[test]
fn all_flags_set_parses_without_panic() {
let data = make_auth_data(&[0u8; 32], 0xFF, 0, None);
let result = parse_authenticator_data(&data);
assert!(result.is_err());
}
#[test]
fn at_flag_set_but_no_data_returns_error() {
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, None);
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
}
#[test]
fn rejects_zero_length_credential_id() {
let pk = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 32]);
let cred_data = make_attested_cred_data(&[], &pk); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
assert!(err.to_string().contains("0"));
}
#[test]
fn rejects_credential_id_too_large() {
let mut cred_data = vec![0u8; 16]; let oversized_len: u16 = 1024;
cred_data.extend_from_slice(&oversized_len.to_be_bytes());
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
assert!(err.to_string().contains("1024"));
}
#[test]
fn rejects_credential_id_extends_past_buffer() {
let mut cred_data = vec![0u8; 16]; cred_data.extend_from_slice(&50u16.to_be_bytes()); cred_data.extend_from_slice(&[0xABu8; 10]); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
}
#[test]
fn rejects_empty_cbor_after_credential_id() {
let mut cred_data = vec![0u8; 16]; cred_data.extend_from_slice(&1u16.to_be_bytes());
cred_data.push(0xAB); let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT, 0, Some(&cred_data));
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
}
fn make_extension_cbor(entries: &[(&str, Value)]) -> Vec<u8> {
let map = Value::Map(
entries
.iter()
.map(|(k, v)| (Value::Text(k.to_string()), v.clone()))
.collect(),
);
let mut buf = Vec::new();
ciborium::into_writer(&map, &mut buf).expect("test setup");
buf
}
#[test]
fn ed_flag_not_set_produces_none_extensions() {
let data = make_auth_data(&[0u8; 32], FLAG_UP, 1, None);
let parsed = parse_authenticator_data(&data).expect("test setup");
assert!(parsed.extensions.is_none());
}
#[test]
fn ed_flag_set_with_valid_map_populates_extensions() {
let ext_cbor = make_extension_cbor(&[("appid", Value::Bool(true))]);
let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
data.extend_from_slice(&ext_cbor);
let parsed = parse_authenticator_data(&data).expect("test setup");
let exts = parsed.extensions.expect("test setup");
assert_eq!(exts.get("appid"), Some(&Value::Bool(true)));
}
#[test]
fn ed_flag_set_with_multiple_extensions() {
let cred_props = Value::Map(vec![(Value::Text("rk".to_string()), Value::Bool(true))]);
let ext_cbor = make_extension_cbor(&[
("credProps", cred_props.clone()),
("appid", Value::Bool(false)),
]);
let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
data.extend_from_slice(&ext_cbor);
let parsed = parse_authenticator_data(&data).expect("test setup");
let exts = parsed.extensions.expect("test setup");
assert_eq!(exts.get("credProps"), Some(&cred_props));
assert_eq!(exts.get("appid"), Some(&Value::Bool(false)));
}
#[test]
fn ed_flag_set_with_at_flag_parses_extensions_after_cose_key() {
let pk_cbor = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 32]);
let cred_data = make_attested_cred_data(&[0xAAu8; 8], &pk_cbor);
let ext_cbor = make_extension_cbor(&[("appid", Value::Bool(true))]);
let mut combined = cred_data;
combined.extend_from_slice(&ext_cbor);
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_AT | FLAG_ED, 0, Some(&combined));
let parsed = parse_authenticator_data(&data).expect("test setup");
assert!(parsed.attested_credential_data.is_some());
let exts = parsed.extensions.expect("test setup");
assert_eq!(exts.get("appid"), Some(&Value::Bool(true)));
}
#[test]
fn ed_flag_set_with_no_bytes_returns_error() {
let data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
assert!(err.to_string().contains("ED"));
}
#[test]
fn ed_flag_set_with_malformed_cbor_returns_error() {
let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
data.extend_from_slice(&[0xFF, 0xFF]); let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::CborDecodeError(_)));
}
#[test]
fn ed_flag_set_with_non_map_cbor_returns_error() {
let mut data = make_auth_data(&[0u8; 32], FLAG_UP | FLAG_ED, 0, None);
let not_a_map = Value::Integer(42i64.into());
let mut cbor_buf = Vec::new();
ciborium::into_writer(¬_a_map, &mut cbor_buf).expect("test setup");
data.extend_from_slice(&cbor_buf);
let err = parse_authenticator_data(&data).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidAuthenticatorData(_)));
assert!(err.to_string().contains("map"));
}
#[test]
fn parse_cose_key_valid_es256() {
let x = vec![0x01u8; 32];
let y = vec![0x02u8; 32];
let cbor = make_cose_key_cbor(&x, &y);
let key = parse_cose_key(&cbor).expect("test setup");
match key {
CoseKey::EC2 {
alg,
crv,
x: kx,
y: ky,
} => {
assert_eq!(alg, -7);
assert_eq!(crv, 1);
assert_eq!(kx, x);
assert_eq!(ky, y);
}
_ => panic!("expected EC2 key"),
}
}
#[test]
fn parse_cose_key_rejects_missing_kty() {
let cose = Value::Map(vec![
(Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
(Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
(Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
(Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
assert!(err.to_string().contains("kty"));
}
#[test]
fn parse_cose_key_rejects_missing_alg() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(2i64.into())),
(Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
(Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
(Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
assert!(err.to_string().contains("alg"));
}
#[test]
fn parse_cose_key_rejects_missing_crv() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(2i64.into())),
(Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
(Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
(Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
assert!(err.to_string().contains("crv"));
}
#[test]
fn parse_cose_key_rejects_missing_x() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(2i64.into())),
(Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
(Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
(Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
assert!(err.to_string().contains("x"));
}
#[test]
fn parse_cose_key_rejects_missing_y() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(2i64.into())),
(Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
(Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
(Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
assert!(err.to_string().contains("y"));
}
#[test]
fn parse_cose_key_rejects_unsupported_kty() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(4i64.into())),
(Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("4")));
}
#[test]
fn parse_cose_key_rejects_unsupported_crv() {
let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 32], &[0x02u8; 32], -7, 2);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("2")));
}
#[test]
fn parse_cose_key_valid_es384() {
let x = vec![0x01u8; 48];
let y = vec![0x02u8; 48];
let cbor = make_cose_key_cbor_with_alg_crv(&x, &y, -35, 2);
let key = parse_cose_key(&cbor).expect("test setup");
match key {
CoseKey::EC2 {
alg,
crv,
x: kx,
y: ky,
} => {
assert_eq!(alg, -35);
assert_eq!(crv, 2);
assert_eq!(kx, x);
assert_eq!(ky, y);
}
_ => panic!("expected EC2 key"),
}
}
#[test]
fn parse_cose_key_es384_rejects_short_x_coordinate() {
let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 32], &[0x02u8; 48], -35, 2);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("32")));
}
#[test]
fn parse_cose_key_es384_rejects_short_y_coordinate() {
let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 48], &[0x02u8; 32], -35, 2);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("32")));
}
#[test]
fn parse_cose_key_es384_rejects_wrong_crv() {
let cbor = make_cose_key_cbor_with_alg_crv(&[0x01u8; 48], &[0x02u8; 48], -35, 1);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("1")));
}
#[test]
fn parse_cose_key_rejects_short_x_coordinate() {
let cbor = make_cose_key_cbor(&[0x01u8; 31], &[0x02u8; 32]);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("31")));
}
#[test]
fn parse_cose_key_rejects_short_y_coordinate() {
let cbor = make_cose_key_cbor(&[0x01u8; 32], &[0x02u8; 10]);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("10")));
}
#[test]
fn parse_cose_key_rejects_long_x_coordinate() {
let cbor = make_cose_key_cbor(&[0x01u8; 33], &[0x02u8; 32]);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("33")));
}
#[test]
fn parse_cose_key_rejects_duplicate_key() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(2i64.into())),
(Value::Integer(1i64.into()), Value::Integer(3i64.into())), (Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
(Value::Integer((-1i64).into()), Value::Integer(1i64.into())),
(Value::Integer((-2i64).into()), Value::Bytes(vec![0u8; 32])),
(Value::Integer((-3i64).into()), Value::Bytes(vec![0u8; 32])),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("duplicate")));
}
#[test]
fn parse_cose_key_rejects_not_a_map() {
let cose = Value::Integer(42i64.into());
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(_)));
}
#[test]
fn parse_cose_key_rejects_empty_input() {
let err = parse_cose_key(&[]).expect_err("expected error");
assert!(matches!(err, WebAuthnError::CborDecodeError(_)));
}
#[test]
fn parse_cose_key_valid_rs256() {
let n = vec![0x01u8; 256]; let e = vec![0x01u8, 0x00, 0x01];
let cbor = make_rsa_cose_key_cbor(&n, &e);
let key = parse_cose_key(&cbor).expect("test setup");
match key {
CoseKey::RSA { alg, n: kn, e: ke } => {
assert_eq!(alg, -257);
assert_eq!(kn, n);
assert_eq!(ke, e);
}
_ => panic!("expected RSA key"),
}
}
#[test]
fn parse_cose_key_rs256_rejects_short_modulus() {
let n = vec![0x01u8; 255];
let e = vec![0x01u8, 0x00, 0x01];
let cbor = make_rsa_cose_key_cbor(&n, &e);
let err = parse_cose_key(&cbor).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("255")));
}
#[test]
fn parse_cose_key_rs256_rejects_missing_n() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(3i64.into())),
(
Value::Integer(3i64.into()),
Value::Integer((-257i64).into()),
),
(
Value::Integer((-2i64).into()),
Value::Bytes(vec![0x01, 0x00, 0x01]),
),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("n")));
}
#[test]
fn parse_cose_key_rs256_rejects_missing_e() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(3i64.into())),
(
Value::Integer(3i64.into()),
Value::Integer((-257i64).into()),
),
(
Value::Integer((-1i64).into()),
Value::Bytes(vec![0x01u8; 256]),
),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::InvalidPublicKey(ref m) if m.contains("e")));
}
#[test]
fn parse_cose_key_rsa_with_wrong_alg_returns_unsupported() {
let cose = Value::Map(vec![
(Value::Integer(1i64.into()), Value::Integer(3i64.into())),
(Value::Integer(3i64.into()), Value::Integer((-7i64).into())),
(
Value::Integer((-1i64).into()),
Value::Bytes(vec![0x01u8; 256]),
),
(
Value::Integer((-2i64).into()),
Value::Bytes(vec![0x01, 0x00, 0x01]),
),
]);
let mut buf = Vec::new();
ciborium::into_writer(&cose, &mut buf).expect("test setup");
let err = parse_cose_key(&buf).expect_err("expected error");
assert!(matches!(err, WebAuthnError::UnsupportedAlgorithm(-7)));
}
}