From b26f296a7541c6910e272058d437ef05df3f6990 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 1 Aug 2024 10:58:35 -0400 Subject: [PATCH] keyfork-derive-path-data: move all pathcrafting here --- Cargo.lock | 3 + .../derive/keyfork-derive-openpgp/Cargo.toml | 1 + .../derive/keyfork-derive-openpgp/src/main.rs | 7 +- .../keyfork-derive-path-data/Cargo.toml | 1 + .../keyfork-derive-path-data/src/lib.rs | 110 ++++++++++++++++-- crates/keyfork/Cargo.toml | 1 + crates/keyfork/src/cli/derive.rs | 8 +- crates/keyfork/src/cli/wizard.rs | 52 ++++++--- 8 files changed, 148 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e36276..4d3dd15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1681,6 +1681,7 @@ dependencies = [ "clap_complete", "keyfork-bin", "keyfork-derive-openpgp", + "keyfork-derive-path-data", "keyfork-derive-util", "keyfork-entropy", "keyfork-mnemonic", @@ -1748,6 +1749,7 @@ version = "0.1.2" dependencies = [ "anyhow", "ed25519-dalek", + "keyfork-derive-path-data", "keyfork-derive-util", "keyforkd-client", "sequoia-openpgp", @@ -1759,6 +1761,7 @@ name = "keyfork-derive-path-data" version = "0.1.1" dependencies = [ "keyfork-derive-util", + "once_cell", ] [[package]] diff --git a/crates/derive/keyfork-derive-openpgp/Cargo.toml b/crates/derive/keyfork-derive-openpgp/Cargo.toml index bd71c94..f679ed9 100644 --- a/crates/derive/keyfork-derive-openpgp/Cargo.toml +++ b/crates/derive/keyfork-derive-openpgp/Cargo.toml @@ -16,3 +16,4 @@ ed25519-dalek = "2.0.0" sequoia-openpgp = { version = "1.17.0", default-features = false } anyhow = "1.0.75" thiserror = "1.0.49" +keyfork-derive-path-data = { version = "0.1.1", path = "../keyfork-derive-path-data" } diff --git a/crates/derive/keyfork-derive-openpgp/src/main.rs b/crates/derive/keyfork-derive-openpgp/src/main.rs index c803ad4..b2521d6 100644 --- a/crates/derive/keyfork-derive-openpgp/src/main.rs +++ b/crates/derive/keyfork-derive-openpgp/src/main.rs @@ -3,6 +3,7 @@ use std::{env, process::ExitCode, str::FromStr}; use keyfork_derive_util::{DerivationIndex, DerivationPath}; +use keyfork_derive_path_data::paths; use keyforkd_client::Client; use ed25519_dalek::SigningKey; @@ -78,16 +79,14 @@ fn validate( subkey_format: &str, default_userid: &str, ) -> Result<(DerivationPath, Vec, UserID), Box> { - let mut pgp_u32 = [0u8; 4]; - pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::>()); - let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?; + let index = paths::OPENPGP.inner().first().unwrap(); let path = DerivationPath::from_str(path)?; 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!( - &index, given_index, + index, given_index, "Expected derivation path starting with m/{index}, got: {given_index}", ); diff --git a/crates/derive/keyfork-derive-path-data/Cargo.toml b/crates/derive/keyfork-derive-path-data/Cargo.toml index 7b3c903..520b58b 100644 --- a/crates/derive/keyfork-derive-path-data/Cargo.toml +++ b/crates/derive/keyfork-derive-path-data/Cargo.toml @@ -8,3 +8,4 @@ license = "MIT" [dependencies] keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" } +once_cell = "1.19.0" diff --git a/crates/derive/keyfork-derive-path-data/src/lib.rs b/crates/derive/keyfork-derive-path-data/src/lib.rs index 24cbe7c..97822ad 100644 --- a/crates/derive/keyfork-derive-path-data/src/lib.rs +++ b/crates/derive/keyfork-derive-path-data/src/lib.rs @@ -2,32 +2,128 @@ #![allow(clippy::unreadable_literal)] +use once_cell::sync::Lazy; + use keyfork_derive_util::{DerivationIndex, DerivationPath}; -/// The default derivation path for OpenPGP. -pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true); +pub mod paths { + use super::*; + + /// The default derivation path for OpenPGP. + pub static OPENPGP: Lazy = Lazy::new(|| { + DerivationPath::default().chain_push(DerivationIndex::new_unchecked( + u32::from_be_bytes(*b"\x00pgp"), + true, + )) + }); + + /// The derivation path for OpenPGP certificates used for sharding. + pub static OPENPGP_SHARD: Lazy = Lazy::new(|| { + DerivationPath::default() + .chain_push(DerivationIndex::new_unchecked( + u32::from_be_bytes(*b"\x00pgp"), + true, + )) + .chain_push(DerivationIndex::new_unchecked( + u32::from_be_bytes(*b"shrd"), + true, + )) + }); + + /// The derivation path for OpenPGP certificates used for disaster recovery. + pub static OPENPGP_DISASTER_RECOVERY: Lazy = Lazy::new(|| { + DerivationPath::default() + .chain_push(DerivationIndex::new_unchecked( + u32::from_be_bytes(*b"\x00pgp"), + true, + )) + .chain_push(DerivationIndex::new_unchecked( + u32::from_be_bytes(*b"\x00\x00dr"), + true, + )) + }); +} + +/// Determine if a prefix matches and whether the next index exists. +fn prefix_matches(given: &DerivationPath, target: &DerivationPath) -> Option { + if given.len() <= target.len() { + return None; + } + if target + .iter() + .zip(given.iter()) + .all(|(left, right)| left == right) + { + given.iter().nth(target.len()).cloned() + } else { + None + } +} /// A derivation target. +#[derive(Debug)] +#[non_exhaustive] pub enum Target { /// An OpenPGP key, whose account is the given index. OpenPGP(DerivationIndex), + + /// An OpenPGP key used for sharding. + OpenPGPShard(DerivationIndex), + + /// An OpenPGP key used for disaster recovery. + OpenPGPDisasterRecovery(DerivationIndex), } impl std::fmt::Display for Target { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::OpenPGP(account) => { + Target::OpenPGP(account) => { write!(f, "OpenPGP key (account {account})") } + Target::OpenPGPShard(shard_index) => { + write!(f, "OpenPGP Shard key (shard index {shard_index})") + } + Target::OpenPGPDisasterRecovery(account) => { + write!(f, "OpenPGP Disaster Recovery key (account {account})") + } } } } +macro_rules! test_match { + ($var:ident, $shard:path, $target:path) => { + if let Some(index) = prefix_matches($var, &$shard) { + return Some($target(index)); + } + }; +} + /// Determine the closest [`Target`] for the given path. This method is intended to be used by /// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive. pub fn guess_target(path: &DerivationPath) -> Option { - Some(match path.iter().collect::>()[..] { - [t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()), - _ => return None, - }) + test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard); + test_match!( + path, + paths::OPENPGP_DISASTER_RECOVERY, + Target::OpenPGPDisasterRecovery + ); + test_match!(path, paths::OPENPGP, Target::OpenPGP); + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let index = DerivationIndex::new(5312, false).unwrap(); + let dr_key = paths::OPENPGP_DISASTER_RECOVERY + .clone() + .chain_push(index.clone()); + match guess_target(&dr_key) { + Some(Target::OpenPGPDisasterRecovery(idx)) if idx == index => (), + bad => panic!("invalid value: {bad:?}"), + } + } } diff --git a/crates/keyfork/Cargo.toml b/crates/keyfork/Cargo.toml index 872aae1..5a732a8 100644 --- a/crates/keyfork/Cargo.toml +++ b/crates/keyfork/Cargo.toml @@ -44,3 +44,4 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false } openpgp-card = "0.4.1" clap_complete = { version = "4.4.6", optional = true } sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] } +keyfork-derive-path-data = { version = "0.1.1", path = "../derive/keyfork-derive-path-data" } diff --git a/crates/keyfork/src/cli/derive.rs b/crates/keyfork/src/cli/derive.rs index 24a4004..55f0c82 100644 --- a/crates/keyfork/src/cli/derive.rs +++ b/crates/keyfork/src/cli/derive.rs @@ -11,6 +11,7 @@ use keyfork_derive_openpgp::{ XPrvKey, }; use keyfork_derive_util::{DerivationIndex, DerivationPath}; +use keyfork_derive_path_data::paths; use keyforkd_client::Client; type Result> = std::result::Result; @@ -37,12 +38,7 @@ 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); + let path = paths::OPENPGP.clone().chain_push(account); // TODO: should this be customizable? let subkeys = vec![ KeyFlags::empty().set_certification(), diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index 6914902..29fa09b 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -11,16 +11,24 @@ 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_path_data::paths; use keyfork_derive_util::{DerivationIndex, DerivationPath}; +use keyfork_mnemonic::Mnemonic; use keyfork_prompt::{ default_terminal, validators::{SecurePinValidator, Validator}, DefaultTerminal, Message, PromptHandler, }; -use keyfork_mnemonic::Mnemonic; use keyfork_shard::{openpgp::OpenPGP, Format}; @@ -42,17 +50,8 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result { KeyFlags::empty().set_authentication(), ]; - 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 mut shrd_u32 = [0u8; 4]; - shrd_u32[..].copy_from_slice(&"shrd".bytes().collect::>()); - let account = DerivationIndex::new(u32::from_be_bytes(shrd_u32), true)?; let subkey = DerivationIndex::new(u32::from(index), true)?; - let path = DerivationPath::default() - .chain_push(chain) - .chain_push(account) - .chain_push(subkey); + 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)?; @@ -193,16 +192,21 @@ fn generate_shard_secret( Ok(()) } -fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, output_cert: &Path, user_id: &str,) -> Result<()> { +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_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 = 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)?) + let path = paths::OPENPGP_DISASTER_RECOVERY + .clone() .chain_push(DerivationIndex::new(0, true)?); let subkeys = [ KeyFlags::empty().set_certification(), @@ -227,7 +231,13 @@ fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, outp 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)?; + opgp.shard_and_encrypt( + threshold, + certs.len() as u8, + &entropy, + &certs[..], + shardfile, + )?; Ok(()) } @@ -299,7 +309,13 @@ impl WizardSubcommands { output_shardfile, output_cert, user_id, - } => bottoms_up(key_discovery, *threshold, output_shardfile, output_cert, user_id), + } => bottoms_up( + key_discovery, + *threshold, + output_shardfile, + output_cert, + user_id, + ), } } }