canic-core 0.29.2

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use super::canonical::{public_key_hash, root_key_certificate_payload_hash};
use crate::{
    cdk::types::Principal,
    dto::auth::{RootKeyCertificateV2, RootPublicKeyV2, RootTrustAnchorV2, SignatureAlgorithmV2},
};
use thiserror::Error;

#[derive(Debug, Eq, Error, PartialEq)]
pub enum RootKeyResolutionV2Error {
    #[error("delegated auth v2 root pid mismatch (expected {expected}, found {found})")]
    RootPidMismatch {
        expected: Principal,
        found: Principal,
    },
    #[error("delegated auth v2 root key is unknown")]
    UnknownRootKey,
    #[error("delegated auth v2 root key hash mismatch")]
    RootKeyHashMismatch,
    #[error("delegated auth v2 root key certificate mismatch")]
    RootKeyCertificateMismatch,
    #[error("delegated auth v2 root key authority hash mismatch")]
    RootKeyAuthorityHashMismatch,
    #[error("delegated auth v2 root key signature is invalid: {0}")]
    RootKeyCertificateSignatureInvalid(String),
    #[error("delegated auth v2 root key not valid yet (not_before {not_before}, now {now_secs})")]
    RootKeyNotYetValid { not_before: u64, now_secs: u64 },
    #[error("delegated auth v2 root key expired at {not_after} (now {now_secs})")]
    RootKeyExpired { not_after: u64, now_secs: u64 },
}

pub struct RootKeyResolveRequestV2<'a> {
    pub root_pid: Principal,
    pub key_id: &'a str,
    pub key_hash: [u8; 32],
    pub alg: SignatureAlgorithmV2,
    pub embedded_key: Option<&'a [u8]>,
    pub embedded_key_cert: Option<&'a RootKeyCertificateV2>,
    pub now_secs: u64,
}

pub fn resolve_root_key<F>(
    trust: &RootTrustAnchorV2,
    req: RootKeyResolveRequestV2<'_>,
    verify_authority_sig: F,
) -> Result<RootPublicKeyV2, RootKeyResolutionV2Error>
where
    F: FnOnce(&[u8], [u8; 32], &[u8], SignatureAlgorithmV2) -> Result<(), String>,
{
    if req.root_pid != trust.root_pid {
        return Err(RootKeyResolutionV2Error::RootPidMismatch {
            expected: trust.root_pid,
            found: req.root_pid,
        });
    }

    if let Some(key) = find_trusted_key(trust, &req) {
        validate_key_window(key.not_before, key.not_after, req.now_secs)?;
        return Ok(key.clone());
    }

    let embedded_key = req
        .embedded_key
        .ok_or(RootKeyResolutionV2Error::UnknownRootKey)?;
    if public_key_hash(embedded_key) != req.key_hash {
        return Err(RootKeyResolutionV2Error::RootKeyHashMismatch);
    }

    let key_cert = req
        .embedded_key_cert
        .ok_or(RootKeyResolutionV2Error::UnknownRootKey)?;
    let authority = trust
        .key_authority
        .as_ref()
        .ok_or(RootKeyResolutionV2Error::UnknownRootKey)?;

    if key_cert.root_pid != req.root_pid
        || key_cert.key_id != req.key_id
        || key_cert.alg != req.alg
        || key_cert.public_key_sec1.as_slice() != embedded_key
        || key_cert.key_hash != req.key_hash
    {
        return Err(RootKeyResolutionV2Error::RootKeyCertificateMismatch);
    }

    if public_key_hash(&authority.authority_public_key_sec1) != authority.authority_key_hash {
        return Err(RootKeyResolutionV2Error::RootKeyAuthorityHashMismatch);
    }

    validate_key_window(key_cert.not_before, key_cert.not_after, req.now_secs)?;

    verify_authority_sig(
        &authority.authority_public_key_sec1,
        root_key_certificate_payload_hash(key_cert),
        &key_cert.authority_sig,
        authority.authority_alg,
    )
    .map_err(RootKeyResolutionV2Error::RootKeyCertificateSignatureInvalid)?;

    Ok(RootPublicKeyV2 {
        root_pid: req.root_pid,
        key_id: req.key_id.to_string(),
        alg: req.alg,
        public_key_sec1: embedded_key.to_vec(),
        key_hash: req.key_hash,
        not_before: key_cert.not_before,
        not_after: key_cert.not_after,
    })
}

fn find_trusted_key<'a>(
    trust: &'a RootTrustAnchorV2,
    req: &RootKeyResolveRequestV2<'_>,
) -> Option<&'a RootPublicKeyV2> {
    trust.trusted_root_keys.keys.iter().find(|key| {
        key.root_pid == req.root_pid
            && key.key_id == req.key_id
            && key.key_hash == req.key_hash
            && key.alg == req.alg
    })
}

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

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

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dto::auth::{RootKeyAuthorityV2, RootKeySetV2};

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

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

    fn authority() -> RootKeyAuthorityV2 {
        let authority_public_key_sec1 = vec![9, 8, 7];
        RootKeyAuthorityV2 {
            authority_key_id: "authority-key".to_string(),
            authority_alg: SignatureAlgorithmV2::EcdsaP256Sha256,
            authority_key_hash: public_key_hash(&authority_public_key_sec1),
            authority_public_key_sec1,
        }
    }

    fn trust_with_key(key: RootPublicKeyV2) -> RootTrustAnchorV2 {
        RootTrustAnchorV2 {
            root_pid: p(1),
            trusted_root_keys: RootKeySetV2 { keys: vec![key] },
            key_authority: Some(authority()),
        }
    }

    fn trust_without_key() -> RootTrustAnchorV2 {
        RootTrustAnchorV2 {
            root_pid: p(1),
            trusted_root_keys: RootKeySetV2 { keys: vec![] },
            key_authority: Some(authority()),
        }
    }

    fn key_cert(key: &RootPublicKeyV2) -> RootKeyCertificateV2 {
        RootKeyCertificateV2 {
            root_pid: key.root_pid,
            key_id: key.key_id.clone(),
            alg: key.alg,
            public_key_sec1: key.public_key_sec1.clone(),
            key_hash: key.key_hash,
            not_before: key.not_before,
            not_after: key.not_after,
            authority_sig: vec![1, 2, 3],
        }
    }

    fn req<'a>(
        key: &'a RootPublicKeyV2,
        root_pid: Principal,
        embedded_key: Option<&'a [u8]>,
        embedded_key_cert: Option<&'a RootKeyCertificateV2>,
        now_secs: u64,
    ) -> RootKeyResolveRequestV2<'a> {
        RootKeyResolveRequestV2 {
            root_pid,
            key_id: &key.key_id,
            key_hash: key.key_hash,
            alg: key.alg,
            embedded_key,
            embedded_key_cert,
            now_secs,
        }
    }

    #[test]
    fn resolve_root_key_uses_trusted_local_key_without_fallback() {
        let key = trusted_key();
        let resolved = resolve_root_key(
            &trust_with_key(key.clone()),
            req(&key, p(1), None, None, 150),
            |_, _, _, _| Err("must not verify fallback".to_string()),
        )
        .unwrap();

        assert_eq!(resolved, key);
    }

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

        assert_eq!(
            resolve_root_key(
                &trust_without_key(),
                req(&key, p(1), Some(&key.public_key_sec1), None, 150),
                |_, _, _, _| Ok(()),
            ),
            Err(RootKeyResolutionV2Error::UnknownRootKey)
        );
    }

    #[test]
    fn resolve_root_key_accepts_unknown_key_with_authority_cert() {
        let key = trusted_key();
        let cert = key_cert(&key);
        let expected_hash = root_key_certificate_payload_hash(&cert);

        let resolved = resolve_root_key(
            &trust_without_key(),
            req(&key, p(1), Some(&key.public_key_sec1), Some(&cert), 150),
            |authority_key, hash, sig, alg| {
                assert_eq!(
                    authority_key,
                    authority().authority_public_key_sec1.as_slice()
                );
                assert_eq!(hash, expected_hash);
                assert_eq!(sig, [1, 2, 3]);
                assert_eq!(alg, SignatureAlgorithmV2::EcdsaP256Sha256);
                Ok(())
            },
        )
        .unwrap();

        assert_eq!(resolved, key);
    }

    #[test]
    fn resolve_root_key_rejects_self_signed_unknown_key() {
        let key = trusted_key();
        let mut cert = key_cert(&key);
        cert.authority_sig = vec![];

        assert_eq!(
            resolve_root_key(
                &trust_without_key(),
                req(&key, p(1), Some(&key.public_key_sec1), Some(&cert), 150),
                |_, _, _, _| Err("bad signature".to_string()),
            ),
            Err(
                RootKeyResolutionV2Error::RootKeyCertificateSignatureInvalid(
                    "bad signature".to_string(),
                ),
            )
        );
    }

    #[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), None, None, 150),
                |_, _, _, _| Ok(()),
            ),
            Err(RootKeyResolutionV2Error::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), None, None, 99),
                |_, _, _, _| Ok(()),
            ),
            Err(RootKeyResolutionV2Error::RootKeyNotYetValid {
                not_before: 100,
                now_secs: 99,
            })
        );

        assert_eq!(
            resolve_root_key(
                &trust_with_key(key.clone()),
                req(&key, p(1), None, None, 300),
                |_, _, _, _| Ok(()),
            ),
            Err(RootKeyResolutionV2Error::RootKeyExpired {
                not_after: 300,
                now_secs: 300,
            })
        );
    }

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

        assert_eq!(
            resolve_root_key(
                &trust_without_key(),
                req(&key, p(1), Some(&[0, 1, 2]), Some(&key_cert(&key)), 150),
                |_, _, _, _| Ok(()),
            ),
            Err(RootKeyResolutionV2Error::RootKeyHashMismatch)
        );
    }
}