canic-core 0.26.4

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use crate::{
    cdk::types::Principal,
    dto::{
        capability::{CapabilityService, DelegatedGrant, DelegatedGrantProof},
        error::Error,
        rpc::Request,
    },
    ops::{ic::ecdsa::EcdsaOps, storage::auth::DelegationStateOps},
};
use candid::encode_one;
use sha2::{Digest, Sha256};

pub(super) fn verify_delegated_grant_hash_binding(
    proof: &DelegatedGrantProof,
) -> Result<(), Error> {
    if proof.grant.capability_hash != proof.capability_hash {
        return Err(Error::invalid(
            "delegated grant capability_hash does not match proof capability_hash",
        ));
    }

    Ok(())
}

pub(super) fn verify_root_delegated_grant_proof(
    capability: &Request,
    proof: &DelegatedGrantProof,
    caller: Principal,
    target_canister: Principal,
    now_secs: u64,
) -> Result<(), Error> {
    verify_root_delegated_grant_claims(capability, proof, caller, target_canister, now_secs)?;
    verify_root_delegated_grant_signature(&proof.grant, &proof.grant_sig)
}

pub(super) fn verify_root_delegated_grant_claims(
    capability: &Request,
    proof: &DelegatedGrantProof,
    caller: Principal,
    target_canister: Principal,
    now_secs: u64,
) -> Result<(), Error> {
    if proof.key_id != super::DELEGATED_GRANT_KEY_ID_V1 {
        return Err(Error::invalid(format!(
            "unsupported delegated grant key_id: {}",
            proof.key_id
        )));
    }

    let grant = &proof.grant;
    if grant.issuer != target_canister {
        return Err(Error::forbidden(
            "delegated grant issuer must match target canister",
        ));
    }
    if grant.subject != caller {
        return Err(Error::forbidden(
            "delegated grant subject must match caller",
        ));
    }
    if !grant.audience.contains(&target_canister) {
        return Err(Error::forbidden(
            "delegated grant audience must include target canister",
        ));
    }
    if grant.scope.service != CapabilityService::Root {
        return Err(Error::forbidden(
            "delegated grant scope service must be Root",
        ));
    }
    let expected_family = root_capability_family(capability);
    if grant.scope.capability_family != expected_family {
        return Err(Error::forbidden(format!(
            "delegated grant scope capability_family '{}' does not match '{}'",
            grant.scope.capability_family, expected_family
        )));
    }
    if grant.quota == 0 {
        return Err(Error::invalid(
            "delegated grant quota must be greater than zero",
        ));
    }
    if grant.expires_at <= grant.issued_at {
        return Err(Error::invalid(
            "delegated grant expires_at must be greater than issued_at",
        ));
    }
    if now_secs < grant.issued_at {
        return Err(Error::forbidden(
            "delegated grant is not valid yet for current time",
        ));
    }
    if now_secs > grant.expires_at {
        return Err(Error::forbidden("delegated grant has expired"));
    }

    Ok(())
}

pub(super) fn verify_root_delegated_grant_signature(
    grant: &DelegatedGrant,
    signature: &[u8],
) -> Result<(), Error> {
    if signature.is_empty() {
        return Err(Error::forbidden("delegated grant signature is required"));
    }

    let root_public_key = DelegationStateOps::root_public_key()
        .ok_or_else(|| Error::forbidden("delegated grant root public key unavailable"))?;
    let grant_hash = delegated_grant_hash(grant)?;
    EcdsaOps::verify_signature(&root_public_key, grant_hash, signature)
        .map_err(|err| Error::forbidden(format!("delegated grant signature invalid: {err}")))?;

    Ok(())
}

pub(super) const fn root_capability_family(capability: &Request) -> &'static str {
    capability.family().label()
}

pub(super) fn delegated_grant_hash(grant: &DelegatedGrant) -> Result<[u8; 32], Error> {
    let payload = encode_one(grant)
        .map_err(|err| Error::internal(format!("failed to encode delegated grant: {err}")))?;
    let mut hasher = Sha256::new();
    hasher.update(super::DELEGATED_GRANT_SIGNING_DOMAIN_V1);
    hasher.update(payload);
    Ok(hasher.finalize().into())
}