diff --git a/Cargo.lock b/Cargo.lock index cb4d05a..aae8e12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.8" @@ -191,6 +219,29 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.5" @@ -226,6 +277,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" + [[package]] name = "futures-core" version = "0.3.28" @@ -352,6 +409,7 @@ name = "keyfork-derive-util" version = "0.1.0" dependencies = [ "digest", + "ed25519-dalek", "hex-literal", "hmac", "k256", @@ -548,6 +606,12 @@ dependencies = [ "spki", ] +[[package]] +name = "platforms" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -634,6 +698,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.15" @@ -654,6 +727,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + [[package]] name = "serde" version = "1.0.186" diff --git a/keyfork-derive-util/Cargo.toml b/keyfork-derive-util/Cargo.toml index d2fb004..d400a95 100644 --- a/keyfork-derive-util/Cargo.toml +++ b/keyfork-derive-util/Cargo.toml @@ -6,8 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["secp256k1"] +default = ["secp256k1", "ed25519"] secp256k1 = ["k256"] +ed25519 = ["ed25519-dalek"] [dependencies] # Included in Rust @@ -24,6 +25,7 @@ thiserror = "1.0.47" # Optional, not personally audited k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true } +ed25519-dalek = { version = "2.0.0", optional = true } [dev-dependencies] hex-literal = "0.4.1" diff --git a/keyfork-derive-util/src/extended_key/private_key.rs b/keyfork-derive-util/src/extended_key/private_key.rs index 19b0b6a..c0a94be 100644 --- a/keyfork-derive-util/src/extended_key/private_key.rs +++ b/keyfork-derive-util/src/extended_key/private_key.rs @@ -25,6 +25,10 @@ pub enum Error { /// An unknown error occurred while deriving a child key. #[error("Unknown error while deriving child key")] Derivation, + + /// The algorithm used mandates hardened derivation only. + #[error("The algorithm used mandates hardened derivation only")] + HardenedDerivationRequired, } type Result = std::result::Result; @@ -132,8 +136,10 @@ where if index.is_hardened() { hmac.update(&[0]); hmac.update(&self.private_key.to_bytes()); - } else { + } else if !K::requires_hardened_derivation() { hmac.update(&self.private_key.public_key().to_bytes()); + } else { + return Err(Error::HardenedDerivationRequired); } hmac.update(&index.to_bytes()); let result = hmac.finalize().into_bytes(); diff --git a/keyfork-derive-util/src/path.rs b/keyfork-derive-util/src/path.rs index bb524cd..03522e5 100644 --- a/keyfork-derive-util/src/path.rs +++ b/keyfork-derive-util/src/path.rs @@ -48,7 +48,7 @@ impl std::str::FromStr for DerivationPath { path: iter .map(DerivationIndex::from_str) .map(|maybe_err| maybe_err.map_err(From::from)) - .collect::>>()?, + .collect::>()?, }) } } diff --git a/keyfork-derive-util/src/private_key.rs b/keyfork-derive-util/src/private_key.rs index c9efd89..9bf7769 100644 --- a/keyfork-derive-util/src/private_key.rs +++ b/keyfork-derive-util/src/private_key.rs @@ -18,10 +18,13 @@ pub trait PrivateKey: Sized { /// Convert a &Self to bytes. fn to_bytes(&self) -> PrivateKeyBytes; + /* + * Freak of nature, unsupported? /// Whether or not zero is a valid public key (such as with ed25519 keys). fn is_zero_valid_public_key() -> bool { false } + */ /// The initial key for BIP-0032 and SLIP-0010 derivation, such as secp256k1's "Bitcoin seed". fn key() -> &'static str; @@ -37,6 +40,12 @@ pub trait PrivateKey: Sized { /// * A nonzero `other` is provided. /// * An error specific to the given algorithm was encountered. fn derive_child(&self, other: &PrivateKeyBytes) -> Result; + + /// Whether the algorithm requires hardened derivation, such as for ed25519. + fn requires_hardened_derivation() -> bool { + false + } + } /// Errors associated with creating and arithmetic on private keys. This specific error is only @@ -73,6 +82,10 @@ impl PrivateKey for k256::SecretKey { self.to_bytes().into() } + fn public_key(&self) -> Self::PublicKey { + self.public_key() + } + fn derive_child(&self, other: &PrivateKeyBytes) -> Result { if other.iter().all(|n| n == &0) { return Err(PrivateKeyError::NonZero); @@ -89,8 +102,35 @@ impl PrivateKey for k256::SecretKey { .expect("Should be able to make Key"), ) } +} + +#[cfg(feature = "ed25519")] +impl PrivateKey for ed25519_dalek::SigningKey { + type Err = PrivateKeyError; + type PublicKey = ed25519_dalek::VerifyingKey; + + fn key() -> &'static str { + "ed25519 seed" + } + + fn from_bytes(b: &PrivateKeyBytes) -> Self { + Self::from_bytes(b) + } + + fn to_bytes(&self) -> PrivateKeyBytes { + self.to_bytes() + } fn public_key(&self) -> Self::PublicKey { - self.public_key() + self.verifying_key() + } + + fn derive_child(&self, other: &PrivateKeyBytes) -> Result { + // SLIP-0010: No arithmetic required for ed25519 keys. + Ok(Self::from_bytes(other)) + } + + fn requires_hardened_derivation() -> bool { + true } } diff --git a/keyfork-derive-util/src/public_key.rs b/keyfork-derive-util/src/public_key.rs index 9475438..409c440 100644 --- a/keyfork-derive-util/src/public_key.rs +++ b/keyfork-derive-util/src/public_key.rs @@ -12,10 +12,13 @@ pub trait PublicKey: Sized { /// The error returned by [`PublicKey::derive_child()`]. type Err: std::error::Error; + /* + * This may not be doable given ed25519 public keys must be derived from the private key. /// Create a Self from bytes. fn from_bytes(b: &PublicKeyBytes) -> Self; + */ - /// Convert a &Self to bytse. + /// Convert a &Self to bytes. fn to_bytes(&self) -> PublicKeyBytes; /// Derive a child [`PublicKey`] with given `PrivateKeyBytes`. @@ -47,6 +50,10 @@ pub enum PublicKeyError { /// Unable to convert a point to a key. #[error("Unable to convert point to key")] PointToKey(#[from] k256::elliptic_curve::Error), + + /// Public key derivation is unsupported for this algorithm. + #[error("Public key derivation is unsupported for this algorithm")] + DerivationUnsupported, } #[cfg(feature = "secp256k1")] @@ -59,9 +66,11 @@ use k256::{ 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 @@ -80,3 +89,27 @@ impl PublicKey for k256::PublicKey { Self::from_affine(point.into()).map_err(From::from) } } + +#[cfg(feature = "ed25519")] +use ed25519_dalek::VerifyingKey; + +#[cfg(feature = "ed25519")] +impl PublicKey for VerifyingKey { + type Err = PublicKeyError; + + /* + fn from_bytes(b: &PublicKeyBytes) -> Self { + Self::from_bytes(b).expect("Invalid public key bytes") + } + */ + + fn to_bytes(&self) -> PublicKeyBytes { + let mut result = [0u8; 33]; + result[1..33].copy_from_slice(&self.to_bytes()[..]); + result + } + + fn derive_child(&self, _other: PrivateKeyBytes) -> Result { + Err(Self::Err::DerivationUnsupported) + } +} diff --git a/keyfork-derive-util/src/tests.rs b/keyfork-derive-util/src/tests.rs index eec2d7b..d310cf2 100644 --- a/keyfork-derive-util/src/tests.rs +++ b/keyfork-derive-util/src/tests.rs @@ -1,12 +1,14 @@ 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 +#[cfg(feature = "secp256k1")] #[test] -fn example() { +fn secp256k1() { + use k256::SecretKey; + // seed, chain, chain code, private, public let tests = [( &hex!("000102030405060708090a0b0c0d0e0f")[..], @@ -65,3 +67,37 @@ fn example() { assert_eq!(derived_key.public_key().to_bytes(), public_key); } } + +#[cfg(feature = "ed25519")] +#[test] +fn ed25519() { + use ed25519_dalek::SigningKey; + + // seed, chain, chain code, private, public + let tests = [( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m").unwrap(), + hex!("90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb"), + hex!("2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7"), + hex!("00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed"), + ), ( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m/0'").unwrap(), + hex!("8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69"), + hex!("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3"), + hex!("008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c"), + ), ( + &hex!("000102030405060708090a0b0c0d0e0f")[..], + DerivationPath::from_str("m/0'/1'/2'/2'/1000000000'").unwrap(), + hex!("68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230"), + hex!("8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793"), + hex!("003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a"), + )]; + for (seed, chain, chain_code, private_key, public_key) in tests { + let xkey = ExtendedPrivateKey::::new(seed).unwrap(); + let derived_key = xkey.derive_path(&chain).unwrap(); + assert_eq!(derived_key.chain_code, chain_code); + assert_eq!(PrivateKey::to_bytes(&derived_key.private_key), private_key); + assert_eq!(PublicKey::to_bytes(&derived_key.public_key()), public_key); + } +}