diff --git a/CHANGELOG.md b/CHANGELOG.md index 63af0057..c9830186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index f734426c..ecff22a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,14 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "zcash_spec" +version = "0.1.1" +source = "git+https://github.com/zcash/zcash_spec.git?rev=569f92d01504deb7b092f4cff1c07a4f60ecfa11#569f92d01504deb7b092f4cff1c07a4f60ecfa11" +dependencies = [ + "blake2b_simd", +] + [[package]] name = "zip32" version = "0.1.1" @@ -57,4 +65,5 @@ dependencies = [ "blake2b_simd", "memuse", "subtle", + "zcash_spec", ] diff --git a/Cargo.toml b/Cargo.toml index e13783cb..665c4e78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ 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" @@ -24,3 +25,6 @@ assert_matches = "1.5" [features] default = ["std"] std = [] + +[patch.crates-io] +zcash_spec = { git = "https://github.com/zcash/zcash_spec.git", rev = "569f92d01504deb7b092f4cff1c07a4f60ecfa11" } diff --git a/src/arbitrary.rs b/src/arbitrary.rs new file mode 100644 index 00000000..8b6ea548 --- /dev/null +++ b/src/arbitrary.rs @@ -0,0 +1,116 @@ +//! 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, +} + +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. + sk.into_iter().chain(c.0).collect() + } +} diff --git a/src/hardened_only.rs b/src/hardened_only.rs new file mode 100644 index 00000000..f2a3b54f --- /dev/null +++ b/src/hardened_only.rs @@ -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 { + sk: [u8; 32], + chain_code: ChainCode, + _context: PhantomData, +} + +impl ConstantTimeEq for HardenedOnlyKey { + 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 HardenedOnlyKey { + /// 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, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f0122c44..81f45a94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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. ///