diff --git a/crates/derive/keyfork-derive-openpgp/src/main.rs b/crates/derive/keyfork-derive-openpgp/src/main.rs index 8431b17..c803ad4 100644 --- a/crates/derive/keyfork-derive-openpgp/src/main.rs +++ b/crates/derive/keyfork-derive-openpgp/src/main.rs @@ -83,7 +83,7 @@ fn validate( let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?; let path = DerivationPath::from_str(path)?; - assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'"); + assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'"); let given_index = path.iter().next().expect("checked .len() above"); assert_eq!( diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index 1dcdf19..dc9cbb1 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -1,20 +1,26 @@ use super::Keyfork; use clap::{Parser, Subcommand}; -use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf}; +use std::{ + collections::HashSet, + fs::File, + io::IsTerminal, + path::{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}, + openpgp::{self, packet::UserID, types::KeyFlags, Cert, serialize::Marshal, armor::{Writer, Kind}}, XPrv, }; -use keyfork_derive_util::{DerivationIndex, DerivationPath}; +use keyfork_derive_util::{DerivationIndex, DerivationPath, VariableLengthSeed}; use keyfork_prompt::{ default_terminal, validators::{SecurePinValidator, Validator}, DefaultTerminal, Message, PromptHandler, }; +use keyfork_mnemonic_util::Mnemonic; use keyfork_shard::{openpgp::OpenPGP, Format}; @@ -24,6 +30,8 @@ pub struct PinLength(usize); type Result> = std::result::Result; +// TODO: refactor to use mnemonic derived seed instead of 256 bit entropy to allow for possible +// recovery in the future. fn derive_key(seed: [u8; 32], index: u8) -> Result { let subkeys = vec![ KeyFlags::empty().set_certification(), @@ -185,6 +193,46 @@ fn generate_shard_secret( 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. @@ -209,6 +257,32 @@ pub enum WizardSubcommands { #[arg(long)] 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. + 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 { @@ -220,6 +294,13 @@ impl WizardSubcommands { 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), } } }