use crate::canonical::{CanonicalAuthError, cert_hash, claims_hash};
use crate::certificate_issuance::{GrantValidationError, validate_grants};
use ic_auth_protocol_types::{
AuthRole, DelegatedRoleGrant, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
DelegationProof, IssuerProof, Principal,
};
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TokenPreparationLimits {
pub max_grants: usize,
pub max_scopes_per_grant: usize,
}
pub struct PrepareDelegatedTokenInput<'a> {
pub proof: &'a DelegationProof,
pub authority_expires_at_ns: u64,
pub operation_id: [u8; 32],
pub prepared_by: Principal,
pub audience: DelegationAudience,
pub grants: Vec<DelegatedRoleGrant>,
pub ttl_ns: u64,
pub ext: Option<Vec<u8>>,
pub now_ns: u64,
pub limits: TokenPreparationLimits,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedDelegatedToken {
claims: DelegatedTokenClaims,
claims_hash: [u8; 32],
proof: DelegationProof,
}
impl PreparedDelegatedToken {
pub const fn claims(&self) -> &DelegatedTokenClaims {
&self.claims
}
pub const fn claims_hash(&self) -> [u8; 32] {
self.claims_hash
}
pub const fn proof(&self) -> &DelegationProof {
&self.proof
}
}
#[derive(Debug, Eq, Error, PartialEq)]
pub enum PrepareDelegatedTokenError {
#[error("delegation certificate is not yet valid")]
CertNotYetValid,
#[error("delegation certificate or local authority expired")]
CertExpired,
#[error("token TTL must be greater than zero")]
TokenTtlZero,
#[error("token TTL {ttl_ns} exceeds certificate maximum {max_ttl_ns}")]
TokenTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
#[error("token expiry overflows")]
TokenExpiresAtOverflow,
#[error("token outlives certificate or local authority")]
TokenOutlivesCert,
#[error("token grants must not be empty")]
GrantsEmpty,
#[error("token grants {found} exceed host maximum {max}")]
TooManyGrants { found: usize, max: usize },
#[error("grant for {role:?} has no scopes")]
EmptyGrantScopes { role: AuthRole },
#[error("grant for {role:?} has {found} scopes exceeding host maximum {max}")]
TooManyGrantScopes {
role: AuthRole,
found: usize,
max: usize,
},
#[error("token audience is not a subset of certificate audience")]
AudienceNotSubset,
#[error("token grants are not a subset of certificate grants")]
GrantsNotSubset,
#[error(transparent)]
Canonical(#[from] CanonicalAuthError),
}
impl From<GrantValidationError> for PrepareDelegatedTokenError {
fn from(error: GrantValidationError) -> Self {
use GrantValidationError as E;
match error {
E::Empty => Self::GrantsEmpty,
E::TooMany { found, max } => Self::TooManyGrants { found, max },
E::EmptyScopes { role } => Self::EmptyGrantScopes { role },
E::TooManyScopes { role, found, max } => Self::TooManyGrantScopes { role, found, max },
E::Canonical(error) => Self::Canonical(error),
}
}
}
pub fn prepare_delegated_token(
input: PrepareDelegatedTokenInput<'_>,
) -> Result<PreparedDelegatedToken, PrepareDelegatedTokenError> {
use PrepareDelegatedTokenError as Error;
let cert = &input.proof.cert;
let deadline = cert.expires_at_ns.min(input.authority_expires_at_ns);
if input.now_ns < cert.not_before_ns {
return Err(Error::CertNotYetValid);
}
if input.now_ns >= deadline {
return Err(Error::CertExpired);
}
if input.ttl_ns == 0 {
return Err(Error::TokenTtlZero);
}
if input.ttl_ns > cert.max_token_ttl_ns {
return Err(Error::TokenTtlExceeded {
ttl_ns: input.ttl_ns,
max_ttl_ns: cert.max_token_ttl_ns,
});
}
let expires_at_ns = input
.now_ns
.checked_add(input.ttl_ns)
.ok_or(Error::TokenExpiresAtOverflow)?;
if expires_at_ns > deadline {
return Err(Error::TokenOutlivesCert);
}
validate_grants(
&input.grants,
input.limits.max_grants,
input.limits.max_scopes_per_grant,
)
.map_err(Error::from)?;
if input.audience != cert.aud {
return Err(Error::AudienceNotSubset);
}
if !input.grants.iter().all(|child| {
cert.grants
.iter()
.find(|parent| parent.target == child.target)
.is_some_and(|parent| {
child
.scopes
.iter()
.all(|scope| parent.scopes.contains(scope))
})
}) {
return Err(Error::GrantsNotSubset);
}
let cert_hash = cert_hash(cert)?;
let nonce = delegated_token_nonce(
input.prepared_by,
input.operation_id,
cert.issuer_pid,
cert_hash,
);
let claims = DelegatedTokenClaims {
presenter: input.prepared_by,
subject: input.prepared_by,
issuer_pid: cert.issuer_pid,
cert_hash,
issued_at_ns: input.now_ns,
expires_at_ns,
aud: input.audience,
grants: input.grants,
nonce,
ext: input.ext,
};
Ok(PreparedDelegatedToken {
claims_hash: claims_hash(&claims)?,
claims,
proof: input.proof.clone(),
})
}
fn delegated_token_nonce(
presenter: Principal,
operation_id: [u8; 32],
issuer: Principal,
cert_hash: [u8; 32],
) -> [u8; 16] {
let mut hash = Sha256::new();
hash.update(b"canic-token-nonce-v1");
hash.update(presenter.as_slice());
hash.update(operation_id);
hash.update(issuer.as_slice());
hash.update(cert_hash);
let digest: [u8; 32] = hash.finalize().into();
let mut nonce = [0; 16];
nonce.copy_from_slice(&digest[..16]);
nonce
}
pub fn finish_delegated_token(
prepared: PreparedDelegatedToken,
issuer_proof: IssuerProof,
) -> DelegatedToken {
DelegatedToken {
claims: prepared.claims,
proof: prepared.proof,
issuer_proof,
}
}