Skip to content

Commit

Permalink
Merge pull request #19 from zcash/arb-keys
Browse files Browse the repository at this point in the history
Implement ZIP 32 arbitrary key derivation
  • Loading branch information
str4d authored Oct 22, 2024
2 parents ea6a62c + 3c9086a commit f4e1c2f
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ and this library adheres to Rust's notion of

## [Unreleased]

## [0.1.2] - 2024-10-22

### Added
- `zip32::arbitrary` module, implementing hardened-only "arbitrary" key
derivation that needs no ecosystem-wide coordination.
- `zip32::hardened_only` module, providing a generic hardened-only key
derivation framework (initially used for Orchard and `zip32::arbitrary`).
- `impl {PartialOrd, Ord, Hash}` for `zip32::DiversifierIndex`

## [0.1.1] - 2024-03-14
Expand Down
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "zip32"
version = "0.1.1"
version = "0.1.2"
authors = [
"Jack Grigg <[email protected]>",
"Kris Nuttycombe <[email protected]>",
Expand All @@ -17,6 +17,7 @@ rust-version = "1.60"
blake2b_simd = "1"
memuse = "0.2.1"
subtle = "2.2.3"
zcash_spec = "0.1.2"

[dev-dependencies]
assert_matches = "1.5"
Expand Down
119 changes: 119 additions & 0 deletions src/arbitrary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//! Arbitrary key derivation.
//!
//! In some contexts there is a need for deriving arbitrary keys with the same derivation
//! path as existing key material (for example, deriving an arbitrary account-level key),
//! without the need for ecosystem-wide coordination. The following instantiation of the
//! [hardened key generation framework] may be used for this purpose.
//!
//! Defined in [ZIP32: Arbitrary key derivation][arbkd].
//!
//! [hardened key generation framework]: crate::hardened_only
//! [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation

use zcash_spec::PrfExpand;

use crate::{
hardened_only::{Context, HardenedOnlyKey},
ChainCode, ChildIndex,
};

struct Arbitrary;

impl Context for Arbitrary {
const MKG_DOMAIN: [u8; 16] = *b"ZcashArbitraryKD";
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4])> = PrfExpand::ARBITRARY_ZIP32_CHILD;
}

/// An arbitrary extended secret key.
///
/// Defined in [ZIP32: Arbitrary key derivation][arbkd].
///
/// [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation
pub struct SecretKey {
inner: HardenedOnlyKey<Arbitrary>,
}

impl SecretKey {
/// Derives an arbitrary key at the given path from the given seed.
///
/// `context_string` is an identifier for the context in which this key will be used.
/// It must be globally unique.
///
/// # Panics
///
/// Panics if:
/// - the context string is empty or longer than 252 bytes.
/// - the seed is shorter than 32 bytes or longer than 252 bytes.
pub fn from_path(context_string: &[u8], seed: &[u8], path: &[ChildIndex]) -> Self {
let mut xsk = Self::master(context_string, seed);
for i in path {
xsk = xsk.derive_child(*i);
}
xsk
}

/// Generates the master key of an Arbitrary extended secret key.
///
/// Defined in [ZIP32: Arbitrary master key generation][mkgarb].
///
/// [mkgarb]: https://zips.z.cash/zip-0032#arbitrary-master-key-generation
///
/// # Panics
///
/// Panics if:
/// - the context string is empty or longer than 252 bytes.
/// - the seed is shorter than 32 bytes or longer than 252 bytes.
fn master(context_string: &[u8], seed: &[u8]) -> Self {
let context_len =
u8::try_from(context_string.len()).expect("context string should be at most 252 bytes");
assert!((1..=252).contains(&context_len));

let seed_len = u8::try_from(seed.len()).expect("seed should be at most 252 bytes");
assert!((32..=252).contains(&seed_len));

let ikm = &[&[context_len], context_string, &[seed_len], seed];

Self {
inner: HardenedOnlyKey::master(ikm),
}
}

/// Derives a child key from a parent key at a given index.
///
/// Defined in [ZIP32: Arbitrary-only child key derivation][ckdarb].
///
/// [ckdarb]: https://zips.z.cash/zip-0032#arbitrary-child-key-derivation
fn derive_child(&self, index: ChildIndex) -> Self {
Self {
inner: self.inner.derive_child(index),
}
}

/// Returns the key material for this arbitrary key.
pub fn data(&self) -> &[u8; 32] {
self.inner.parts().0
}

/// Returns the chain code for this arbitrary key.
pub fn chain_code(&self) -> &ChainCode {
self.inner.parts().1
}

/// Concatenates the key data and chain code to obtain a full-width key.
///
/// This may be used when a context requires a 64-byte key instead of a 32-byte key
/// (for example, to avoid an entropy bottleneck in its particular subsequent
/// operations).
///
/// Child keys MUST NOT be derived from any key on which this method is called. For
/// the current API, this means that [`SecretKey::from_path`] MUST NOT be called with
/// a `path` for which this key's path is a prefix.
pub fn into_full_width_key(self) -> [u8; 64] {
let (sk, c) = self.inner.into_parts();
// Re-concatenate the key parts.
let mut key = [0; 64];
key[..32].copy_from_slice(&sk);
key[32..].copy_from_slice(&c.0);
key
}
}
121 changes: 121 additions & 0 deletions src/hardened_only.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! Generic framework for hardened-only key derivation.
//!
//! Defined in [ZIP32: Hardened-only key derivation][hkd].
//!
//! Any usage of the types in this module needs to have a corresponding ZIP. If you just
//! want to derive an arbitrary key in a ZIP 32-compatible manner without ecosystem-wide
//! coordination, use [`arbitrary::SecretKey`].
//!
//! [hkd]: https://zips.z.cash/zip-0032#specification-hardened-only-key-derivation
//! [`arbitrary::SecretKey`]: crate::arbitrary::SecretKey

use core::marker::PhantomData;

use blake2b_simd::Params as Blake2bParams;
use subtle::{Choice, ConstantTimeEq};
use zcash_spec::PrfExpand;

use crate::{ChainCode, ChildIndex};

/// The context in which hardened-only key derivation is instantiated.
pub trait Context {
/// A 16-byte domain separator used during master key generation.
///
/// It SHOULD be disjoint from other domain separators used with BLAKE2b in Zcash
/// protocols.
const MKG_DOMAIN: [u8; 16];
/// The `PrfExpand` domain used during child key derivation.
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4])>;
}

/// An arbitrary extended secret key.
///
/// Defined in [ZIP32: Hardened-only key derivation][hkd].
///
/// [hkd]: https://zips.z.cash/zip-0032#specification-hardened-only-key-derivation
#[derive(Clone, Debug)]
pub struct HardenedOnlyKey<C: Context> {
sk: [u8; 32],
chain_code: ChainCode,
_context: PhantomData<C>,
}

impl<C: Context> ConstantTimeEq for HardenedOnlyKey<C> {
fn ct_eq(&self, rhs: &Self) -> Choice {
self.chain_code.ct_eq(&rhs.chain_code) & self.sk.ct_eq(&rhs.sk)
}
}

#[allow(non_snake_case)]
impl<C: Context> HardenedOnlyKey<C> {
/// Exposes the parts of this key.
pub fn parts(&self) -> (&[u8; 32], &ChainCode) {
(&self.sk, &self.chain_code)
}

/// Decomposes this key into its parts.
pub(crate) fn into_parts(self) -> ([u8; 32], ChainCode) {
(self.sk, self.chain_code)
}

/// Generates the master key of a hardened-only extended secret key.
///
/// Defined in [ZIP32: Hardened-only master key generation][mkgh].
///
/// [mkgh]: https://zips.z.cash/zip-0032#hardened-only-master-key-generation
pub fn master(ikm: &[&[u8]]) -> Self {
// I := BLAKE2b-512(Context.MKGDomain, IKM)
let I: [u8; 64] = {
let mut I = Blake2bParams::new()
.hash_length(64)
.personal(&C::MKG_DOMAIN)
.to_state();
for input in ikm {
I.update(input);
}
I.finalize().as_bytes().try_into().unwrap()
};

let (I_L, I_R) = I.split_at(32);

// I_L is used as the master secret key sk_m.
let sk_m = I_L.try_into().unwrap();

// I_R is used as the master chain code c_m.
let c_m = ChainCode::new(I_R.try_into().unwrap());

Self {
sk: sk_m,
chain_code: c_m,
_context: PhantomData,
}
}

/// Derives a child key from a parent key at a given index.
///
/// Defined in [ZIP32: Hardened-only child key derivation][ckdh].
///
/// [ckdh]: https://zips.z.cash/zip-0032#hardened-only-child-key-derivation
pub fn derive_child(&self, index: ChildIndex) -> Self {
// I := PRF^Expand(c_par, [Context.CKDDomain] || sk_par || I2LEOSP(i))
let I: [u8; 64] = C::CKD_DOMAIN.with(
self.chain_code.as_bytes(),
&self.sk,
&index.index().to_le_bytes(),
);

let (I_L, I_R) = I.split_at(32);

// I_L is used as the child spending key sk_i.
let sk_i = I_L.try_into().unwrap();

// I_R is used as the child chain code c_i.
let c_i = ChainCode::new(I_R.try_into().unwrap());

Self {
sk: sk_i,
chain_code: c_i,
_context: PhantomData,
}
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ use core::mem;
use memuse::{self, DynamicUsage};
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};

pub mod arbitrary;
pub mod fingerprint;
pub mod hardened_only;

/// A type-safe wrapper for account identifiers.
///
Expand Down

0 comments on commit f4e1c2f

Please sign in to comment.