canic-host 0.99.4

Host-side App build, Fleet install, deployment, and release-set library for Canic workspaces
Documentation
use super::*;
use crate::test_support::temp_dir;
use std::{fmt::Write as _, fs};

fn fixture(name: &str) -> (PathBuf, PathBuf, Vec<u8>, String) {
    let root = temp_dir(&format!("canic-network-{name}"));
    fs::create_dir_all(&root).expect("create project root");
    let mut root_key = vec![
        0x30, 0x81, 0x82, 0x30, 0x1d, 0x06, 0x0d, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xdc, 0x7c,
        0x05, 0x03, 0x01, 0x02, 0x01, 0x06, 0x0c, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xdc, 0x7c,
        0x05, 0x03, 0x02, 0x01, 0x03, 0x61, 0x00,
    ];
    root_key.extend_from_slice(&[9; 96]);
    let root_key_path = root.join("root-key.der");
    fs::write(&root_key_path, &root_key).expect("write root key");
    let fingerprint = encode_digest(sha256_digest(&root_key));
    (root, root_key_path, root_key, fingerprint)
}

fn enroll<'a>(
    root: &'a Path,
    environment: &'a str,
    root_key: &'a Path,
    fingerprint: &'a str,
) -> NetworkEnrollmentOptions<'a> {
    NetworkEnrollmentOptions {
        project_root: root,
        environment,
        root_key,
        fingerprint,
    }
}

fn write_named_local_environment(root: &Path, names: &[&str]) {
    let environments = names.iter().fold(String::new(), |mut output, name| {
        writeln!(output, "  - name: {name}\n    network: shared")
            .expect("writing to a String cannot fail");
        output
    });
    fs::write(
        root.join("icp.yaml"),
        format!(
            "networks:\n  - name: shared\n    mode: managed\n    gateway:\n      port: 8000\nenvironments:\n{environments}"
        ),
    )
    .expect("write icp.yaml");
}

#[test]
fn enrollment_publishes_authority_before_profile_and_resolves_it() {
    let (root, root_key_path, root_key, fingerprint) = fixture("success");

    let report = enroll_network(enroll(&root, "local", &root_key_path, &fingerprint))
        .expect("enroll network");

    assert!(report.created_profile);
    assert_eq!(
        fs::read(report.authority_directory.join(ROOT_KEY_RELATIVE_PATH))
            .expect("read enrolled root key"),
        root_key
    );
    let profile = fs::read_to_string(&report.profile_path).expect("read profile");
    assert_eq!(
        profile,
        format!(
            "{{\n  \"canonical_network_id\": \"{}\"\n}}\n",
            report.canonical_network_id
        )
    );
    assert_eq!(
        resolve_canonical_network_id_from_root(&root, "local").expect("resolve network"),
        report.canonical_network_id
    );

    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn exact_reenrollment_is_idempotent() {
    let (root, root_key_path, _, fingerprint) = fixture("idempotent");
    let options = enroll(&root, "local", &root_key_path, &fingerprint);

    let first = enroll_network(options).expect("first enrollment");
    let second = enroll_network(options).expect("repeat enrollment");

    assert!(first.created_profile);
    assert!(!second.created_profile);
    assert_eq!(first.canonical_network_id, second.canonical_network_id);
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn enrollment_resumes_after_authority_root_was_published() {
    let (root, root_key_path, _, fingerprint) = fixture("resume");
    let first = enroll_network(enroll(&root, "local", &root_key_path, &fingerprint))
        .expect("initial enrollment");
    fs::remove_file(first.authority_directory.join(ENROLLMENT_FILE))
        .expect("remove enrollment record");
    fs::remove_file(&first.profile_path).expect("remove profile");

    let resumed = enroll_network(enroll(&root, "local", &root_key_path, &fingerprint))
        .expect("resume enrollment");

    assert!(resumed.created_profile);
    assert_eq!(resumed.canonical_network_id, first.canonical_network_id);
    assert_eq!(
        resolve_canonical_network_id_from_root(&root, "local").expect("resolve resumed network"),
        first.canonical_network_id
    );
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn two_environment_aliases_share_one_network_authority() {
    let (root, root_key_path, _, fingerprint) = fixture("aliases");
    write_named_local_environment(&root, &["dev", "qa"]);

    let dev =
        enroll_network(enroll(&root, "dev", &root_key_path, &fingerprint)).expect("enroll dev");
    let qa = enroll_network(enroll(&root, "qa", &root_key_path, &fingerprint)).expect("enroll qa");

    assert_eq!(dev.canonical_network_id, qa.canonical_network_id);
    assert_eq!(dev.authority_directory, qa.authority_directory);
    assert_eq!(
        resolve_canonical_network_id_from_root(&root, "qa").expect("resolve qa"),
        dev.canonical_network_id
    );
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn fingerprint_mismatch_writes_nothing() {
    let (root, root_key_path, _, _) = fixture("mismatch");

    let error = enroll_network(enroll(&root, "local", &root_key_path, &"00".repeat(32)))
        .expect_err("mismatched fingerprint must reject");

    std::assert_matches!(error, NetworkIdentityError::FingerprintMismatch { .. });
    assert!(!root.join(CANIC_STATE_DIRECTORY).exists());
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn noncanonical_fingerprint_writes_nothing() {
    let (root, root_key_path, _, fingerprint) = fixture("noncanonical-fingerprint");

    let error = enroll_network(enroll(
        &root,
        "local",
        &root_key_path,
        &fingerprint.to_uppercase(),
    ))
    .expect_err("uppercase fingerprint must reject");

    std::assert_matches!(error, NetworkIdentityError::InvalidFingerprint);
    assert!(!root.join(CANIC_STATE_DIRECTORY).exists());
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn changed_anchor_cannot_rebind_an_existing_profile() {
    let (root, root_key_path, mut root_key, fingerprint) = fixture("rotation");
    let first = enroll_network(enroll(&root, "local", &root_key_path, &fingerprint))
        .expect("enroll first anchor");
    *root_key.last_mut().expect("root key byte") ^= 1;
    let changed_path = root.join("changed-root-key.der");
    fs::write(&changed_path, &root_key).expect("write changed root key");
    let changed_fingerprint = encode_digest(sha256_digest(&root_key));

    let error = enroll_network(enroll(&root, "local", &changed_path, &changed_fingerprint))
        .expect_err("profile rebinding must reject");

    std::assert_matches!(
        error,
        NetworkIdentityError::ProfileConflict { existing, .. }
            if existing == first.canonical_network_id
    );
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn resolver_rejects_a_profile_without_complete_authority() {
    let (root, _, _, _) = fixture("incomplete");
    let identity = "03".repeat(32).parse().expect("canonical network ID");
    let profile_path = environment_profile_path(&root, "local");
    fs::create_dir_all(profile_path.parent().expect("profile parent"))
        .expect("create profile parent");
    fs::write(
        &profile_path,
        serde_json::to_vec(&EnvironmentNetworkProfile {
            canonical_network_id: identity,
        })
        .expect("encode profile"),
    )
    .expect("write profile");

    let error = resolve_canonical_network_id_from_root(&root, "local")
        .expect_err("incomplete authority must reject");

    std::assert_matches!(error, NetworkIdentityError::MissingAuthority { .. });
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn resolver_rejects_a_digest_that_disagrees_with_the_exact_anchor() {
    let (root, root_key_path, _, fingerprint) = fixture("digest-conflict");
    let report = enroll_network(enroll(&root, "local", &root_key_path, &fingerprint))
        .expect("enroll network");
    let enrollment_path = report.authority_directory.join(ENROLLMENT_FILE);
    let enrollment_bytes = fs::read(&enrollment_path).expect("read enrollment");
    let mut enrollment = serde_json::from_slice::<NetworkEnrollmentRecord>(&enrollment_bytes)
        .expect("decode enrollment");
    enrollment.root_key_digest = [0; 32];
    fs::write(
        &enrollment_path,
        serde_json::to_vec_pretty(&enrollment).expect("encode changed enrollment"),
    )
    .expect("replace enrollment");

    let error = resolve_canonical_network_id_from_root(&root, "local")
        .expect_err("contradictory digest must reject");

    std::assert_matches!(error, NetworkIdentityError::ContradictoryAuthority { .. });
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn public_ic_environment_aliases_resolve_the_compiled_identity() {
    let (root, _, _, _) = fixture("public");
    fs::write(
        root.join("icp.yaml"),
        "environments:\n  - name: staging\n    network: ic\n  - name: production\n    network: ic\n",
    )
    .expect("write public aliases");

    assert_eq!(
        resolve_canonical_network_id_from_root(&root, "ic").expect("resolve ic"),
        resolve_canonical_network_id_from_root(&root, "staging").expect("resolve staging")
    );
    assert_eq!(
        resolve_canonical_network_id_from_root(&root, "staging").expect("resolve staging"),
        resolve_canonical_network_id_from_root(&root, "production").expect("resolve production")
    );
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn public_ic_trust_anchor_cannot_be_enrolled() {
    let (root, root_key_path, _, fingerprint) = fixture("public-enrollment");

    let error = enroll_network(enroll(&root, "ic", &root_key_path, &fingerprint))
        .expect_err("public IC enrollment must reject");

    std::assert_matches!(error, NetworkIdentityError::PublicIcEnrollment { .. });
    assert!(!root.join(CANIC_STATE_DIRECTORY).exists());
    fs::remove_dir_all(root).expect("remove fixture");
}

#[cfg(unix)]
#[test]
fn enrollment_rejects_symlinked_root_key_input_without_writing() {
    use std::os::unix::fs::symlink;

    let (root, root_key_path, _, fingerprint) = fixture("symlink-input");
    let symlink_path = root.join("root-key-link.der");
    symlink(&root_key_path, &symlink_path).expect("create symlink");

    let symlink_error = enroll_network(enroll(&root, "local", &symlink_path, &fingerprint))
        .expect_err("symlink input must reject");
    std::assert_matches!(
        symlink_error,
        NetworkIdentityError::RootKeyNotRegular { .. }
    );
    assert!(!root.join(CANIC_STATE_DIRECTORY).exists());
    fs::remove_dir_all(root).expect("remove fixture");
}

#[cfg(unix)]
#[test]
fn enrollment_rejects_fifo_root_key_input_without_writing() {
    use rustix::fs::{CWD, Mode, mkfifoat};

    let (root, _, _, fingerprint) = fixture("fifo-input");
    let fifo_path = root.join("root-key.fifo");
    mkfifoat(CWD, &fifo_path, Mode::from_raw_mode(0o600)).expect("create FIFO");

    let fifo_error = enroll_network(enroll(&root, "local", &fifo_path, &fingerprint))
        .expect_err("special input must reject");
    std::assert_matches!(fifo_error, NetworkIdentityError::RootKeyNotRegular { .. });
    assert!(!root.join(CANIC_STATE_DIRECTORY).exists());
    fs::remove_file(fifo_path).expect("remove FIFO");
    fs::remove_dir_all(root).expect("remove fixture");
}