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

nordisk/gunnar

assets/test/vetra-test-pki/generator/src/main.rs

//! Generates the **Vetra Test PKI** — a three-link chain for development only.
//!
//! root CA  ->  issuing CA  ->  signer (leaf)
//!
//! Pure Rust: `rcgen` on the `ring` backend. No openssl, no shell-out.
//! Every key here is a THROWAWAY minted by this program. Nothing in this chain
//! is, or may become, a production identity.
use rcgen::{
    BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType,
    ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
};
use std::fs;
use time::{Duration, OffsetDateTime};

fn years(p: &mut CertificateParams, n: i64) {
    let now = OffsetDateTime::now_utc();
    p.not_before = now - Duration::days(1);
    p.not_after = now + Duration::days(365 * n);
}

fn dn(cn: &str) -> DistinguishedName {
    let mut d = DistinguishedName::new();
    d.push(DnType::CountryName, "SE");
    d.push(DnType::OrganizationName, "Vetra Automation");
    d.push(DnType::OrganizationalUnitName, "Vetra Test PKI");
    d.push(DnType::CommonName, cn);
    d
}

fn write(dir: &str, name: &str, pem: &str) {
    use base64::Engine;
    let b64 = base64::engine::general_purpose::STANDARD.encode(pem.as_bytes());
    fs::write(format!("{dir}/{name}.pem"), pem).unwrap();
    fs::write(format!("{dir}/{name}.pem.b64"), format!("{b64}\n")).unwrap();
    println!("  {name}.pem  +  {name}.pem.b64  ({} bytes pem)", pem.len());
}

fn main() {
    let dir = std::env::args()
        .nth(1)
        .expect("usage: vetra-test-pki <outdir>");
    fs::create_dir_all(&dir).unwrap();

    // ── root CA: self-signed, may issue CAs one level down ──
    let mut root_p = CertificateParams::new(vec![]).unwrap();
    root_p.distinguished_name = dn("Vetra Test Root CA");
    root_p.is_ca = IsCa::Ca(BasicConstraints::Constrained(1));
    root_p.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
    years(&mut root_p, 5);
    let root_kp = KeyPair::generate().unwrap();
    let root_crt = root_p.self_signed(&root_kp).unwrap();

    // ── issuing CA: signed by root, may NOT issue further CAs ──
    let mut iss_p = CertificateParams::new(vec![]).unwrap();
    iss_p.distinguished_name = dn("Vetra Test Issuing CA");
    iss_p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
    iss_p.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
    // The issuing CA must ASSERT the purposes it may delegate, or a verifier that
    // walks EKU down the chain rejects a leaf whose EKU the issuer never carried.
    iss_p.extended_key_usages = vec![
        ExtendedKeyUsagePurpose::CodeSigning,
        ExtendedKeyUsagePurpose::EmailProtection,
    ];
    years(&mut iss_p, 3);
    let iss_kp = KeyPair::generate().unwrap();
    let iss_crt = iss_p.signed_by(&iss_kp, &root_crt, &root_kp).unwrap();

    // ── signer leaf: signed by the issuing CA, code-signing only ──
    let mut sgn_p = CertificateParams::new(vec![]).unwrap();
    sgn_p.distinguished_name = dn("Vetra Test Signer");
    sgn_p.is_ca = IsCa::NoCa;
    // DigitalSignature signs; ContentCommitment (nonRepudiation) is the bit a
    // detached DATA signature needs, which is what znippy's CMS provenance path
    // produces. CodeSigning + EmailProtection are the matching EKUs: code for the
    // appliance payloads, EmailProtection because that is the OID CMS/SMIME
    // verifiers look for on a detached data signature.
    sgn_p.key_usages = vec![
        KeyUsagePurpose::DigitalSignature,
        KeyUsagePurpose::ContentCommitment,
    ];
    sgn_p.extended_key_usages = vec![
        ExtendedKeyUsagePurpose::CodeSigning,
        ExtendedKeyUsagePurpose::EmailProtection,
    ];
    years(&mut sgn_p, 3);
    let sgn_kp = KeyPair::generate().unwrap();
    let sgn_crt = sgn_p.signed_by(&sgn_kp, &iss_crt, &iss_kp).unwrap();

    println!("Vetra Test PKI -> {dir}");
    for (n, c, k) in [
        ("root-ca", &root_crt, &root_kp),
        ("issuing-ca", &iss_crt, &iss_kp),
        ("signer", &sgn_crt, &sgn_kp),
    ] {
        write(&dir, &format!("{n}-cert"), &c.pem());
        write(&dir, &format!("{n}-key"), &k.serialize_pem());
    }
    let chain = format!("{}{}{}", sgn_crt.pem(), iss_crt.pem(), root_crt.pem());
    write(&dir, "chain-signer-to-root", &chain);
}