diff --git a/src/main.rs b/src/main.rs index 2acb14d..3206667 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,125 +1,26 @@ -use std::{fs::write, path::PathBuf, time::Duration}; - -use anyhow::{anyhow, Context, Result}; -use clap::Parser; - -use serde::Deserialize; +use anyhow::{Context, Result}; 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, -} +use spec::KeyFlag; -#[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(), - } - } -} +mod paths; +mod setup; +mod spec; 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 (paths, spec) = crate::setup::setup().context("Failed to setup application")?; let mut builder = CertBuilder::new() - .set_primary_key_flags(primary_key_flags) + .set_primary_key_flags( + spec.primary + .flags + .iter() + .fold(KeyFlags::empty(), KeyFlag::fold), + ) .set_validity_period( spec.primary .expiry @@ -128,12 +29,8 @@ fn main() -> Result<()> { ) .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.flags.iter().fold(KeyFlags::empty(), KeyFlag::fold), sub_key .expiry .validity_period @@ -145,16 +42,26 @@ fn main() -> Result<()> { 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)?; + sig_builder = sig_builder + .add_notation(key, value, None, false) + .context(format!( + "Failed to add notation to signature builder for {}", + &user_id.value + ))?; } - builder = builder.add_userid_with(user_id.value, sig_builder)?; + builder = builder + .add_userid_with(user_id.value.clone(), sig_builder) + .context(format!( + "Failed to add user ID {} to certificate builder", + &user_id.value + ))?; } - 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()?)?; - + let (cert, rev) = builder + .generate() + .context("Failed to generate certificate!")?; + paths + .write(cert, rev) + .context("Failed to store certificate!")?; Ok(()) } diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..0c29e54 --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,70 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use sequoia_openpgp::{packet::Signature, serialize::SerializeInto, Cert}; + +pub(crate) struct Paths { + pub(crate) spec: PathBuf, + pub(crate) secret: PathBuf, + pub(crate) public: PathBuf, + pub(crate) rev: PathBuf, +} + +impl Paths { + pub(crate) 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, + } + } + pub(crate) fn write(&self, cert: Cert, rev: Signature) -> Result<()> { + self.write_part(Self::public, cert.armored()) + .context("Failed to write public certificate to file")?; + self.write_part(Self::secret, cert.as_tsk().armored()) + .context("Failed to certificate including private keys to file")?; + self.write_part( + Self::rev, + cert.insert_packets(rev) + .context("Failed to write revocation signature packet into certificate")? + .armored(), + ) + .context("Failed to write revoked certificate to file")?; + Ok(()) + } + + fn write_part(&self, path_selector: F, data: S) -> Result<()> + where + F: Fn(&Self) -> &Path, + S: SerializeInto, + { + std::fs::write( + path_selector(self), + SerializeInto::to_vec(&data) + .context("Failed to serialize certificate into byte vec")?, + ) + .context("Failed to write serialized certificate into file")?; + Ok(()) + } + + fn public(&self) -> &Path { + self.public.as_path() + } + + fn secret(&self) -> &Path { + self.secret.as_path() + } + + fn rev(&self) -> &Path { + self.rev.as_path() + } +} diff --git a/src/setup.rs b/src/setup.rs new file mode 100644 index 0000000..cb749de --- /dev/null +++ b/src/setup.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Context, Result}; +use clap::Parser; + +use crate::{paths::Paths, spec::Spec}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub(crate) struct Cli { + /// Path to the directory it should operate on. Defaults to the current working directory. + #[arg(value_name = "DIR")] + pub(crate) path: Option, +} + +pub(crate) fn setup() -> Result<(Paths, Spec)> { + 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).context("Failed to read spec into string")?; + let spec: Spec = + serde_yaml::from_str(&spec_string).context("Failed to parse spec from string")?; + Ok((paths, spec)) +} diff --git a/src/spec.rs b/src/spec.rs new file mode 100644 index 0000000..26d5c90 --- /dev/null +++ b/src/spec.rs @@ -0,0 +1,85 @@ +use std::time::Duration; + +use serde::Deserialize; + +#[derive(Deserialize)] +/// Specification for how the OpenPGP certificate is supposed to look like +pub(crate) struct Spec { + /// KeySpec for the primary key + pub(crate) primary: KeySpec, + /// List of KeySpec elements for the sub keys + pub(crate) subs: Vec, + /// List of UserIdSpec elements for the user ids + pub(crate) user_ids: Vec, + #[serde(flatten)] + /// Expiry information + pub(crate) expiry: Expiry, +} + +#[derive(Deserialize)] +/// Specification for how the (sub) key is supposed to be configured +pub(crate) struct KeySpec { + /// List of flags to set for the key, detailing what for and how the key can be used. + pub(crate) flags: Vec, + /// Which kind of cryptography the key is going to use + pub(crate) cipher_suite: sequoia_openpgp::cert::CipherSuite, + #[serde(flatten)] + /// Expiry information + pub(crate) expiry: Expiry, +} + +#[derive(Deserialize)] +/// Specification on how a user ID is supposed to look +pub(crate) struct UserIdSpec { + /// The string value of the user ID itself + pub(crate) value: String, + #[serde(default)] + /// A list of notation keys and values to add to the binding signature of the user ID. + pub(crate) notation: Vec<(String, String)>, +} + +#[derive(Deserialize)] +/// Expiry spec, including right now only a validity period +pub(crate) struct Expiry { + #[serde(with = "humantime_serde::option", default)] + /// Validity period, how long a key is supposed to be usable for, starting with the date it was + /// created + pub(crate) validity_period: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "snake_case")] +/// Flags that can be set for keys +pub(crate) enum KeyFlag { + /// Key can certify + Certify, + /// Key can sign + Sign, + /// Key can be used for transport encryption + EncryptForTransport, + /// Key can be used for encrypting data at rest + EncryptAtRest, + /// Key is split by a secret-sharing mechanism + SplitKey, + /// Key can be used for authentication + Authenticate, + /// Key is in the possession of more than one person + GroupKey, +} + +impl KeyFlag { + pub(crate) fn fold( + flags: sequoia_openpgp::types::KeyFlags, + flag: &Self, + ) -> sequoia_openpgp::types::KeyFlags { + match flag { + 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(), + } + } +}