Compare commits

...

2 Commits

Author SHA1 Message Date
Ryan Heywood 5424e66aed
**/Cargo.toml: refactorings 2023-08-31 23:11:42 -05:00
Ryan Heywood e850c75879
keyfork-derive-util: Flesh out most of secp256k1 2023-08-31 23:10:56 -05:00
21 changed files with 1017 additions and 88 deletions

223
Cargo.lock generated
View File

@ -41,6 +41,18 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
version = "1.3.3"
@ -67,12 +79,6 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -115,6 +121,12 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "const-oid"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "cpufeatures"
version = "0.2.9"
@ -124,6 +136,18 @@ dependencies = [
"libc",
]
[[package]]
name = "crypto-bigint"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15"
dependencies = [
"generic-array",
"rand_core",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -134,6 +158,16 @@ dependencies = [
"typenum",
]
[[package]]
name = "der"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -142,27 +176,38 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "dirs"
version = "5.0.1"
name = "ecdsa"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
dependencies = [
"dirs-sys",
"der",
"elliptic-curve",
"signature",
"spki",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
name = "elliptic-curve"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"pkcs8",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
[[package]]
@ -171,6 +216,16 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "ff"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core",
"subtle",
]
[[package]]
name = "futures-core"
version = "0.3.28"
@ -203,6 +258,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
"zeroize",
]
[[package]]
@ -222,6 +278,17 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle",
]
[[package]]
name = "hermit-abi"
version = "0.3.2"
@ -234,6 +301,21 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "insta"
version = "1.31.0"
@ -253,11 +335,30 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "k256"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc"
dependencies = [
"cfg-if",
"ecdsa",
"elliptic-curve",
"once_cell",
]
[[package]]
name = "keyfork-derive-util"
version = "0.1.0"
dependencies = [
"digest",
"hex-literal",
"hmac",
"k256",
"ripemd",
"serde",
"sha2",
"thiserror",
]
[[package]]
@ -293,7 +394,6 @@ name = "keyforkd"
version = "0.1.0"
dependencies = [
"bincode",
"dirs",
"keyfork-derive-util",
"keyfork-frame",
"keyfork-mnemonic-util",
@ -400,12 +500,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "overload"
version = "0.1.1"
@ -444,6 +538,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
@ -463,23 +567,12 @@ dependencies = [
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"redox_syscall",
"thiserror",
]
[[package]]
@ -526,6 +619,15 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "ripemd"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
dependencies = [
"digest",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -538,6 +640,20 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "serde"
version = "1.0.186"
@ -598,6 +714,15 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
dependencies = [
"rand_core",
]
[[package]]
name = "similar"
version = "2.2.1"
@ -620,6 +745,22 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "spki"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.29"
@ -1008,3 +1149,9 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zeroize"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"

View File

@ -1,5 +1,6 @@
[workspace]
resolver = "2"
members = [
"keyfork-mnemonic-generate",
"keyfork-mnemonic-util",

View File

@ -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"

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 struct DerivablePath {
pub(crate) path: Vec<u32>,
}
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,
};

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

@ -10,9 +10,14 @@ default = ["async"]
async = ["dep:tokio"]
[dependencies]
hex = "0.4.3"
# Included in Rust
sha2 = "0.10.7"
# Personally audited
thiserror = "1.0.47"
hex = "0.4.3"
# Optional, not personally audited
tokio = { version = "1.32.0", optional = true, features = ["io-util"] }
[dev-dependencies]

View File

@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Included in rust
sha2 = "0.10.7"
[dev-dependencies]

View File

@ -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"] }

View File

@ -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,
}

View File

@ -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<dyn std::error::Error>> {
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::<HashMap<String, String>>();
let mut runtime_path: PathBuf;
#[allow(clippy::single_match_else)]
match runtime_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
runtime_path = PathBuf::from(occupied);
}
runtime_dir.push("keyforkd.sock");
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");
}
}
#[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(())
}

View File

@ -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<DerivablePath, Box<dyn std::error::Error + Send>> {
) -> Result<DerivationPath, Box<dyn std::error::Error + Send>> {
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<Vec<u8>, DerivationError> {
async fn wait_and_run(app: &mut Keyforkd, path: DerivationPath) -> Result<Vec<u8>, 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<dyn std::error::Error>> {
#[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::<Vec<_>>();
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::<Vec<_>>();
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]

View File

@ -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<DerivablePath> for Keyforkd {
impl Service<DerivationPath> for Keyforkd {
type Response = Vec<u8>;
type Error = DerivationError;
@ -37,7 +37,7 @@ impl Service<DerivablePath> 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![]) })
}