use std::{fs::write, path::PathBuf, time::Duration}; use anyhow::{anyhow, Context, Result}; use clap::Parser; use serde::Deserialize; use sequoia_openpgp::{ cert::CertBuilder, packet::signature::SignatureBuilder, serialize::SerializeInto, types::{KeyFlags, SignatureType}, }; #[derive(Deserialize)] struct Spec { primary: KeyConfig, subs: Vec, user_ids: Vec, #[serde(flatten)] expiry: Expiry, } #[derive(Deserialize)] struct KeyConfig { flags: Vec, cipher_suite: sequoia_openpgp::cert::CipherSuite, #[serde(flatten)] expiry: Expiry, } #[derive(Deserialize)] struct UserIdConfig { value: String, #[serde(default)] notation: Vec<(String, String)>, } #[derive(Deserialize)] struct Expiry { #[serde(with = "humantime_serde::option", default)] validity_period: Option, } #[derive(Deserialize)] #[serde(rename_all = "snake_case")] enum KeyFlag { Certify, Sign, EncryptForTransport, EncryptAtRest, SplitKey, Authenticate, GroupKey, } #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// Path to the directory it should operate on. Defaults to the current working directory. #[arg(value_name = "DIR")] path: Option, } struct Paths { spec: PathBuf, secret: PathBuf, public: PathBuf, rev: PathBuf, } impl Paths { fn new(base: PathBuf) -> Paths { let mut spec = base.clone(); spec.push("spec.yml"); let mut secret = base.clone(); secret.push("secret.asc"); let mut public = base.clone(); public.push("public.asc"); let mut rev = base; rev.push("rev.asc"); Paths { spec, secret, public, rev, } } } impl KeyFlag { fn apply(&self, flags: sequoia_openpgp::types::KeyFlags) -> sequoia_openpgp::types::KeyFlags { match self { Self::Certify => flags.set_certification(), Self::Sign => flags.set_signing(), Self::EncryptForTransport => flags.set_transport_encryption(), Self::EncryptAtRest => flags.set_storage_encryption(), Self::SplitKey => flags.set_split_key(), Self::Authenticate => flags.set_authentication(), Self::GroupKey => flags.set_group_key(), } } } fn main() -> Result<()> { let cli = Cli::parse(); let base_dir = cli .path .ok_or_else(|| anyhow!("No path specified in CLI parameters.")) .or_else(|_| std::env::current_dir()) .context( "Couldn't get current dir from env, and no path was specified in CLI parameters either", )?; let paths = Paths::new(base_dir); let spec_string = std::fs::read_to_string(&paths.spec)?; let spec: Spec = serde_yaml::from_str(&spec_string)?; let mut primary_key_flags = KeyFlags::empty(); for flag in spec.primary.flags { primary_key_flags = flag.apply(primary_key_flags); } let mut builder = CertBuilder::new() .set_primary_key_flags(primary_key_flags) .set_validity_period( spec.primary .expiry .validity_period .or(spec.expiry.validity_period), ) .set_cipher_suite(spec.primary.cipher_suite); for sub_key in spec.subs { let mut sub_key_flags = KeyFlags::empty(); for flag in sub_key.flags { sub_key_flags = flag.apply(sub_key_flags); } builder = builder.add_subkey_with( sub_key_flags, sub_key .expiry .validity_period .or(spec.expiry.validity_period), Some(sub_key.cipher_suite), SignatureBuilder::new(SignatureType::SubkeyBinding), )?; } for user_id in spec.user_ids { let mut sig_builder = SignatureBuilder::new(SignatureType::PositiveCertification); for (key, value) in user_id.notation { sig_builder = sig_builder.add_notation(key, value, None, false)?; } builder = builder.add_userid_with(user_id.value, sig_builder)?; } let (cert, rev) = builder.generate()?; write(&paths.public, cert.armored().to_vec()?)?; write(&paths.secret, cert.as_tsk().armored().to_vec()?)?; write(&paths.rev, cert.insert_packets(rev)?.armored().to_vec()?)?; Ok(()) }