From 76421004bdd23ba064f641c8c076643ed2cd605e Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 1 Oct 2024 14:19:31 +0000 Subject: [PATCH] Implement ZIP 32 arbitrary key derivation --- Cargo.lock | 9 ++++ Cargo.toml | 4 ++ src/arbitrary.rs | 88 ++++++++++++++++++++++++++++++++ src/hardened_only.rs | 116 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 5 files changed, 219 insertions(+) create mode 100644 src/arbitrary.rs create mode 100644 src/hardened_only.rs 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..66960ecc --- /dev/null +++ b/src/arbitrary.rs @@ -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, +} + +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), + } + } +} diff --git a/src/hardened_only.rs b/src/hardened_only.rs new file mode 100644 index 00000000..6d22fa7d --- /dev/null +++ b/src/hardened_only.rs @@ -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 { + 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) + } + + /// 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. ///