Fearless Conversions #27

Manually merged
ryan merged 4 commits from ryan/fearless-conversions into main 2024-02-12 06:28:50 +00:00
23 changed files with 485 additions and 282 deletions

22
Cargo.lock generated
View File

@ -244,7 +244,7 @@ dependencies = [
"futures-lite 2.2.0", "futures-lite 2.2.0",
"parking", "parking",
"polling 3.3.2", "polling 3.3.2",
"rustix 0.38.30", "rustix 0.38.31",
"slab", "slab",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -1610,7 +1610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"rustix 0.38.30", "rustix 0.38.31",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -1870,6 +1870,7 @@ dependencies = [
"keyfork-slip10-test-data", "keyfork-slip10-test-data",
"keyforkd-models", "keyforkd-models",
"serde", "serde",
"tempfile",
"thiserror", "thiserror",
"tokio", "tokio",
"tower", "tower",
@ -2533,7 +2534,7 @@ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"pin-project-lite", "pin-project-lite",
"rustix 0.38.30", "rustix 0.38.31",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -2806,9 +2807,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.30" version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [ dependencies = [
"bitflags 2.4.2", "bitflags 2.4.2",
"errno", "errno",
@ -3184,14 +3185,13 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.9.0" version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand 2.0.1", "fastrand 2.0.1",
"redox_syscall", "rustix 0.38.31",
"rustix 0.38.30",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -3212,7 +3212,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [ dependencies = [
"rustix 0.38.30", "rustix 0.38.31",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -3606,7 +3606,7 @@ dependencies = [
"either", "either",
"home", "home",
"once_cell", "once_cell",
"rustix 0.38.30", "rustix 0.38.31",
] ]
[[package]] [[package]]

View File

@ -2,8 +2,13 @@
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf}; 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 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)] #[cfg(test)]
mod tests; mod tests;
@ -11,6 +16,10 @@ mod tests;
/// An error occurred while interacting with Keyforkd. /// An error occurred while interacting with Keyforkd.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum 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. /// The environment variables used for determining a Keyforkd socket path were not set.
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")] #[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
EnvVarsNotFound, EnvVarsNotFound,
@ -37,7 +46,7 @@ pub enum Error {
/// An error encountered in Keyforkd. /// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")] #[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError) Keyforkd(#[from] KeyforkdError),
} }
#[allow(missing_docs)] #[allow(missing_docs)]
@ -95,6 +104,38 @@ impl Client {
get_socket().map(|socket| Self { socket }) 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<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
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<Response>`]. /// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
/// ///
/// # Errors /// # Errors

View File

@ -1,40 +1,20 @@
use crate::Client; use crate::Client;
use keyforkd::test_util::{run_test, Infallible};
use keyfork_derive_util::{request::*, DerivationPath}; use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_slip10_test_data::test_data; use keyfork_slip10_test_data::test_data;
use std::sync::mpsc::channel;
use std::{os::unix::net::UnixStream, str::FromStr}; use std::{os::unix::net::UnixStream, str::FromStr};
use tokio::runtime::Builder;
#[test] #[test]
fn secp256k1() { fn secp256k1_test_suite() {
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove(&"secp256k1".to_string()) .remove(&"secp256k1".to_string())
.unwrap(); .unwrap();
// note: since client is non async, can't be single threaded for seed_test in tests {
let rt = Builder::new_multi_thread().enable_io().build().unwrap(); let seed = seed_test.seed;
let tempdir = tempfile::tempdir().unwrap(); run_test(&seed, move |socket_path| {
for (i, per_seed) in tests.into_iter().enumerate() { for test in seed_test.tests {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
let (tx, rx) = channel();
let handle = rt.spawn({
let socket_path = socket_path.clone();
async move {
let seed = per_seed.seed.clone();
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
tx.send(()).unwrap();
let service = keyforkd::ServiceBuilder::new()
.layer(keyforkd::middleware::BincodeLayer::new())
.service(keyforkd::Keyforkd::new(seed));
server.run(service).await.unwrap();
}
});
rx.recv().unwrap();
for test in &per_seed.tests {
let socket = UnixStream::connect(&socket_path).unwrap(); let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket); let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap(); let chain = DerivationPath::from_str(test.chain).unwrap();
@ -47,39 +27,24 @@ fn secp256k1() {
); );
let response = let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key); assert_eq!(&response.data, test.private_key.as_slice());
} }
Infallible::Ok(())
handle.abort(); }).unwrap();
} }
} }
#[test] #[test]
fn ed25519() { fn ed25519_test_suite() {
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap(); let tests = test_data()
.unwrap()
.remove(&"ed25519".to_string())
.unwrap();
let rt = Builder::new_multi_thread().enable_io().build().unwrap(); for seed_test in tests {
let tempdir = tempfile::tempdir().unwrap(); let seed = seed_test.seed;
for (i, per_seed) in tests.into_iter().enumerate() { run_test(&seed, move |socket_path| {
let mut socket_name = i.to_string(); for test in seed_test.tests {
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
let (tx, rx) = channel();
let handle = rt.spawn({
let socket_path = socket_path.clone();
async move {
let seed = per_seed.seed.clone();
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
tx.send(()).unwrap();
let service = keyforkd::ServiceBuilder::new()
.layer(keyforkd::middleware::BincodeLayer::new())
.service(keyforkd::Keyforkd::new(seed));
server.run(service).await.unwrap();
}
});
rx.recv().unwrap();
for test in &per_seed.tests {
let socket = UnixStream::connect(&socket_path).unwrap(); let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket); let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap(); let chain = DerivationPath::from_str(test.chain).unwrap();
@ -92,9 +57,9 @@ fn ed25519() {
); );
let response = let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key); assert_eq!(&response.data, test.private_key.as_slice());
} }
Infallible::Ok(())
handle.abort(); }).unwrap();
} }
} }

View File

@ -31,6 +31,7 @@ tower = { version = "0.4.13", features = ["tokio", "util"] }
# Personally audited # Personally audited
thiserror = "1.0.47" thiserror = "1.0.47"
serde = { version = "1.0.186", features = ["derive"] } serde = { version = "1.0.186", features = ["derive"] }
tempfile = { version = "3.10.0", default-features = false }
[dev-dependencies] [dev-dependencies]
hex-literal = "0.4.1" hex-literal = "0.4.1"

View File

@ -30,6 +30,8 @@ pub use error::Keyforkd as KeyforkdError;
pub use server::UnixServer; pub use server::UnixServer;
pub use service::Keyforkd; pub use service::Keyforkd;
pub mod test_util;
/// Set up a Tracing subscriber, defaulting to debug mode. /// Set up a Tracing subscriber, defaulting to debug mode.
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
pub fn setup_registry() { pub fn setup_registry() {

View File

@ -76,7 +76,7 @@ impl Service<Request> for Keyforkd {
info!("Deriving path: {}", req.path()); info!("Deriving path: {}", req.path());
} }
req.derive_with_master_seed((*seed).clone()) req.derive_with_master_seed(seed.as_ref())
.map(Response::Derivation) .map(Response::Derivation)
.map_err(|e| DerivationError::Derivation(e.to_string()).into()) .map_err(|e| DerivationError::Derivation(e.to_string()).into())
}), }),
@ -120,7 +120,7 @@ mod tests {
.unwrap() .unwrap()
.try_into() .try_into()
.unwrap(); .unwrap();
assert_eq!(response.data, test.private_key); assert_eq!(&response.data, test.private_key.as_slice());
assert_eq!(response.chain_code.as_slice(), test.chain_code); assert_eq!(response.chain_code.as_slice(), test.chain_code);
} }
} }
@ -150,7 +150,7 @@ mod tests {
.unwrap() .unwrap()
.try_into() .try_into()
.unwrap(); .unwrap();
assert_eq!(response.data, test.private_key); assert_eq!(&response.data, test.private_key.as_slice());
assert_eq!(response.chain_code.as_slice(), test.chain_code); assert_eq!(response.chain_code.as_slice(), test.chain_code);
} }
} }

View File

@ -0,0 +1,85 @@
//! # Keyforkd Test Utilities
//!
//! This module adds a helper to set up a Tokio runtime, start a Tokio runtime with a given seed,
//! start a Keyfork server on that runtime, and run a given test closure.
use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer};
use tokio::runtime::Builder;
#[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")]
#[doc(hidden)]
pub struct InfallibleError {
protected: (),
}
/// An infallible result. This type can be used to represent a function that should never error.
///
/// ```rust
/// use keyforkd::test_util::Infallible;
/// let closure = || {
/// Infallible::Ok(())
/// };
/// assert!(closure().is_ok());
/// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
/// provided as the only argument to the closure. The closure is expected to return a Result; the
/// Error field of the Result may be an error returned by a test.
///
/// # Panics
///
/// The function is not expected to run in production; therefore, the function plays "fast and
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
E: Send + 'static,
{
let rt = Builder::new_multi_thread()
.worker_threads(2)
.enable_io()
.build()
.expect("tokio threaded IO runtime");
let socket_dir = tempfile::tempdir().expect("can't create tempdir");
let socket_path = socket_dir.path().join("keyforkd.sock");
rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({
let socket_path = socket_path.clone();
let seed = seed.to_vec();
async move {
let mut server = UnixServer::bind(&socket_path).expect("can't bind unix socket");
tx.send(()).await.expect("couldn't send server start signal");
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec()));
server.run(service).await.unwrap();
}
});
rx.recv()
.await
.expect("can't receive server start signal from channel");
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await;
server_handle.abort();
result
})
.expect("runtime could not join all threads")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_test() {
let seed = b"beefbeef";
run_test(seed, |_path| Infallible::Ok(())).expect("infallible");
}
}

View File

@ -2,13 +2,10 @@
use std::time::{Duration, SystemTime, SystemTimeError}; use std::time::{Duration, SystemTime, SystemTimeError};
use derive_util::{ use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
request::{DerivationResponse, TryFromDerivationResponseError},
DerivationIndex, ExtendedPrivateKey, PrivateKey,
IndexError,
};
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
pub use keyfork_derive_util as derive_util; pub use keyfork_derive_util as derive_util;
pub use sequoia_openpgp as openpgp;
use sequoia_openpgp::{ use sequoia_openpgp::{
packet::{ packet::{
key::{Key4, PrimaryRole, SubordinateRole}, key::{Key4, PrimaryRole, SubordinateRole},
@ -18,7 +15,9 @@ use sequoia_openpgp::{
types::{KeyFlags, SignatureType}, types::{KeyFlags, SignatureType},
Cert, Packet, Cert, Packet,
}; };
pub use sequoia_openpgp as openpgp;
pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// An error occurred while creating an OpenPGP key. /// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -32,10 +31,6 @@ pub enum Error {
#[error("Key configured with both encryption and non-encryption key flags: {0:?}")] #[error("Key configured with both encryption and non-encryption key flags: {0:?}")]
InvalidKeyFlags(KeyFlags), 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. /// A derivation index could not be created from the given index.
#[error("Could not create derivation index: {0}")] #[error("Could not create derivation index: {0}")]
Index(#[from] IndexError), Index(#[from] IndexError),
@ -66,7 +61,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
/// ///
/// # Errors /// # Errors
/// The function may error for any condition mentioned in [`Error`]. /// The function may error for any condition mentioned in [`Error`].
pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> { pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.get(0) { let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf, Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert), _ => 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); let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
// Create certificate with initial key and signature // Create certificate with initial key and signature
let xprv = ExtendedPrivateKey::<SigningKey>::try_from(data)?;
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?; let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519( let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_primary_key.private_key()), &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[0] &= 0b1111_1000;
bytes[31] &= !0b1000_0000; bytes[31] &= !0b1000_0000;
bytes[31] |= 0b0100_0000; bytes[31] |= 0b0100_0000;
Key::from( Key::from(Key4::<_, SubordinateRole>::import_secret_cv25519(
Key4::<_, SubordinateRole>::import_secret_cv25519( &bytes, None, None, epoch,
&bytes, )?)
None,
None,
epoch,
)?
)
} else { } else {
Key::from( Key::from(Key4::<_, SubordinateRole>::import_secret_ed25519(
Key4::<_, SubordinateRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_key.private_key()), &PrivateKey::to_bytes(derived_key.private_key()),
epoch, epoch,
)? )?)
)
}; };
// As per OpenPGP spec, signing keys must backsig the primary key // As per OpenPGP spec, signing keys must backsig the primary key

View File

@ -2,12 +2,16 @@
use std::{env, process::ExitCode, str::FromStr}; use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{ use keyfork_derive_util::{DerivationIndex, DerivationPath};
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyforkd_client::Client; 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)] #[derive(Debug, thiserror::Error)]
enum Error { enum Error {
@ -108,16 +112,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
_ => panic!("Usage: {program_name} path subkey_format default_userid"), _ => panic!("Usage: {program_name} path subkey_format default_userid"),
}; };
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); let derived_xprv = Client::discover_socket()?.request_xprv::<SigningKey>(&path)?;
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let subkeys = subkey_format let subkeys = subkey_format
.iter() .iter()
.map(|kt| kt.inner().clone()) .map(|kt| kt.inner().clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
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)?; let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;

View File

@ -40,7 +40,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let key1 = request.derive_with_mnemonic(&mnemonic)?; let key1 = request.derive_with_mnemonic(&mnemonic)?;
let seed = mnemonic.seed(None)?; let seed = mnemonic.seed(None)?;
let key2 = request.derive_with_master_seed(seed)?; let key2 = request.derive_with_master_seed(&seed)?;
assert_eq!(key1, key2); assert_eq!(key1, key2);

View File

@ -10,18 +10,10 @@ const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Private Keys. /// Errors associated with creating or deriving Extended Private Keys.
#[derive(Error, Clone, Debug)] #[derive(Error, Clone, Debug)]
pub enum Error { pub enum Error {
/// The seed has an unsuitable length; supported lengths are 16 bytes, 32 bytes, or 64 bytes.
#[error("Seed had an unsuitable length: {0}")]
BadSeedLength(usize),
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255. /// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
#[error("Reached maximum depth for key derivation")] #[error("Reached maximum depth for key derivation")]
Depth, Depth,
/// This should never happen. HMAC keys should be able to take any size input.
#[error("Invalid length for HMAC key while generating master key (report me!)")]
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
/// An unknown error occurred while deriving a child key. /// An unknown error occurred while deriving a child key.
#[error("Unknown error while deriving child key")] #[error("Unknown error while deriving child key")]
Derivation, Derivation,
@ -39,17 +31,104 @@ type Result<T, E = Error> = std::result::Result<T, E>;
type ChainCode = [u8; 32]; type ChainCode = [u8; 32];
type HmacSha512 = Hmac<Sha512>; type HmacSha512 = Hmac<Sha512>;
/// A reference to a variable-length seed. Keyfork automatically supports a seed of 128 bits,
/// 256 bits, or 512 bits, but because the master key is derived from a hashed seed, in theory
/// any amount of bytes could be used. It is not advised to use a variable-length seed longer
/// than 256 bits, as a brute-force attack on the master key could be performed in 2^256
/// attempts.
///
/// Mnemonics use a 512 bit seed, as knowledge of the mnemonics' words (such as through a side
/// channel attack) could leak which individual word is used, but not the order the words are
/// used in. Using a 512 bit hash to generate the seed results in a more computationally
/// expensive brute-force requirement.
pub struct VariableLengthSeed<'a> {
seed: &'a [u8],
}
impl<'a> VariableLengthSeed<'a> {
/// Create a new VariableLengthSeed.
///
/// # Examples
/// ```rust
/// use sha2::{Sha256, Digest};
/// use keyfork_derive_util::VariableLengthSeed;
///
/// let data = b"the missile is very eepy and wants to take a small sleeb";
/// let seed = VariableLengthSeed::new(data);
/// ```
pub fn new(seed: &'a [u8]) -> Self {
Self { seed }
}
}
mod as_private_key {
use super::VariableLengthSeed;
pub trait AsPrivateKey {
fn as_private_key(&self) -> &[u8];
}
impl AsPrivateKey for [u8; 16] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for [u8; 32] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for [u8; 64] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for VariableLengthSeed<'_> {
fn as_private_key(&self) -> &[u8] {
self.seed
}
}
}
/// Extended private keys derived using BIP-0032. /// Extended private keys derived using BIP-0032.
/// ///
/// Generic over types implementing [`PrivateKey`]. /// Generic over types implementing [`PrivateKey`].
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> { pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
/// The internal private key data. /// The internal private key data.
#[serde(with = "serde_with")]
private_key: K, private_key: K,
depth: u8, depth: u8,
chain_code: ChainCode, chain_code: ChainCode,
} }
mod serde_with {
use super::*;
pub(crate) fn serialize<S, K>(value: &K, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
K: PrivateKey + Clone,
{
serializer.serialize_bytes(&value.to_bytes())
}
pub(crate) fn deserialize<'de, D, K>(deserializer: D) -> Result<K, D::Error>
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<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> { impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtendedPrivateKey") f.debug_struct("ExtendedPrivateKey")
@ -73,7 +152,7 @@ where
/// # Errors /// # Errors
/// An error may be returned if: /// An error may be returned if:
/// * The given seed had an incorrect length. /// * The given seed had an incorrect length.
/// * A `HmacSha512` can't be constructed - this should be impossible. /// * A `HmacSha512` can't be constructed.
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
@ -82,31 +161,26 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(seed)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// # Ok(())
/// # }
/// ``` /// ```
pub fn new(seed: impl AsRef<[u8]>) -> Result<Self> { pub fn new(seed: impl as_private_key::AsPrivateKey) -> Self {
Self::new_internal(seed.as_ref()) Self::new_internal(seed.as_private_key())
} }
fn new_internal(seed: &[u8]) -> Result<Self> { fn new_internal(seed: &[u8]) -> Self {
let len = seed.len(); let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
if ![16, 32, 64].contains(&len) { .expect("HmacSha512 InvalidLength should be infallible")
return Err(Error::BadSeedLength(len));
}
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())?
.chain_update(seed) .chain_update(seed)
.finalize() .finalize()
.into_bytes(); .into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8); let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
Self::new_from_parts( Self::new_from_parts(
private_key, private_key
.try_into()
.expect("KEY_SIZE / 8 did not give a 32 byte slice"),
0, 0,
// Checked: chain_code is always the same length, hash is static size // Checked: chain_code is always the same length, hash is static size
chain_code.try_into().expect("Invalid chain code length"), chain_code.try_into().expect("Invalid chain code length"),
@ -125,21 +199,18 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// # Ok(())
/// # }
/// ``` /// ```
pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> { pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
Ok(Self { Self {
private_key: K::from_bytes(seed.try_into()?), private_key: K::from_bytes(key),
depth, depth,
chain_code, chain_code,
}) }
} }
/// Returns a reference to the [`PrivateKey`]. /// Returns a reference to the [`PrivateKey`].
@ -152,15 +223,12 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key)); /// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
/// # Ok(())
/// # }
/// ``` /// ```
pub fn private_key(&self) -> &K { pub fn private_key(&self) -> &K {
&self.private_key &self.private_key
@ -185,7 +253,7 @@ where
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27, /// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251 /// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
/// # ]; /// # ];
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(seed)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xpub = xprv.extended_public_key(); /// let xpub = xprv.extended_public_key();
/// assert_eq!(known_key, xpub.public_key().to_bytes()); /// assert_eq!(known_key, xpub.public_key().to_bytes());
/// # Ok(()) /// # Ok(())
@ -209,7 +277,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(seed)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let pubkey = xprv.public_key(); /// let pubkey = xprv.public_key();
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -227,15 +295,12 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.depth(), 4); /// assert_eq!(xprv.depth(), 4);
/// # Ok(())
/// # }
/// ``` /// ```
pub fn depth(&self) -> u8 { pub fn depth(&self) -> u8 {
self.depth self.depth
@ -250,15 +315,12 @@ where
/// # public_key::TestPublicKey as PublicKey, /// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey, /// # private_key::TestPrivateKey as PrivateKey,
/// # }; /// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 32] = // /// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = // /// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; /// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code)?; /// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(chain_code, &xprv.chain_code()); /// assert_eq!(chain_code, &xprv.chain_code());
/// # Ok(())
/// # }
/// ``` /// ```
pub fn chain_code(&self) -> [u8; 32] { pub fn chain_code(&self) -> [u8; 32] {
self.chain_code self.chain_code
@ -280,7 +342,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(seed)?; /// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let path = DerivationPath::default() /// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?) /// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?) /// .chain_push(DerivationIndex::new(0, true)?)
@ -326,7 +388,7 @@ where
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = // /// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(seed)?; /// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let bip44_wallet = DerivationPath::default() /// let bip44_wallet = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?) /// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?) /// .chain_push(DerivationIndex::new(0, true)?)
@ -342,8 +404,8 @@ where
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> { pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?; let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
let mut hmac = let mut hmac = HmacSha512::new_from_slice(&self.chain_code)
HmacSha512::new_from_slice(&self.chain_code).map_err(Error::HmacInvalidLength)?; .expect("HmacSha512 InvalidLength should be infallible");
if index.is_hardened() { if index.is_hardened() {
hmac.update(&[0]); hmac.update(&[0]);
hmac.update(&self.private_key.to_bytes()); hmac.update(&self.private_key.to_bytes());

View File

@ -17,7 +17,7 @@ pub mod public_key;
mod tests; mod tests;
#[doc(inline)] #[doc(inline)]
pub use crate::extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey}; pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
pub use crate::{ pub use crate::{
index::{DerivationIndex, Error as IndexError}, index::{DerivationIndex, Error as IndexError},

View File

@ -19,10 +19,11 @@
//! ``` //! ```
use crate::{ use crate::{
extended_key::private_key::Error as XPrvError, extended_key::private_key::{Error as XPrvError, VariableLengthSeed},
private_key::{PrivateKey, TestPrivateKey}, private_key::{PrivateKey, TestPrivateKey},
DerivationPath, ExtendedPrivateKey, DerivationPath, ExtendedPrivateKey,
}; };
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError}; use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -64,11 +65,12 @@ impl DerivationAlgorithm {
/// ///
/// # Errors /// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported. /// The method may error if the derivation fails or if the algorithm is not supported.
fn derive(&self, seed: Vec<u8>, path: &DerivationPath) -> Result<DerivationResponse> { fn derive(&self, seed: &[u8], path: &DerivationPath) -> Result<DerivationResponse> {
let seed = VariableLengthSeed::new(seed);
match self { match self {
#[cfg(feature = "ed25519")] #[cfg(feature = "ed25519")]
Self::Ed25519 => { Self::Ed25519 => {
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed)?; let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
self.clone(), self.clone(),
@ -77,7 +79,7 @@ impl DerivationAlgorithm {
} }
#[cfg(feature = "secp256k1")] #[cfg(feature = "secp256k1")]
Self::Secp256k1 => { Self::Secp256k1 => {
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed)?; let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
self.clone(), self.clone(),
@ -85,7 +87,7 @@ impl DerivationAlgorithm {
)) ))
} }
Self::Internal => { Self::Internal => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed)?; let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
let derived_key = key.derive_path(path)?; let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv( Ok(DerivationResponse::with_algo_and_xprv(
self.clone(), self.clone(),
@ -110,6 +112,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. /// A derivation request.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct DerivationRequest { pub struct DerivationRequest {
@ -195,7 +209,7 @@ impl DerivationRequest {
/// # } /// # }
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> { pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
// TODO: passphrase support and/or store passphrase within mnemonic // TODO: passphrase support and/or store passphrase within mnemonic
self.derive_with_master_seed(mnemonic.seed(None)?) self.derive_with_master_seed(&mnemonic.seed(None)?)
} }
/// Derive an [`ExtendedPrivateKey`] using the given seed. /// Derive an [`ExtendedPrivateKey`] using the given seed.
@ -219,10 +233,10 @@ impl DerivationRequest {
/// let path: DerivationPath = // /// let path: DerivationPath = //
/// # DerivationPath::default(); /// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path); /// let request = DerivationRequest::new(algo, &path);
/// let response = request.derive_with_master_seed(seed.to_vec())?; /// let response = request.derive_with_master_seed(seed)?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
pub fn derive_with_master_seed(&self, seed: Vec<u8>) -> Result<DerivationResponse> { pub fn derive_with_master_seed(&self, seed: &[u8]) -> Result<DerivationResponse> {
self.algorithm.derive(seed, &self.path) self.algorithm.derive(seed, &self.path)
} }
} }
@ -234,7 +248,7 @@ pub struct DerivationResponse {
pub algorithm: DerivationAlgorithm, pub algorithm: DerivationAlgorithm,
/// The derived private key. /// The derived private key.
pub data: Vec<u8>, pub data: [u8; 32],
/// The chain code, used for further derivation. /// The chain code, used for further derivation.
pub chain_code: [u8; 32], pub chain_code: [u8; 32],
@ -251,7 +265,7 @@ impl DerivationResponse {
) -> Self { ) -> Self {
Self { Self {
algorithm, algorithm,
data: PrivateKey::to_bytes(xprv.private_key()).to_vec(), data: PrivateKey::to_bytes(xprv.private_key()),
chain_code: xprv.chain_code(), chain_code: xprv.chain_code(),
depth: xprv.depth(), depth: xprv.depth(),
} }
@ -272,47 +286,71 @@ pub enum TryFromDerivationResponseError {
} }
#[cfg(feature = "secp256k1")] #[cfg(feature = "secp256k1")]
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> { mod secp256k1 {
use super::*;
use k256::SecretKey;
impl AsAlgorithm for SecretKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Secp256k1
}
}
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SecretKey> {
type Error = TryFromDerivationResponseError; type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> { fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm { match value.algorithm {
DerivationAlgorithm::Secp256k1 => { DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts(
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from) &value.data,
} value.depth,
value.chain_code,
)),
_ => Err(Self::Error::Algorithm), _ => Err(Self::Error::Algorithm),
} }
} }
} }
#[cfg(feature = "secp256k1")] impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SecretKey> {
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
type Error = TryFromDerivationResponseError; type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> { fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<k256::SecretKey>::try_from(&value) ExtendedPrivateKey::<SecretKey>::try_from(&value)
}
}
#[cfg(feature = "ed25519")]
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Ed25519 => {
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
}
_ => Err(Self::Error::Algorithm),
} }
} }
} }
#[cfg(feature = "ed25519")] #[cfg(feature = "ed25519")]
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> { mod ed25519 {
use super::*;
use ed25519_dalek::SigningKey;
impl AsAlgorithm for SigningKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Ed25519
}
}
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SigningKey> {
type Error = TryFromDerivationResponseError; type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> { fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<ed25519_dalek::SigningKey>::try_from(&value) match value.algorithm {
DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
&value.data,
value.depth,
value.chain_code,
)),
_ => Err(Self::Error::Algorithm),
}
}
}
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<SigningKey>::try_from(&value)
}
} }
} }

View File

@ -30,7 +30,8 @@ fn secp256k1() {
} = test; } = test;
// Tests for ExtendedPrivateKey // Tests for ExtendedPrivateKey
let xkey = ExtendedPrivateKey::<SecretKey>::new(seed).unwrap(); let varlen_seed = VariableLengthSeed::new(&seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap(); let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!( assert_eq!(
derived_key.chain_code().as_slice(), derived_key.chain_code().as_slice(),
@ -50,8 +51,8 @@ fn secp256k1() {
// Tests for DerivationRequest // Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain); let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response = request.derive_with_master_seed(seed.clone()).unwrap(); let response = request.derive_with_master_seed(&seed).unwrap();
assert_eq!(&response.data, private_key, "test: {chain}"); assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
} }
} }
} }
@ -75,7 +76,8 @@ fn ed25519() {
} = test; } = test;
// Tests for ExtendedPrivateKey // Tests for ExtendedPrivateKey
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap(); let varlen_seed = VariableLengthSeed::new(&seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap(); let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!( assert_eq!(
derived_key.chain_code().as_slice(), derived_key.chain_code().as_slice(),
@ -95,8 +97,8 @@ fn ed25519() {
// Tests for DerivationRequest // Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain); let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
let response = request.derive_with_master_seed(seed.to_vec()).unwrap(); let response = request.derive_with_master_seed(&seed).unwrap();
assert_eq!(&response.data, private_key, "test: {chain}"); assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
} }
} }
} }
@ -108,7 +110,7 @@ fn panics_with_unhardened_derivation() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f"); let seed = hex!("000102030405060708090a0b0c0d0e0f");
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap(); let xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap()) xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
.unwrap(); .unwrap();
} }
@ -120,7 +122,7 @@ fn panics_at_depth() {
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f"); let seed = hex!("000102030405060708090a0b0c0d0e0f");
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap(); let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
for i in 0..=u32::from(u8::MAX) { for i in 0..=u32::from(u8::MAX) {
xkey = xkey xkey = xkey
.derive_child(&DerivationIndex::new(i, true).unwrap()) .derive_child(&DerivationIndex::new(i, true).unwrap())

View File

@ -7,11 +7,10 @@ license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["openpgp", "openpgp-card", "qrcode"] default = ["openpgp", "openpgp-card", "qrcode", "sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
openpgp = ["sequoia-openpgp", "anyhow"] openpgp = ["sequoia-openpgp", "anyhow"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
qrcode = ["keyfork-qrcode"] qrcode = ["keyfork-qrcode"]
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
[dependencies] [dependencies]
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] } keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] }

View File

@ -13,9 +13,9 @@ use aes_gcm::{
Aes256Gcm, Error as AesError, KeyInit, Nonce, Aes256Gcm, Error as AesError, KeyInit, Nonce,
}; };
use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength}; use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength};
use keyfork_derive_openpgp::derive_util::{ use keyfork_derive_openpgp::{
request::{DerivationAlgorithm, DerivationRequest}, derive_util::{DerivationPath, PathError, VariableLengthSeed},
DerivationPath, PathError, XPrv,
}; };
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
use keyfork_prompt::{ use keyfork_prompt::{
@ -123,6 +123,10 @@ pub enum Error {
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[source] std::io::Error), 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. /// An error occurred while parsing a derivation path.
#[error("Derivation path: {0}")] #[error("Derivation path: {0}")]
DerivationPath(#[from] PathError), DerivationPath(#[from] PathError),
@ -643,13 +647,11 @@ pub fn combine(
// TODO: extract as function // TODO: extract as function
let userid = UserID::from("keyfork-sss"); let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new( let path = DerivationPath::from_str("m/7366512'/0'")?;
DerivationAlgorithm::Ed25519, let seed = VariableLengthSeed::new(&secret);
&DerivationPath::from_str("m/7366512'/0'")?, let xprv = XPrv::new(seed).derive_path(&path)?;
)
.derive_with_master_seed(secret.clone())?;
let derived_cert = keyfork_derive_openpgp::derive( let derived_cert = keyfork_derive_openpgp::derive(
kdr, xprv,
&[KeyFlags::empty().set_certification().set_signing()], &[KeyFlags::empty().set_certification().set_signing()],
&userid, &userid,
)?; )?;
@ -678,15 +680,13 @@ pub fn combine(
/// The function may panic if the metadata can't properly store the certificates used to generate /// The function may panic if the metadata can't properly store the certificates used to generate
/// the encrypted shares. /// the encrypted shares.
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> { pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
let seed = VariableLengthSeed::new(secret);
// build cert to sign encrypted shares // build cert to sign encrypted shares
let userid = UserID::from("keyfork-sss"); let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new( let path = DerivationPath::from_str("m/7366512'/0'")?;
DerivationAlgorithm::Ed25519, let xprv = XPrv::new(seed).derive_path(&path)?;
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.to_vec())?;
let derived_cert = keyfork_derive_openpgp::derive( let derived_cert = keyfork_derive_openpgp::derive(
kdr, xprv,
&[KeyFlags::empty().set_certification().set_signing()], &[KeyFlags::empty().set_certification().set_signing()],
&userid, &userid,
)?; )?;

View File

@ -1,16 +1,16 @@
use super::Keyfork; use super::Keyfork;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use keyfork_derive_openpgp::openpgp::{ use keyfork_derive_openpgp::{
openpgp::{
armor::{Kind, Writer}, armor::{Kind, Writer},
packet::UserID, packet::UserID,
serialize::Marshal, serialize::Marshal,
types::KeyFlags, types::KeyFlags,
},
XPrvKey,
}; };
use keyfork_derive_util::{ use keyfork_derive_util::{DerivationIndex, DerivationPath};
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
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>;
@ -48,12 +48,9 @@ impl DeriveSubcommands {
.set_storage_encryption(), .set_storage_encryption(),
KeyFlags::empty().set_authentication(), KeyFlags::empty().set_authentication(),
]; ];
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let default_userid = UserID::from(user_id.as_str()); 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)?; let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;

View File

@ -5,11 +5,11 @@ use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
use card_backend_pcsc::PcscBackend; 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::openpgp::{self, packet::UserID, types::KeyFlags, Cert}; use keyfork_derive_openpgp::{
use keyfork_derive_util::{ openpgp::{self, packet::UserID, types::KeyFlags, Cert},
request::{DerivationAlgorithm, DerivationRequest}, XPrv,
DerivationIndex, DerivationPath,
}; };
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{PinValidator, Validator}, validators::{PinValidator, Validator},
Message, PromptHandler, Terminal, Message, PromptHandler, Terminal,
@ -21,7 +21,7 @@ pub struct PinLength(usize);
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>;
fn derive_key(seed: &[u8], index: u8) -> Result<Cert> { fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
let subkeys = vec![ let subkeys = vec![
KeyFlags::empty().set_certification(), KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(), KeyFlags::empty().set_signing(),
@ -42,10 +42,9 @@ fn derive_key(seed: &[u8], index: u8) -> Result<Cert> {
.chain_push(chain) .chain_push(chain)
.chain_push(account) .chain_push(account)
.chain_push(subkey); .chain_push(subkey);
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path); let xprv = XPrv::new(seed).derive_path(&path)?;
let response = request.derive_with_master_seed(seed.to_vec())?;
let userid = UserID::from(format!("Keyfork Shard {index}")); 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) Ok(cert)
} }
@ -103,7 +102,7 @@ fn generate_shard_secret(
keys_per_shard: u8, keys_per_shard: u8,
output_file: &Option<PathBuf>, output_file: &Option<PathBuf>,
) -> Result<()> { ) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_size(256 / 8)?; let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?;
let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?; let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?;
let mut certs = vec![]; let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new(); let mut seen_cards: HashSet<String> = HashSet::new();
@ -127,7 +126,7 @@ fn generate_shard_secret(
.to_fn(); .to_fn();
for index in 0..max { for index in 0..max {
let cert = derive_key(&seed, index)?; let cert = derive_key(seed, index)?;
for i in 0..keys_per_shard { for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!( pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}", "Please remove all keys and insert key #{} for user #{}",

View File

@ -7,7 +7,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = [] default = ["bin"]
bin = ["smex"] bin = ["smex"]
[dependencies] [dependencies]

View File

@ -1,6 +1,9 @@
//! Utilities for reading entropy from secure sources. //! Utilities for reading entropy from secure sources.
use std::{fs::{read_dir, read_to_string, File}, io::Read}; use std::{
fs::{read_dir, read_to_string, File},
io::Read,
};
static WARNING_LINKS: [&str; 1] = static WARNING_LINKS: [&str; 1] =
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"]; ["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
@ -84,3 +87,24 @@ pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::E
entropy_file.read_exact(&mut vec[..])?; entropy_file.read_exact(&mut vec[..])?;
Ok(vec) Ok(vec)
} }
/// Read system entropy of a constant size.
///
/// # Errors
/// An error may be returned if an error occurred while reading from the random source.
///
/// # Examples
/// ```rust,no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// let entropy = keyfork_entropy::generate_entropy_of_const_size::<64>()?;
/// assert_eq!(entropy.len(), 64);
/// # Ok(())
/// # }
/// ```
pub fn generate_entropy_of_const_size<const N: usize>() -> Result<[u8; N], std::io::Error> {
let mut output = [0u8; N];
let mut entropy_file = File::open("/dev/urandom")?;
entropy_file.read_exact(&mut output[..])?;
Ok(output)
}

View File

@ -7,7 +7,7 @@ edition = "2021"
license = "MIT" license = "MIT"
[features] [features]
default = [] default = ["bin"]
bin = ["smex"] bin = ["smex"]
[dependencies] [dependencies]

View File

@ -276,7 +276,7 @@ impl Mnemonic {
} }
/// Clone the existing entropy. /// Clone the existing entropy.
#[deprecated] #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> { pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone() self.entropy.clone()
} }
@ -353,8 +353,8 @@ mod tests {
random_handle.read_exact(&mut entropy[..]).unwrap(); random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc(); let wordlist = Wordlist::default().arc();
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap(); let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let new_entropy = mnemonic.entropy(); let new_entropy = mnemonic.as_bytes();
assert_eq!(&new_entropy, entropy); assert_eq!(new_entropy, entropy);
} }
#[test] #[test]

View File

@ -22,8 +22,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3, 3,
transport_validator.to_fn(), transport_validator.to_fn(),
)?; )?;
assert_eq!(mnemonics[0].entropy().len(), 12); assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].entropy().len(), 32); assert_eq!(mnemonics[1].as_bytes().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist( let mnemonics = mgr.prompt_validated_wordlist(
"Enter a 24 and 48-word mnemonic: ", "Enter a 24 and 48-word mnemonic: ",
@ -31,8 +31,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3, 3,
combine_validator.to_fn(), combine_validator.to_fn(),
)?; )?;
assert_eq!(mnemonics[0].entropy().len(), 32); assert_eq!(mnemonics[0].as_bytes().len(), 32);
assert_eq!(mnemonics[1].entropy().len(), 64); assert_eq!(mnemonics[1].as_bytes().len(), 64);
Ok(()) Ok(())
} }