#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::print_stderr
)]
use std::fs;
use std::path::PathBuf;
use rcgen::{
BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair,
KeyUsagePurpose, SanType,
};
const DEFAULT_HOST: &str = "test.agent.local";
const DEFAULT_VERSION: &str = "v1.0.0";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let output_dir = if let Some(idx) = args.iter().position(|a| a == "--output-dir") {
PathBuf::from(args.get(idx + 1).expect("Missing output directory"))
} else {
PathBuf::from("./test-certs")
};
let host = std::env::var("ANS_TEST_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
let version = std::env::var("ANS_TEST_VERSION").unwrap_or_else(|_| DEFAULT_VERSION.to_string());
println!("Generating test certificates...");
println!(" Output directory: {}", output_dir.display());
println!(" Host: {}", host);
println!(" Version: {}", version);
println!();
fs::create_dir_all(&output_dir)?;
println!("Generating CA certificate...");
let (ca_cert, ca_params, ca_key) = generate_ca("ANS Test CA")?;
let ca_cert_pem = ca_cert.pem();
let ca_key_pem = ca_key.serialize_pem();
fs::write(output_dir.join("ca.pem"), &ca_cert_pem)?;
fs::write(output_dir.join("ca.key"), &ca_key_pem)?;
println!(" Created: ca.pem, ca.key");
println!("Generating server certificate...");
let (server_cert_pem, server_key_pem, server_fingerprint) =
generate_server_cert(&host, &version, &ca_params, &ca_key)?;
fs::write(output_dir.join("server.pem"), &server_cert_pem)?;
fs::write(output_dir.join("server.key"), &server_key_pem)?;
println!(" Created: server.pem, server.key");
println!(" Fingerprint: {}", server_fingerprint);
println!("Generating client certificate...");
let (client_cert_pem, client_key_pem, client_fingerprint) =
generate_client_cert(&host, &version, &ca_params, &ca_key)?;
fs::write(output_dir.join("client.pem"), &client_cert_pem)?;
fs::write(output_dir.join("client.key"), &client_key_pem)?;
println!(" Created: client.pem, client.key");
println!(" Fingerprint: {}", client_fingerprint);
let summary = format!(
r#"# Test Certificate Summary
# Generated for ANS local testing
HOST={}
VERSION={}
ANS_NAME=ans://{}.{}
CA_FINGERPRINT={}
SERVER_FINGERPRINT={}
CLIENT_FINGERPRINT={}
"#,
host,
version,
version,
host,
compute_fingerprint(ca_cert.der()),
server_fingerprint,
client_fingerprint
);
fs::write(output_dir.join("summary.txt"), summary)?;
println!();
println!("Certificate generation complete!");
println!();
println!("To use these certificates:");
println!(" export ANS_TEST_CERTS_DIR={}", output_dir.display());
println!(" cargo run --example local_mtls");
Ok(())
}
fn generate_ca(cn: &str) -> Result<(rcgen::Certificate, CertificateParams, KeyPair), rcgen::Error> {
let key_pair = KeyPair::generate()?;
let mut params = CertificateParams::default();
params.distinguished_name.push(DnType::CommonName, cn);
params
.distinguished_name
.push(DnType::OrganizationName, "ANS Test");
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let cert = params.self_signed(&key_pair)?;
Ok((cert, params, key_pair))
}
fn generate_server_cert(
host: &str,
version: &str,
ca_params: &CertificateParams,
ca_key: &KeyPair,
) -> Result<(String, String, String), rcgen::Error> {
let key_pair = KeyPair::generate()?;
let mut params = CertificateParams::default();
params.distinguished_name.push(DnType::CommonName, host);
params
.distinguished_name
.push(DnType::OrganizationName, "ANS Test");
params
.subject_alt_names
.push(SanType::DnsName(host.to_string().try_into().unwrap()));
let ans_name = format!("ans://{}.{}", version, host);
params
.subject_alt_names
.push(SanType::URI(ans_name.try_into().unwrap()));
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
let issuer = rcgen::Issuer::from_params(ca_params, ca_key);
let cert = params.signed_by(&key_pair, &issuer)?;
let fingerprint = compute_fingerprint(cert.der());
Ok((cert.pem(), key_pair.serialize_pem(), fingerprint))
}
fn generate_client_cert(
host: &str,
version: &str,
ca_params: &CertificateParams,
ca_key: &KeyPair,
) -> Result<(String, String, String), rcgen::Error> {
let key_pair = KeyPair::generate()?;
let mut params = CertificateParams::default();
params.distinguished_name.push(DnType::CommonName, host);
params
.distinguished_name
.push(DnType::OrganizationName, "ANS Test");
params
.subject_alt_names
.push(SanType::DnsName(host.to_string().try_into().unwrap()));
let ans_name = format!("ans://{}.{}", version, host);
params
.subject_alt_names
.push(SanType::URI(ans_name.try_into().unwrap()));
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
let issuer = rcgen::Issuer::from_params(ca_params, ca_key);
let cert = params.signed_by(&key_pair, &issuer)?;
let fingerprint = compute_fingerprint(cert.der());
Ok((cert.pem(), key_pair.serialize_pem(), fingerprint))
}
fn compute_fingerprint(der: &[u8]) -> String {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(der);
format!("SHA256:{}", hex::encode(hash))
}