keyfork-derive-path-data: move all pathcrafting here

This commit is contained in:
Ryan Heywood 2024-08-01 10:58:35 -04:00
parent 35ab5e65a4
commit b26f296a75
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
8 changed files with 148 additions and 35 deletions

3
Cargo.lock generated
View File

@ -1681,6 +1681,7 @@ dependencies = [
"clap_complete", "clap_complete",
"keyfork-bin", "keyfork-bin",
"keyfork-derive-openpgp", "keyfork-derive-openpgp",
"keyfork-derive-path-data",
"keyfork-derive-util", "keyfork-derive-util",
"keyfork-entropy", "keyfork-entropy",
"keyfork-mnemonic", "keyfork-mnemonic",
@ -1748,6 +1749,7 @@ version = "0.1.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ed25519-dalek", "ed25519-dalek",
"keyfork-derive-path-data",
"keyfork-derive-util", "keyfork-derive-util",
"keyforkd-client", "keyforkd-client",
"sequoia-openpgp", "sequoia-openpgp",
@ -1759,6 +1761,7 @@ name = "keyfork-derive-path-data"
version = "0.1.1" version = "0.1.1"
dependencies = [ dependencies = [
"keyfork-derive-util", "keyfork-derive-util",
"once_cell",
] ]
[[package]] [[package]]

View File

@ -16,3 +16,4 @@ ed25519-dalek = "2.0.0"
sequoia-openpgp = { version = "1.17.0", default-features = false } sequoia-openpgp = { version = "1.17.0", default-features = false }
anyhow = "1.0.75" anyhow = "1.0.75"
thiserror = "1.0.49" thiserror = "1.0.49"
keyfork-derive-path-data = { version = "0.1.1", path = "../keyfork-derive-path-data" }

View File

@ -3,6 +3,7 @@
use std::{env, process::ExitCode, str::FromStr}; use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_derive_path_data::paths;
use keyforkd_client::Client; use keyforkd_client::Client;
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
@ -78,16 +79,14 @@ fn validate(
subkey_format: &str, subkey_format: &str,
default_userid: &str, default_userid: &str,
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> { ) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
let mut pgp_u32 = [0u8; 4]; let index = paths::OPENPGP.inner().first().unwrap();
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
let path = DerivationPath::from_str(path)?; let path = DerivationPath::from_str(path)?;
assert!(path.len() >= 2, "Expected path of at least 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"); let given_index = path.iter().next().expect("checked .len() above");
assert_eq!( assert_eq!(
&index, given_index, index, given_index,
"Expected derivation path starting with m/{index}, got: {given_index}", "Expected derivation path starting with m/{index}, got: {given_index}",
); );

View File

@ -8,3 +8,4 @@ license = "MIT"
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" } keyfork-derive-util = { version = "0.2.0", path = "../keyfork-derive-util", default-features = false, registry = "distrust" }
once_cell = "1.19.0"

View File

@ -2,32 +2,128 @@
#![allow(clippy::unreadable_literal)] #![allow(clippy::unreadable_literal)]
use once_cell::sync::Lazy;
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
/// The default derivation path for OpenPGP. pub mod paths {
pub static OPENPGP: DerivationIndex = DerivationIndex::new_unchecked(7366512, true); use super::*;
/// The default derivation path for OpenPGP.
pub static OPENPGP: Lazy<DerivationPath> = 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<DerivationPath> = 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<DerivationPath> = 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<DerivationIndex> {
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. /// A derivation target.
#[derive(Debug)]
#[non_exhaustive]
pub enum Target { pub enum Target {
/// An OpenPGP key, whose account is the given index. /// An OpenPGP key, whose account is the given index.
OpenPGP(DerivationIndex), 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 { impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::OpenPGP(account) => { Target::OpenPGP(account) => {
write!(f, "OpenPGP key (account {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 /// 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. /// `keyforkd` to provide an optional textual prompt to what a client is attempting to derive.
pub fn guess_target(path: &DerivationPath) -> Option<Target> { pub fn guess_target(path: &DerivationPath) -> Option<Target> {
Some(match path.iter().collect::<Vec<_>>()[..] { test_match!(path, paths::OPENPGP_SHARD, Target::OpenPGPShard);
[t, index] if t == &OPENPGP => Target::OpenPGP(index.clone()), test_match!(
_ => return None, 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:?}"),
}
}
} }

View File

@ -44,3 +44,4 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false }
openpgp-card = "0.4.1" openpgp-card = "0.4.1"
clap_complete = { version = "4.4.6", optional = true } clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] } 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" }

View File

@ -11,6 +11,7 @@ use keyfork_derive_openpgp::{
XPrvKey, XPrvKey,
}; };
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_derive_path_data::paths;
use keyforkd_client::Client; use keyforkd_client::Client;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -37,12 +38,7 @@ impl DeriveSubcommands {
fn handle(&self, account: DerivationIndex) -> Result<()> { fn handle(&self, account: DerivationIndex) -> Result<()> {
match self { match self {
DeriveSubcommands::OpenPGP { user_id } => { DeriveSubcommands::OpenPGP { user_id } => {
let mut pgp_u32 = [0u8; 4]; let path = paths::OPENPGP.clone().chain_push(account);
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
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? // TODO: should this be customizable?
let subkeys = vec![ let subkeys = vec![
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),

View File

@ -11,16 +11,24 @@ use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card}; use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::{ 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, XPrv,
}; };
use keyfork_derive_path_data::paths;
use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_mnemonic::Mnemonic;
use keyfork_prompt::{ use keyfork_prompt::{
default_terminal, default_terminal,
validators::{SecurePinValidator, Validator}, validators::{SecurePinValidator, Validator},
DefaultTerminal, Message, PromptHandler, DefaultTerminal, Message, PromptHandler,
}; };
use keyfork_mnemonic::Mnemonic;
use keyfork_shard::{openpgp::OpenPGP, Format}; use keyfork_shard::{openpgp::OpenPGP, Format};
@ -42,17 +50,8 @@ fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
KeyFlags::empty().set_authentication(), KeyFlags::empty().set_authentication(),
]; ];
let mut pgp_u32 = [0u8; 4];
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
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::<Vec<u8>>());
let account = DerivationIndex::new(u32::from_be_bytes(shrd_u32), true)?;
let subkey = DerivationIndex::new(u32::from(index), true)?; let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = DerivationPath::default() let path = paths::OPENPGP_SHARD.clone().chain_push(subkey);
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let xprv = XPrv::new(seed) let xprv = XPrv::new(seed)
.expect("could not construct master key from seed") .expect("could not construct master key from seed")
.derive_path(&path)?; .derive_path(&path)?;
@ -193,16 +192,21 @@ fn generate_shard_secret(
Ok(()) 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 entropy = keyfork_entropy::generate_entropy_of_const_size::<{ 256 / 8 }>()?;
let mnemonic = Mnemonic::from_array(entropy); let mnemonic = Mnemonic::from_array(entropy);
let seed = mnemonic.generate_seed(None); let seed = mnemonic.generate_seed(None);
// TODO: should this allow for customizing the account index from 0? Potential for key reuse // TODO: should this allow for customizing the account index from 0? Potential for key reuse
// errors. // errors.
let path = DerivationPath::default() let path = paths::OPENPGP_DISASTER_RECOVERY
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00pgp"), true)?) .clone()
.chain_push(DerivationIndex::new(u32::from_be_bytes(*b"\x00\x00dr"), true)?)
.chain_push(DerivationIndex::new(0, true)?); .chain_push(DerivationIndex::new(0, true)?);
let subkeys = [ let subkeys = [
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),
@ -227,7 +231,13 @@ fn bottoms_up(key_discovery: &Path, threshold: u8, output_shardfile: &Path, outp
let certs = OpenPGP::<DefaultTerminal>::discover_certs(key_discovery)?; let certs = OpenPGP::<DefaultTerminal>::discover_certs(key_discovery)?;
let shardfile = File::create(output_shardfile)?; 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(()) Ok(())
} }
@ -299,7 +309,13 @@ impl WizardSubcommands {
output_shardfile, output_shardfile,
output_cert, output_cert,
user_id, user_id,
} => bottoms_up(key_discovery, *threshold, output_shardfile, output_cert, user_id), } => bottoms_up(
key_discovery,
*threshold,
output_shardfile,
output_cert,
user_id,
),
} }
} }
} }