/* Copyright James Pace 2025 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #![feature(duration_constructors)] use anyhow::Result; use clap::Parser; use rcgen::{ CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, Issuer, KeyPair, KeyUsagePurpose, SanType, }; use rcgen::string::Ia5String; use std::fs; use std::path::PathBuf; use time::{Duration, OffsetDateTime}; /// Generate a Certificate Authority with some opinionated /// options selected. #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// Common name for this cert. #[arg(long)] common_name: String, /// Email address to assign to cert. #[arg(long)] email_address: Option, /// Domain address to assign to cert. #[arg(long)] domain_name: Option, /// Set to make this cert valid for client authentication. #[arg(long)] client_auth: bool, /// Set to make this cert valid for server authentication. #[arg(long)] server_auth: bool, /// Days for CA to valid for. #[arg(long, default_value = "365")] valid_length: i64, /// Path to CA folder. #[arg(long)] ca_path: String, /// Output directory for artifacts. #[arg(long, short)] output: String, } fn main() -> Result<()> { let args = Args::parse(); if (args.client_auth && args.server_auth) || (!args.client_auth && !args.server_auth) { return Err(anyhow::Error::msg( "Must set one and only one of client or server auth.", )); } // Set up our identity. let mut params: CertificateParams = Default::default(); let earliest_date = OffsetDateTime::now_utc(); let latest_date = OffsetDateTime::now_utc() .checked_add(Duration::days(args.valid_length)) .ok_or(anyhow::Error::msg("Could not get date."))?; params.not_before = earliest_date; params.not_after = latest_date; params.distinguished_name = DistinguishedName::new(); params .distinguished_name .push(DnType::CommonName, args.common_name); if args.email_address.is_some() { let email_address = Ia5String::try_from(args.email_address.unwrap())?; params .subject_alt_names .push(SanType::Rfc822Name(email_address)); } if args.domain_name.is_some() { let domain_name = Ia5String::try_from(args.domain_name.unwrap())?; params .subject_alt_names .push(SanType::DnsName(domain_name)); } // Set up our purposes. params.key_usages.push(KeyUsagePurpose::DigitalSignature); if args.client_auth { params .extended_key_usages .push(ExtendedKeyUsagePurpose::ClientAuth); } if args.server_auth { params .extended_key_usages .push(ExtendedKeyUsagePurpose::ServerAuth); } // Get the ca key pair and cert. let ca_keys_file = PathBuf::from(format!("{}/key.pem", &args.ca_path)); let ca_cert_file = PathBuf::from(format!("{}/cert.pem", &args.ca_path)); let ca_keys_pem = fs::read_to_string(&ca_keys_file)?; let ca_cert_pem = fs::read_to_string(&ca_cert_file)?; let ca_key_pair = KeyPair::from_pem(&ca_keys_pem)?; let signer = Issuer::from_ca_cert_pem(&ca_cert_pem, ca_key_pair)?; let key_pair = KeyPair::generate()?; let cert = params.signed_by(&key_pair, &signer)?; let pem_serialized = cert.pem(); println!("Saving artifacts."); fs::create_dir_all(&args.output)?; fs::write( format!("{}/cert.pem", &args.output), pem_serialized.as_bytes(), )?; fs::write( format!("{}/key.pem", &args.output), key_pair.serialize_pem().as_bytes(), )?; Ok(()) }