161 lines
4.4 KiB
Rust
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(())
|
||
|
}
|