Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

update-bundle/src/tests/testkit.rs

//! Test-only PKI + bundle builders. Uses rcgen (a dev-dependency) to mint ed25519
//! signer certificates in-process — never a committed private key.

use chrono::{DateTime, Duration, Utc};
use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyPair, PKCS_ED25519};

use crate::core::{EvidenceKind, PolicyResult, SourceProvenance};
use crate::digest::{hex_lower, sha256_hex};
use crate::finalize::{add_approval, finalize};
use crate::policy::{ApprovedSigner, CertificateValidity, ExpiredPosture, TrustMode, TrustPolicy};
use crate::prepare::{EvidenceInput, PayloadInput, Preparation};
use crate::schema::POLICY_SCHEMA;
use crate::MemberMap;

pub struct TestSigner {
    pub cert_der: Vec<u8>,
    pub key_pkcs8: Vec<u8>,
    pub fingerprint: String,
}

#[allow(dead_code)] // fingerprint kept for symmetry with TestSigner; not every test reads it
pub struct TestCa {
    /// rcgen 0.14 (2026-10-02, W3): the CA's params + key, which signs leaves —
    /// what 0.13 kept inside its `Certificate`.
    pub issuer: rcgen::Issuer<'static, KeyPair>,
    pub der: Vec<u8>,
    pub fingerprint: String,
}

/// A self-signed ed25519 signer (certificate-pinning mode).
pub fn self_signed(cn: &str) -> TestSigner {
    let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
    let mut params = CertificateParams::new(vec![]).unwrap();
    params.distinguished_name.push(DnType::CommonName, cn);
    let cert = params.self_signed(&key).unwrap();
    let der = cert.der().to_vec();
    TestSigner {
        fingerprint: sha256_hex(&der),
        cert_der: der,
        key_pkcs8: key.serialize_der(),
    }
}

/// A self-signed ed25519 CA.
pub fn ca(cn: &str) -> TestCa {
    let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
    let mut params = CertificateParams::new(vec![]).unwrap();
    params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
    params.distinguished_name.push(DnType::CommonName, cn);
    let cert = params.self_signed(&key).unwrap();
    let der = cert.der().to_vec();
    TestCa {
        fingerprint: sha256_hex(&der),
        der,
        issuer: rcgen::Issuer::new(params, key),
    }
}

/// A leaf ed25519 certificate issued by `ca`.
pub fn issue_leaf(cn: &str, ca: &TestCa) -> TestSigner {
    let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
    let mut params = CertificateParams::new(vec![]).unwrap();
    params.distinguished_name.push(DnType::CommonName, cn);
    let cert = params.signed_by(&key, &ca.issuer).unwrap();
    let der = cert.der().to_vec();
    TestSigner {
        fingerprint: sha256_hex(&der),
        cert_der: der,
        key_pkcs8: key.serialize_der(),
    }
}

/// A pinned-fingerprint critical policy: 2 distinct approvers (security-reviewer +
/// system-owner), a distinct trusted release signer, SBOM required, expiry required.
pub fn pinned_policy(
    reviewer: &TestSigner,
    owner: &TestSigner,
    release: &TestSigner,
) -> TrustPolicy {
    TrustPolicy {
        schema_version: POLICY_SCHEMA.to_string(),
        // Pinning is what these bundles are built for, and it is now SAID rather
        // than inferred from an empty root list.
        trust_mode: Some(TrustMode::PinnedSigners),
        trusted_roots: vec![],
        certificate_validity: CertificateValidity::Enforce,
        approved_signers: vec![
            ApprovedSigner {
                fingerprint: reviewer.fingerprint.clone(),
                subject: None,
                roles: vec!["security-reviewer".into()],
            },
            ApprovedSigner {
                fingerprint: owner.fingerprint.clone(),
                subject: None,
                roles: vec!["system-owner".into()],
            },
        ],
        required_approval_roles: vec!["security-reviewer".into(), "system-owner".into()],
        minimum_distinct_approvers: 2,
        trusted_release_signers: vec![release.fingerprint.clone()],
        release_signer_must_be_distinct: true,
        maximum_bundle_age_secs: None,
        require_expiry: true,
        on_expired_metadata: Some(ExpiredPosture::ReadOnly),
        require_sbom: true,
        require_vex: false,
        require_scan: false,
        allowed_products: vec![],
        allowed_models: vec![],
    }
}

#[allow(dead_code)] // the signer handles are available to tests that need them
pub struct BuiltBundle {
    pub members: MemberMap,
    pub policy: TrustPolicy,
    pub reviewer: TestSigner,
    pub owner: TestSigner,
    pub release: TestSigner,
    pub candidate_digest: String,
    pub bundle_id: String,
    pub now: DateTime<Utc>,
}

/// Build a valid, two-approval, release-signed bundle (pinned mode) at `sequence`.
pub fn valid_bundle_seq(sequence: u64, bundle_id: &str) -> BuiltBundle {
    build_valid_bundle(sequence, bundle_id, None, "pass")
}

/// A bundle that is signed, approved and released exactly like [`valid_bundle`],
/// except that its recorded policy verdict is `verdict`. Every signature in it is
/// genuine, so the verdict is the ONLY reason a verifier may refuse it.
#[allow(dead_code)] // used by tests::verdict only
pub fn valid_bundle_with_verdict(verdict: &str) -> BuiltBundle {
    build_valid_bundle(5, "bundle-0005", None, verdict)
}

/// A valid bundle whose single payload is `payload` instead of the default
/// `b"firmware-v1"` — for the bomb-shape tests, which need a payload whose SIZE
/// (or whose stored bytes) is the thing under test.
#[allow(dead_code)] // used by tests::bomb only
pub fn valid_bundle_with_payload(payload: Vec<u8>) -> BuiltBundle {
    build_valid_bundle(5, "bundle-0005", Some(payload), "pass")
}

fn build_valid_bundle(
    sequence: u64,
    bundle_id: &str,
    payload: Option<Vec<u8>>,
    verdict: &str,
) -> BuiltBundle {
    let reviewer = self_signed("security-reviewer");
    let owner = self_signed("system-owner");
    let release = self_signed("release-signer");
    let now = Utc::now();
    let mut prep = base_preparation(sequence, bundle_id, now);
    if let Some(bytes) = payload {
        prep.payloads[0].bytes = bytes;
    }
    prep.policy_result.verdict = verdict.to_string();
    let candidate = prep.build().unwrap();
    let candidate_digest = candidate.candidate_digest.clone();
    let mut members = candidate.members;
    add_approval(
        &mut members,
        "security-reviewer",
        &reviewer.cert_der,
        &reviewer.key_pkcs8,
        None,
        now,
    )
    .unwrap();
    add_approval(
        &mut members,
        "system-owner",
        &owner.cert_der,
        &owner.key_pkcs8,
        None,
        now,
    )
    .unwrap();
    let policy = pinned_policy(&reviewer, &owner, &release);
    if verdict == "pass" {
        finalize(
            &mut members,
            &policy,
            &release.cert_der,
            &release.key_pkcs8,
            now,
        )
        .unwrap();
    } else {
        // `finalize` refuses to release-sign a bundle whose reviewers did not
        // pass it, so a hostile or careless release signer is modelled by hand:
        // the same three members `finalize` writes, with a genuine signature.
        members.insert(
            crate::schema::RELEASE_CRT.to_string(),
            release.cert_der.clone(),
        );
        let manifest =
            crate::manifest::ReleaseManifest::build(&members, bundle_id, &candidate_digest);
        let manifest_bytes = manifest.to_json_bytes().unwrap();
        let sig = crate::signer::sign_pkcs8(&release.key_pkcs8, &manifest_bytes).unwrap();
        members.insert(
            crate::schema::RELEASE_MANIFEST_JSON.to_string(),
            manifest_bytes,
        );
        members.insert(crate::schema::RELEASE_SIG.to_string(), sig);
    }
    BuiltBundle {
        members,
        policy,
        reviewer,
        owner,
        release,
        candidate_digest,
        bundle_id: bundle_id.to_string(),
        now,
    }
}

pub fn valid_bundle() -> BuiltBundle {
    valid_bundle_seq(5, "bundle-0005")
}

pub fn base_preparation(sequence: u64, bundle_id: &str, now: DateTime<Utc>) -> Preparation {
    Preparation {
        bundle_id: bundle_id.to_string(),
        sequence,
        created_at: now,
        expires_at: Some(now + Duration::days(30)),
        producer: "intake-station".into(),
        supplier: "ExampleCorp".into(),
        product: "ExampleDrive".into(),
        release_version: "1.2.3".into(),
        supported_hardware_models: vec!["ACS880".into()],
        supported_hardware_revisions: vec!["R4".into()],
        source_provenance: SourceProvenance {
            source_kind: "vendor-portal".into(),
            source_uri: Some("https://vendor.example/downloads/fw".into()),
            source_repository: None,
            source_asset_path: None,
            fetched_at: Some(now),
            reported_size: Some(11),
            vendor_checksum: None,
            vendor_signature_status: Some("unverified".into()),
            tls_origin: Some("vendor.example".into()),
            transfer_receipt_digest: None,
        },
        policy_result: PolicyResult {
            policy_digest: sha256_hex(b"trust-policy-v1"),
            advisory_snapshot_digest: None,
            verdict: "pass".into(),
            non_production: true,
        },
        supersedes: vec![],
        withdraws: vec![],
        payloads: vec![PayloadInput {
            relative_path: "firmware.bin".into(),
            bytes: b"firmware-v1".to_vec(),
            media_type: "application/octet-stream".into(),
        }],
        evidence: vec![
            EvidenceInput {
                relative_path: "sbom/app.cdx.json".into(),
                bytes: br#"{"bomFormat":"CycloneDX","specVersion":"1.5"}"#.to_vec(),
                media_type: "application/json".into(),
                kind: EvidenceKind::Sbom,
            },
            EvidenceInput {
                relative_path: "source-receipt.json".into(),
                bytes: br#"{"mode":"Strict","complete":true}"#.to_vec(),
                media_type: "application/json".into(),
                kind: EvidenceKind::SourceReceipt,
            },
        ],
    }
}

/// Hex-encode a byte slice (used to embed a CA DER into a trust policy).
pub fn hex(bytes: &[u8]) -> String {
    hex_lower(bytes)
}