canic-core 0.56.0

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use super::canonical::{CanonicalAuthError, role_hash};
use crate::{cdk::types::Principal, dto::auth::DelegationAudience, ids::CanisterRole};
use thiserror::Error;

#[derive(Debug, Eq, Error, PartialEq)]
pub enum AudienceError {
    #[error("delegated auth role hash mismatch")]
    RoleHashMismatch,
    #[error("delegated auth principal audience is anonymous principal")]
    AnonymousPrincipal,
    #[error(transparent)]
    Canonical(#[from] CanonicalAuthError),
}

pub fn validate_audience_shape(audience: &DelegationAudience) -> Result<(), AudienceError> {
    match audience {
        DelegationAudience::Role(role) => {
            role_hash(role)?;
            Ok(())
        }
        DelegationAudience::Principal(principal) => validate_principal(*principal),
    }
}

pub fn expected_role_hash_for_cert_audience(
    audience: &DelegationAudience,
) -> Result<Option<[u8; 32]>, AudienceError> {
    validate_audience_shape(audience)?;

    match audience {
        DelegationAudience::Principal(_) => Ok(None),
        DelegationAudience::Role(role) => Ok(Some(role_hash(role)?)),
    }
}

pub fn validate_cert_role_hash(
    audience: &DelegationAudience,
    verifier_role_hash: Option<[u8; 32]>,
) -> Result<(), AudienceError> {
    let expected = expected_role_hash_for_cert_audience(audience)?;
    if verifier_role_hash != expected {
        return Err(AudienceError::RoleHashMismatch);
    }
    Ok(())
}

pub const fn audience_uses_role(audience: &DelegationAudience) -> bool {
    matches!(audience, DelegationAudience::Role(_))
}

pub fn verifier_is_in_audience(
    local_principal: Principal,
    local_role: Option<&CanisterRole>,
    audience: &DelegationAudience,
) -> bool {
    match audience {
        DelegationAudience::Role(role) => local_role.is_some_and(|local| local == role),
        DelegationAudience::Principal(principal) => local_principal == *principal,
    }
}

pub fn audience_subset(child: &DelegationAudience, parent: &DelegationAudience) -> bool {
    match (child, parent) {
        (DelegationAudience::Role(child), DelegationAudience::Role(parent)) => child == parent,
        (DelegationAudience::Principal(child), DelegationAudience::Principal(parent)) => {
            child == parent
        }
        (DelegationAudience::Role(_), DelegationAudience::Principal(_))
        | (DelegationAudience::Principal(_), DelegationAudience::Role(_)) => false,
    }
}

fn validate_principal(principal: Principal) -> Result<(), AudienceError> {
    if principal == Principal::anonymous() {
        return Err(AudienceError::AnonymousPrincipal);
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn p(id: u8) -> Principal {
        Principal::from_slice(&[id; 29])
    }

    #[test]
    fn matching_roles_are_audience_subset() {
        let cert = DelegationAudience::Role(CanisterRole::new("project_instance"));
        let claims = DelegationAudience::Role(CanisterRole::new("project_instance"));

        assert!(audience_subset(&claims, &cert));
    }

    #[test]
    fn role_and_principal_do_not_cross_match() {
        let cert = DelegationAudience::Role(CanisterRole::new("project_instance"));
        let claims = DelegationAudience::Principal(p(1));

        assert!(!audience_subset(&claims, &cert));
    }

    #[test]
    fn different_roles_are_not_audience_subset() {
        let cert = DelegationAudience::Role(CanisterRole::new("project_instance"));
        let claims = DelegationAudience::Role(CanisterRole::new("project_hub"));

        assert!(!audience_subset(&claims, &cert));
    }

    #[test]
    fn verifier_membership_accepts_matching_role() {
        let audience = DelegationAudience::Role(CanisterRole::new("project_instance"));

        assert!(verifier_is_in_audience(
            p(9),
            Some(&CanisterRole::new("project_instance")),
            &audience
        ));
        assert!(!verifier_is_in_audience(
            p(9),
            Some(&CanisterRole::new("project_hub")),
            &audience
        ));
    }

    #[test]
    fn cert_role_hash_requires_exact_single_role_hash() {
        let role = CanisterRole::new("project_instance");
        let audience = DelegationAudience::Role(role.clone());
        let expected = role_hash(&role).unwrap();

        validate_cert_role_hash(&audience, Some(expected)).unwrap();
        assert_eq!(
            validate_cert_role_hash(&audience, None),
            Err(AudienceError::RoleHashMismatch)
        );
    }

    #[test]
    fn principal_only_cert_requires_absent_role_hash() {
        let audience = DelegationAudience::Principal(p(1));

        validate_cert_role_hash(&audience, None).unwrap();
        assert_eq!(
            validate_cert_role_hash(&audience, Some([1; 32])),
            Err(AudienceError::RoleHashMismatch)
        );
    }

    #[test]
    fn audience_shape_rejects_anonymous_principal_audience() {
        assert_eq!(
            validate_audience_shape(&DelegationAudience::Principal(Principal::anonymous())),
            Err(AudienceError::AnonymousPrincipal)
        );
    }
}