use super::{PortableEncodingError, browser_principal, validate_shape};
use crate::certificate_issuance::{grants_are_subset, positive_duration, validate_grants};
use ic_auth_protocol_types::{
AudienceId, DelegatedRoleGrant, PortableClaims, PortableMechanism, Principal,
};
use thiserror::Error;
#[derive(Clone, Copy, Debug)]
pub struct PortableClaimsContext<'a> {
pub audience: &'a AudienceId,
pub issuer: Principal,
pub mechanism: PortableMechanism,
pub key_binding_id: [u8; 32],
pub grants: &'a [DelegatedRoleGrant],
pub caller: Principal,
pub now_ns: u64,
pub authority_not_before_ns: u64,
pub authority_expires_at_ns: u64,
pub enrollment_expires_at_ns: u64,
pub min_login_started_at_ns: u64,
pub min_proof_issued_at_ns: u64,
pub max_login_ttl_ns: u64,
pub max_proof_ttl_ns: u64,
}
impl PortableClaimsContext<'_> {
pub fn advance_reset_floors(&mut self, clock_skew_ns: u64) -> Result<(), PortableClaimsError> {
let proof_floor = self
.now_ns
.checked_add(1)
.ok_or(PortableClaimsError::InvalidContext)?;
let login_floor = proof_floor
.checked_add(clock_skew_ns)
.ok_or(PortableClaimsError::InvalidContext)?;
self.min_login_started_at_ns = self.min_login_started_at_ns.max(login_floor);
self.min_proof_issued_at_ns = self.min_proof_issued_at_ns.max(proof_floor);
Ok(())
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum PortableClaimsError {
#[error("invalid protected portable claims context")]
InvalidContext,
#[error("portable audience does not match protected authority")]
AudienceMismatch,
#[error("portable issuer does not match protected authority")]
IssuerMismatch,
#[error("portable mechanism does not match protected authority")]
MechanismMismatch,
#[error("portable key binding does not match protected authority")]
KeyMismatch,
#[error("browser key does not identify the authenticated caller")]
CallerMismatch,
#[error("portable grants exceed protected authority")]
GrantsExceeded,
#[error("portable login or proof window is invalid")]
InvalidWindow,
#[error("portable proof is not yet valid")]
NotYetValid,
#[error("portable proof has expired")]
Expired,
#[error("portable login or issuance predates the protected reset floor")]
BeforeReset,
#[error("portable authority or enrollment is not current")]
StaleAuthority,
#[error(transparent)]
Encoding(#[from] PortableEncodingError),
}
pub fn validate_claims_context(
claims: &PortableClaims,
context: &PortableClaimsContext<'_>,
) -> Result<(), PortableClaimsError> {
use PortableClaimsError as Error;
if context.max_login_ttl_ns == 0
|| context.max_login_ttl_ns > crate::limits::MAX_LOGIN_NS
|| context.max_proof_ttl_ns == 0
|| context.max_proof_ttl_ns > crate::limits::MAX_PROOF_NS
|| context.issuer == Principal::anonymous()
|| context.authority_not_before_ns >= context.authority_expires_at_ns
|| context.enrollment_expires_at_ns == 0
|| validate_grants(
context.grants,
crate::limits::MAX_GRANTS,
crate::limits::MAX_SCOPES_PER_GRANT,
)
.is_err()
{
return Err(Error::InvalidContext);
}
validate_shape(claims)?;
if claims.audience != *context.audience {
return Err(Error::AudienceMismatch);
}
if claims.issuer != context.issuer {
return Err(Error::IssuerMismatch);
}
if claims.mechanism != context.mechanism {
return Err(Error::MechanismMismatch);
}
if claims.key_binding_id != context.key_binding_id {
return Err(Error::KeyMismatch);
}
if browser_principal(&claims.browser_public_key) != context.caller {
return Err(Error::CallerMismatch);
}
if !grants_are_subset(&claims.grants, context.grants) {
return Err(Error::GrantsExceeded);
}
if positive_duration(claims.login_started_at_ns, claims.login_deadline_ns)
.is_none_or(|ttl| ttl > context.max_login_ttl_ns)
|| positive_duration(claims.issued_at_ns, claims.admission_expires_at_ns)
.is_none_or(|ttl| ttl > context.max_proof_ttl_ns)
|| claims.login_started_at_ns > claims.issued_at_ns
|| claims.admission_expires_at_ns > claims.login_deadline_ns
{
return Err(Error::InvalidWindow);
}
if claims.login_started_at_ns < context.min_login_started_at_ns
|| claims.issued_at_ns < context.min_proof_issued_at_ns
{
return Err(Error::BeforeReset);
}
if context.now_ns < context.authority_not_before_ns
|| context.now_ns >= context.authority_expires_at_ns
|| context.now_ns >= context.enrollment_expires_at_ns
|| claims.issued_at_ns < context.authority_not_before_ns
{
return Err(Error::StaleAuthority);
}
if context.now_ns < claims.issued_at_ns {
return Err(Error::NotYetValid);
}
if context.now_ns >= claims.admission_expires_at_ns {
return Err(Error::Expired);
}
Ok(())
}