boxlite 0.10.7

Embeddable virtual machine runtime for secure, isolated code execution
use super::*;
use x509_cert::{Certificate, der::DecodePem};

// Exercise the portable trust installer on macOS as well as Linux.
#[path = "../../../guest/src/ca_trust.rs"]
mod ca_trust;

/// Model a 24-hour CA: expiry +31 days deliberately makes not_before +30 days.
fn short_lived_ca(key: &KeyPair, expires: OffsetDateTime) -> String {
    let mut params = CertificateParams::default();
    params.distinguished_name = DistinguishedName::new();
    params
        .distinguished_name
        .push(DnType::CommonName, "BoxLite MITM CA");
    params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
    params.not_before = expires - Duration::hours(24);
    params.not_after = expires;
    params.self_signed(key).unwrap().pem()
}

/// Check the generated certificate's encoded expiry rather than a configured lifetime.
fn assert_long_lived(pem: &str) {
    let cert = Certificate::from_pem(pem).unwrap();
    let expires = cert
        .tbs_certificate
        .validity
        .not_after
        .to_unix_duration()
        .as_secs();
    assert!(
        expires > (OffsetDateTime::now_utc() + Duration::days(3649)).unix_timestamp() as u64,
        "CA must remain valid for approximately ten years"
    );
}

/// Guard the encoded CA lifetime that guest TLS clients actually validate.
#[test]
fn generated_ca_survives_day_two() {
    assert_long_lived(&generate().unwrap().cert_pem);
}

/// Expired, near-expiry, and future-dated CAs must retain their signing identity.
#[test]
fn restart_renews_legacy_ca_and_preserves_key() {
    for remaining in [Duration::hours(-1), Duration::hours(12), Duration::days(31)] {
        let dir = tempfile::tempdir().unwrap();
        let key = KeyPair::generate().unwrap();
        let old = short_lived_ca(&key, OffsetDateTime::now_utc() + remaining);
        std::fs::write(dir.path().join("cert.pem"), &old).unwrap();
        write_private_key(&dir.path().join("key.pem"), &key.serialize_pem()).unwrap();

        let renewed = load_or_generate(dir.path()).unwrap();
        assert_ne!(renewed.cert_pem, old, "restart must replace the legacy CA");
        assert_eq!(renewed.key_pem, key.serialize_pem());
        assert_long_lived(&renewed.cert_pem);
        assert_eq!(
            load_or_generate(dir.path()).unwrap().cert_pem,
            renewed.cert_pem
        );
    }
}

/// Broken persisted credentials must not be silently replaced with a new identity.
#[test]
fn invalid_persisted_ca_material_is_rejected_without_overwriting_it() {
    let key = KeyPair::generate().unwrap();
    let expired = short_lived_ca(&key, OffsetDateTime::now_utc() - Duration::hours(1));
    for (cert_pem, key_pem) in [
        ("invalid", key.serialize_pem()),
        (expired.as_str(), "invalid".into()),
    ] {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("cert.pem"), cert_pem).unwrap();
        write_private_key(&dir.path().join("key.pem"), &key_pem).unwrap();
        assert!(load_or_generate(dir.path()).is_err());
        assert_eq!(
            std::fs::read_to_string(dir.path().join("cert.pem")).unwrap(),
            cert_pem
        );
    }
}

/// Remove stale matching issuers without normalizing unknown trust-bundle content.
#[test]
fn trust_renewal_removes_old_certificate_and_preserves_other_roots() {
    let dir = tempfile::tempdir().unwrap();
    let bundle = dir.path().join("ca-certificates.crt");
    let key = KeyPair::generate().unwrap();
    let now = OffsetDateTime::now_utc();
    let old = short_lived_ca(&key, now - Duration::hours(1));
    std::fs::write(dir.path().join("cert.pem"), &old).unwrap();
    write_private_key(&dir.path().join("key.pem"), &key.serialize_pem()).unwrap();
    let renewed = load_or_generate(dir.path()).unwrap().cert_pem;
    let mut unrelated = generate().unwrap().cert_pem.into_bytes();
    unrelated.extend_from_slice(
        b"\n-----BEGIN CERTIFICATE-----\n\xffbroken\n-----END CERTIFICATE-----\n\xfe-----BEGIN CERTIFICATE-----\nincomplete\n",
    );
    std::fs::write(
        &bundle,
        [b"# system roots\n".as_slice(), &unrelated, old.as_bytes()].concat(),
    )
    .unwrap();

    let installer = ca_trust::CaInstaller::with_bundle(bundle.clone());
    installer.install(renewed.as_bytes()).unwrap();
    installer.install(renewed.as_bytes()).unwrap();
    let installed = std::fs::read(bundle).unwrap();
    assert!(
        !installed
            .windows(old.len())
            .any(|block| block == old.as_bytes()),
        "expired CA must leave the trust bundle"
    );
    assert!(
        installed
            .windows(unrelated.len())
            .any(|block| block == unrelated)
    );
    assert!(installed.starts_with(b"# system roots\n"));
    assert_eq!(
        installed
            .windows(renewed.len())
            .filter(|block| *block == renewed.as_bytes())
            .count(),
        1
    );
}

/// Parsed certificates must be usable issuers before they can replace existing trust.
#[test]
fn unusable_incoming_ca_does_not_change_trust_bundle() {
    let original = generate().unwrap();
    let key = KeyPair::from_pem(&original.key_pem).unwrap();
    let now = OffsetDateTime::now_utc();
    let mut failures = Vec::new();
    for case in [
        "not CA",
        "missing usage",
        "wrong usage",
        "expired",
        "future",
    ] {
        let mut params = CertificateParams::default();
        params
            .distinguished_name
            .push(DnType::CommonName, "BoxLite MITM CA");
        params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
        params.key_usages = vec![KeyUsagePurpose::KeyCertSign];
        params.not_before = now - Duration::days(1);
        params.not_after = now + Duration::days(1);
        match case {
            "not CA" => params.is_ca = IsCa::NoCa,
            "missing usage" => params.key_usages.clear(),
            "wrong usage" => params.key_usages = vec![KeyUsagePurpose::DigitalSignature],
            "expired" => params.not_after = now - Duration::hours(1),
            "future" => params.not_before = now + Duration::hours(1),
            _ => unreachable!(),
        }
        let incoming = params.self_signed(&key).unwrap().pem();
        let dir = tempfile::tempdir().unwrap();
        let bundle = dir.path().join("ca-certificates.crt");
        std::fs::write(&bundle, &original.cert_pem).unwrap();
        let rejected = ca_trust::CaInstaller::with_bundle(bundle.clone())
            .install(incoming.as_bytes())
            .is_err_and(|error| error.kind() == std::io::ErrorKind::InvalidData);
        let preserved = std::fs::read(&bundle).unwrap() == original.cert_pem.as_bytes();
        if !rejected || !preserved {
            failures.push((case, rejected, preserved));
        }
    }
    assert!(
        failures.is_empty(),
        "unusable incoming CAs must be rejected without replacing trust: {failures:?}"
    );
}

/// Decode failures must leave the previously installed trust store intact.
#[test]
fn invalid_ca_does_not_change_trust_bundle() {
    let dir = tempfile::tempdir().unwrap();
    let bundle = dir.path().join("ca-certificates.crt");
    let original = generate().unwrap().cert_pem;
    std::fs::write(&bundle, &original).unwrap();
    assert!(
        ca_trust::CaInstaller::with_bundle(bundle.clone())
            .install(b"invalid")
            .is_err()
    );
    assert_eq!(std::fs::read_to_string(bundle).unwrap(), original);
}