From a24a0166cc45eec69f2c4ad1540b097417931a72 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 12 Feb 2024 02:31:22 -0500 Subject: [PATCH] keyforkd-client: add examples and integrity checks --- Cargo.lock | 10 +- crates/daemon/keyforkd-client/Cargo.toml | 8 +- crates/daemon/keyforkd-client/src/lib.rs | 96 ++++++++++++++++++- crates/daemon/keyforkd-client/src/tests.rs | 75 ++++++++++++--- .../src/extended_key/private_key.rs | 2 +- crates/derive/keyfork-derive-util/src/path.rs | 5 + 6 files changed, 173 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66e9d0b..5f1369c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1023,9 +1023,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", @@ -1663,6 +1663,8 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", + "sha2", + "signature", ] [[package]] @@ -1884,14 +1886,14 @@ name = "keyforkd-client" version = "0.1.0" dependencies = [ "bincode", + "ed25519-dalek", + "k256", "keyfork-derive-util", "keyfork-frame", "keyfork-slip10-test-data", "keyforkd", "keyforkd-models", - "tempfile", "thiserror", - "tokio", ] [[package]] diff --git a/crates/daemon/keyforkd-client/Cargo.toml b/crates/daemon/keyforkd-client/Cargo.toml index e7a1748..2d23161 100644 --- a/crates/daemon/keyforkd-client/Cargo.toml +++ b/crates/daemon/keyforkd-client/Cargo.toml @@ -8,8 +8,8 @@ license = "MIT" [features] default = ["ed25519", "secp256k1"] -ed25519 = ["keyfork-derive-util/ed25519"] -secp256k1 = ["keyfork-derive-util/secp256k1"] +ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"] +secp256k1 = ["keyfork-derive-util/secp256k1", "k256"] [dependencies] keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false } @@ -17,9 +17,9 @@ keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame" } keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" } bincode = "1.3.3" thiserror = "1.0.49" +k256 = { version = "0.13.3", optional = true } +ed25519-dalek = { version = "2.1.1", optional = true } [dev-dependencies] keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" } keyforkd = { path = "../keyforkd" } -tempfile = "3.9.0" -tokio = { version = "1.32.0", features = ["rt", "sync", "rt-multi-thread"] } diff --git a/crates/daemon/keyforkd-client/src/lib.rs b/crates/daemon/keyforkd-client/src/lib.rs index 8ba4e79..81720e6 100644 --- a/crates/daemon/keyforkd-client/src/lib.rs +++ b/crates/daemon/keyforkd-client/src/lib.rs @@ -1,6 +1,48 @@ -//! A client for Keyforkd. +//! # The Keyforkd Client +//! +//! Keyfork allows securing the master key and highest-level derivation keys by having derivation +//! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated +//! on a UNIX socket with messages sent using the Keyfork Frame format. +//! +//! Programs using the Keyfork Client should ensure they are built against a compatible version of +//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e., +//! `0.5.x`) will be compatible, but `0.5.x` will not be compatible with `0.6.x`. For versions +//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but +//! `1.x.y` will not be compatible with `2.0.0`. +//! +//! Presently, the Keyfork server only supports the following requests: +//! +//! * Derive Key +//! +//! ## Extended Private Keys +//! +//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended +//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed. +//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two +//! will be derived similarly between the server and the client. +//! +//! # Examples +//! ```rust +//! use std::str::FromStr; +//! +//! use keyforkd_client::Client; +//! use keyfork_derive_util::DerivationPath; +//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey; +//! // use k256::SecretKey as PrivateKey; +//! // use ed25519_dalek::SigningKey as PrivateKey; +//! +//! # let seed = b"funky accordion noises"; +//! # keyforkd::test_util::run_test(seed, |socket_path| { +//! # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); +//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); +//! let mut client = Client::discover_socket().unwrap(); +//! let xprv = client.request_xprv::(&derivation_path).unwrap(); +//! # keyforkd::test_util::Infallible::Ok(()) +//! # }).unwrap(); +//! ``` -use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf}; +pub use std::os::unix::net::UnixStream; +use std::{collections::HashMap, path::PathBuf}; use keyfork_derive_util::{ request::{AsAlgorithm, DerivationRequest}, @@ -90,7 +132,22 @@ pub struct Client { } impl Client { - /// Create a new client from a given already-connected [`UnixStream`]. + /// Create a new client from a given already-connected [`UnixStream`]. This function is + /// provided in case a specific UnixStream has to be used; otherwise, + /// [`Client::discover_socket`] should be preferred. + /// + /// # Examples + /// ```rust + /// use keyforkd_client::{Client, get_socket}; + /// + /// # let seed = b"funky accordion noises"; + /// # keyforkd::test_util::run_test(seed, |socket_path| { + /// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); + /// let mut socket = get_socket().unwrap(); + /// let mut client = Client::new(socket); + /// # keyforkd::test_util::Infallible::Ok(()) + /// # }).unwrap(); + /// ``` pub fn new(socket: UnixStream) -> Self { Self { socket } } @@ -100,6 +157,18 @@ impl Client { /// # Errors /// An error may be returned if the required environment variables were not set or if the /// socket could not be connected to. + /// + /// # Examples + /// ```rust + /// use keyforkd_client::Client; + /// + /// # let seed = b"funky accordion noises"; + /// # keyforkd::test_util::run_test(seed, |socket_path| { + /// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); + /// let mut client = Client::discover_socket().unwrap(); + /// # keyforkd::test_util::Infallible::Ok(()) + /// # }).unwrap(); + /// ``` pub fn discover_socket() -> Result { get_socket().map(|socket| Self { socket }) } @@ -112,6 +181,26 @@ impl Client { /// * Bincode could not serialize the request or deserialize the response. /// * An error occurred in Keyforkd. /// * Keyforkd returned invalid data. + /// + /// # Examples + /// ```rust + /// use std::str::FromStr; + /// + /// use keyforkd_client::Client; + /// use keyfork_derive_util::DerivationPath; + /// # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey; + /// // use k256::SecretKey as PrivateKey; + /// // use ed25519_dalek::SigningKey as PrivateKey; + /// + /// # let seed = b"funky accordion noises"; + /// # keyforkd::test_util::run_test(seed, |socket_path| { + /// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path); + /// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap(); + /// let mut client = Client::discover_socket().unwrap(); + /// let xprv = client.request_xprv::(&derivation_path).unwrap(); + /// # keyforkd::test_util::Infallible::Ok(()) + /// # }).unwrap(); + /// ``` pub fn request_xprv(&mut self, path: &DerivationPath) -> Result> where K: PrivateKey + Clone + AsAlgorithm, @@ -143,6 +232,7 @@ impl Client { /// * 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. + #[doc(hidden)] pub fn request(&mut self, req: &Request) -> Result { try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?; let resp = try_decode_from(&mut self.socket)?; diff --git a/crates/daemon/keyforkd-client/src/tests.rs b/crates/daemon/keyforkd-client/src/tests.rs index 7dae50c..e551b0d 100644 --- a/crates/daemon/keyforkd-client/src/tests.rs +++ b/crates/daemon/keyforkd-client/src/tests.rs @@ -1,11 +1,14 @@ use crate::Client; -use keyforkd::test_util::{run_test, Infallible}; use keyfork_derive_util::{request::*, DerivationPath}; use keyfork_slip10_test_data::test_data; +use keyforkd::test_util::{run_test, Infallible}; use std::{os::unix::net::UnixStream, str::FromStr}; #[test] +#[cfg(feature = "secp256k1")] fn secp256k1_test_suite() { + use k256::SecretKey; + let tests = test_data() .unwrap() .remove(&"secp256k1".to_string()) @@ -13,14 +16,38 @@ fn secp256k1_test_suite() { for seed_test in tests { let seed = seed_test.seed; - run_test(&seed, move |socket_path| { + run_test(&seed, move |socket_path| -> Result<(), Box> { for test in seed_test.tests { let socket = UnixStream::connect(&socket_path).unwrap(); let mut client = Client::new(socket); let chain = DerivationPath::from_str(test.chain).unwrap(); - if chain.len() < 2 { + let chain_len = chain.len(); + if chain_len < 2 { continue; } + // Consistency check: ensure the server and the client can each derive the same + // key using an XPrv, for all but the last XPrv, which is verified after this + for i in 2..chain_len { + // FIXME: Keyfork will only allow one request per session + let socket = UnixStream::connect(&socket_path).unwrap(); + let mut client = Client::new(socket); + let path = DerivationPath::from_str(test.chain).unwrap(); + let left_path = path.inner()[..i] + .iter() + .fold(DerivationPath::default(), |p, i| p.chain_push(i.clone())); + let right_path = path.inner()[i..] + .iter() + .fold(DerivationPath::default(), |p, i| p.chain_push(i.clone())); + let xprv = dbg!(client.request_xprv::(&left_path)).unwrap(); + let derived_xprv = xprv.derive_path(&right_path).unwrap(); + let socket = UnixStream::connect(&socket_path).unwrap(); + let mut client = Client::new(socket); + let keyforkd_xprv = client.request_xprv::(&path).unwrap(); + assert_eq!( + derived_xprv, keyforkd_xprv, + "{left_path} + {right_path} != {path}" + ); + } let req = DerivationRequest::new( DerivationAlgorithm::Secp256k1, &DerivationPath::from_str(test.chain).unwrap(), @@ -29,17 +56,18 @@ fn secp256k1_test_suite() { DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); assert_eq!(&response.data, test.private_key.as_slice()); } - Infallible::Ok(()) - }).unwrap(); + Ok(()) + }) + .unwrap(); } } #[test] +#[cfg(feature = "ed25519")] fn ed25519_test_suite() { - let tests = test_data() - .unwrap() - .remove(&"ed25519".to_string()) - .unwrap(); + use ed25519_dalek::SigningKey; + + let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap(); for seed_test in tests { let seed = seed_test.seed; @@ -48,9 +76,33 @@ fn ed25519_test_suite() { let socket = UnixStream::connect(&socket_path).unwrap(); let mut client = Client::new(socket); let chain = DerivationPath::from_str(test.chain).unwrap(); - if chain.len() < 2 { + let chain_len = chain.len(); + if chain_len < 2 { continue; } + for i in 2..chain_len { + // FIXME: Keyfork will only allow one request per session + let socket = UnixStream::connect(&socket_path).unwrap(); + let mut client = Client::new(socket); + // Consistency check: ensure the server and the client can each derive the same + // key using an XPrv, for all but the last XPrv, which is verified after this + let path = DerivationPath::from_str(test.chain).unwrap(); + let left_path = path.inner()[..i] + .iter() + .fold(DerivationPath::default(), |p, i| p.chain_push(i.clone())); + let right_path = path.inner()[i..] + .iter() + .fold(DerivationPath::default(), |p, i| p.chain_push(i.clone())); + let xprv = dbg!(client.request_xprv::(&left_path)).unwrap(); + let derived_xprv = xprv.derive_path(&right_path).unwrap(); + let socket = UnixStream::connect(&socket_path).unwrap(); + let mut client = Client::new(socket); + let keyforkd_xprv = client.request_xprv::(&path).unwrap(); + assert_eq!( + derived_xprv, keyforkd_xprv, + "{left_path} + {right_path} != {path}" + ); + } let req = DerivationRequest::new( DerivationAlgorithm::Ed25519, &DerivationPath::from_str(test.chain).unwrap(), @@ -60,6 +112,7 @@ fn ed25519_test_suite() { assert_eq!(&response.data, test.private_key.as_slice()); } Infallible::Ok(()) - }).unwrap(); + }) + .unwrap(); } } 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 ad15994..5770092 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 @@ -96,7 +96,7 @@ mod as_private_key { /// Extended private keys derived using BIP-0032. /// /// Generic over types implementing [`PrivateKey`]. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ExtendedPrivateKey { /// The internal private key data. #[serde(with = "serde_with")] diff --git a/crates/derive/keyfork-derive-util/src/path.rs b/crates/derive/keyfork-derive-util/src/path.rs index ce5df57..3241e82 100644 --- a/crates/derive/keyfork-derive-util/src/path.rs +++ b/crates/derive/keyfork-derive-util/src/path.rs @@ -51,6 +51,11 @@ impl DerivationPath { self.path.push(index); } + /// Return the inner path. + pub fn inner(&self) -> &Vec { + &self.path + } + /// Append an index to the path, returning self to allow chaining method calls. /// /// # Examples