chore: split code out into modules
This commit is contained in:
parent
ddc6e088f4
commit
52f3bd2620
4 changed files with 217 additions and 124 deletions
155
src/main.rs
155
src/main.rs
|
@ -1,125 +1,26 @@
|
||||||
use std::{fs::write, path::PathBuf, time::Duration};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use sequoia_openpgp::{
|
use sequoia_openpgp::{
|
||||||
cert::CertBuilder,
|
cert::CertBuilder,
|
||||||
packet::signature::SignatureBuilder,
|
packet::signature::SignatureBuilder,
|
||||||
serialize::SerializeInto,
|
|
||||||
types::{KeyFlags, SignatureType},
|
types::{KeyFlags, SignatureType},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
use spec::KeyFlag;
|
||||||
struct Spec {
|
|
||||||
primary: KeyConfig,
|
|
||||||
subs: Vec<KeyConfig>,
|
|
||||||
user_ids: Vec<UserIdConfig>,
|
|
||||||
#[serde(flatten)]
|
|
||||||
expiry: Expiry,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
mod paths;
|
||||||
struct KeyConfig {
|
mod setup;
|
||||||
flags: Vec<KeyFlag>,
|
mod spec;
|
||||||
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<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let (paths, spec) = crate::setup::setup().context("Failed to setup application")?;
|
||||||
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()
|
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(
|
.set_validity_period(
|
||||||
spec.primary
|
spec.primary
|
||||||
.expiry
|
.expiry
|
||||||
|
@ -128,12 +29,8 @@ fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
.set_cipher_suite(spec.primary.cipher_suite);
|
.set_cipher_suite(spec.primary.cipher_suite);
|
||||||
for sub_key in spec.subs {
|
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(
|
builder = builder.add_subkey_with(
|
||||||
sub_key_flags,
|
sub_key.flags.iter().fold(KeyFlags::empty(), KeyFlag::fold),
|
||||||
sub_key
|
sub_key
|
||||||
.expiry
|
.expiry
|
||||||
.validity_period
|
.validity_period
|
||||||
|
@ -145,16 +42,26 @@ fn main() -> Result<()> {
|
||||||
for user_id in spec.user_ids {
|
for user_id in spec.user_ids {
|
||||||
let mut sig_builder = SignatureBuilder::new(SignatureType::PositiveCertification);
|
let mut sig_builder = SignatureBuilder::new(SignatureType::PositiveCertification);
|
||||||
for (key, value) in user_id.notation {
|
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()?;
|
let (cert, rev) = builder
|
||||||
|
.generate()
|
||||||
write(&paths.public, cert.armored().to_vec()?)?;
|
.context("Failed to generate certificate!")?;
|
||||||
write(&paths.secret, cert.as_tsk().armored().to_vec()?)?;
|
paths
|
||||||
write(&paths.rev, cert.insert_packets(rev)?.armored().to_vec()?)?;
|
.write(cert, rev)
|
||||||
|
.context("Failed to store certificate!")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
70
src/paths.rs
Normal file
70
src/paths.rs
Normal file
|
@ -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<F, S>(&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()
|
||||||
|
}
|
||||||
|
}
|
31
src/setup.rs
Normal file
31
src/setup.rs
Normal file
|
@ -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<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
85
src/spec.rs
Normal file
85
src/spec.rs
Normal file
|
@ -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<KeySpec>,
|
||||||
|
/// List of UserIdSpec elements for the user ids
|
||||||
|
pub(crate) user_ids: Vec<UserIdSpec>,
|
||||||
|
#[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<KeyFlag>,
|
||||||
|
/// 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<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue