From e850c758791eb93600cecd970494c365cabf0318 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 31 Aug 2023 23:10:56 -0500 Subject: [PATCH] keyfork-derive-util: Flesh out most of secp256k1 --- keyfork-derive-util/Cargo.toml | 20 +++ keyfork-derive-util/src/error.rs | 26 +++ keyfork-derive-util/src/extended_key/mod.rs | 2 + .../src/extended_key/private_key.rs | 150 ++++++++++++++++++ .../src/extended_key/public_key.rs | 87 ++++++++++ keyfork-derive-util/src/index.rs | 116 ++++++++++++++ keyfork-derive-util/src/lib.rs | 40 +++-- keyfork-derive-util/src/master_key.rs | 54 +++++++ keyfork-derive-util/src/path.rs | 61 +++++++ keyfork-derive-util/src/private_key.rs | 81 ++++++++++ keyfork-derive-util/src/public_key.rs | 72 +++++++++ keyfork-derive-util/src/tests.rs | 67 ++++++++ keyforkd/Cargo.toml | 15 +- keyforkd/src/error.rs | 6 +- keyforkd/src/main.rs | 54 +++++-- keyforkd/src/server.rs | 16 +- keyforkd/src/service.rs | 6 +- 17 files changed, 824 insertions(+), 49 deletions(-) create mode 100644 keyfork-derive-util/src/error.rs create mode 100644 keyfork-derive-util/src/extended_key/mod.rs create mode 100644 keyfork-derive-util/src/extended_key/private_key.rs create mode 100644 keyfork-derive-util/src/extended_key/public_key.rs create mode 100644 keyfork-derive-util/src/index.rs create mode 100644 keyfork-derive-util/src/master_key.rs create mode 100644 keyfork-derive-util/src/path.rs create mode 100644 keyfork-derive-util/src/private_key.rs create mode 100644 keyfork-derive-util/src/public_key.rs create mode 100644 keyfork-derive-util/src/tests.rs diff --git a/keyfork-derive-util/Cargo.toml b/keyfork-derive-util/Cargo.toml index e7c3b63..d2fb004 100644 --- a/keyfork-derive-util/Cargo.toml +++ b/keyfork-derive-util/Cargo.toml @@ -5,5 +5,25 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["secp256k1"] +secp256k1 = ["k256"] + [dependencies] +# Included in Rust +digest = "0.10.7" +sha2 = "0.10.7" + +# Rust-Crypto ecosystem, not personally audited +ripemd = "0.1.3" +hmac = { version = "0.12.1", features = ["std"] } + +# Personally audited serde = { version = "1.0.186", features = ["derive"] } +thiserror = "1.0.47" + +# Optional, not personally audited +k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true } + +[dev-dependencies] +hex-literal = "0.4.1" diff --git a/keyfork-derive-util/src/error.rs b/keyfork-derive-util/src/error.rs new file mode 100644 index 0000000..5dc4dcb --- /dev/null +++ b/keyfork-derive-util/src/error.rs @@ -0,0 +1,26 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Index is too large, must be less than 0x80000000: {0}")] + IndexTooLarge(u32), + + #[error("Unable to parse integer for index")] + IntParseError(#[from] std::num::ParseIntError), + + #[error("Unable to parse path due to bad path prefix")] + UnknownPathPrefix, + + #[error("Seed length in bits must be divisible by 32")] + BadSeedLength(usize), + + /// 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), + + /// There's a 1 in 2^256 chance this will happen. If it does, I'm sorry. Pick a new mnemonic. + #[error("Seed hash generated 32 bytes of zero, pick a new seed")] + HashedSeedIsZero, +} + +pub type Result = std::result::Result; diff --git a/keyfork-derive-util/src/extended_key/mod.rs b/keyfork-derive-util/src/extended_key/mod.rs new file mode 100644 index 0000000..a37c90a --- /dev/null +++ b/keyfork-derive-util/src/extended_key/mod.rs @@ -0,0 +1,2 @@ +pub mod private_key; +pub mod public_key; diff --git a/keyfork-derive-util/src/extended_key/private_key.rs b/keyfork-derive-util/src/extended_key/private_key.rs new file mode 100644 index 0000000..8eb809e --- /dev/null +++ b/keyfork-derive-util/src/extended_key/private_key.rs @@ -0,0 +1,150 @@ +use crate::{DerivationIndex, DerivationPath, PrivateKey, PublicKey}; + +use hmac::{Hmac, Mac}; +use serde::{Deserialize, Serialize}; +use sha2::Sha512; +use thiserror::Error; + +const KEY_SIZE: usize = 256; + +#[derive(Error, Clone, Debug)] +pub enum Error { + #[error("Seed had an unsuitable length: {0}")] + BadSeedLength(usize), + + #[error("Reached maximum depth for key derivation")] + 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), + + #[error("Unknown error while deriving child key")] + Derivation, +} + +pub type Result = std::result::Result; +pub type ChainCode = [u8; 32]; +type HmacSha512 = Hmac; + +#[derive(Clone, Serialize, Deserialize)] +pub struct ExtendedPrivateKey { + pub private_key: K, + depth: u8, + pub(crate) chain_code: ChainCode, +} + +impl std::fmt::Debug for ExtendedPrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExtendedPrivateKey") + .field("private_key", &"obscured") + .field("depth", &self.depth) + .field("chain_code", &self.chain_code) + .finish() + } +} + +impl ExtendedPrivateKey +where + K: PrivateKey + Clone, +{ + /// Generate a new [`ExtendedPublicKey`] from a seed, ideally from a 12-word or 24-word + /// mnemonic, but may take 16-byte seeds. + /// + /// # Panics + /// + /// The method performs unchecked `try_into()` operations on a constant-sized slice. + /// + /// # Errors + /// + /// An error may be returned if: + /// * The given seed had an incorrect length. + /// * A `HmacSha512` can't be constructed - this should be impossible. + pub fn new(seed: impl AsRef<[u8]>) -> Result { + Self::new_internal(seed.as_ref()) + } + + fn new_internal(seed: &[u8]) -> Result { + let len = seed.len(); + if ![16, 32, 64].contains(&len) { + return Err(Error::BadSeedLength(len)); + } + + let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::>())? + .chain_update(seed) + .finalize() + .into_bytes(); + let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8); + + Ok(Self { + private_key: K::from_bytes(private_key.try_into().expect("Invalid key length")), + depth: 0, + chain_code: chain_code.try_into().expect("Invalid chain code length"), + }) + } + + pub fn public_key(&self) -> K::PublicKey { + self.private_key.public_key() + } + + /// Derive a child using the given [`DerivationPath`]. + /// + /// # Errors + /// + /// An error may be returned under the same circumstances as + /// [`ExtendedPrivateKey::derive_child`]. + pub fn derive_path(&self, path: &DerivationPath) -> Result { + if path.path.is_empty() { + Ok(self.clone()) + } else { + path.iter() + .try_fold(self.clone(), |key, index| key.derive_child(index)) + } + } + + /// Derive a child with a given [`DerivationIndex`]. + /// + /// # Panics + /// + /// The method performs unchecked `try_into()` operations on constant-sized slice. + /// + /// # Errors + /// + /// An error may be returned if: + /// + /// * The depth exceeds the maximum depth [`u8::MAX`]. + /// * A `HmacSha512` can't be constructed - this should be impossible. + /// * Deriving a child key fails. Check the documentation for your [`PrivateKey`]. + pub fn derive_child(&self, index: &DerivationIndex) -> Result { + let depth = self.depth.checked_add(1).ok_or(Error::Depth)?; + + let mut hmac = + HmacSha512::new_from_slice(&self.chain_code).map_err(Error::HmacInvalidLength)?; + if index.is_hardened() { + hmac.update(&[0]); + hmac.update(&self.private_key.to_bytes()); + } else { + hmac.update(&self.private_key.public_key().to_bytes()); + } + hmac.update(&index.to_bytes()); + let result = hmac.finalize().into_bytes(); + let (private_key, chain_code) = result.split_at(KEY_SIZE / 8); + + let private_key = self + .private_key + .derive_child( + &private_key + .try_into() + .expect("Invalid length for private key"), + ) + .map_err(|_| Error::Derivation)?; + + Ok(Self { + private_key, + depth, + chain_code: chain_code + .try_into() + .expect("Invalid length for chain code"), + }) + } +} diff --git a/keyfork-derive-util/src/extended_key/public_key.rs b/keyfork-derive-util/src/extended_key/public_key.rs new file mode 100644 index 0000000..a455738 --- /dev/null +++ b/keyfork-derive-util/src/extended_key/public_key.rs @@ -0,0 +1,87 @@ +use crate::{DerivationIndex, PublicKey}; + +use hmac::{Hmac, Mac}; +use sha2::Sha512; +use thiserror::Error; + +const KEY_SIZE: usize = 256; + +#[derive(Error, Clone, Debug)] +pub enum Error { + #[error("Public keys may not be derived when hardened")] + HardenedIndex, + + #[error("Reached maximum depth for key derivation")] + 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), + + #[error("Unknown error while deriving child key")] + Derivation, +} + +pub type Result = std::result::Result; +pub type ChainCode = [u8; 32]; +type HmacSha512 = Hmac; + +pub struct ExtendedPublicKey { + public_key: K, + depth: u8, + chain_code: ChainCode, +} + +impl ExtendedPublicKey +where + K: PublicKey, +{ + pub fn new(public_key: K, chain_code: ChainCode) -> Self { + Self { + public_key, + depth: 0, + chain_code, + } + } + + /// Derive a child with a given [`DerivationIndex`]. + /// + /// # Panics + /// + /// The method performs unchecked `try_into()` operations on a constant-sized slice. + /// + /// # Errors + /// + /// An error may be returned if: + /// + /// * The depth exceeds the maximum depth [`u8::MAX`]. + /// * A `HmacSha512` can't be constructed - this should be impossible. + /// * Deriving a child key fails. Check the documentation for your [`PublicKey`]. + pub fn derive_child(&self, index: &DerivationIndex) -> Result { + if index.is_hardened() { + return Err(Error::HardenedIndex); + } + + let depth = self.depth.checked_add(1).ok_or(Error::Depth)?; + + let hmac = HmacSha512::new_from_slice(&self.chain_code) + .map_err(Error::from)? + .chain_update(self.public_key.to_bytes()) + .chain_update(index.to_bytes()) + .finalize() + .into_bytes(); + + let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8); + let derived_key = self + .public_key + .derive_child(child_key.try_into().expect("Invalid key length")) + .map_err(|_| Error::Derivation)?; + let chain_code = chain_code.try_into().expect("Invalid chain code length"); + + Ok(Self { + public_key: derived_key, + depth, + chain_code, + }) + } +} diff --git a/keyfork-derive-util/src/index.rs b/keyfork-derive-util/src/index.rs new file mode 100644 index 0000000..779ca27 --- /dev/null +++ b/keyfork-derive-util/src/index.rs @@ -0,0 +1,116 @@ +use crate::error::{Error, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DerivationIndex(pub(crate) u32); + +impl DerivationIndex { + /// Creates a new [`DerivationIndex`]. + /// + /// # Errors + /// + /// Returns an error if the index is larger than the hardened flag. + pub fn new(index: u32, hardened: bool) -> Result { + if index & (0b1 << 31) > 0 { + return Err(Error::IndexTooLarge(index)); + } + Ok(Self(index | (u32::from(hardened) << 31))) + } + + /* + * Probably never used. + pub(crate) fn from_bytes(bytes: [u8; 4]) -> Self { + Self(u32::from_be_bytes(bytes)) + } + */ + + pub(crate) fn to_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } + + pub fn is_hardened(&self) -> bool { + self.0 & (0b1 << 31) != 0 + } +} + +impl std::str::FromStr for DerivationIndex { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + // Returns &str without suffix if suffix is found + let (s, is_hardened) = match s.strip_suffix('\'') { + Some(subslice) => (subslice, true), + None => (s, false), + }; + let index: u32 = s.parse()?; + Self::new(index, is_hardened) + } +} + +impl std::fmt::Display for DerivationIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0 & (u32::MAX >> 1))?; + if self.0 & (0b1 << 31) != 0 { + write!(f, "'")?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + #[should_panic] + fn fails_on_high_index() { + DerivationIndex::new(0x80000001, false).unwrap(); + } + + #[test] + fn has_hardened_bit() { + assert_eq!(DerivationIndex::new(0x0, true).unwrap().0, 0b1 << 31); + } + + #[test] + fn misc_values() -> Result<()> { + assert_eq!(DerivationIndex::new(0x80000000 - 1, true)?.0, u32::MAX); + assert_eq!(DerivationIndex::new(0x2, false)?.0, 2); + assert_eq!(DerivationIndex::new(0x00ABCDEF, true)?.0, 0x80ABCDEF); + assert_eq!(DerivationIndex::new(0x00ABCDEF, false)?.0, 0x00ABCDEF); + Ok(()) + } + + #[test] + fn from_str() -> Result<()> { + assert_eq!(DerivationIndex::from_str("100000")?.0, 100000); + assert_eq!( + DerivationIndex::from_str("100000'")?.0, + (0b1 << 31) + 100000 + ); + Ok(()) + } + + #[test] + fn display() -> Result<()> { + assert_eq!(&DerivationIndex::new(3232, false)?.to_string(), "3232"); + assert_eq!(&DerivationIndex::new(3232, true)?.to_string(), "3232'"); + Ok(()) + } + + #[test] + fn equivalency() -> Result<()> { + let values = ["123456'", "123456", "1726562", "0'", "0"]; + for value in values { + assert_eq!(value, DerivationIndex::from_str(value)?.to_string()); + } + Ok(()) + } + + #[test] + #[should_panic] + fn from_str_fails_on_high_index() { + DerivationIndex::from_str(&0x80000001u32.to_string()).unwrap(); + } +} diff --git a/keyfork-derive-util/src/lib.rs b/keyfork-derive-util/src/lib.rs index 83382e1..b1e075e 100644 --- a/keyfork-derive-util/src/lib.rs +++ b/keyfork-derive-util/src/lib.rs @@ -1,23 +1,21 @@ -use serde::{Deserialize, Serialize}; +#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)] -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct DerivablePath { - pub(crate) path: Vec, -} +pub mod error; +pub mod extended_key; +pub mod index; +pub mod master_key; +pub mod path; +pub mod private_key; +pub mod public_key; -// TODO: move DerivablePath into a models crate for clients to produce? -/* -impl DerivablePath { - pub fn new(input: &[&[u8]]) -> DerivablePath { - DerivablePath { - path: input - .iter() - .map(|&word| { - // perform path validation - word.to_vec() - }) - .collect(), - } - } -} -*/ +#[cfg(test)] +mod tests; + +pub use crate::{ + error::{Error, Result}, + extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey}, + index::DerivationIndex, + path::DerivationPath, + private_key::PrivateKey, + public_key::PublicKey, +}; diff --git a/keyfork-derive-util/src/master_key.rs b/keyfork-derive-util/src/master_key.rs new file mode 100644 index 0000000..a8cbe5e --- /dev/null +++ b/keyfork-derive-util/src/master_key.rs @@ -0,0 +1,54 @@ +use crate::error::{Error, Result}; + +use hmac::{Hmac, Mac}; +use sha2::Sha512; + +pub trait MasterKey<'a> { + /// Return the textual content used to derive the master key, as specified in BIP 0032 and SLIP + /// 0010. For example, a secp256k1 master key would use the textual content "Bitcoin seed", to + /// ensure compatibility with BIP 0032, despite the key being used for more functionality than + /// purely Bitcoin. + fn key() -> &'static str; + + /// Some key algorithms, such as Ed25519, allow 0 as a valid private key. Those algorithhms + /// should override this method to indicate as such. + fn is_zero_valid_private_key() -> bool { + false + } + + /// Return the seed used to derive the Master Key, with a size between 128 to 512 bits. Most + /// seeds should be 256 bits, the largest size available from a BIP-0039 mnemonic. + fn seed(&self) -> &'a [u8]; +} + +type HmacSha512 = Hmac; + +/// Generate a Master Secret Key and Chain Code (what is this used for?). +/// +/// # Errors +/// +/// An error may be returned if: +/// * The `HmacSha512` key returned by `T::key()` is invalid. This should never happen. +/// * The generated master key is all zeroes. This has a cosmically small chance of happening. +pub fn generate<'a, T: MasterKey<'a>>(generator: &T) -> Result<(Vec, Vec)> { + let seed = generator.seed(); + let len = seed.len(); + if len * 8 % 32 != 0 { + return Err(Error::BadSeedLength(len)); + } + + let mut hmac = HmacSha512::new_from_slice(&T::key().bytes().collect::>())?; + hmac.update(seed); + let result = hmac.finalize().into_bytes(); + let left = &result[..32]; + let right = &result[32..]; + if left.iter().all(|n| n == &0) { + // Wow. Impressive. + // NOTE: SLIP-0010 says to retry if this happens, but uses some weird terminology to do so. + // I do not trust it. BIP-0032 says this key is "invalid", with no instructions to retry. + // This is a low enough chance I am fine with it being freak-of-nature error. + return Err(Error::HashedSeedIsZero); + } + + Ok((left.to_vec(), right.to_vec())) +} diff --git a/keyfork-derive-util/src/path.rs b/keyfork-derive-util/src/path.rs new file mode 100644 index 0000000..9adb4a3 --- /dev/null +++ b/keyfork-derive-util/src/path.rs @@ -0,0 +1,61 @@ +use crate::error::{Error, Result}; +use crate::index::DerivationIndex; +use serde::{Deserialize, Serialize}; + +const PREFIX: &str = "m"; + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct DerivationPath { + pub(crate) path: Vec, +} + +impl DerivationPath { + pub fn iter(&self) -> impl Iterator { + self.path.iter() + } +} + +impl std::str::FromStr for DerivationPath { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut iter = s.split('/'); + if iter.next() != Some(PREFIX) { + return Err(Error::UnknownPathPrefix); + } + Ok(Self { + path: iter.map(DerivationIndex::from_str).collect::>()?, + }) + } +} + +impl std::fmt::Display for DerivationPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{PREFIX}")?; + for index in self.iter() { + write!(f, "/{index}")?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + #[should_panic] + fn requires_master_path() { + DerivationPath::from_str("1234/5678'").unwrap(); + } + + #[test] + fn equivalency() -> Result<()> { + let paths = ["m/1234'/5678", "m/44'/0'/0'", "m"]; + for path in paths { + assert_eq!(&DerivationPath::from_str(path)?.to_string(), path); + } + Ok(()) + } +} diff --git a/keyfork-derive-util/src/private_key.rs b/keyfork-derive-util/src/private_key.rs new file mode 100644 index 0000000..a94231d --- /dev/null +++ b/keyfork-derive-util/src/private_key.rs @@ -0,0 +1,81 @@ +use crate::PublicKey; + +use thiserror::Error; + +pub type PrivateKeyBytes = [u8; 32]; + +pub trait PrivateKey: Sized { + type PublicKey: PublicKey; + type Err: std::error::Error; + + fn from_bytes(b: &PrivateKeyBytes) -> Self; + fn to_bytes(&self) -> PrivateKeyBytes; + + fn is_zero_valid_public_key() -> bool { + false + } + fn key() -> &'static str; + + fn public_key(&self) -> Self::PublicKey; + + /// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`. + /// + /// # Errors + /// + /// An error may be returned if: + /// * A nonzero `other` is provided. + /// * An error specific to the given algorithm was encountered. + fn derive_child(&self, other: &PrivateKeyBytes) -> Result; +} + +#[derive(Clone, Debug, Error)] +pub enum PrivateKeyError { + #[error("The provided private key must be nonzero, but is not")] + NonZero, + + #[error("Unable to convert point to key")] + PointToKey(#[from] k256::elliptic_curve::Error), +} + +#[cfg(feature = "secp256k1")] +use k256::NonZeroScalar; + +#[cfg(feature = "secp256k1")] +impl PrivateKey for k256::SecretKey { + type Err = PrivateKeyError; + type PublicKey = k256::PublicKey; + + fn key() -> &'static str { + "Bitcoin seed" + } + + fn from_bytes(b: &PrivateKeyBytes) -> Self { + Self::from_slice(b).expect("Invalid private key bytes") + } + + fn to_bytes(&self) -> PrivateKeyBytes { + // Note: Safety assured by type returned from EncodedPoint + self.to_bytes().into() + } + + fn derive_child(&self, other: &PrivateKeyBytes) -> Result { + if other.iter().all(|n| n == &0) { + return Err(PrivateKeyError::NonZero); + } + let other = *other; + // Checked: See above nonzero check + let scalar = Option::::from(NonZeroScalar::from_repr(other.into())) + .expect("Should have been able to get a NonZeroScalar"); + + let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref(); + Ok( + Option::::from(NonZeroScalar::new(derived_scalar)) + .map(Into::into) + .expect("Should be able to make Key"), + ) + } + + fn public_key(&self) -> Self::PublicKey { + self.public_key() + } +} diff --git a/keyfork-derive-util/src/public_key.rs b/keyfork-derive-util/src/public_key.rs new file mode 100644 index 0000000..9412357 --- /dev/null +++ b/keyfork-derive-util/src/public_key.rs @@ -0,0 +1,72 @@ +use crate::private_key::PrivateKeyBytes; + +use digest::Digest; +use ripemd::Ripemd160; +use sha2::Sha256; +use thiserror::Error; + +pub type PublicKeyBytes = [u8; 33]; + +pub trait PublicKey: Sized { + type Err: std::error::Error; + + fn from_bytes(b: &PublicKeyBytes) -> Self; + fn to_bytes(&self) -> PublicKeyBytes; + + /// Derive a child [`PublicKey`] with given `PrivateKeyBytes`. + /// + /// # Errors + /// + /// An error may be returned if: + /// * A nonzero `other` is provided. + /// * An error specific to the given algorithm was encountered. + fn derive_child(&self, other: PrivateKeyBytes) -> Result; + + fn fingerprint(&self) -> [u8; 4] { + let hash = Sha256::new().chain_update(self.to_bytes()).finalize(); + let hash = Ripemd160::new().chain_update(hash).finalize(); + // Note: Safety assured by type returned from Ripemd160 + hash[..4].try_into().unwrap() + } +} + +#[derive(Clone, Debug, Error)] +pub enum PublicKeyError { + #[error("The provided public key must be nonzero, but is not")] + NonZero, + + #[error("Unable to convert point to key")] + PointToKey(#[from] k256::elliptic_curve::Error), +} + +#[cfg(feature = "secp256k1")] +use k256::{ + elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint}, + AffinePoint, NonZeroScalar, +}; + +#[cfg(feature = "secp256k1")] +impl PublicKey for k256::PublicKey { + type Err = PublicKeyError; + + fn from_bytes(b: &PublicKeyBytes) -> Self { + Self::from_sec1_bytes(b).expect("Invalid public key bytes") + } + + fn to_bytes(&self) -> PublicKeyBytes { + // Note: Safety assured by type returned from EncodedPoint + self.to_encoded_point(true).as_bytes().try_into().unwrap() + } + + fn derive_child(&self, other: PrivateKeyBytes) -> Result { + if other.iter().all(|n| n == &0) { + return Err(PublicKeyError::NonZero); + } + // Checked: See above + let scalar = Option::::from(NonZeroScalar::from_repr(other.into())) + .expect("Should have been able to get a NonZeroScalar"); + + let point = self.to_projective() + (AffinePoint::generator() * *scalar); + Self::from_affine(point.into()).map_err(From::from) + } +} diff --git a/keyfork-derive-util/src/tests.rs b/keyfork-derive-util/src/tests.rs new file mode 100644 index 0000000..eec2d7b --- /dev/null +++ b/keyfork-derive-util/src/tests.rs @@ -0,0 +1,67 @@ +use crate::*; +use hex_literal::hex; +use k256::SecretKey; +use std::str::FromStr; + +// Pulled from: https://github.com/satoshilabs/slips/blob/master/slip-0010.md + +#[test] +fn example() { + // seed, chain, chain code, private, public + let tests = [( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m").unwrap(), + hex!("873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"), + hex!("e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"), + hex!("0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"), + ), ( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m/0'").unwrap(), + hex!("47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141"), + hex!("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea"), + hex!("035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56"), + ), ( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m/0'/1").unwrap(), + hex!("2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19"), + hex!("3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368"), + hex!("03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c"), + ), ( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m/0'/1/2'").unwrap(), + hex!("04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f"), + hex!("cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca"), + hex!("0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2"), + ), ( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m/0'/1/2'/2").unwrap(), + hex!("cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd"), + hex!("0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4"), + hex!("02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29"), + ), ( + &hex!("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")[..], + DerivationPath::from_str("m").unwrap(), + hex!("60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689"), + hex!("4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e"), + hex!("03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7"), + ), ( + &hex!("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")[..], + DerivationPath::from_str("m/0").unwrap(), + hex!("f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c"), + hex!("abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e"), + hex!("02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea"), + ), ( + &hex!("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")[..], + DerivationPath::from_str("m/0/2147483647'/1/2147483646'/2").unwrap(), + hex!("9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271"), + hex!("bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23"), + hex!("024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c"), + )]; + for (seed, chain, chain_code, private_key, public_key) in tests { + let xkey = ExtendedPrivateKey::::new(seed).unwrap(); + let derived_key = xkey.derive_path(&chain).unwrap(); + assert_eq!(derived_key.chain_code, chain_code); + assert_eq!(derived_key.private_key.to_bytes().as_slice(), private_key); + assert_eq!(derived_key.public_key().to_bytes(), public_key); + } +} diff --git a/keyforkd/Cargo.toml b/keyforkd/Cargo.toml index ea426b8..5d05c85 100644 --- a/keyforkd/Cargo.toml +++ b/keyforkd/Cargo.toml @@ -11,15 +11,20 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri multithread = ["tokio/rt-multi-thread"] [dependencies] -bincode = "1.3.3" -dirs = "5.0.1" keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" } keyfork-frame = { version = "0.1.0", path = "../keyfork-frame" } keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" } -serde = { version = "1.0.186", features = ["derive"] } -thiserror = "1.0.47" + +# Not personally audited +bincode = "1.3.3" + +# Ecosystem trust, not personally audited tokio = { version = "1.32.0", features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] } -tower = { version = "0.4.13", features = ["tokio", "util"] } tracing = { version = "0.1.37", optional = true } tracing-error = { version = "0.2.0", optional = true } tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] } +tower = { version = "0.4.13", features = ["tokio", "util"] } + +# Personally audited +thiserror = "1.0.47" +serde = { version = "1.0.186", features = ["derive"] } diff --git a/keyforkd/src/error.rs b/keyforkd/src/error.rs index 08bbe4f..e8465f5 100644 --- a/keyforkd/src/error.rs +++ b/keyforkd/src/error.rs @@ -1,7 +1,7 @@ use thiserror::Error; #[derive(Debug, Clone, Error)] -pub(crate) enum KeycloakdError { - #[error("No runtime directory found from dirs::runtime_dir()")] - NoRuntimeDir, +pub(crate) enum KeyforkdError { + #[error("Neither KEYFORKD_SOCKET_PATH nor XDG_RUNTIME_DIR were set, nowhere to mount socket")] + NoSocketPath, } diff --git a/keyforkd/src/main.rs b/keyforkd/src/main.rs index 9606032..082809f 100644 --- a/keyforkd/src/main.rs +++ b/keyforkd/src/main.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, path::PathBuf}; + use keyfork_mnemonic_util::Mnemonic; use tokio::io::{self, AsyncBufReadExt, BufReader}; @@ -15,7 +17,7 @@ use tracing_subscriber::{ mod error; mod server; mod service; -use error::KeycloakdError; +use error::KeyforkdError; use server::UnixServer; use service::Keyforkd; @@ -52,21 +54,51 @@ async fn main() -> Result<(), Box> { let service = Keyforkd::new(mnemonic); - let mut runtime_dir = dirs::runtime_dir().ok_or(KeycloakdError::NoRuntimeDir)?; - runtime_dir.push("keyforkd"); - #[cfg(feature = "tracing")] - debug!("ensuring directory exists: {}", runtime_dir.display()); - if !runtime_dir.is_dir() { - tokio::fs::create_dir(&runtime_dir).await?; + let runtime_vars = std::env::vars() + .filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str())) + .collect::>(); + let mut runtime_path: PathBuf; + #[allow(clippy::single_match_else)] + match runtime_vars.get("KEYFORKD_SOCKET_PATH") { + Some(occupied) => { + runtime_path = PathBuf::from(occupied); + } + None => { + runtime_path = PathBuf::from( + runtime_vars + .get("XDG_RUNTIME_DIR") + .ok_or(KeyforkdError::NoSocketPath)?, + ); + runtime_path.push("keyforkd"); + #[cfg(feature = "tracing")] + debug!("ensuring directory exists: {}", runtime_path.display()); + if !runtime_path.is_dir() { + tokio::fs::create_dir(&runtime_path).await?; + } + runtime_path.push("keyforkd.sock"); + } } - runtime_dir.push("keyforkd.sock"); + #[cfg(feature = "tracing")] debug!( "binding UNIX socket in runtime dir: {}", - runtime_dir.display() + runtime_path.display() ); - let mut server = UnixServer::bind(&runtime_dir)?; - let _ = server.run(service).await; + let mut server = match UnixServer::bind(&runtime_path) { + Ok(s) => s, + Err(e) => { + #[cfg(feature = "tracing")] + debug!(%e, "Encountered error attempting to bind socket: {}", runtime_path.display()); + return Err(e.into()); + } + }; + match server.run(service).await { + Ok(_) => (), + Err(e) => { + #[cfg(feature = "tracing")] + debug!(%e, "Encountered error while running"); + } + } Ok(()) } diff --git a/keyforkd/src/server.rs b/keyforkd/src/server.rs index 87dfddd..35e0fdf 100644 --- a/keyforkd/src/server.rs +++ b/keyforkd/src/server.rs @@ -1,5 +1,5 @@ use crate::service::{DerivationError, Keyforkd}; -use keyfork_derive_util::DerivablePath; +use keyfork_derive_util::DerivationPath; use keyfork_frame::asyncext::{try_decode_from, try_encode_to}; use std::{ io::Error, @@ -13,13 +13,13 @@ use tracing::debug; async fn read_path_from_socket( socket: &mut UnixStream, -) -> Result> { +) -> Result> { let data = try_decode_from(socket).await.unwrap(); - let path: DerivablePath = bincode::deserialize(&data[..]).unwrap(); + let path: DerivationPath = bincode::deserialize(&data[..]).unwrap(); Ok(path) } -async fn wait_and_run(app: &mut Keyforkd, path: DerivablePath) -> Result, DerivationError> { +async fn wait_and_run(app: &mut Keyforkd, path: DerivationPath) -> Result, DerivationError> { app.ready().await?.call(path).await } @@ -33,6 +33,8 @@ impl UnixServer { let mut path = PathBuf::new(); path.extend(address.as_ref().components()); tokio::spawn(async move { + #[cfg(feature = "tracing")] + debug!("Binding tokio ctrl-c handler"); let result = tokio::signal::ctrl_c().await; #[cfg(feature = "tracing")] debug!( @@ -53,6 +55,8 @@ impl UnixServer { } pub async fn run(&mut self, app: Keyforkd) -> Result<(), Box> { + #[cfg(feature = "tracing")] + debug!("Listening for clients"); loop { let mut app = app.clone(); let (mut socket, _) = self.listener.accept().await?; @@ -63,7 +67,7 @@ impl UnixServer { Ok(path) => path, Err(e) => { #[cfg(feature = "tracing")] - debug!(%e, "Error reading DerivablePath from socket"); + debug!(%e, "Error reading DerivationPath from socket"); let content = e.to_string().bytes().collect::>(); let result = try_encode_to(&content[..], &mut socket).await; #[cfg(feature = "tracing")] @@ -78,7 +82,7 @@ impl UnixServer { Ok(response) => response, Err(e) => { #[cfg(feature = "tracing")] - debug!(%e, "Error reading DerivablePath from socket"); + debug!(%e, "Error reading DerivationPath from socket"); let content = e.to_string().bytes().collect::>(); let result = try_encode_to(&content[..], &mut socket).await; #[cfg(feature = "tracing")] diff --git a/keyforkd/src/service.rs b/keyforkd/src/service.rs index f0de0fd..f576a74 100644 --- a/keyforkd/src/service.rs +++ b/keyforkd/src/service.rs @@ -1,6 +1,6 @@ use std::{future::Future, pin::Pin, sync::Arc, task::Poll}; -use keyfork_derive_util::DerivablePath; +use keyfork_derive_util::DerivationPath; use keyfork_mnemonic_util::Mnemonic; use thiserror::Error; use tower::Service; @@ -22,7 +22,7 @@ impl Keyforkd { } } -impl Service for Keyforkd { +impl Service for Keyforkd { type Response = Vec; type Error = DerivationError; @@ -37,7 +37,7 @@ impl Service for Keyforkd { } #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] - fn call(&mut self, req: DerivablePath) -> Self::Future { + fn call(&mut self, req: DerivationPath) -> Self::Future { dbg!(&req, &self.mnemonic); Box::pin(async { Ok(vec![]) }) }