use super::error::AppAttestError;
use byteorder::{BigEndian, ByteOrder};
use sha2::{Digest, Sha256};
use std::error::Error;
#[allow(dead_code)]
pub(crate) struct AuthenticatorData {
pub(crate) bytes: Vec<u8>,
pub(crate) rp_id_hash: Vec<u8>,
pub(crate) flags: u8,
pub(crate) counter: u32,
pub(crate) aaguid: Option<AAGUID>,
pub(crate) credential_id: Option<Vec<u8>>,
}
impl AuthenticatorData {
pub(crate) fn new(auth_data_byte: Vec<u8>) -> Result<Self, AppAttestError> {
if auth_data_byte.len() < 37 {
return Err(AppAttestError::Message(
"Authenticator data is too short".to_string(),
));
}
let mut auth_data = AuthenticatorData {
bytes: auth_data_byte.clone(),
rp_id_hash: auth_data_byte[0..32].to_vec(),
flags: auth_data_byte[32],
counter: BigEndian::read_u32(&auth_data_byte[33..37]),
aaguid: None,
credential_id: None,
};
auth_data
.populate_optional_data()
.map_err(|e| AppAttestError::Message(e.to_string()))?;
Ok(auth_data)
}
fn populate_optional_data(&mut self) -> Result<(), Box<dyn Error>> {
if self.bytes.len() < 55 {
return Ok(());
}
let length = BigEndian::read_u16(&self.bytes[53..55]) as usize;
let credential_id = self.bytes[55..55 + length].to_vec();
let aaguid = AAGUID::new(self.bytes[37..53].to_vec())?;
self.credential_id = Some(credential_id);
self.aaguid = Some(aaguid);
Ok(())
}
pub(crate) fn is_valid_aaguid(&self) -> bool {
let expected_aaguid = APP_ATTEST.as_bytes().to_vec();
let mut prod_aaguid = expected_aaguid.clone();
prod_aaguid.extend(std::iter::repeat_n(0x00, 7));
let dev_aaguid = APP_ATTEST_DEVELOP.as_bytes().to_vec();
if let Some(aaguid) = &self.aaguid {
return aaguid.bytes() == expected_aaguid
|| aaguid.bytes() == prod_aaguid
|| aaguid.bytes() == dev_aaguid;
}
false
}
pub(crate) fn verify_counter(&self) -> Result<(), AppAttestError> {
if self.counter != 0 {
return Err(AppAttestError::InvalidCounter);
}
Ok(())
}
pub(crate) fn verify_app_id(&self, app_id: &str) -> Result<(), AppAttestError> {
let mut hasher = Sha256::new();
hasher.update(app_id.as_bytes());
if self.rp_id_hash != hasher.finalize().as_slice() {
Err(AppAttestError::InvalidAppID)
} else {
Ok(())
}
}
pub(crate) fn verify_key_id(&self, key_id: &[u8]) -> Result<(), AppAttestError> {
if let Some(credential_id) = &self.credential_id {
if credential_id == key_id {
return Ok(());
}
}
Err(AppAttestError::InvalidCredentialID)
}
}
#[allow(clippy::upper_case_acronyms)]
pub(crate) struct AAGUID(String);
const APP_ATTEST: &str = "appattest";
const APP_ATTEST_DEVELOP: &str = "appattestdevelop";
impl AAGUID {
fn new(b: Vec<u8>) -> Result<Self, AppAttestError> {
let ids: [&str; 2] = [APP_ATTEST, APP_ATTEST_DEVELOP];
for &id in ids.iter() {
if id.as_bytes() == AAGUID::trim_trailing_zeros(b.as_slice()) {
return Ok(AAGUID(id.to_string()));
}
}
Err(AppAttestError::InvalidAAGUID)
}
fn bytes(&self) -> Vec<u8> {
self.0.as_bytes().to_vec()
}
fn trim_trailing_zeros(bytes: &[u8]) -> &[u8] {
let mut last_non_zero = None;
for (index, &value) in bytes.iter().enumerate() {
if value != 0 {
last_non_zero = Some(index);
}
}
match last_non_zero {
Some(index) => &bytes[..=index],
None => &[],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_data_new_valid() {
let mut bytes = vec![0u8; 37];
bytes[32] = 0b00000001;
BigEndian::write_u32(&mut bytes[33..37], 1);
let result = AuthenticatorData::new(bytes);
assert!(result.is_ok());
let auth_data = result.unwrap();
assert_eq!(auth_data.counter, 1);
}
#[test]
fn test_auth_data_new_too_short() {
let bytes = vec![0u8; 36];
let result = AuthenticatorData::new(bytes);
assert!(result.is_err());
}
#[test]
fn test_auth_data_new_exact_minimum() {
let bytes = vec![0u8; 37];
let result = AuthenticatorData::new(bytes);
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.counter, 0);
assert!(data.aaguid.is_none());
assert!(data.credential_id.is_none());
}
#[test]
fn test_auth_data_counter_value() {
let mut bytes = vec![0u8; 37];
BigEndian::write_u32(&mut bytes[33..37], 0xDEADBEEF);
let data = AuthenticatorData::new(bytes).unwrap();
assert_eq!(data.counter, 0xDEADBEEF);
}
#[test]
fn test_auth_data_rp_id_hash() {
let mut bytes = vec![0u8; 37];
for i in 0..32 {
bytes[i] = i as u8;
}
let data = AuthenticatorData::new(bytes).unwrap();
assert_eq!(data.rp_id_hash.len(), 32);
assert_eq!(data.rp_id_hash[0], 0);
assert_eq!(data.rp_id_hash[31], 31);
}
#[test]
fn test_aaguid_new_valid() {
let appattest_bytes = APP_ATTEST.as_bytes().to_vec();
let result = AAGUID::new(appattest_bytes);
assert!(result.is_ok());
assert_eq!(result.unwrap().0, APP_ATTEST);
}
#[test]
fn test_aaguid_new_develop() {
let develop_bytes = APP_ATTEST_DEVELOP.as_bytes().to_vec();
let result = AAGUID::new(develop_bytes);
assert!(result.is_ok());
assert_eq!(result.unwrap().0, APP_ATTEST_DEVELOP);
}
#[test]
fn test_aaguid_new_invalid() {
let invalid_bytes = vec![0u8; 16];
let result = AAGUID::new(invalid_bytes);
assert!(result.is_err());
}
#[test]
fn test_aaguid_new_with_trailing_zeros() {
let mut bytes = APP_ATTEST.as_bytes().to_vec();
bytes.resize(16, 0x00);
let result = AAGUID::new(bytes);
assert!(result.is_ok());
}
#[test]
fn test_is_valid_aaguid_production() {
let mut bytes = vec![0u8; 55 + 4]; let aaguid_bytes = APP_ATTEST.as_bytes();
bytes[37..37 + aaguid_bytes.len()].copy_from_slice(aaguid_bytes);
BigEndian::write_u16(&mut bytes[53..55], 4);
bytes[55] = 0xAA;
bytes[56] = 0xBB;
bytes[57] = 0xCC;
bytes[58] = 0xDD;
BigEndian::write_u32(&mut bytes[33..37], 0);
let data = AuthenticatorData::new(bytes).unwrap();
assert!(data.is_valid_aaguid());
}
#[test]
fn test_is_valid_aaguid_develop() {
let mut bytes = vec![0u8; 55 + 4];
let aaguid_bytes = APP_ATTEST_DEVELOP.as_bytes();
bytes[37..37 + aaguid_bytes.len()].copy_from_slice(aaguid_bytes);
BigEndian::write_u16(&mut bytes[53..55], 4);
BigEndian::write_u32(&mut bytes[33..37], 0);
let data = AuthenticatorData::new(bytes).unwrap();
assert!(data.is_valid_aaguid());
}
#[test]
fn test_is_valid_aaguid_none() {
let bytes = vec![0u8; 37];
let data = AuthenticatorData::new(bytes).unwrap();
assert!(!data.is_valid_aaguid());
}
#[test]
fn test_verify_counter_zero_passes() {
let bytes = vec![0u8; 37];
let data = AuthenticatorData::new(bytes).unwrap();
assert!(data.verify_counter().is_ok());
}
#[test]
fn test_verify_counter_nonzero_fails() {
let mut bytes = vec![0u8; 37];
BigEndian::write_u32(&mut bytes[33..37], 5);
let data = AuthenticatorData::new(bytes).unwrap();
assert!(data.verify_counter().is_err());
}
#[test]
fn test_verify_app_id() {
let app_id = "app.apple.connect";
let mut hasher = Sha256::new();
hasher.update(app_id.as_bytes());
let hash = hasher.finalize().to_vec();
let auth_data = AuthenticatorData {
bytes: vec![],
rp_id_hash: hash,
flags: 0,
counter: 0,
aaguid: None,
credential_id: None,
};
assert!(auth_data.verify_app_id("app.apple.connect").is_ok());
assert!(auth_data.verify_app_id("invalid.apple.connect").is_err());
}
#[test]
fn test_verify_key_id() {
let key_id = vec![1, 2, 3, 4];
let auth_data = AuthenticatorData {
bytes: vec![],
rp_id_hash: vec![],
flags: 0,
counter: 0,
aaguid: None,
credential_id: Some(key_id.clone()),
};
assert!(auth_data.verify_key_id(&key_id).is_ok());
assert!(auth_data.verify_key_id(&vec![4, 3, 2, 1]).is_err());
}
#[test]
fn test_verify_key_id_none() {
let auth_data = AuthenticatorData {
bytes: vec![],
rp_id_hash: vec![],
flags: 0,
counter: 0,
aaguid: None,
credential_id: None,
};
assert!(auth_data.verify_key_id(&[1, 2, 3]).is_err());
}
#[test]
fn test_trim_trailing_zeros() {
assert_eq!(AAGUID::trim_trailing_zeros(&[1, 2, 0, 0]), &[1, 2]);
assert_eq!(AAGUID::trim_trailing_zeros(&[1, 2, 3]), &[1, 2, 3]);
assert_eq!(AAGUID::trim_trailing_zeros(&[0, 0, 0]), &[] as &[u8]);
assert_eq!(AAGUID::trim_trailing_zeros(&[]), &[] as &[u8]);
}
}