Skip to content

Commit

Permalink
Implement ZIP 32 arbitrary key derivation
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Oct 1, 2024
1 parent ea6a62c commit 7642100
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ rust-version = "1.60"
blake2b_simd = "1"
memuse = "0.2.1"
subtle = "2.2.3"
zcash_spec = "0.1"

[dev-dependencies]
assert_matches = "1.5"

[features]
default = ["std"]
std = []

[patch.crates-io]
zcash_spec = { git = "https://github.com/zcash/zcash_spec.git", rev = "569f92d01504deb7b092f4cff1c07a4f60ecfa11" }
88 changes: 88 additions & 0 deletions src/arbitrary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Arbitrary key derivation.
//!
//! Defined in [ZIP32: Arbitrary key derivation][arbkd].
//!
//! [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation

use zcash_spec::PrfExpand;

use crate::{
hardened_only::{Context, HardenedOnlyKey},
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 {
/// Returns the the child key corresponding to the path derived from the master key
/// for the given input key material.
///
/// # Panics
///
/// Panics if:
/// - the context string is empty or longer than 65535 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 65535 bytes.
/// - the seed is shorter than 32 bytes or longer than 252 bytes.
fn master(context_string: &[u8], seed: &[u8]) -> Self {
assert!(!context_string.is_empty());
let context_len = u16::try_from(context_string.len())
.expect("context string should be at most 65535 bytes");

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.to_le_bytes(),
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),
}
}
}
116 changes: 116 additions & 0 deletions src/hardened_only.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Generic implementation of 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)
}

/// 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 7642100

Please sign in to comment.