From 5219c5a99fb0d46841d09378d677f4fde6ef8f57 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 5 Aug 2024 17:43:22 -0400 Subject: [PATCH] keyfork: enum-trait-ify choose-your-own commands --- crates/keyfork/src/cli/derive.rs | 66 +++--- crates/keyfork/src/cli/wizard.rs | 377 ++++++++++++++++--------------- 2 files changed, 227 insertions(+), 216 deletions(-) diff --git a/crates/keyfork/src/cli/derive.rs b/crates/keyfork/src/cli/derive.rs index 24a4004..47492f0 100644 --- a/crates/keyfork/src/cli/derive.rs +++ b/crates/keyfork/src/cli/derive.rs @@ -1,5 +1,5 @@ use super::Keyfork; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use keyfork_derive_openpgp::{ openpgp::{ @@ -27,45 +27,51 @@ pub enum DeriveSubcommands { /// It is recommended to use the default expiration of one day and to change the expiration /// using an external utility, to ensure the Certify key is usable. #[command(name = "openpgp")] - OpenPGP { - /// Default User ID for the certificate, using the OpenPGP User ID format. - user_id: String, - }, + OpenPGP(OpenPGP) +} + +#[derive(Args, Clone, Debug)] +pub struct OpenPGP { + /// Default User ID for the certificate, using the OpenPGP User ID format. + user_id: String, } impl DeriveSubcommands { fn handle(&self, account: DerivationIndex) -> Result<()> { match self { - DeriveSubcommands::OpenPGP { user_id } => { - let mut pgp_u32 = [0u8; 4]; - pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::>()); - let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?; - let path = DerivationPath::default() - .chain_push(chain) - .chain_push(account); - // TODO: should this be customizable? - let subkeys = vec![ - KeyFlags::empty().set_certification(), - KeyFlags::empty().set_signing(), - KeyFlags::empty() - .set_transport_encryption() - .set_storage_encryption(), - KeyFlags::empty().set_authentication(), - ]; - let xprv = Client::discover_socket()?.request_xprv::(&path)?; - let default_userid = UserID::from(user_id.as_str()); - let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?; + DeriveSubcommands::OpenPGP(opgp) => opgp.handle(account), + } + } +} - let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?; +impl OpenPGP { + pub fn handle(&self, account: DerivationIndex) -> Result<()> { + let mut pgp_u32 = [0u8; 4]; + pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::>()); + let chain = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?; + let path = DerivationPath::default() + .chain_push(chain) + .chain_push(account); + // TODO: should this be customizable? + let subkeys = vec![ + KeyFlags::empty().set_certification(), + KeyFlags::empty().set_signing(), + KeyFlags::empty() + .set_transport_encryption() + .set_storage_encryption(), + KeyFlags::empty().set_authentication(), + ]; + let xprv = Client::discover_socket()?.request_xprv::(&path)?; + let default_userid = UserID::from(self.user_id.as_str()); + let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?; - for packet in cert.into_packets() { - packet.serialize(&mut w)?; - } + let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?; - w.finalize()?; - } + for packet in cert.into_packets() { + packet.serialize(&mut w)?; } + w.finalize()?; Ok(()) } } diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index dc9cbb1..618a9e2 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -1,26 +1,28 @@ use super::Keyfork; -use clap::{Parser, Subcommand}; -use std::{ - collections::HashSet, - fs::File, - io::IsTerminal, - path::{Path, PathBuf}, -}; +use clap::{Args, Parser, Subcommand}; +use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf}; use card_backend_pcsc::PcscBackend; use openpgp_card_sequoia::{state::Open, types::KeyType, Card}; use keyfork_derive_openpgp::{ - openpgp::{self, packet::UserID, types::KeyFlags, Cert, serialize::Marshal, armor::{Writer, Kind}}, + openpgp::{ + self, + armor::{Kind, Writer}, + packet::UserID, + serialize::Marshal, + types::KeyFlags, + Cert, + }, XPrv, }; use keyfork_derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed}; +use keyfork_mnemonic_util::Mnemonic; use keyfork_prompt::{ default_terminal, validators::{SecurePinValidator, Validator}, DefaultTerminal, Message, PromptHandler, }; -use keyfork_mnemonic_util::Mnemonic; use keyfork_shard::{openpgp::OpenPGP, Format}; @@ -109,202 +111,205 @@ fn factory_reset_current_card( Ok(()) } -fn generate_shard_secret( - threshold: u8, - max: u8, - keys_per_shard: u8, - output_file: &Option, -) -> Result<()> { - let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; - let mut pm = default_terminal()?; - let mut certs = vec![]; - let mut seen_cards: HashSet = HashSet::new(); - let stdout = std::io::stdout(); - if output_file.is_none() { - assert!( - !stdout.is_terminal(), - "not printing shard to terminal, redirect output" - ); - } - - let user_pin_validator = SecurePinValidator { - min_length: Some(6), - ..Default::default() - } - .to_fn(); - let admin_pin_validator = SecurePinValidator { - min_length: Some(8), - ..Default::default() - } - .to_fn(); - - for index in 0..max { - let cert = derive_key(seed, index)?; - for i in 0..keys_per_shard { - pm.prompt_message(Message::Text(format!( - "Please remove all keys and insert key #{} for user #{}", - (i as u16) + 1, - (index as u16) + 1, - )))?; - let card_backend = loop { - if let Some(c) = PcscBackend::cards(None)?.next().transpose()? { - break c; - } - pm.prompt_message(Message::Text( - "No smart card was found. Please plug in a smart card and press enter" - .to_string(), - ))?; - }; - let user_pin = pm.prompt_validated_passphrase( - "Please enter the new smartcard User PIN: ", - 3, - &user_pin_validator, - )?; - let admin_pin = pm.prompt_validated_passphrase( - "Please enter the new smartcard Admin PIN: ", - 3, - &admin_pin_validator, - )?; - factory_reset_current_card( - &mut seen_cards, - user_pin.trim(), - admin_pin.trim(), - &cert, - card_backend, - )?; - } - certs.push(cert); - } - - let opgp = OpenPGP::::new(); - - if let Some(output_file) = output_file { - let output = File::create(output_file)?; - opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?; - } else { - opgp.shard_and_encrypt( - threshold, - certs.len() as u8, - &seed, - &certs[..], - std::io::stdout(), - )?; - } - Ok(()) -} - -fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, output_cert: &Path, user_id: &str,) -> Result<()> { - let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; - let mnemonic = Mnemonic::from_nonstandard_bytes(entropy); - // TODO: make this return const size, since is hash based - let seed = mnemonic.generate_seed(None); - - // TODO: should this allow for customizing the account index from 0? Potential for key reuse - // errors. - let path = DerivationPath::default() - .chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?) - .chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00\x00dr"), true)?) - .chain_push(DerivationIndex::new(0, true)?); - let subkeys = [ - KeyFlags::empty().set_certification(), - KeyFlags::empty().set_signing(), - KeyFlags::empty() - .set_transport_encryption() - .set_storage_encryption(), - KeyFlags::empty().set_authentication(), - ]; - let xprv = XPrv::new(VariableLengthSeed::new(&seed)) - .expect("could not construct master key from seed") - .derive_path(&path)?; - let userid = UserID::from(user_id); - - let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?; - let certfile = File::create(output_cert)?; - let mut w = Writer::new(certfile, Kind::PublicKey)?; - cert.serialize(&mut w)?; - w.finalize()?; - - let opgp = OpenPGP::::new(); - let certs = OpenPGP::::discover_certs(key_discovery)?; - - let shardfile = File::create(output_shardfile)?; - opgp.shard_and_encrypt(threshold, certs.len() as u8, &entropy, &certs[..], shardfile)?; - - Ok(()) -} - #[derive(Subcommand, Clone, Debug)] pub enum WizardSubcommands { - /// Create a 256 bit secret and shard the secret to smart cards. - /// - /// Smart cards will need to be plugged in periodically during the wizard, where they will be factory reset and - /// provisioned to `m/pgp'/shrd'/`. The secret can then be recovered with `keyfork recover shard` or - /// `keyfork recover remote-shard`. The share file will be printed to standard output. - GenerateShardSecret { - /// The minimum amount of keys required to decrypt the secret. - #[arg(long)] - threshold: u8, + GenerateShardSecret(GenerateShardSecret), + BottomsUp(BottomsUp), +} - /// The maximum amount of shards. - #[arg(long)] - max: u8, +/// Create a 256 bit secret and shard the secret to smart cards. +/// +/// Smart cards will need to be plugged in periodically during the wizard, where they will be +/// factory reset and provisioned to `m/pgp'/shrd'/`. The secret can then be recovered +/// with `keyfork recover shard` or `keyfork recover remote-shard`. The share file will be printed +/// to standard output. +#[derive(Args, Clone, Debug)] +pub struct GenerateShardSecret { + /// The minimum amount of keys required to decrypt the secret. + #[arg(long)] + threshold: u8, - /// The amount of smart cards to provision per-shard. - #[arg(long, default_value = "1")] - keys_per_shard: u8, + /// The maximum amount of shards. + #[arg(long)] + max: u8, - /// The file to write the generated shard file to. - #[arg(long)] - output: Option, - }, + /// The amount of smart cards to provision per-shard. + #[arg(long, default_value = "1")] + keys_per_shard: u8, - /// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates, - /// deriving the default OpenPGP certificate for the secret. - /// - /// This command was purpose-built for DEFCON and is not intended to be used normally, as it - /// implies keys used for sharding have been generated by a custom source. - BottomsUp { - /// The location of OpenPGP certificates to use when sharding. - key_discovery: PathBuf, + /// The file to write the generated shard file to. + #[arg(long)] + output: Option, +} - /// The minimum amount of keys required to decrypt the secret. - #[arg(long)] - threshold: u8, +/// Create a 256 bit secret and shard the secret to previously known OpenPGP certificates, +/// deriving the default OpenPGP certificate for the secret. +/// +/// This command was purpose-built for DEFCON and is not intended to be used normally, as it +/// implies keys used for sharding have been generated by a custom source. +#[derive(Args, Clone, Debug)] +pub struct BottomsUp { + /// The location of OpenPGP certificates to use when sharding. + key_discovery: PathBuf, - /// The file to write the generated shard file to. - #[arg(long)] - output_shardfile: PathBuf, + /// The minimum amount of keys required to decrypt the secret. + #[arg(long)] + threshold: u8, - /// The file to write the generated OpenPGP certificate to. - #[arg(long)] - output_cert: PathBuf, + /// The file to write the generated shard file to. + #[arg(long)] + output_shardfile: PathBuf, - /// The User ID for the generated OpenPGP certificate. - #[arg(long, default_value = "Disaster Recovery")] - user_id: String, - }, + /// The file to write the generated OpenPGP certificate to. + #[arg(long)] + output_cert: PathBuf, + + /// The User ID for the generated OpenPGP certificate. + #[arg(long, default_value = "Disaster Recovery")] + user_id: String, } impl WizardSubcommands { + // dispatch fn handle(&self) -> Result<()> { match self { - WizardSubcommands::GenerateShardSecret { - threshold, - max, - keys_per_shard, - output, - } => generate_shard_secret(*threshold, *max, *keys_per_shard, output), - WizardSubcommands::BottomsUp { - key_discovery, - threshold, - output_shardfile, - output_cert, - user_id, - } => bottoms_up(key_discovery, *threshold, output_shardfile, output_cert, user_id), + WizardSubcommands::GenerateShardSecret(gss) => gss.handle(), + WizardSubcommands::BottomsUp(bu) => bu.handle(), } } } +impl GenerateShardSecret { + fn handle(&self) -> Result<()> { + let seed = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; + let mut pm = default_terminal()?; + let mut certs = vec![]; + let mut seen_cards: HashSet = HashSet::new(); + let stdout = std::io::stdout(); + if self.output.is_none() { + assert!( + !stdout.is_terminal(), + "not printing shard to terminal, redirect output" + ); + } + + let user_pin_validator = SecurePinValidator { + min_length: Some(6), + ..Default::default() + } + .to_fn(); + let admin_pin_validator = SecurePinValidator { + min_length: Some(8), + ..Default::default() + } + .to_fn(); + + for index in 0..self.max { + let cert = derive_key(seed, index)?; + for i in 0..self.keys_per_shard { + pm.prompt_message(Message::Text(format!( + "Please remove all keys and insert key #{} for user #{}", + (i as u16) + 1, + (index as u16) + 1, + )))?; + let card_backend = loop { + if let Some(c) = PcscBackend::cards(None)?.next().transpose()? { + break c; + } + pm.prompt_message(Message::Text( + "No smart card was found. Please plug in a smart card and press enter" + .to_string(), + ))?; + }; + let user_pin = pm.prompt_validated_passphrase( + "Please enter the new smartcard User PIN: ", + 3, + &user_pin_validator, + )?; + let admin_pin = pm.prompt_validated_passphrase( + "Please enter the new smartcard Admin PIN: ", + 3, + &admin_pin_validator, + )?; + factory_reset_current_card( + &mut seen_cards, + user_pin.trim(), + admin_pin.trim(), + &cert, + card_backend, + )?; + } + certs.push(cert); + } + + let opgp = OpenPGP::::new(); + + if let Some(output_file) = self.output.as_ref() { + let output = File::create(output_file)?; + opgp.shard_and_encrypt(self.threshold, certs.len() as u8, &seed, &certs[..], output)?; + } else { + opgp.shard_and_encrypt( + self.threshold, + certs.len() as u8, + &seed, + &certs[..], + std::io::stdout(), + )?; + } + Ok(()) + } +} + +impl BottomsUp { + fn handle(&self) -> Result<()> { + let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; + let mnemonic = Mnemonic::from_nonstandard_bytes(entropy); + // TODO: make this return const size, since is hash based + let seed = mnemonic.generate_seed(None); + + // TODO: should this allow for customizing the account index from 0? Potential for key reuse + // errors. + let path = DerivationPath::default() + .chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?) + .chain_push(DerivationIndex::new( + u32::from_be_bytes(*b"\x00\x00dr"), + true, + )?) + .chain_push(DerivationIndex::new(0, true)?); + let subkeys = [ + KeyFlags::empty().set_certification(), + KeyFlags::empty().set_signing(), + KeyFlags::empty() + .set_transport_encryption() + .set_storage_encryption(), + KeyFlags::empty().set_authentication(), + ]; + let xprv = XPrv::new(VariableLengthSeed::new(&seed)) + .expect("could not construct master key from seed") + .derive_path(&path)?; + let userid = UserID::from(self.user_id.as_str()); + + let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?; + let certfile = File::create(&self.output_cert)?; + let mut w = Writer::new(certfile, Kind::PublicKey)?; + cert.serialize(&mut w)?; + w.finalize()?; + + let opgp = OpenPGP::::new(); + let certs = OpenPGP::::discover_certs(&self.key_discovery)?; + + let shardfile = File::create(&self.output_shardfile)?; + opgp.shard_and_encrypt( + self.threshold, + certs.len() as u8, + &entropy, + &certs[..], + shardfile, + )?; + Ok(()) + } +} + #[derive(Parser, Debug, Clone)] pub struct Wizard { #[command(subcommand)]