use crate::{
FieldElement,
poseidon::{self, ds},
};
use eddsa_babyjubjub::{EdDSAPrivateKey, EdDSAPublicKey, EdDSASignature};
use serde::{Deserialize, Serialize};
pub const MAX_AAT_LIFETIME_SECS: u32 = 1800;
pub const MAX_SEC_META: u8 = 0x7;
pub const EAT_PROFILE: &str = "https://world.org/eat/aat/v1";
pub const COSE_ALG_AAT: i64 = -65537;
const MIN_EXP: u32 = 1 << 16;
const PROTECTED: [u8; 8] = [0x47, 0xa1, 0x01, 0x3a, 0x00, 0x01, 0x00, 0x00];
const PAYLOAD_LEN: usize = 89;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Platform {
Unspecified = 0,
Ios = 2,
Android = 4,
Web = 6,
}
impl From<Platform> for u8 {
fn from(platform: Platform) -> Self {
platform as Self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SecLevel {
Unspecified = 0,
HardwareKey = 1,
PlatformVerdict = 3,
InternallyVerified = 5,
UserBound = 10,
}
impl From<SecLevel> for u8 {
fn from(sec_level: SecLevel) -> Self {
sec_level as Self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "u8", try_from = "u8")]
#[repr(u8)]
pub enum UserPresence {
Undetermined = 0,
NotPresent = 1,
PresentVerified = 2,
PresentWithin7Days = 3,
PresentWithin30Days = 4,
}
impl From<UserPresence> for u8 {
fn from(presence: UserPresence) -> Self {
presence as Self
}
}
impl TryFrom<u8> for UserPresence {
type Error = AssertionError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Undetermined),
1 => Ok(Self::NotPresent),
2 => Ok(Self::PresentVerified),
3 => Ok(Self::PresentWithin7Days),
4 => Ok(Self::PresentWithin30Days),
_ => Err(AssertionError::ReservedPresence(value)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecFlags {
platform: u8,
sec_level: u8,
build_version: u32,
sec_meta: u8,
user_presence: UserPresence,
}
impl SecFlags {
pub const fn new(
platform: u8,
sec_level: u8,
build_version: u32,
sec_meta: u8,
user_presence: UserPresence,
) -> Result<Self, AssertionError> {
if sec_meta > MAX_SEC_META {
return Err(AssertionError::SecMetaTooLarge(sec_meta));
}
Ok(Self {
platform,
sec_level,
build_version,
sec_meta,
user_presence,
})
}
#[must_use]
pub const fn platform(&self) -> u8 {
self.platform
}
#[must_use]
pub const fn sec_level(&self) -> u8 {
self.sec_level
}
#[must_use]
pub const fn build_version(&self) -> u32 {
self.build_version
}
#[must_use]
pub const fn sec_meta(&self) -> u8 {
self.sec_meta
}
#[must_use]
pub const fn user_presence(&self) -> UserPresence {
self.user_presence
}
pub fn unpack(packed: u64) -> Result<Self, AssertionError> {
let [reserved, top, v3, v2, v1, v0, sec_level, platform] = packed.to_be_bytes();
if reserved != 0 || top >> 6 != 0 {
return Err(AssertionError::InvalidSecFlags(packed));
}
let user_presence = UserPresence::try_from(top >> 3)
.map_err(|_| AssertionError::InvalidSecFlags(packed))?;
Ok(Self {
platform,
sec_level,
build_version: u32::from_be_bytes([v3, v2, v1, v0]),
sec_meta: top & MAX_SEC_META,
user_presence,
})
}
#[must_use]
pub fn pack(&self) -> u64 {
u64::from(self.platform)
| (u64::from(self.sec_level) << 8)
| (u64::from(self.build_version) << 16)
| (u64::from(self.sec_meta) << 48)
| (u64::from(u8::from(self.user_presence)) << 51)
}
}
#[derive(Debug, thiserror::Error)]
pub enum AssertionError {
#[error("sec_meta must carry at most 3 bits of data, got {0:#b}")]
SecMetaTooLarge(u8),
#[error("exp {0} must be in [2^16, 2^32) for its fixed-width encoding")]
ExpirationOutOfRange(u32),
#[error("invalid sec_flags {0:#x}")]
InvalidSecFlags(u64),
#[error("presence values 5 and above are reserved, got {0}")]
ReservedPresence(u8),
#[error("not a canonical AAT encoding: {0}")]
InvalidEncoding(&'static str),
#[error("invalid key material: {0}")]
KeyEncoding(String),
}
#[derive(Debug, Clone)]
pub struct SignedAuthenticatorAssertionToken {
pub token: AuthenticatorAssertionToken,
pub signature: EdDSASignature,
pub kid: Option<[u8; 32]>,
}
#[must_use]
pub fn request_commitment(
aud: FieldElement,
nonce: FieldElement,
cdh: FieldElement,
blind: FieldElement,
) -> FieldElement {
poseidon::hash(
ds::AUTHENTICATOR_ASSERTION_REQUEST,
[aud, nonce, cdh, blind],
)
}
#[must_use]
pub fn message_hash(exp: u32, aat_commitment: FieldElement, sec_flags: u64) -> FieldElement {
poseidon::hash(
ds::AUTHENTICATOR_ASSERTION_TOKEN,
[
FieldElement::from(u64::from(exp)),
aat_commitment,
FieldElement::from(sec_flags),
],
)
}
#[derive(Debug, Clone, Copy)]
pub struct AuthenticatorAssertionToken {
exp: u32,
aat_commitment: FieldElement,
sec_flags: SecFlags,
}
impl AuthenticatorAssertionToken {
pub const fn new(
exp: u32,
aat_commitment: FieldElement,
sec_flags: SecFlags,
) -> Result<Self, AssertionError> {
if exp < MIN_EXP {
return Err(AssertionError::ExpirationOutOfRange(exp));
}
Ok(Self {
exp,
aat_commitment,
sec_flags,
})
}
#[must_use]
pub fn message_hash(&self) -> FieldElement {
message_hash(self.exp, self.aat_commitment, self.sec_flags.pack())
}
#[must_use]
pub const fn exp(&self) -> u32 {
self.exp
}
#[must_use]
pub const fn aat_commitment(&self) -> FieldElement {
self.aat_commitment
}
#[must_use]
pub const fn sec_flags(&self) -> SecFlags {
self.sec_flags
}
pub fn sign(
&self,
authenticator_provider_key: &EdDSAPrivateKey,
) -> Result<Vec<u8>, AssertionError> {
let signature = authenticator_provider_key
.sign(*self.message_hash())
.to_compressed_bytes()
.map_err(|e| AssertionError::KeyEncoding(e.to_string()))?;
let kid = authenticator_provider_key
.public()
.to_compressed_bytes()
.map_err(|e| AssertionError::KeyEncoding(e.to_string()))?;
let mut cwt = vec![0x84];
cwt.extend_from_slice(&PROTECTED);
cwt.extend_from_slice(&[0xa1, 0x04, 0x58, 0x20]);
cwt.extend_from_slice(&kid);
cwt.extend_from_slice(&[0x58, 0x59]);
cwt.extend_from_slice(&self.payload());
cwt.extend_from_slice(&[0x58, 0x40]);
cwt.extend_from_slice(&signature);
Ok(cwt)
}
fn payload(&self) -> [u8; PAYLOAD_LEN] {
let mut payload = [0u8; PAYLOAD_LEN];
let mut parts: Vec<&[u8]> = Vec::with_capacity(8);
let exp = self.exp.to_be_bytes();
let aat_commitment = self.aat_commitment.to_be_bytes();
let sec_flags = self.sec_flags.pack().to_be_bytes();
parts.push(&[0xa4, 0x04, 0x1a]);
parts.push(&exp);
parts.push(&[0x0a, 0x58, 0x20]);
parts.push(&aat_commitment);
parts.push(&[0x19, 0x01, 0x09, 0x78, 0x1c]);
parts.push(EAT_PROFILE.as_bytes());
parts.push(&[0x3a, 0x00, 0x01, 0x11, 0x6f, 0x48]);
parts.push(&sec_flags);
let mut offset = 0;
for part in parts {
payload[offset..offset + part.len()].copy_from_slice(part);
offset += part.len();
}
debug_assert_eq!(offset, PAYLOAD_LEN);
payload
}
}
impl SignedAuthenticatorAssertionToken {
pub fn decode(cwt: &[u8]) -> Result<Self, AssertionError> {
let rest = cwt
.strip_prefix(&[0x84])
.and_then(|r| r.strip_prefix(&PROTECTED))
.ok_or(AssertionError::InvalidEncoding("header"))?;
let (kid, rest) = if let Some(r) = rest.strip_prefix(&[0xa0]) {
(None, r)
} else {
let r = rest
.strip_prefix(&[0xa1, 0x04, 0x58, 0x20])
.ok_or(AssertionError::InvalidEncoding("unprotected header"))?;
let (kid, r) = r
.split_at_checked(32)
.ok_or(AssertionError::InvalidEncoding("kid"))?;
(Some(<[u8; 32]>::try_from(kid).expect("split at 32")), r)
};
let rest = rest
.strip_prefix(&[0x58, 0x59])
.ok_or(AssertionError::InvalidEncoding("payload"))?;
let (payload, rest) = rest
.split_at_checked(PAYLOAD_LEN)
.ok_or(AssertionError::InvalidEncoding("payload"))?;
let signature = rest
.strip_prefix(&[0x58, 0x40])
.and_then(|r| <[u8; 64]>::try_from(r).ok())
.ok_or(AssertionError::InvalidEncoding("signature"))?;
let exp = u32::from_be_bytes(payload[3..7].try_into().expect("4 bytes"));
let aat_commitment =
FieldElement::from_be_bytes(payload[10..42].try_into().expect("32 bytes"))
.map_err(|_| AssertionError::InvalidEncoding("non-canonical aat_commitment"))?;
let sec_flags = SecFlags::unpack(u64::from_be_bytes(
payload[81..89].try_into().expect("8 bytes"),
))?;
let token = AuthenticatorAssertionToken::new(exp, aat_commitment, sec_flags)?;
if token.payload() != payload {
return Err(AssertionError::InvalidEncoding("claims"));
}
let signature = EdDSASignature::from_compressed_bytes(signature)
.map_err(|e| AssertionError::KeyEncoding(e.to_string()))?;
Ok(Self {
token,
signature,
kid,
})
}
#[must_use]
pub fn into_private_inputs(
self,
authenticator_provider_key: EdDSAPublicKey,
cdh: FieldElement,
blind: FieldElement,
) -> AuthenticatorAssertionPrivateInputs {
AuthenticatorAssertionPrivateInputs {
authenticator_provider_key,
exp: self.token.exp,
sec_flags: self.token.sec_flags.pack(),
sig: self.signature,
cdh,
blind,
}
}
}
#[must_use]
pub fn authenticator_provider_key_hash(key: &EdDSAPublicKey) -> FieldElement {
poseidon::hash(
ds::AUTHENTICATOR_PROVIDER_KEY,
[FieldElement::from(key.pk.x), FieldElement::from(key.pk.y)],
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticatorAssertionPublicInputs {
pub authenticator_provider_key_hash: FieldElement,
pub now: u32,
pub aud: FieldElement,
pub nonce: FieldElement,
pub platform: u8,
pub sec_level: u8,
pub sec_meta: u8,
pub user_presence: UserPresence,
pub min_build_version: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticatorAssertionPrivateInputs {
pub authenticator_provider_key: EdDSAPublicKey,
pub exp: u32,
pub sec_flags: u64,
pub sig: EdDSASignature,
pub cdh: FieldElement,
pub blind: FieldElement,
}
impl AuthenticatorAssertionPrivateInputs {
pub fn public_inputs(
&self,
now: u32,
aud: FieldElement,
nonce: FieldElement,
min_build_version: u32,
) -> Result<AuthenticatorAssertionPublicInputs, VerificationError> {
let flags = SecFlags::unpack(self.sec_flags)
.map_err(|_| VerificationError::InvalidSecFlags(self.sec_flags))?;
Ok(AuthenticatorAssertionPublicInputs {
authenticator_provider_key_hash: authenticator_provider_key_hash(
&self.authenticator_provider_key,
),
now,
aud,
nonce,
platform: flags.platform,
sec_level: flags.sec_level,
sec_meta: flags.sec_meta,
user_presence: flags.user_presence,
min_build_version,
})
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum VerificationError {
#[error("nonce must not be 0")]
ZeroNonce,
#[error("authenticator_provider_key does not match its hash")]
KeyHashMismatch,
#[error("invalid signature")]
InvalidSignature,
#[error("token is expired")]
Expired,
#[error("token lifetime exceeds maximum")]
LifetimeExceeded,
#[error("invalid sec_flags {0:#x}")]
InvalidSecFlags(u64),
#[error("sec_flags do not match the public security flags")]
SecFlagsMismatch,
#[error("build_version below minimum")]
BuildVersionBelowMinimum,
}
pub fn verify_aat(
public: &AuthenticatorAssertionPublicInputs,
private: &AuthenticatorAssertionPrivateInputs,
) -> Result<(), VerificationError> {
if public.nonce == FieldElement::ZERO {
return Err(VerificationError::ZeroNonce);
}
if authenticator_provider_key_hash(&private.authenticator_provider_key)
!= public.authenticator_provider_key_hash
{
return Err(VerificationError::KeyHashMismatch);
}
let aat_commitment = request_commitment(public.aud, public.nonce, private.cdh, private.blind);
let message = message_hash(private.exp, aat_commitment, private.sec_flags);
if !private
.authenticator_provider_key
.verify(*message, &private.sig)
{
return Err(VerificationError::InvalidSignature);
}
if public.now >= private.exp {
return Err(VerificationError::Expired);
}
if private.exp - public.now > MAX_AAT_LIFETIME_SECS {
return Err(VerificationError::LifetimeExceeded);
}
let flags = SecFlags::unpack(private.sec_flags)
.map_err(|_| VerificationError::InvalidSecFlags(private.sec_flags))?;
if (
flags.platform,
flags.sec_level,
flags.sec_meta,
flags.user_presence,
) != (
public.platform,
public.sec_level,
public.sec_meta,
public.user_presence,
) {
return Err(VerificationError::SecFlagsMismatch);
}
if flags.build_version < public.min_build_version {
return Err(VerificationError::BuildVersionBelowMinimum);
}
Ok(())
}
#[cfg(test)]
mod tests;