From 8108f5e61a457da3b0950bafb549f98be74e0537 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 11 Feb 2024 20:14:35 -0500 Subject: [PATCH] keyfork-derive-util, keyforkd-client: support fearless conversions --- crates/daemon/keyforkd-client/src/lib.rs | 45 +++++++- .../derive/keyfork-derive-openpgp/src/lib.rs | 39 +++---- .../derive/keyfork-derive-openpgp/src/main.rs | 21 ++-- .../src/extended_key/private_key.rs | 51 ++++++--- .../derive/keyfork-derive-util/src/request.rs | 100 ++++++++++++------ crates/keyfork-shard/src/openpgp.rs | 28 +++-- crates/keyfork/src/cli/derive.rs | 25 ++--- crates/keyfork/src/cli/wizard.rs | 13 ++- 8 files changed, 201 insertions(+), 121 deletions(-) diff --git a/crates/daemon/keyforkd-client/src/lib.rs b/crates/daemon/keyforkd-client/src/lib.rs index 2362895..8ba4e79 100644 --- a/crates/daemon/keyforkd-client/src/lib.rs +++ b/crates/daemon/keyforkd-client/src/lib.rs @@ -2,8 +2,13 @@ use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf}; +use keyfork_derive_util::{ + request::{AsAlgorithm, DerivationRequest}, + DerivationPath, ExtendedPrivateKey, PrivateKey, +}; + use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError}; -use keyforkd_models::{Request, Response, Error as KeyforkdError}; +use keyforkd_models::{Error as KeyforkdError, Request, Response}; #[cfg(test)] mod tests; @@ -11,6 +16,10 @@ mod tests; /// An error occurred while interacting with Keyforkd. #[derive(Debug, thiserror::Error)] pub enum Error { + /// The response from the server did not match the request. + #[error("The response from the server did not match the request")] + InvalidResponse, + /// The environment variables used for determining a Keyforkd socket path were not set. #[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")] EnvVarsNotFound, @@ -37,7 +46,7 @@ pub enum Error { /// An error encountered in Keyforkd. #[error("Error in Keyforkd: {0}")] - Keyforkd(#[from] KeyforkdError) + Keyforkd(#[from] KeyforkdError), } #[allow(missing_docs)] @@ -95,6 +104,38 @@ impl Client { get_socket().map(|socket| Self { socket }) } + /// Request an [`ExtendedPrivateKey`] for a given [`DerivationPath`]. + /// + /// # Errors + /// An error may be returned if: + /// * Reading or writing from or to the socket encountered an error. + /// * Bincode could not serialize the request or deserialize the response. + /// * An error occurred in Keyforkd. + /// * Keyforkd returned invalid data. + pub fn request_xprv(&mut self, path: &DerivationPath) -> Result> + where + K: PrivateKey + Clone + AsAlgorithm, + { + let algo = K::as_algorithm(); + let request = Request::Derivation(DerivationRequest::new(algo.clone(), path)); + let response = self.request(&request)?; + match response { + Response::Derivation(d) => { + if d.algorithm != algo { + return Err(Error::InvalidResponse); + } + + let depth = path.len() as u8; + Ok(ExtendedPrivateKey::new_from_parts( + &d.data, + depth, + d.chain_code, + )) + } + _ => Err(Error::InvalidResponse), + } + } + /// Serialize and send a [`Request`] to the server, awaiting a [`Result`]. /// /// # Errors diff --git a/crates/derive/keyfork-derive-openpgp/src/lib.rs b/crates/derive/keyfork-derive-openpgp/src/lib.rs index 2055734..1292ca7 100644 --- a/crates/derive/keyfork-derive-openpgp/src/lib.rs +++ b/crates/derive/keyfork-derive-openpgp/src/lib.rs @@ -2,13 +2,10 @@ use std::time::{Duration, SystemTime, SystemTimeError}; -use derive_util::{ - request::{DerivationResponse, TryFromDerivationResponseError}, - DerivationIndex, ExtendedPrivateKey, PrivateKey, - IndexError, -}; +use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey}; use ed25519_dalek::SigningKey; pub use keyfork_derive_util as derive_util; +pub use sequoia_openpgp as openpgp; use sequoia_openpgp::{ packet::{ key::{Key4, PrimaryRole, SubordinateRole}, @@ -18,7 +15,9 @@ use sequoia_openpgp::{ types::{KeyFlags, SignatureType}, Cert, Packet, }; -pub use sequoia_openpgp as openpgp; + +pub type XPrvKey = SigningKey; +pub type XPrv = ExtendedPrivateKey; /// An error occurred while creating an OpenPGP key. #[derive(Debug, thiserror::Error)] @@ -32,10 +31,6 @@ pub enum Error { #[error("Key configured with both encryption and non-encryption key flags: {0:?}")] InvalidKeyFlags(KeyFlags), - /// The derivation response contained incorrect data. - #[error("Incorrect derived data: {0}")] - IncorrectDerivedData(#[from] TryFromDerivationResponseError), - /// A derivation index could not be created from the given index. #[error("Could not create derivation index: {0}")] Index(#[from] IndexError), @@ -66,7 +61,7 @@ pub type Result = std::result::Result; /// /// # Errors /// The function may error for any condition mentioned in [`Error`]. -pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result { +pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result { let primary_key_flags = match keys.get(0) { Some(kf) if kf.for_certification() => kf, _ => return Err(Error::NotCert), @@ -76,7 +71,6 @@ pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> R let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24); // Create certificate with initial key and signature - let xprv = ExtendedPrivateKey::::try_from(data)?; let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?; let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519( &PrivateKey::to_bytes(derived_primary_key.private_key()), @@ -118,21 +112,14 @@ pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> R bytes[0] &= 0b1111_1000; bytes[31] &= !0b1000_0000; bytes[31] |= 0b0100_0000; - Key::from( - Key4::<_, SubordinateRole>::import_secret_cv25519( - &bytes, - None, - None, - epoch, - )? - ) + Key::from(Key4::<_, SubordinateRole>::import_secret_cv25519( + &bytes, None, None, epoch, + )?) } else { - Key::from( - Key4::<_, SubordinateRole>::import_secret_ed25519( - &PrivateKey::to_bytes(derived_key.private_key()), - epoch, - )? - ) + Key::from(Key4::<_, SubordinateRole>::import_secret_ed25519( + &PrivateKey::to_bytes(derived_key.private_key()), + epoch, + )?) }; // As per OpenPGP spec, signing keys must backsig the primary key diff --git a/crates/derive/keyfork-derive-openpgp/src/main.rs b/crates/derive/keyfork-derive-openpgp/src/main.rs index 3aec137..8431b17 100644 --- a/crates/derive/keyfork-derive-openpgp/src/main.rs +++ b/crates/derive/keyfork-derive-openpgp/src/main.rs @@ -2,12 +2,16 @@ use std::{env, process::ExitCode, str::FromStr}; -use keyfork_derive_util::{ - request::{DerivationAlgorithm, DerivationRequest, DerivationResponse}, - DerivationIndex, DerivationPath, -}; +use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyforkd_client::Client; -use sequoia_openpgp::{packet::UserID, types::KeyFlags, armor::{Kind, Writer}, serialize::Marshal}; + +use ed25519_dalek::SigningKey; +use sequoia_openpgp::{ + armor::{Kind, Writer}, + packet::UserID, + serialize::Marshal, + types::KeyFlags, +}; #[derive(Debug, thiserror::Error)] enum Error { @@ -108,16 +112,13 @@ fn run() -> Result<(), Box> { _ => panic!("Usage: {program_name} path subkey_format default_userid"), }; - let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); - let derived_data: DerivationResponse = Client::discover_socket()? - .request(&request.into())? - .try_into()?; + let derived_xprv = Client::discover_socket()?.request_xprv::(&path)?; let subkeys = subkey_format .iter() .map(|kt| kt.inner().clone()) .collect::>(); - let cert = keyfork_derive_openpgp::derive(derived_data, subkeys.as_slice(), &default_userid)?; + let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?; let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?; diff --git a/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs b/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs index 1bbf3ca..5921159 100644 --- a/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs +++ b/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs @@ -45,11 +45,36 @@ type HmacSha512 = Hmac; #[derive(Clone, Serialize, Deserialize)] pub struct ExtendedPrivateKey { /// The internal private key data. + #[serde(with = "serde_with")] private_key: K, depth: u8, chain_code: ChainCode, } +mod serde_with { + use super::*; + + pub(crate) fn serialize(value: &K, serializer: S) -> Result + where + S: serde::Serializer, + K: PrivateKey + Clone, + { + serializer.serialize_bytes(&value.to_bytes()) + } + + pub(crate) fn deserialize<'de, D, K>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + K: PrivateKey + Clone, + { + let variable_len_bytes = <&[u8]>::deserialize(deserializer)?; + let bytes: [u8; 32] = variable_len_bytes + .try_into() + .expect("unable to parse serialized private key; no support for static len"); + Ok(K::from_bytes(&bytes)) + } +} + impl std::fmt::Debug for ExtendedPrivateKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExtendedPrivateKey") @@ -105,12 +130,14 @@ where .into_bytes(); let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8); - Self::new_from_parts( - private_key, + Ok(Self::new_from_parts( + private_key + .try_into() + .expect("KEY_SIZE / 8 did not give a 32 byte slice"), 0, // Checked: chain_code is always the same length, hash is static size chain_code.try_into().expect("Invalid chain code length"), - ) + )) } /// Create an [`ExtendedPrivateKey`] from a given `seed`, `depth`, and `chain_code`. @@ -125,21 +152,18 @@ where /// # public_key::TestPublicKey as PublicKey, /// # private_key::TestPrivateKey as PrivateKey, /// # }; - /// # fn main() -> Result<(), Box> { /// let key: &[u8; 32] = // /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let chain_code: &[u8; 32] = // /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; - /// let xprv = ExtendedPrivateKey::::new_from_parts(key, 4, *chain_code)?; - /// # Ok(()) - /// # } + /// let xprv = ExtendedPrivateKey::::new_from_parts(key, 4, *chain_code); /// ``` - pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result { - Ok(Self { - private_key: K::from_bytes(seed.try_into()?), + pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self { + Self { + private_key: K::from_bytes(&key), depth, chain_code, - }) + } } /// Returns a reference to the [`PrivateKey`]. @@ -152,15 +176,12 @@ where /// # public_key::TestPublicKey as PublicKey, /// # private_key::TestPrivateKey as PrivateKey, /// # }; - /// # fn main() -> Result<(), Box> { /// let key: &[u8; 32] = // /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// let chain_code: &[u8; 32] = // /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; - /// let xprv = ExtendedPrivateKey::::new_from_parts(key, 4, *chain_code)?; + /// let xprv = ExtendedPrivateKey::::new_from_parts(key, 4, *chain_code); /// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)); - /// # Ok(()) - /// # } /// ``` pub fn private_key(&self) -> &K { &self.private_key diff --git a/crates/derive/keyfork-derive-util/src/request.rs b/crates/derive/keyfork-derive-util/src/request.rs index 57fc810..c609adc 100644 --- a/crates/derive/keyfork-derive-util/src/request.rs +++ b/crates/derive/keyfork-derive-util/src/request.rs @@ -110,6 +110,18 @@ impl std::str::FromStr for DerivationAlgorithm { } } +/// Acquire the associated [`DerivationAlgorithm`] for a [`PrivateKey`]. +pub trait AsAlgorithm: PrivateKey { + /// Return the appropriate [`DerivationAlgorithm`]. + fn as_algorithm() -> DerivationAlgorithm; +} + +impl AsAlgorithm for TestPrivateKey { + fn as_algorithm() -> DerivationAlgorithm { + DerivationAlgorithm::Internal + } +} + /// A derivation request. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct DerivationRequest { @@ -234,7 +246,7 @@ pub struct DerivationResponse { pub algorithm: DerivationAlgorithm, /// The derived private key. - pub data: Vec, + pub data: [u8; 32], /// The chain code, used for further derivation. pub chain_code: [u8; 32], @@ -251,7 +263,7 @@ impl DerivationResponse { ) -> Self { Self { algorithm, - data: PrivateKey::to_bytes(xprv.private_key()).to_vec(), + data: PrivateKey::to_bytes(xprv.private_key()), chain_code: xprv.chain_code(), depth: xprv.depth(), } @@ -272,47 +284,71 @@ pub enum TryFromDerivationResponseError { } #[cfg(feature = "secp256k1")] -impl TryFrom<&DerivationResponse> for ExtendedPrivateKey { - type Error = TryFromDerivationResponseError; +mod secp256k1 { + use super::*; + use k256::SecretKey; - fn try_from(value: &DerivationResponse) -> std::result::Result { - match value.algorithm { - DerivationAlgorithm::Secp256k1 => { - Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from) - } - _ => Err(Self::Error::Algorithm), + impl AsAlgorithm for SecretKey { + fn as_algorithm() -> DerivationAlgorithm { + DerivationAlgorithm::Secp256k1 } } -} -#[cfg(feature = "secp256k1")] -impl TryFrom for ExtendedPrivateKey { - type Error = TryFromDerivationResponseError; + impl TryFrom<&DerivationResponse> for ExtendedPrivateKey { + type Error = TryFromDerivationResponseError; - fn try_from(value: DerivationResponse) -> std::result::Result { - ExtendedPrivateKey::::try_from(&value) - } -} - -#[cfg(feature = "ed25519")] -impl TryFrom<&DerivationResponse> for ExtendedPrivateKey { - type Error = TryFromDerivationResponseError; - - fn try_from(value: &DerivationResponse) -> std::result::Result { - match value.algorithm { - DerivationAlgorithm::Ed25519 => { - Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from) + fn try_from(value: &DerivationResponse) -> Result { + match value.algorithm { + DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts( + &value.data, + value.depth, + value.chain_code, + )), + _ => Err(Self::Error::Algorithm), } - _ => Err(Self::Error::Algorithm), + } + } + + impl TryFrom for ExtendedPrivateKey { + type Error = TryFromDerivationResponseError; + + fn try_from(value: DerivationResponse) -> Result { + ExtendedPrivateKey::::try_from(&value) } } } #[cfg(feature = "ed25519")] -impl TryFrom for ExtendedPrivateKey { - type Error = TryFromDerivationResponseError; +mod ed25519 { + use super::*; + use ed25519_dalek::SigningKey; - fn try_from(value: DerivationResponse) -> std::result::Result { - ExtendedPrivateKey::::try_from(&value) + impl AsAlgorithm for SigningKey { + fn as_algorithm() -> DerivationAlgorithm { + DerivationAlgorithm::Ed25519 + } + } + + impl TryFrom<&DerivationResponse> for ExtendedPrivateKey { + type Error = TryFromDerivationResponseError; + + fn try_from(value: &DerivationResponse) -> Result { + match value.algorithm { + DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts( + &value.data, + value.depth, + value.chain_code, + )), + _ => Err(Self::Error::Algorithm), + } + } + } + + impl TryFrom for ExtendedPrivateKey { + type Error = TryFromDerivationResponseError; + + fn try_from(value: DerivationResponse) -> Result { + ExtendedPrivateKey::::try_from(&value) + } } } diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index 36ab011..a081b51 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -13,9 +13,9 @@ use aes_gcm::{ Aes256Gcm, Error as AesError, KeyInit, Nonce, }; use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength}; -use keyfork_derive_openpgp::derive_util::{ - request::{DerivationAlgorithm, DerivationRequest}, - DerivationPath, PathError, +use keyfork_derive_openpgp::{ + derive_util::{DerivationPath, PathError}, + XPrv, }; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; use keyfork_prompt::{ @@ -123,6 +123,10 @@ pub enum Error { #[error("IO error: {0}")] Io(#[source] std::io::Error), + /// An error occurred while deriving data. + #[error("Derivation: {0}")] + Derivation(#[from] keyfork_derive_openpgp::derive_util::extended_key::private_key::Error), + /// An error occurred while parsing a derivation path. #[error("Derivation path: {0}")] DerivationPath(#[from] PathError), @@ -643,13 +647,10 @@ pub fn combine( // TODO: extract as function let userid = UserID::from("keyfork-sss"); - let kdr = DerivationRequest::new( - DerivationAlgorithm::Ed25519, - &DerivationPath::from_str("m/7366512'/0'")?, - ) - .derive_with_master_seed(secret.clone())?; + let path = DerivationPath::from_str("m/7366512'/0'")?; + let xprv = XPrv::new(&secret)?.derive_path(&path)?; let derived_cert = keyfork_derive_openpgp::derive( - kdr, + xprv, &[KeyFlags::empty().set_certification().set_signing()], &userid, )?; @@ -680,13 +681,10 @@ pub fn combine( pub fn split(threshold: u8, certs: Vec, secret: &[u8], output: impl Write) -> Result<()> { // build cert to sign encrypted shares let userid = UserID::from("keyfork-sss"); - let kdr = DerivationRequest::new( - DerivationAlgorithm::Ed25519, - &DerivationPath::from_str("m/7366512'/0'")?, - ) - .derive_with_master_seed(secret.to_vec())?; + let path = DerivationPath::from_str("m/7366512'/0'")?; + let xprv = XPrv::new(&secret)?.derive_path(&path)?; let derived_cert = keyfork_derive_openpgp::derive( - kdr, + xprv, &[KeyFlags::empty().set_certification().set_signing()], &userid, )?; diff --git a/crates/keyfork/src/cli/derive.rs b/crates/keyfork/src/cli/derive.rs index 7b2dc2f..27cd731 100644 --- a/crates/keyfork/src/cli/derive.rs +++ b/crates/keyfork/src/cli/derive.rs @@ -1,16 +1,16 @@ use super::Keyfork; use clap::{Parser, Subcommand}; -use keyfork_derive_openpgp::openpgp::{ - armor::{Kind, Writer}, - packet::UserID, - serialize::Marshal, - types::KeyFlags, -}; -use keyfork_derive_util::{ - request::{DerivationAlgorithm, DerivationRequest, DerivationResponse}, - DerivationIndex, DerivationPath, +use keyfork_derive_openpgp::{ + openpgp::{ + armor::{Kind, Writer}, + packet::UserID, + serialize::Marshal, + types::KeyFlags, + }, + XPrvKey, }; +use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyforkd_client::Client; type Result> = std::result::Result; @@ -48,12 +48,9 @@ impl DeriveSubcommands { .set_storage_encryption(), KeyFlags::empty().set_authentication(), ]; - let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); - let derived_data: DerivationResponse = Client::discover_socket()? - .request(&request.into())? - .try_into()?; + let xprv = Client::discover_socket()?.request_xprv::(&path)?; let default_userid = UserID::from(user_id.as_str()); - let cert = keyfork_derive_openpgp::derive(derived_data, &subkeys, &default_userid)?; + let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?; let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?; diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index 2793890..f3f9992 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -5,11 +5,11 @@ 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}; -use keyfork_derive_util::{ - request::{DerivationAlgorithm, DerivationRequest}, - DerivationIndex, DerivationPath, +use keyfork_derive_openpgp::{ + openpgp::{self, packet::UserID, types::KeyFlags, Cert}, + XPrv, }; +use keyfork_derive_util::{DerivationIndex, DerivationPath}; use keyfork_prompt::{ validators::{PinValidator, Validator}, Message, PromptHandler, Terminal, @@ -42,10 +42,9 @@ fn derive_key(seed: &[u8], index: u8) -> Result { .chain_push(chain) .chain_push(account) .chain_push(subkey); - let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); - let response = request.derive_with_master_seed(seed.to_vec())?; + let xprv = XPrv::new(seed)?.derive_path(&path)?; let userid = UserID::from(format!("Keyfork Shard {index}")); - let cert = keyfork_derive_openpgp::derive(response, &subkeys, &userid)?; + let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?; Ok(cert) }