Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ZIP 32 arbitrary key derivation #19

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this library adheres to Rust's notion of
## [Unreleased]

### 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
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" }
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