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);
}