canic-core 0.33.4

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use crate::{
    cdk::types::Principal,
    dto::auth::{RootPublicKey, RootTrustAnchor, SignatureAlgorithm},
};
use thiserror::Error;

#[derive(Debug, Eq, Error, PartialEq)]
pub enum RootKeyResolutionError {
    #[error("delegated auth root pid mismatch (expected {expected}, found {found})")]
    RootPidMismatch {
        expected: Principal,
        found: Principal,
    },
    #[error("delegated auth root key is unknown")]
    UnknownRootKey,
    #[error("delegated auth root key not valid yet (not_before {not_before}, now {now_secs})")]
    RootKeyNotYetValid { not_before: u64, now_secs: u64 },
    #[error("delegated auth root key expired at {not_after} (now {now_secs})")]
    RootKeyExpired { not_after: u64, now_secs: u64 },
}

pub struct RootKeyResolveRequest<'a> {
    pub root_pid: Principal,
    pub key_id: &'a str,
    pub key_hash: [u8; 32],
    pub alg: SignatureAlgorithm,
    pub now_secs: u64,
}

pub fn resolve_root_key(
    trust: &RootTrustAnchor,
    req: RootKeyResolveRequest<'_>,
) -> Result<RootPublicKey, RootKeyResolutionError> {
    if req.root_pid != trust.root_pid {
        return Err(RootKeyResolutionError::RootPidMismatch {
            expected: trust.root_pid,
            found: req.root_pid,
        });
    }

    let key = &trust.root_key;
    if key.root_pid != req.root_pid
        || key.key_id != req.key_id
        || key.key_hash != req.key_hash
        || key.alg != req.alg
    {
        return Err(RootKeyResolutionError::UnknownRootKey);
    }

    validate_key_window(key.not_before, key.not_after, req.now_secs)?;
    Ok(key.clone())
}

const fn validate_key_window(
    not_before: u64,
    not_after: Option<u64>,
    now_secs: u64,
) -> Result<(), RootKeyResolutionError> {
    if now_secs < not_before {
        return Err(RootKeyResolutionError::RootKeyNotYetValid {
            not_before,
            now_secs,
        });
    }

    if let Some(not_after) = not_after
        && now_secs >= not_after
    {
        return Err(RootKeyResolutionError::RootKeyExpired {
            not_after,
            now_secs,
        });
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ops::auth::delegated::canonical::public_key_hash;

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

    fn trusted_key() -> RootPublicKey {
        let public_key_sec1 = vec![2, 3, 4];
        RootPublicKey {
            root_pid: p(1),
            key_id: "root-key".to_string(),
            alg: SignatureAlgorithm::EcdsaP256Sha256,
            key_hash: public_key_hash(&public_key_sec1),
            public_key_sec1,
            not_before: 100,
            not_after: Some(300),
        }
    }

    fn trust_with_key(key: RootPublicKey) -> RootTrustAnchor {
        RootTrustAnchor {
            root_pid: p(1),
            root_key: key,
        }
    }

    fn req(key: &RootPublicKey, root_pid: Principal, now_secs: u64) -> RootKeyResolveRequest<'_> {
        RootKeyResolveRequest {
            root_pid,
            key_id: &key.key_id,
            key_hash: key.key_hash,
            alg: key.alg,
            now_secs,
        }
    }

    #[test]
    fn resolve_root_key_uses_explicit_trusted_root_key() {
        let key = trusted_key();
        let resolved =
            resolve_root_key(&trust_with_key(key.clone()), req(&key, p(1), 150)).unwrap();

        assert_eq!(resolved, key);
    }

    #[test]
    fn resolve_root_key_rejects_unknown_key() {
        let key = trusted_key();
        let mut other = key.clone();
        other.key_id = "other-root-key".to_string();

        assert_eq!(
            resolve_root_key(&trust_with_key(other), req(&key, p(1), 150)),
            Err(RootKeyResolutionError::UnknownRootKey)
        );
    }

    #[test]
    fn resolve_root_key_enforces_root_pid_binding_before_key_lookup() {
        let key = trusted_key();

        assert_eq!(
            resolve_root_key(&trust_with_key(key.clone()), req(&key, p(9), 150)),
            Err(RootKeyResolutionError::RootPidMismatch {
                expected: p(1),
                found: p(9),
            })
        );
    }

    #[test]
    fn resolve_root_key_enforces_key_validity_window() {
        let key = trusted_key();

        assert_eq!(
            resolve_root_key(&trust_with_key(key.clone()), req(&key, p(1), 99)),
            Err(RootKeyResolutionError::RootKeyNotYetValid {
                not_before: 100,
                now_secs: 99,
            })
        );

        assert_eq!(
            resolve_root_key(&trust_with_key(key.clone()), req(&key, p(1), 300)),
            Err(RootKeyResolutionError::RootKeyExpired {
                not_after: 300,
                now_secs: 300,
            })
        );
    }
}