openpgp-key-janitor/src/main.rs
2023-05-31 18:13:25 +02:00

161 lines
4.4 KiB
Rust

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<KeyConfig>,
user_ids: Vec<UserIdConfig>,
#[serde(flatten)]
expiry: Expiry,
}
#[derive(Deserialize)]
struct KeyConfig {
flags: Vec<KeyFlag>,
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<Duration>,
}
#[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<PathBuf>,
}
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(())
}