mod root;
mod rules;
use crate::{
canister_signature::{
CanisterSignatureError, CanisterSignaturePolicy, domain_separated_message,
verify_canister_signature,
},
canonical::CanonicalAuthError,
};
use ic_auth_protocol_types::{
AudienceId, AuthRole, ChainKeyAlgorithm, ChainKeyKeyId, DelegatedToken, DelegatedTokenClaims,
IssuerProof, IssuerProofBinding, Principal,
};
use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RootKeyPolicy {
pub root_canister_id: Principal,
pub algorithm: ChainKeyAlgorithm,
pub key_id: ChainKeyKeyId,
pub derivation_path_hash: [u8; 32],
pub public_key: Vec<u8>,
pub key_version: u64,
pub min_accepted_key_version: u64,
pub min_accepted_proof_epoch: u64,
pub min_accepted_registry_epoch: u64,
pub valid_from_ns: u64,
pub accept_until_ns: u64,
pub max_revocation_latency_ns: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TokenVerificationLimits {
pub max_cert_ttl_ns: u64,
pub max_token_ttl_ns: u64,
pub max_future_skew_ns: u64,
pub max_certificate_age_ns: u64,
pub max_variable_bytes: usize,
pub max_issuer_signature_bytes: usize,
pub max_witness_steps: usize,
}
pub struct TokenVerificationContext<'a> {
pub caller: Principal,
pub audience: AudienceId,
pub role: &'a AuthRole,
pub allowed_scopes: &'a [String],
pub required_scopes: &'a [String],
pub root_key: &'a RootKeyPolicy,
pub ic_root_public_key_raw: &'a [u8],
pub now_ns: u64,
pub limits: TokenVerificationLimits,
}
#[derive(Clone, Debug)]
pub struct VerifiedToken<'a> {
claims: &'a DelegatedTokenClaims,
role: &'a AuthRole,
scopes: &'a [String],
claims_hash: [u8; 32],
expires_at_ns: u64,
}
impl<'a> VerifiedToken<'a> {
pub fn claims(&self) -> &'a DelegatedTokenClaims {
self.claims
}
pub fn role(&self) -> &'a AuthRole {
self.role
}
pub fn scopes(&self) -> &'a [String] {
self.scopes
}
pub const fn claims_hash(&self) -> [u8; 32] {
self.claims_hash
}
pub const fn expires_at_ns(&self) -> u64 {
self.expires_at_ns
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum TokenVerificationError {
#[error("token variable material exceeds host limits")]
InputTooLarge,
#[error("anonymous {field} is not permitted")]
AnonymousPrincipal { field: &'static str },
#[error("presenter must equal the authenticated caller")]
PresenterMismatch,
#[error("presenter and subject must be the same identity")]
SubjectMismatch,
#[error("proof binding mismatch: {field}")]
BindingMismatch { field: &'static str },
#[error("invalid {target} validity window")]
InvalidWindow { target: &'static str },
#[error("{target} is not yet valid")]
NotYetValid { target: &'static str },
#[error("{target} has expired")]
Expired { target: &'static str },
#[error("{target} TTL {ttl_ns} exceeds {max_ttl_ns}")]
TtlExceeded {
target: &'static str,
ttl_ns: u64,
max_ttl_ns: u64,
},
#[error("grants must be nonempty, with at most 16 roles and 32 scopes per role")]
InvalidGrants,
#[error("audience does not match protected local context")]
AudienceRejected,
#[error("token grants exceed the root certificate grants")]
GrantsNotSubset,
#[error("local role is not granted")]
RoleRejected,
#[error("scope is outside the protected local ceiling or absent from the grant: {scope}")]
ScopeRejected { scope: String },
#[error("root proof signature or public key encoding is invalid")]
RootSignatureInvalid,
#[error("root proof signature has high-s encoding")]
HighSSignature,
#[error("root proof Merkle witness is invalid")]
InvalidMerkleWitness,
#[error("root proof {field} is below the protected epoch/version floor")]
StaleAuthority { field: &'static str },
#[error(transparent)]
Canonical(#[from] CanonicalAuthError),
#[error(transparent)]
IssuerSignature(#[from] CanisterSignatureError),
}
pub fn verify_token<'a>(
token: &'a DelegatedToken,
context: &TokenVerificationContext<'_>,
) -> Result<VerifiedToken<'a>, TokenVerificationError> {
rules::check_size(token, context.limits)?;
let (grant, claims_hash) = rules::verify_material(token, context)?;
root::verify(&token.proof.cert, &token.proof.root_proof, context)?;
let cert = &token.proof.cert;
let IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } = cert.issuer_proof_binding;
let IssuerProof::IcCanisterSignatureV1(proof) = &token.issuer_proof;
let message = domain_separated_message(b"canic-issuer-delegated-token", claims_hash)?;
verify_canister_signature(
&message,
proof,
&CanisterSignaturePolicy {
signing_canister: cert.issuer_pid,
seed_hash,
ic_root_public_key_raw: context.ic_root_public_key_raw,
now_ns: context.now_ns,
max_certificate_age_ns: context.limits.max_certificate_age_ns,
max_future_skew_ns: context.limits.max_future_skew_ns,
max_signature_bytes: context.limits.max_issuer_signature_bytes,
max_message_bytes: message.len(),
},
)?;
Ok(VerifiedToken {
claims: &token.claims,
role: &grant.target,
scopes: &grant.scopes,
claims_hash,
expires_at_ns: token
.claims
.expires_at_ns
.min(context.root_key.accept_until_ns),
})
}
pub(super) fn binding(condition: bool, field: &'static str) -> Result<(), TokenVerificationError> {
if condition {
Ok(())
} else {
Err(TokenVerificationError::BindingMismatch { field })
}
}
pub(super) fn window(
target: &'static str,
start: u64,
end: u64,
now: u64,
skew: u64,
) -> Result<(), TokenVerificationError> {
if start >= end {
return Err(TokenVerificationError::InvalidWindow { target });
}
if start > now && start - now > skew {
return Err(TokenVerificationError::NotYetValid { target });
}
if now >= end {
return Err(TokenVerificationError::Expired { target });
}
Ok(())
}