keyforkd-client: add examples and integrity checks

This commit is contained in:
Ryan Heywood 2024-02-12 02:31:22 -05:00
parent 1209549532
commit a24a0166cc
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
6 changed files with 173 additions and 23 deletions

10
Cargo.lock generated
View File

@ -1023,9 +1023,9 @@ dependencies = [
[[package]] [[package]]
name = "ed25519-dalek" name = "ed25519-dalek"
version = "2.1.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [ dependencies = [
"curve25519-dalek", "curve25519-dalek",
"ed25519", "ed25519",
@ -1663,6 +1663,8 @@ dependencies = [
"ecdsa", "ecdsa",
"elliptic-curve", "elliptic-curve",
"once_cell", "once_cell",
"sha2",
"signature",
] ]
[[package]] [[package]]
@ -1884,14 +1886,14 @@ name = "keyforkd-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"ed25519-dalek",
"k256",
"keyfork-derive-util", "keyfork-derive-util",
"keyfork-frame", "keyfork-frame",
"keyfork-slip10-test-data", "keyfork-slip10-test-data",
"keyforkd", "keyforkd",
"keyforkd-models", "keyforkd-models",
"tempfile",
"thiserror", "thiserror",
"tokio",
] ]
[[package]] [[package]]

View File

@ -8,8 +8,8 @@ license = "MIT"
[features] [features]
default = ["ed25519", "secp256k1"] default = ["ed25519", "secp256k1"]
ed25519 = ["keyfork-derive-util/ed25519"] ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
secp256k1 = ["keyfork-derive-util/secp256k1"] secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
[dependencies] [dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false } 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" } keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
bincode = "1.3.3" bincode = "1.3.3"
thiserror = "1.0.49" thiserror = "1.0.49"
k256 = { version = "0.13.3", optional = true }
ed25519-dalek = { version = "2.1.1", optional = true }
[dev-dependencies] [dev-dependencies]
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" } keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" }
keyforkd = { path = "../keyforkd" } keyforkd = { path = "../keyforkd" }
tempfile = "3.9.0"
tokio = { version = "1.32.0", features = ["rt", "sync", "rt-multi-thread"] }

View File

@ -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::<PrivateKey>(&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::{ use keyfork_derive_util::{
request::{AsAlgorithm, DerivationRequest}, request::{AsAlgorithm, DerivationRequest},
@ -90,7 +132,22 @@ pub struct Client {
} }
impl 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 { pub fn new(socket: UnixStream) -> Self {
Self { socket } Self { socket }
} }
@ -100,6 +157,18 @@ impl Client {
/// # Errors /// # Errors
/// An error may be returned if the required environment variables were not set or if the /// An error may be returned if the required environment variables were not set or if the
/// socket could not be connected to. /// 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<Self> { pub fn discover_socket() -> Result<Self> {
get_socket().map(|socket| Self { socket }) get_socket().map(|socket| Self { socket })
} }
@ -112,6 +181,26 @@ impl Client {
/// * Bincode could not serialize the request or deserialize the response. /// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd. /// * An error occurred in Keyforkd.
/// * Keyforkd returned invalid data. /// * 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::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap();
/// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>> pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
where where
K: PrivateKey + Clone + AsAlgorithm, K: PrivateKey + Clone + AsAlgorithm,
@ -143,6 +232,7 @@ impl Client {
/// * Reading or writing from or to the socket encountered an error. /// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response. /// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd. /// * An error occurred in Keyforkd.
#[doc(hidden)]
pub fn request(&mut self, req: &Request) -> Result<Response> { pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?; try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?; let resp = try_decode_from(&mut self.socket)?;

View File

@ -1,11 +1,14 @@
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 keyforkd::test_util::{run_test, Infallible};
use std::{os::unix::net::UnixStream, str::FromStr}; use std::{os::unix::net::UnixStream, str::FromStr};
#[test] #[test]
#[cfg(feature = "secp256k1")]
fn secp256k1_test_suite() { fn secp256k1_test_suite() {
use k256::SecretKey;
let tests = test_data() let tests = test_data()
.unwrap() .unwrap()
.remove(&"secp256k1".to_string()) .remove(&"secp256k1".to_string())
@ -13,14 +16,38 @@ fn secp256k1_test_suite() {
for seed_test in tests { for seed_test in tests {
let seed = seed_test.seed; let seed = seed_test.seed;
run_test(&seed, move |socket_path| { run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests { for test in seed_test.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();
if chain.len() < 2 { let chain_len = chain.len();
if chain_len < 2 {
continue; 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::<SecretKey>(&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::<SecretKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new( let req = DerivationRequest::new(
DerivationAlgorithm::Secp256k1, DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(), &DerivationPath::from_str(test.chain).unwrap(),
@ -29,17 +56,18 @@ fn secp256k1_test_suite() {
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.as_slice()); assert_eq!(&response.data, test.private_key.as_slice());
} }
Infallible::Ok(()) Ok(())
}).unwrap(); })
.unwrap();
} }
} }
#[test] #[test]
#[cfg(feature = "ed25519")]
fn ed25519_test_suite() { fn ed25519_test_suite() {
let tests = test_data() use ed25519_dalek::SigningKey;
.unwrap()
.remove(&"ed25519".to_string()) let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
.unwrap();
for seed_test in tests { for seed_test in tests {
let seed = seed_test.seed; let seed = seed_test.seed;
@ -48,9 +76,33 @@ fn ed25519_test_suite() {
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();
if chain.len() < 2 { let chain_len = chain.len();
if chain_len < 2 {
continue; 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::<SigningKey>(&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::<SigningKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new( let req = DerivationRequest::new(
DerivationAlgorithm::Ed25519, DerivationAlgorithm::Ed25519,
&DerivationPath::from_str(test.chain).unwrap(), &DerivationPath::from_str(test.chain).unwrap(),
@ -60,6 +112,7 @@ fn ed25519_test_suite() {
assert_eq!(&response.data, test.private_key.as_slice()); assert_eq!(&response.data, test.private_key.as_slice());
} }
Infallible::Ok(()) Infallible::Ok(())
}).unwrap(); })
.unwrap();
} }
} }

View File

@ -96,7 +96,7 @@ mod as_private_key {
/// 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, PartialEq, Eq)]
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")] #[serde(with = "serde_with")]

View File

@ -51,6 +51,11 @@ impl DerivationPath {
self.path.push(index); self.path.push(index);
} }
/// Return the inner path.
pub fn inner(&self) -> &Vec<DerivationIndex> {
&self.path
}
/// Append an index to the path, returning self to allow chaining method calls. /// Append an index to the path, returning self to allow chaining method calls.
/// ///
/// # Examples /// # Examples