keyfork-derive-util: Flesh out most of secp256k1

This commit is contained in:
Ryan Heywood 2023-08-31 23:10:56 -05:00
parent da09b95bae
commit e850c75879
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
17 changed files with 824 additions and 49 deletions

View File

@ -5,5 +5,25 @@ edition = "2021"
# 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]
default = ["secp256k1"]
secp256k1 = ["k256"]
[dependencies] [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"] } 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"

View File

@ -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<T, E = Error> = std::result::Result<T, E>;

View File

@ -0,0 +1,2 @@
pub mod private_key;
pub mod public_key;

View File

@ -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<T, E = Error> = std::result::Result<T, E>;
pub type ChainCode = [u8; 32];
type HmacSha512 = Hmac<Sha512>;
#[derive(Clone, Serialize, Deserialize)]
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
pub private_key: K,
depth: u8,
pub(crate) chain_code: ChainCode,
}
impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
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<K> ExtendedPrivateKey<K>
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> {
Self::new_internal(seed.as_ref())
}
fn new_internal(seed: &[u8]) -> Result<Self> {
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::<Vec<_>>())?
.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<Self> {
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<Self> {
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"),
})
}
}

View File

@ -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<T, E = Error> = std::result::Result<T, E>;
pub type ChainCode = [u8; 32];
type HmacSha512 = Hmac<Sha512>;
pub struct ExtendedPublicKey<K: PublicKey> {
public_key: K,
depth: u8,
chain_code: ChainCode,
}
impl<K> ExtendedPublicKey<K>
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<Self> {
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,
})
}
}

View File

@ -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<Self> {
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<Self, Self::Err> {
// 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();
}
}

View File

@ -1,23 +1,21 @@
use serde::{Deserialize, Serialize}; #![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
#[derive(Serialize, Deserialize, Clone, Debug)] pub mod error;
pub struct DerivablePath { pub mod extended_key;
pub(crate) path: Vec<u32>, 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? #[cfg(test)]
/* mod tests;
impl DerivablePath {
pub fn new(input: &[&[u8]]) -> DerivablePath { pub use crate::{
DerivablePath { error::{Error, Result},
path: input extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey},
.iter() index::DerivationIndex,
.map(|&word| { path::DerivationPath,
// perform path validation private_key::PrivateKey,
word.to_vec() public_key::PublicKey,
}) };
.collect(),
}
}
}
*/

View File

@ -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<Sha512>;
/// 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<u8>, Vec<u8>)> {
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::<Vec<_>>())?;
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()))
}

View File

@ -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<DerivationIndex>,
}
impl DerivationPath {
pub fn iter(&self) -> impl Iterator<Item = &DerivationIndex> {
self.path.iter()
}
}
impl std::str::FromStr for DerivationPath {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.split('/');
if iter.next() != Some(PREFIX) {
return Err(Error::UnknownPathPrefix);
}
Ok(Self {
path: iter.map(DerivationIndex::from_str).collect::<Result<_>>()?,
})
}
}
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(())
}
}

View File

@ -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<Self, Self::Err>;
}
#[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<Self, Self::Err> {
if other.iter().all(|n| n == &0) {
return Err(PrivateKeyError::NonZero);
}
let other = *other;
// Checked: See above nonzero check
let scalar = Option::<NonZeroScalar>::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::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
.map(Into::into)
.expect("Should be able to make Key"),
)
}
fn public_key(&self) -> Self::PublicKey {
self.public_key()
}
}

View File

@ -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<Self, Self::Err>;
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<Self, Self::Err> {
if other.iter().all(|n| n == &0) {
return Err(PublicKeyError::NonZero);
}
// Checked: See above
let scalar = Option::<NonZeroScalar>::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)
}
}

View File

@ -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::<SecretKey>::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);
}
}

View File

@ -11,15 +11,20 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
multithread = ["tokio/rt-multi-thread"] multithread = ["tokio/rt-multi-thread"]
[dependencies] [dependencies]
bincode = "1.3.3"
dirs = "5.0.1"
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" } keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
keyfork-frame = { version = "0.1.0", path = "../keyfork-frame" } keyfork-frame = { version = "0.1.0", path = "../keyfork-frame" }
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" } 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"] } 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 = { version = "0.1.37", optional = true }
tracing-error = { version = "0.2.0", optional = true } tracing-error = { version = "0.2.0", optional = true }
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] } 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"] }

View File

@ -1,7 +1,7 @@
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]
pub(crate) enum KeycloakdError { pub(crate) enum KeyforkdError {
#[error("No runtime directory found from dirs::runtime_dir()")] #[error("Neither KEYFORKD_SOCKET_PATH nor XDG_RUNTIME_DIR were set, nowhere to mount socket")]
NoRuntimeDir, NoSocketPath,
} }

View File

@ -1,3 +1,5 @@
use std::{collections::HashMap, path::PathBuf};
use keyfork_mnemonic_util::Mnemonic; use keyfork_mnemonic_util::Mnemonic;
use tokio::io::{self, AsyncBufReadExt, BufReader}; use tokio::io::{self, AsyncBufReadExt, BufReader};
@ -15,7 +17,7 @@ use tracing_subscriber::{
mod error; mod error;
mod server; mod server;
mod service; mod service;
use error::KeycloakdError; use error::KeyforkdError;
use server::UnixServer; use server::UnixServer;
use service::Keyforkd; use service::Keyforkd;
@ -52,21 +54,51 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let service = Keyforkd::new(mnemonic); let service = Keyforkd::new(mnemonic);
let mut runtime_dir = dirs::runtime_dir().ok_or(KeycloakdError::NoRuntimeDir)?; let runtime_vars = std::env::vars()
runtime_dir.push("keyforkd"); .filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
#[cfg(feature = "tracing")] .collect::<HashMap<String, String>>();
debug!("ensuring directory exists: {}", runtime_dir.display()); let mut runtime_path: PathBuf;
if !runtime_dir.is_dir() { #[allow(clippy::single_match_else)]
tokio::fs::create_dir(&runtime_dir).await?; 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")] #[cfg(feature = "tracing")]
debug!( debug!(
"binding UNIX socket in runtime dir: {}", "binding UNIX socket in runtime dir: {}",
runtime_dir.display() runtime_path.display()
); );
let mut server = UnixServer::bind(&runtime_dir)?; let mut server = match UnixServer::bind(&runtime_path) {
let _ = server.run(service).await; 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(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use crate::service::{DerivationError, Keyforkd}; 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 keyfork_frame::asyncext::{try_decode_from, try_encode_to};
use std::{ use std::{
io::Error, io::Error,
@ -13,13 +13,13 @@ use tracing::debug;
async fn read_path_from_socket( async fn read_path_from_socket(
socket: &mut UnixStream, socket: &mut UnixStream,
) -> Result<DerivablePath, Box<dyn std::error::Error + Send>> { ) -> Result<DerivationPath, Box<dyn std::error::Error + Send>> {
let data = try_decode_from(socket).await.unwrap(); let data = try_decode_from(socket).await.unwrap();
let path: DerivablePath = bincode::deserialize(&data[..]).unwrap(); let path: DerivationPath = bincode::deserialize(&data[..]).unwrap();
Ok(path) Ok(path)
} }
async fn wait_and_run(app: &mut Keyforkd, path: DerivablePath) -> Result<Vec<u8>, DerivationError> { async fn wait_and_run(app: &mut Keyforkd, path: DerivationPath) -> Result<Vec<u8>, DerivationError> {
app.ready().await?.call(path).await app.ready().await?.call(path).await
} }
@ -33,6 +33,8 @@ impl UnixServer {
let mut path = PathBuf::new(); let mut path = PathBuf::new();
path.extend(address.as_ref().components()); path.extend(address.as_ref().components());
tokio::spawn(async move { tokio::spawn(async move {
#[cfg(feature = "tracing")]
debug!("Binding tokio ctrl-c handler");
let result = tokio::signal::ctrl_c().await; let result = tokio::signal::ctrl_c().await;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
debug!( debug!(
@ -53,6 +55,8 @@ impl UnixServer {
} }
pub async fn run(&mut self, app: Keyforkd) -> Result<(), Box<dyn std::error::Error>> { pub async fn run(&mut self, app: Keyforkd) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tracing")]
debug!("Listening for clients");
loop { loop {
let mut app = app.clone(); let mut app = app.clone();
let (mut socket, _) = self.listener.accept().await?; let (mut socket, _) = self.listener.accept().await?;
@ -63,7 +67,7 @@ impl UnixServer {
Ok(path) => path, Ok(path) => path,
Err(e) => { Err(e) => {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
debug!(%e, "Error reading DerivablePath from socket"); debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>(); let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await; let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@ -78,7 +82,7 @@ impl UnixServer {
Ok(response) => response, Ok(response) => response,
Err(e) => { Err(e) => {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
debug!(%e, "Error reading DerivablePath from socket"); debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>(); let content = e.to_string().bytes().collect::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await; let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]

View File

@ -1,6 +1,6 @@
use std::{future::Future, pin::Pin, sync::Arc, task::Poll}; 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 keyfork_mnemonic_util::Mnemonic;
use thiserror::Error; use thiserror::Error;
use tower::Service; use tower::Service;
@ -22,7 +22,7 @@ impl Keyforkd {
} }
} }
impl Service<DerivablePath> for Keyforkd { impl Service<DerivationPath> for Keyforkd {
type Response = Vec<u8>; type Response = Vec<u8>;
type Error = DerivationError; type Error = DerivationError;
@ -37,7 +37,7 @@ impl Service<DerivablePath> for Keyforkd {
} }
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] #[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); dbg!(&req, &self.mnemonic);
Box::pin(async { Ok(vec![]) }) Box::pin(async { Ok(vec![]) })
} }