diff --git a/crates/keyfork/src/cli/derive.rs b/crates/keyfork/src/cli/derive.rs index 76d095c..92e1c21 100644 --- a/crates/keyfork/src/cli/derive.rs +++ b/crates/keyfork/src/cli/derive.rs @@ -1,4 +1,4 @@ -use super::Keyfork; +use super::{Keyfork, create}; use clap::{Args, Parser, Subcommand, ValueEnum}; use std::{fmt::Display, io::Write, path::PathBuf}; @@ -20,11 +20,6 @@ type OptWrite = Option>; type Result> = std::result::Result; -fn create(path: &std::path::Path) -> std::io::Result { - eprintln!("Writing derived key to: {path}", path=path.display()); - std::fs::File::create(path) -} - pub trait Deriver { type Prv: PrivateKey + Clone; const DERIVATION_ALGORITHM: DerivationAlgorithm; @@ -54,10 +49,47 @@ pub enum DeriveSubcommands { Key(Key), } +/// Derivation path to use when deriving OpenPGP keys. +#[derive(ValueEnum, Clone, Debug, Default)] +pub enum Path { + /// The default derivation path; no additional index is used. + #[default] + Default, + + /// The Disaster Recovery index. + DisasterRecovery, +} + +impl std::fmt::Display for Path { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl Path { + fn as_str(&self) -> &'static str { + match self { + Path::Default => "default", + Path::DisasterRecovery => "disaster-recovery", + } + } + + fn derivation_path(&self) -> DerivationPath { + match self { + Self::Default => paths::OPENPGP.clone(), + Self::DisasterRecovery => paths::OPENPGP_DISASTER_RECOVERY.clone(), + } + } +} + #[derive(Args, Clone, Debug)] pub struct OpenPGP { /// Default User ID for the certificate, using the OpenPGP User ID format. user_id: String, + + /// Derivation path to use when deriving OpenPGP keys. + #[arg(long, required = false, default_value = "default")] + derivation_path: Path, } /// A format for exporting a key. @@ -173,7 +205,7 @@ impl Deriver for OpenPGP { const DERIVATION_ALGORITHM: DerivationAlgorithm = DerivationAlgorithm::Ed25519; fn derivation_path(&self) -> DerivationPath { - paths::OPENPGP.clone() + self.derivation_path.derivation_path() } fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv) -> Result<()> { diff --git a/crates/keyfork/src/cli/mnemonic.rs b/crates/keyfork/src/cli/mnemonic.rs index 89a68f6..973a439 100644 --- a/crates/keyfork/src/cli/mnemonic.rs +++ b/crates/keyfork/src/cli/mnemonic.rs @@ -1,8 +1,11 @@ use super::{ + create, derive::{self, Deriver}, - provision, Keyfork, + provision, + Keyfork, }; -use crate::{clap_ext::*, config}; +use crate::{clap_ext::*, config, openpgp_card::factory_reset_current_card}; +use card_backend_pcsc::PcscBackend; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; use std::{ collections::HashMap, @@ -17,18 +20,21 @@ use keyfork_derive_openpgp::{ openpgp::{ self, armor::{Kind, Writer}, - packet::UserID, + packet::{UserID, signature::SignatureBuilder}, policy::StandardPolicy, serialize::{ stream::{Encryptor2, LiteralWriter, Message, Recipient}, Serialize, }, - types::KeyFlags, + types::{KeyFlags, SignatureType}, }, XPrv, }; use keyfork_derive_util::DerivationIndex; -use keyfork_prompt::default_handler; +use keyfork_prompt::{ + default_handler, prompt_validated_passphrase, + validators::{SecurePinValidator, Validator}, +}; use keyfork_shard::{openpgp::OpenPGP, Format}; type StringMap = HashMap; @@ -153,6 +159,10 @@ pub enum Error { /// An error occurred when interacting iwth a file. #[error("Error while performing IO operation on: {1}")] IOContext(#[source] std::io::Error, PathBuf), + + /// A required option was not provided. + #[error("The required option {0} was not provided")] + MissingOption(&'static str), } fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a { @@ -241,6 +251,23 @@ pub enum MnemonicSubcommands { #[arg(long)] encrypt_to_self: Option, + /// Shard the mnemonic to freshly-generated OpenPGP certificates derived from the mnemonic, + /// writing the output to the provided path, and provisioning OpenPGP smartcards with the + /// new certificates. + /// + /// The following additional arguments are required: + /// + /// * threshold, m: the minimum amount of shares required to reconstitute the shard. + /// + /// * max, n: the maximum amount of shares. + /// + /// * cards_per_shard: the amount of OpenPGP smartcards to provision per shardholder. + /// + /// * cert_output: the file to write all generated OpenPGP certificates to; if not + /// provided, files will be automatically generated for each certificate. + #[arg(long)] + shard_to_self: Option>, + /// Provision a key derived from the mnemonic to a piece of hardware such as an OpenPGP /// smartcard. This argument is required when used with `--encrypt-to-self`. /// @@ -506,6 +533,173 @@ fn do_shard_to( Ok(()) } +fn derive_key(seed: [u8; 64], index: u8) -> Result> { + let subkeys = vec![ + KeyFlags::empty().set_certification(), + KeyFlags::empty().set_signing(), + KeyFlags::empty() + .set_transport_encryption() + .set_storage_encryption(), + KeyFlags::empty().set_authentication(), + ]; + + let subkey = DerivationIndex::new(u32::from(index), true)?; + let path = keyfork_derive_path_data::paths::OPENPGP_SHARD.clone().chain_push(subkey); + let xprv = XPrv::new(seed) + .expect("could not construct master key from seed") + .derive_path(&path)?; + let userid = UserID::from(format!("Keyfork Shard {index}")); + let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?; + Ok(cert) +} + +fn cross_sign_certs(certs: &mut [openpgp::Cert]) -> Result<(), Box> { + let policy = StandardPolicy::new(); + + #[allow(clippy::unnecessary_to_owned)] + for signing_cert in certs.to_vec() { + let mut certify_key = signing_cert + .with_policy(&policy, None)? + .keys() + .unencrypted_secret() + .for_certification() + .next() + .expect("certify key unusable/not found") + .key() + .clone() + .into_keypair()?; + for signable_cert in certs.iter_mut() { + let sb = SignatureBuilder::new(SignatureType::GenericCertification); + let userid = signable_cert + .userids() + .next() + .expect("a signable user ID is necessary to create web of trust"); + let signature = sb.sign_userid_binding( + &mut certify_key, + signable_cert.primary_key().key(), + &userid, + )?; + let changed; + (*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?; + assert!( + changed, + "OpenPGP certificate was unchanged after inserting packets" + ); + } + } + Ok(()) +} +fn do_shard_to_self( + mnemonic: &keyfork_mnemonic::Mnemonic, + path: &Path, + options: &StringMap, +) -> Result<(), Box> { + let seed = mnemonic.generate_seed(None); + let mut pm = default_handler()?; + let mut certs = vec![]; + let mut seen_cards = std::collections::HashSet::new(); + + let threshold: u8 = options + .get("threshold") + .or(options.get("m")) + .ok_or(Error::MissingOption("threshold"))? + .parse()?; + let max: u8 = options + .get("max") + .or(options.get("n")) + .ok_or(Error::MissingOption("max"))? + .parse()?; + let cards_per_shard = options + .get("cards_per_shard") + .as_deref() + .map(|cps| u8::from_str(cps)) + .transpose()?; + + let 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..cards_per_shard.unwrap_or(1) { + pm.prompt_message(keyfork_prompt::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(keyfork_prompt::Message::Text( + "No smart card was found. Please plug in a smart card and press enter" + .to_string(), + ))?; + }; + let pin = prompt_validated_passphrase( + &mut *pm, + "Please enter the new smartcard PIN: ", + 3, + &pin_validator, + )?; + factory_reset_current_card( + &mut |application_identifier| { + if seen_cards.contains(&application_identifier) { + // we were given a previously-seen card, error + // we're gonna panic because this is a significant error + panic!("Previously used card {application_identifier} was reused"); + } else { + seen_cards.insert(application_identifier); + true + } + }, + pin.trim(), + pin.trim(), + &cert, + &openpgp::policy::NullPolicy::new(), + card_backend, + )?; + } + certs.push(cert); + } + + cross_sign_certs(&mut certs)?; + + let opgp = OpenPGP; + let output = File::create(path)?; + opgp.shard_and_encrypt( + threshold, + certs.len() as u8, + mnemonic.as_bytes(), + &certs[..], + output, + )?; + + match options.get("cert_output") { + Some(path) => { + let cert_file = std::fs::File::create(path)?; + let mut writer = Writer::new(cert_file, Kind::PublicKey)?; + for cert in &certs { + cert.serialize(&mut writer)?; + } + writer.finalize()?; + } + None => { + for cert in &certs { + let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc"); + let file = create(&path)?; + let mut writer = Writer::new(file, Kind::PublicKey)?; + cert.serialize(&mut writer)?; + writer.finalize()?; + } + } + } + + Ok(()) +} + fn do_provision( mnemonic: &keyfork_mnemonic::Mnemonic, provision: &provision::Provision, @@ -622,20 +816,29 @@ impl MnemonicSubcommands { shard_to, shard, encrypt_to_self, + shard_to_self, provision, provision_count, provision_config, } => { // NOTE: We should never have a case where there's Some() of empty vec, but // we will make sure to check it just in case. + // + // We do not print the mnemonic if we are: + // * Encrypting to an existing, usable key + // * Encrypting to a newly provisioned key + // * Sharding to an existing Shardfile with usable keys + // * Sharding to existing, usable keys + // * Sharding to newly provisioned keys let mut will_print_mnemonic = encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty()); + will_print_mnemonic = will_print_mnemonic + && (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none()); will_print_mnemonic = will_print_mnemonic && shard_to.is_none() || shard_to.as_ref().is_some_and(|s| s.is_empty()); will_print_mnemonic = will_print_mnemonic && shard.is_none() || shard.as_ref().is_some_and(|s| s.is_empty()); - will_print_mnemonic = will_print_mnemonic - && (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none()); + will_print_mnemonic = will_print_mnemonic && shard_to_self.is_none(); let mnemonic = source.handle(size)?; @@ -684,6 +887,10 @@ impl MnemonicSubcommands { do_encrypt_to_self(&mnemonic, &encrypt_to_self, &indices)?; } + if let Some(shard_to_self) = shard_to_self { + do_shard_to_self(&mnemonic, &shard_to_self.inner, &shard_to_self.values)?; + } + if let Some(provisioner) = provision { // determine if we should write to standard output based on whether we have a // matching pair of provisioner and public derivation output. diff --git a/crates/keyfork/src/cli/mod.rs b/crates/keyfork/src/cli/mod.rs index cdc157e..6fd9e78 100644 --- a/crates/keyfork/src/cli/mod.rs +++ b/crates/keyfork/src/cli/mod.rs @@ -5,7 +5,11 @@ mod mnemonic; mod provision; mod recover; mod shard; -mod wizard; + +pub fn create(path: &std::path::Path) -> std::io::Result { + eprintln!("Writing derived key to: {path}", path=path.display()); + std::fs::File::create(path) +} /// The Kitchen Sink of Entropy. #[derive(Parser, Clone, Debug)] @@ -57,9 +61,6 @@ pub enum KeyforkCommands { /// leaked by any individual deriver. Recover(recover::Recover), - /// Utilities to automatically manage the setup of Keyfork. - Wizard(wizard::Wizard), - /// Print an autocompletion file to standard output. /// /// Keyfork does not manage the installation of completion files. Consult the documentation for @@ -90,9 +91,6 @@ impl KeyforkCommands { KeyforkCommands::Recover(r) => { r.handle(keyfork)?; } - KeyforkCommands::Wizard(w) => { - w.handle(keyfork)?; - } #[cfg(feature = "completion")] KeyforkCommands::Completion { shell } => { let mut command = Keyfork::command(); diff --git a/crates/keyfork/src/cli/provision/mod.rs b/crates/keyfork/src/cli/provision/mod.rs index 426dbbe..f077226 100644 --- a/crates/keyfork/src/cli/provision/mod.rs +++ b/crates/keyfork/src/cli/provision/mod.rs @@ -59,7 +59,6 @@ impl Provisioner { .chain_push(account_index); let mut client = keyforkd_client::Client::discover_socket()?; let xprv: XPrv = client.request_xprv(&path)?; - panic!(); s.provision(xprv, provisioner) } } diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs deleted file mode 100644 index b3a1363..0000000 --- a/crates/keyfork/src/cli/wizard.rs +++ /dev/null @@ -1,335 +0,0 @@ -use super::Keyfork; -use crate::openpgp_card::factory_reset_current_card; -use clap::{Args, Parser, Subcommand}; -use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf}; - -use card_backend_pcsc::PcscBackend; - -use keyfork_derive_openpgp::{ - openpgp::{ - self, - armor::{Kind, Writer}, - packet::{signature::SignatureBuilder, UserID}, - policy::StandardPolicy, - serialize::Marshal, - types::{KeyFlags, SignatureType}, - Cert, - }, - XPrv, -}; -use keyfork_derive_path_data::paths; -use keyfork_derive_util::DerivationIndex; -use keyfork_mnemonic::Mnemonic; -use keyfork_prompt::{ - default_handler, prompt_validated_passphrase, - validators::{SecurePinValidator, Validator}, - Message, -}; - -use keyfork_shard::{openpgp::OpenPGP, Format}; - -#[derive(thiserror::Error, Debug)] -#[error("Invalid PIN length: {0}")] -pub struct PinLength(usize); - -type Result> = std::result::Result; - -fn derive_key(seed: [u8; 64], index: u8) -> Result { - let subkeys = vec![ - KeyFlags::empty().set_certification(), - KeyFlags::empty().set_signing(), - KeyFlags::empty() - .set_transport_encryption() - .set_storage_encryption(), - KeyFlags::empty().set_authentication(), - ]; - - let subkey = DerivationIndex::new(u32::from(index), true)?; - let path = paths::OPENPGP_SHARD.clone().chain_push(subkey); - let xprv = XPrv::new(seed) - .expect("could not construct master key from seed") - .derive_path(&path)?; - let userid = UserID::from(format!("Keyfork Shard {index}")); - let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?; - Ok(cert) -} - -#[derive(Subcommand, Clone, Debug)] -pub enum WizardSubcommands { - GenerateShardSecret(GenerateShardSecret), - BottomsUp(BottomsUp), -} - -/// 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 maximum amount of shards. - #[arg(long)] - max: u8, - - /// The amount of smart cards to provision per-shard. - #[arg(long, default_value = "1")] - keys_per_shard: u8, - - /// The file to write the generated shard file to. - #[arg(long)] - output: Option, - - /// The file to write generated certificates to. - #[arg(long)] - cert_output: Option, -} - -/// 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 minimum amount of keys required to decrypt the secret. - #[arg(long)] - threshold: u8, - - /// The file to write the generated shard file to. - #[arg(long)] - output_shardfile: PathBuf, - - /// 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(gss) => gss.handle(), - WizardSubcommands::BottomsUp(bu) => bu.handle(), - } - } -} - -fn cross_sign_certs(certs: &mut [Cert]) -> Result<(), Box> { - let policy = StandardPolicy::new(); - - #[allow(clippy::unnecessary_to_owned)] - for signing_cert in certs.to_vec() { - let mut certify_key = signing_cert - .with_policy(&policy, None)? - .keys() - .unencrypted_secret() - .for_certification() - .next() - .expect("certify key unusable/not found") - .key() - .clone() - .into_keypair()?; - for signable_cert in certs.iter_mut() { - let sb = SignatureBuilder::new(SignatureType::GenericCertification); - let userid = signable_cert - .userids() - .next() - .expect("a signable user ID is necessary to create web of trust"); - let signature = sb.sign_userid_binding( - &mut certify_key, - signable_cert.primary_key().key(), - &userid, - )?; - let changed; - (*signable_cert, changed) = signable_cert.clone().insert_packets2(signature)?; - assert!( - changed, - "OpenPGP certificate was unchanged after inserting packets" - ); - } - } - Ok(()) -} - -impl GenerateShardSecret { - fn handle(&self) -> Result<()> { - let root_entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; - let mnemonic = Mnemonic::from_array(root_entropy); - let seed = mnemonic.generate_seed(None); - let mut pm = default_handler()?; - 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 = prompt_validated_passphrase( - &mut *pm, - "Please enter the new smartcard User PIN: ", - 3, - &user_pin_validator, - )?; - let admin_pin = prompt_validated_passphrase( - &mut *pm, - "Please enter the new smartcard Admin PIN: ", - 3, - &admin_pin_validator, - )?; - factory_reset_current_card( - &mut |application_identifier| { - if seen_cards.contains(&application_identifier) { - // we were given the same card, error - // we're gonna panic because this is a significant error - panic!("Previously used card {application_identifier} was reused"); - } else { - seen_cards.insert(application_identifier); - true - } - }, - user_pin.trim(), - admin_pin.trim(), - &cert, - &openpgp::policy::NullPolicy::new(), - card_backend, - )?; - } - certs.push(cert); - } - - cross_sign_certs(&mut certs)?; - - let opgp = OpenPGP; - - 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, - mnemonic.as_bytes(), - &certs[..], - output, - )?; - } else { - opgp.shard_and_encrypt( - self.threshold, - certs.len() as u8, - mnemonic.as_bytes(), - &certs[..], - std::io::stdout(), - )?; - } - - if let Some(cert_output_file) = self.cert_output.as_ref() { - let output = File::create(cert_output_file)?; - let mut writer = Writer::new(output, Kind::PublicKey)?; - for cert in certs { - cert.serialize(&mut writer)?; - } - writer.finalize()?; - } - - Ok(()) - } -} - -impl BottomsUp { - fn handle(&self) -> Result<()> { - let entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?; - let mnemonic = Mnemonic::from_array(entropy); - let seed = mnemonic.generate_seed(None); - - // TODO: should this allow for customizing the account index from 0? Potential for key reuse - // errors. - let path = paths::OPENPGP_DISASTER_RECOVERY - .clone() - .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(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; - 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)] - command: WizardSubcommands, -} - -impl Wizard { - pub fn handle(&self, _k: &Keyfork) -> Result<()> { - self.command.handle()?; - Ok(()) - } -}