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);
}
}
#[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>;
}
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")
}
}