mod root;
pub(crate) mod rules;
use crate::canonical::{CanonicalAuthError, cert_hash};
use ic_auth_protocol_types::{
ChainKeyAlgorithm, ChainKeyKeyId, DelegationCert, DelegationProof, 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(Debug, Error, Eq, PartialEq)]
pub enum DelegationProofError {
#[error("delegation proof material exceeds host limits")]
InputTooLarge,
#[error("anonymous {field} is not permitted")]
AnonymousPrincipal { field: &'static str },
#[error("proof binding mismatch: {field}")]
BindingMismatch { field: &'static str },
#[error("root signature binding mismatch: {field}")]
RootSignatureBindingMismatch { field: &'static str },
#[error("root key policy binding mismatch: {field}")]
RootKeyPolicyBindingMismatch { 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("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),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DelegationProofVerificationLimits {
pub max_cert_ttl_ns: u64,
pub max_token_ttl_ns: u64,
pub max_future_skew_ns: u64,
pub max_variable_bytes: usize,
pub max_witness_steps: usize,
}
pub struct DelegationProofVerificationContext<'a> {
pub expected_issuer: Principal,
pub root_key: &'a RootKeyPolicy,
pub now_ns: u64,
pub limits: DelegationProofVerificationLimits,
}
#[derive(Clone, Debug)]
pub struct VerifiedDelegationProof<'a> {
certificate: &'a DelegationCert,
certificate_hash: [u8; 32],
expires_at_ns: u64,
}
impl<'a> VerifiedDelegationProof<'a> {
pub fn certificate(&self) -> &'a DelegationCert {
self.certificate
}
pub const fn certificate_hash(&self) -> [u8; 32] {
self.certificate_hash
}
pub const fn expires_at_ns(&self) -> u64 {
self.expires_at_ns
}
}
pub fn verify_delegation_proof<'a>(
proof: &'a DelegationProof,
context: &DelegationProofVerificationContext<'_>,
) -> Result<VerifiedDelegationProof<'a>, DelegationProofError> {
let mut remaining = context.limits.max_variable_bytes;
rules::check_proof_size(proof, context.limits.max_witness_steps, &mut remaining)?;
let cert = &proof.cert;
for (field, principal) in [("issuer", cert.issuer_pid), ("root", cert.root_pid)] {
if principal == Principal::anonymous() {
return Err(DelegationProofError::AnonymousPrincipal { field });
}
}
binding(
cert.root_pid == context.root_key.root_canister_id,
"root_canister_id",
)?;
binding(
cert.issuer_pid == context.expected_issuer,
"issuer_canister_id",
)?;
rules::verify_certificate_window(cert, context.now_ns, context.limits)?;
rules::verify_certificate_binding(cert)?;
let certificate_hash = cert_hash(cert)?;
root::verify(
cert,
&proof.root_proof,
context.root_key,
context.now_ns,
context.limits.max_future_skew_ns,
)?;
Ok(VerifiedDelegationProof {
certificate: cert,
certificate_hash,
expires_at_ns: cert.expires_at_ns.min(context.root_key.accept_until_ns),
})
}
pub(super) fn binding(condition: bool, field: &'static str) -> Result<(), DelegationProofError> {
if condition {
Ok(())
} else {
Err(DelegationProofError::BindingMismatch { field })
}
}
pub(super) fn window(
target: &'static str,
start: u64,
end: u64,
now: u64,
skew: u64,
) -> Result<(), DelegationProofError> {
if start >= end {
return Err(DelegationProofError::InvalidWindow { target });
}
if start > now && start - now > skew {
return Err(DelegationProofError::NotYetValid { target });
}
if now >= end {
return Err(DelegationProofError::Expired { target });
}
Ok(())
}