Skip to content

Commit

Permalink
bip32: add PrivateKey::derive_tweak() and `PublicKey::derive_tweak(…
Browse files Browse the repository at this point in the history
…)` (#1186)

Looks good, thanks!
  • Loading branch information
fjarri authored Jul 17, 2024
1 parent f98d4cc commit c6b4711
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 41 deletions.
24 changes: 6 additions & 18 deletions bip32/src/extended_key/private_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,11 @@ where
/// Derive a child key for a particular [`ChildNumber`].
pub fn derive_child(&self, child_number: ChildNumber) -> Result<Self> {
let depth = self.attrs.depth.checked_add(1).ok_or(Error::Depth)?;
let (tweak, chain_code) = self
.private_key
.derive_tweak(&self.attrs.chain_code, child_number)?;

let mut hmac =
HmacSha512::new_from_slice(&self.attrs.chain_code).map_err(|_| Error::Crypto)?;

if child_number.is_hardened() {
hmac.update(&[0]);
hmac.update(&self.private_key.to_bytes());
} else {
hmac.update(&self.private_key.public_key().to_bytes());
}

hmac.update(&child_number.to_bytes());

let result = hmac.finalize().into_bytes();
let (child_key, chain_code) = result.split_at(KEY_SIZE);

// We should technically loop here if a `secret_key` is zero or overflows
// We should technically loop here if the tweak is zero or overflows
// the order of the underlying elliptic curve group, incrementing the
// index, however per "Child key derivation (CKD) functions":
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
Expand All @@ -113,12 +101,12 @@ where
//
// ...so instead, we simply return an error if this were ever to happen,
// as the chances of it happening are vanishingly small.
let private_key = self.private_key.derive_child(child_key.try_into()?)?;
let private_key = self.private_key.derive_child(tweak)?;

let attrs = ExtendedKeyAttrs {
parent_fingerprint: self.private_key.public_key().fingerprint(),
child_number,
chain_code: chain_code.try_into()?,
chain_code,
depth,
};

Expand Down
36 changes: 17 additions & 19 deletions bip32/src/extended_key/public_key.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
//! Extended public keys
use crate::{
ChildNumber, Error, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, HmacSha512,
KeyFingerprint, Prefix, PrivateKey, PublicKey, PublicKeyBytes, Result, KEY_SIZE,
ChildNumber, Error, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, KeyFingerprint, Prefix,
PrivateKey, PublicKey, PublicKeyBytes, Result,
};
use core::str::FromStr;
use hmac::Mac;

#[cfg(feature = "alloc")]
use alloc::string::{String, ToString};
Expand Down Expand Up @@ -55,27 +54,26 @@ where

/// Derive a child key for a particular [`ChildNumber`].
pub fn derive_child(&self, child_number: ChildNumber) -> Result<Self> {
if child_number.is_hardened() {
// Cannot derive child public keys for hardened `ChildNumber`s
return Err(Error::ChildNumber);
}

let depth = self.attrs.depth.checked_add(1).ok_or(Error::Depth)?;

let mut hmac =
HmacSha512::new_from_slice(&self.attrs.chain_code).map_err(|_| Error::Crypto)?;

hmac.update(&self.public_key.to_bytes());
hmac.update(&child_number.to_bytes());

let result = hmac.finalize().into_bytes();
let (child_key, chain_code) = result.split_at(KEY_SIZE);
let public_key = self.public_key.derive_child(child_key.try_into()?)?;
let (tweak, chain_code) = self
.public_key
.derive_tweak(&self.attrs.chain_code, child_number)?;

// We should technically loop here if the tweak is zero or overflows
// the order of the underlying elliptic curve group, incrementing the
// index, however per "Child key derivation (CKD) functions":
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
//
// > "Note: this has probability lower than 1 in 2^127."
//
// ...so instead, we simply return an error if this were ever to happen,
// as the chances of it happening are vanishingly small.
let public_key = self.public_key.derive_child(tweak)?;

let attrs = ExtendedKeyAttrs {
parent_fingerprint: self.public_key.fingerprint(),
child_number,
chain_code: chain_code.try_into()?,
chain_code,
depth,
};

Expand Down
42 changes: 40 additions & 2 deletions bip32/src/private_key.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! Trait for deriving child keys on a given type.
use crate::{PublicKey, Result, KEY_SIZE};
use crate::{ChainCode, ChildNumber, Error, HmacSha512, PublicKey, Result, KEY_SIZE};
use hmac::Mac;

#[cfg(feature = "secp256k1")]
use crate::{Error, XPrv};
use crate::XPrv;

/// Bytes which represent a private key.
pub type PrivateKeyBytes = [u8; KEY_SIZE];
Expand All @@ -26,6 +27,43 @@ pub trait PrivateKey: Sized {

/// Get the [`Self::PublicKey`] that corresponds to this private key.
fn public_key(&self) -> Self::PublicKey;

/// Derive a tweak value that can be used to generate the child key (see [`derive_child`]).
///
/// The `chain_code` is either a newly initialized one,
/// or one obtained from the previous invocation of `derive_tweak()`
/// (for a multi-level derivation).
///
/// **Warning:** make sure that if you are creating a new `chain_code`, you are doing so
/// in a cryptographically safe way.
/// Normally this would be done according to BIP-39 (within [`ExtendedPrivateKey::new`]).
fn derive_tweak(
&self,
chain_code: &ChainCode,
child_number: ChildNumber,
) -> Result<(PrivateKeyBytes, ChainCode)> {
let mut hmac = HmacSha512::new_from_slice(chain_code).map_err(|_| Error::Crypto)?;

if child_number.is_hardened() {
hmac.update(&[0]);
hmac.update(&self.to_bytes());
} else {
hmac.update(&self.public_key().to_bytes());
}

hmac.update(&child_number.to_bytes());

let result = hmac.finalize().into_bytes();
let (tweak_bytes, chain_code_bytes) = result.split_at(KEY_SIZE);

// Note that at this point we are only asserting that `tweak_bytes` have the expected size.
// Checking if it actually fits the curve scalar happens in `derive_child()`.
let tweak = tweak_bytes.try_into()?;

let chain_code = chain_code_bytes.try_into()?;

Ok((tweak, chain_code))
}
}

#[cfg(feature = "secp256k1")]
Expand Down
45 changes: 43 additions & 2 deletions bip32/src/public_key.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
//! Trait for deriving child keys on a given type.
use crate::{KeyFingerprint, PrivateKeyBytes, Result, KEY_SIZE};
use crate::{
ChainCode, ChildNumber, Error, HmacSha512, KeyFingerprint, PrivateKeyBytes, Result, KEY_SIZE,
};
use hmac::Mac;
use ripemd::Ripemd160;
use sha2::{Digest, Sha256};

#[cfg(feature = "secp256k1")]
use {
crate::{Error, XPub},
crate::XPub,
k256::elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
};

Expand All @@ -33,6 +36,44 @@ pub trait PublicKey: Sized {
let digest = Ripemd160::digest(Sha256::digest(self.to_bytes()));
digest[..4].try_into().expect("digest truncated")
}

/// Derive a tweak value that can be used to generate the child key (see [`derive_child`]).
///
/// The `chain_code` is either a newly initialized one,
/// or one obtained from the previous invocation of `derive_tweak()`
/// (for a multi-level derivation).
///
/// **Warning:** make sure that if you are creating a new `chain_code`, you are doing so
/// in a cryptographically safe way.
/// Normally this would be done according to BIP-39 (within [`ExtendedPrivateKey::new`]).
///
/// **Note:** `child_number` cannot be a hardened one (will result in an error).
fn derive_tweak(
&self,
chain_code: &ChainCode,
child_number: ChildNumber,
) -> Result<(PrivateKeyBytes, ChainCode)> {
if child_number.is_hardened() {
// Cannot derive child public keys for hardened `ChildNumber`s
return Err(Error::ChildNumber);
}

let mut hmac = HmacSha512::new_from_slice(chain_code).map_err(|_| Error::Crypto)?;

hmac.update(&self.to_bytes());
hmac.update(&child_number.to_bytes());

let result = hmac.finalize().into_bytes();
let (tweak_bytes, chain_code_bytes) = result.split_at(KEY_SIZE);

// Note that at this point we are only asserting that `tweak_bytes` have the expected size.
// Checking if it actually fits the curve scalar happens in `derive_child()`.
let tweak = tweak_bytes.try_into()?;

let chain_code = chain_code_bytes.try_into()?;

Ok((tweak, chain_code))
}
}

#[cfg(feature = "secp256k1")]
Expand Down

0 comments on commit c6b4711

Please sign in to comment.