canic-host 0.104.0

Host-side App build, Fleet install, deployment, and release-set library for Canic workspaces
Documentation
use super::super::*;
use super::{finding, refresh_resume_safety};
use crate::subnet_catalog::load_mainnet_subnet_catalog;
use std::{
    path::Path,
    time::{SystemTime, UNIX_EPOCH},
};

const MAINNET_ENVIRONMENT: &str = "ic";
const CLOUD_ENGINE_SUBNET_KIND: &str = "cloud_engine";
const ROOT_AUTH_SUBNET_EVIDENCE_MISSING_CODE: &str = "root_auth_subnet_evidence_missing";
pub(in crate::deployment_truth) const ROOT_AUTH_CLOUD_ENGINE_SUBNET_CODE: &str =
    "root_auth_cloud_engine_subnet";

pub(in crate::deployment_truth) fn apply_root_auth_signer_subnet_check(
    diff: &mut DeploymentDiffV1,
    inventory: &DeploymentInventoryV1,
    environment: &str,
    icp_root: &Path,
) {
    apply_root_auth_signer_subnet_check_with_source(
        diff,
        inventory,
        environment,
        icp_root,
        &LiveSubnetCatalogRootSubnetEvidenceSource,
    );
}

pub(in crate::deployment_truth) fn apply_root_auth_signer_subnet_check_with_source(
    diff: &mut DeploymentDiffV1,
    inventory: &DeploymentInventoryV1,
    environment: &str,
    icp_root: &Path,
    source: &dyn RootSubnetEvidenceSource,
) {
    if environment != MAINNET_ENVIRONMENT {
        return;
    }
    let Some(root) = &inventory.observed_root else {
        return;
    };
    let evidence = match source.root_subnet_evidence(
        environment,
        icp_root,
        &root.observed_canister_id,
    ) {
        Ok(evidence) => evidence,
        Err(err) => {
            diff.hard_failures.push(finding(
                ROOT_AUTH_SUBNET_EVIDENCE_MISSING_CODE,
                format!(
                    "cannot verify root-auth signer subnet kind for {} with the NNS subnet catalog: {err}",
                    root.observed_canister_id
                ),
                SafetySeverityV1::HardFailure,
                Some(root.observed_canister_id.clone()),
            ));
            refresh_resume_safety(diff);
            return;
        }
    };
    if evidence.subnet_kind == CLOUD_ENGINE_SUBNET_KIND {
        diff.hard_failures.push(finding(
            ROOT_AUTH_CLOUD_ENGINE_SUBNET_CODE,
            format!(
                "root canister {} resolves to cloud_engine subnet {}; Canic root-auth policy does not allow root signing canisters on cloud_engine subnets",
                root.observed_canister_id, evidence.subnet_principal
            ),
            SafetySeverityV1::HardFailure,
            Some(root.observed_canister_id.clone()),
        ));
        refresh_resume_safety(diff);
    }
}

///
/// RootSubnetEvidence
///
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::deployment_truth) struct RootSubnetEvidence {
    pub subnet_principal: String,
    pub subnet_kind: String,
}

pub(in crate::deployment_truth) trait RootSubnetEvidenceSource {
    fn root_subnet_evidence(
        &self,
        environment: &str,
        icp_root: &Path,
        canister_id: &str,
    ) -> Result<RootSubnetEvidence, String>;
}

///
/// LiveSubnetCatalogRootSubnetEvidenceSource
///
struct LiveSubnetCatalogRootSubnetEvidenceSource;

impl RootSubnetEvidenceSource for LiveSubnetCatalogRootSubnetEvidenceSource {
    fn root_subnet_evidence(
        &self,
        environment: &str,
        icp_root: &Path,
        canister_id: &str,
    ) -> Result<RootSubnetEvidence, String> {
        if environment != MAINNET_ENVIRONMENT {
            return Err(format!(
                "Subnet Catalog evidence is supported only for {MAINNET_ENVIRONMENT}, not {environment}"
            ));
        }
        let cached = load_mainnet_subnet_catalog(icp_root, now_unix_secs()?)
            .map_err(|err| err.to_string())?;
        let resolved = cached
            .catalog
            .resolve_canister_route(canister_id)
            .map_err(|err| err.to_string())?;
        let subnet_principal = resolved.subnet.to_text();
        let subnet = resolved.subnet_info;

        Ok(RootSubnetEvidence {
            subnet_principal,
            subnet_kind: subnet.subnet_kind.as_str().to_string(),
        })
    }
}

fn now_unix_secs() -> Result<u64, String> {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|err| format!("system clock is before unix epoch: {err}"))
        .map(|duration| duration.as_secs())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::subnet_catalog::mainnet_subnet_catalog_cache_root;
    use ic_query::subnet_catalog::{
        ClassificationSource, DEFAULT_SUBNET_CATALOG_SOURCE_ENDPOINT, GeographicScope,
        RawSubnetCatalog, RoutingRange, SubnetInfo, SubnetKind, SubnetSpecialization,
        UncertifiedCatalogCollection, catalog_to_pretty_json, subnet_catalog_path,
    };
    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;
    use std::{fs, path::PathBuf};

    const SUBNET: &str = "rwlgt-iiaaa-aaaaa-aaaaa-cai";
    const CANISTER: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai";

    #[test]
    fn subnet_catalog_source_resolves_cached_canister_without_icq_process() {
        let root = temp_root("root-subnet-catalog-source");
        let cache_root = mainnet_subnet_catalog_cache_root(&root);
        let path = subnet_catalog_path(&cache_root, MAINNET_ENVIRONMENT);
        create_private_catalog_parent(&cache_root, &path);
        fs::write(
            &path,
            catalog_to_pretty_json(&fixture_catalog()).expect("catalog serializes"),
        )
        .expect("write catalog");
        #[cfg(unix)]
        fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("secure catalog file");

        let evidence = LiveSubnetCatalogRootSubnetEvidenceSource
            .root_subnet_evidence(MAINNET_ENVIRONMENT, &root, CANISTER)
            .expect("resolve cached canister");

        let _ = fs::remove_dir_all(root);
        assert_eq!(evidence.subnet_principal, SUBNET);
        assert_eq!(evidence.subnet_kind, CLOUD_ENGINE_SUBNET_KIND);
    }

    fn temp_root(name: &str) -> PathBuf {
        let mut path = std::env::temp_dir();
        path.push(format!("canic-{name}-{}", std::process::id()));
        let _ = fs::remove_dir_all(&path);
        path
    }

    fn create_private_catalog_parent(cache_root: &Path, path: &Path) {
        let parent = path.parent().expect("catalog has parent");
        fs::create_dir_all(parent).expect("create catalog parent");
        #[cfg(unix)]
        {
            let mut directory = cache_root.to_path_buf();
            fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))
                .expect("secure catalog root");
            for component in parent.strip_prefix(cache_root).expect("catalog under root") {
                directory.push(component);
                fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))
                    .expect("secure catalog directory");
            }
        }
    }

    fn fixture_catalog() -> RawSubnetCatalog {
        RawSubnetCatalog::new_mainnet_uncertified(
            UncertifiedCatalogCollection::new(
                123_456,
                DEFAULT_SUBNET_CATALOG_SOURCE_ENDPOINT,
                "2026-06-26T00:00:00Z",
                "fixture",
                "0.29.3",
                1,
            ),
            vec![SubnetInfo {
                subnet_principal: SUBNET.to_string(),
                registry_subnet_type: 5,
                subnet_kind: SubnetKind::CloudEngine,
                subnet_kind_source: ClassificationSource::Registry,
                subnet_specialization: SubnetSpecialization::None,
                subnet_specialization_source: ClassificationSource::Computed,
                geographic_scope: GeographicScope::Global,
                geographic_scope_source: ClassificationSource::Computed,
                subnet_label: "cloud_engine".to_string(),
                subnet_label_source: ClassificationSource::Computed,
                node_count: Some(13),
                charges_apply_by_default: true,
            }],
            vec![RoutingRange {
                start_canister_id: CANISTER.to_string(),
                end_canister_id: CANISTER.to_string(),
                subnet_principal: SUBNET.to_string(),
            }],
        )
        .expect("build fixture catalog")
    }
}