keyfork-derive-util: Flesh out most of secp256k1
This commit is contained in:
parent
da09b95bae
commit
e850c75879
|
@ -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"
|
||||
|
|
|
@ -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>;
|
|
@ -0,0 +1,2 @@
|
|||
pub mod private_key;
|
||||
pub mod public_key;
|
|
@ -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"),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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()))
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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![]) })
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue