diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b872182..01c0aa8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ and this library adheres to Rust's notion of [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `zip32::registered` module, implementing hardened-only key derivation for + an application protocol specified in a ZIP. +- `zip32::ChildIndex::PRIVATE_USE` +- `zip32::hardened_only::HardenedOnlyKey::{from_parts, derive_child_with_tag}` + +### Changed +- The type of `zip32::hardened_only::Context::CKD_DOMAIN` has changed, in + order to support child derivation with tags. + +### Deprecated +- `zip32::arbitrary::SecretKey::into_full_width_key`. This API is + cryptographically unsafe because it depends on a restriction that cannot + be enforced. Use `zip32::registered::cryptovalue_from_subpath` instead. ## [0.1.3] - 2024-12-13 diff --git a/Cargo.lock b/Cargo.lock index abedeb79..38ca29d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "zcash_spec" -version = "0.1.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cede95491c2191d3e278cab76e097a44b17fde8d6ca0d4e3a22cf4807b2d857" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" dependencies = [ "blake2b_simd", ] diff --git a/Cargo.toml b/Cargo.toml index d7a6c2d7..584838bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ rust-version = "1.60" blake2b_simd = { version = "1", default-features = false } memuse = { version = "0.2.2", default-features = false } subtle = { version = "2.2.3", default-features = false } -zcash_spec = "0.1.2" +zcash_spec = "0.2.1" [dev-dependencies] assert_matches = "1.5" diff --git a/src/arbitrary.rs b/src/arbitrary.rs index fb58fc34..559f1537 100644 --- a/src/arbitrary.rs +++ b/src/arbitrary.rs @@ -1,56 +1,53 @@ -//! Arbitrary key derivation. +//! Ad-hoc ("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. +//! For compatibility with existing deployments, we define a mechanism to generate +//! ad-hoc key trees for private use by applications, without ecosystem coordination, +//! using the [hardened key derivation framework]. //! -//! Defined in [ZIP32: Arbitrary key derivation][arbkd]. +//! This used to be called "arbitrary key derivation" in ZIP 32, but that term caused +//! confusion as to the applicability of the mechanism and so has been renamed to +//! "ad-hoc key derivation". The module name is still `arbitrary` for compatibility. //! -//! [hardened key generation framework]: crate::hardened_only -//! [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation +//! Since there is no guarantee of non-collision between different application protocols, +//! and no way to tie these key trees to well-defined specification or documentation +//! processes, use of this mechanism is NOT RECOMMENDED for new protocols. +//! +//! The keys derived by the functions in this module will be unrelated to any keys +//! derived by functions in the [`crate::registered`] module, even if the same context +//! string and seed are used. +//! +//! Defined in [ZIP 32: Ad-hoc key derivation (deprecated)][adhockd]. +//! +//! [hardened key derivation framework]: crate::hardened_only +//! [adhockd]: https://zips.z.cash/zip-0032#specification-ad-hoc-key-derivation-deprecated use zcash_spec::PrfExpand; use crate::{ - hardened_only::{Context, HardenedOnlyKey}, + hardened_only::{Context, HardenedOnlyCkdDomain, HardenedOnlyKey}, ChainCode, ChildIndex, }; -struct Arbitrary; +use super::with_ikm; + +struct Adhoc; -impl Context for Arbitrary { +impl Context for Adhoc { const MKG_DOMAIN: [u8; 16] = *b"ZcashArbitraryKD"; - const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4])> = PrfExpand::ARBITRARY_ZIP32_CHILD; + const CKD_DOMAIN: HardenedOnlyCkdDomain = PrfExpand::ADHOC_ZIP32_CHILD; } -/// An arbitrary extended secret key. +/// An ad-hoc extended secret key. /// -/// Defined in [ZIP32: Arbitrary key derivation][arbkd]. +/// Defined in [ZIP 32: Ad-hoc key generation (deprecated)][adhockd]. /// -/// [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation +/// [adhockd]: https://zips.z.cash/zip-0032#specification-ad-hoc-key-derivation-deprecated pub struct SecretKey { - inner: HardenedOnlyKey, -} - -fn with_ikm(context_string: &[u8], seed: &[u8], f: F) -> T -where - F: FnOnce(&[&[u8]]) -> T, -{ - 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]; - - f(ikm) + inner: HardenedOnlyKey, } impl SecretKey { - /// Derives an arbitrary key at the given path from the given seed. + /// Derives an ad-hoc 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. @@ -68,11 +65,11 @@ impl SecretKey { xsk } - /// Generates the master key of an Arbitrary extended secret key. + /// Generates the master key of an ad-hoc extended secret key. /// - /// Defined in [ZIP32: Arbitrary master key generation][mkgarb]. + /// Defined in [ZIP 32: Ad-hoc master key generation (deprecated)][adhocmkg]. /// - /// [mkgarb]: https://zips.z.cash/zip-0032#arbitrary-master-key-generation + /// [adhocmkg]: https://zips.z.cash/zip-0032#ad-hoc-master-key-generation-deprecated /// /// # Panics /// @@ -87,21 +84,21 @@ impl SecretKey { /// Derives a child key from a parent key at a given index. /// - /// Defined in [ZIP32: Arbitrary-only child key derivation][ckdarb]. + /// Defined in [ZIP 32: Ad-hoc child key derivation (deprecated)][adhocckd]. /// - /// [ckdarb]: https://zips.z.cash/zip-0032#arbitrary-child-key-derivation + /// [adhocckd]: https://zips.z.cash/zip-0032#ad-hoc-child-key-derivation-deprecated fn derive_child(&self, index: ChildIndex) -> Self { Self { inner: self.inner.derive_child(index), } } - /// Returns the key material for this arbitrary key. + /// Returns the key material for this key. pub fn data(&self) -> &[u8; 32] { self.inner.parts().0 } - /// Returns the chain code for this arbitrary key. + /// Returns the chain code for this key. pub fn chain_code(&self) -> &ChainCode { self.inner.parts().1 } @@ -114,7 +111,12 @@ impl SecretKey { /// /// 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. + /// a `path` for which this key's path is a prefix. This API is cryptographically + /// unsafe because there is no way to enforce that restriction. + #[deprecated( + since = "0.1.4", + note = "Use [`zip32::registered::cryptovalue_from_subpath`] instead." + )] pub fn into_full_width_key(self) -> [u8; 64] { let (sk, c) = self.inner.into_parts(); // Re-concatenate the key parts. @@ -127,9 +129,7 @@ impl SecretKey { #[cfg(test)] mod tests { - use crate::{arbitrary::with_ikm, ChildIndex}; - - use super::SecretKey; + use super::{with_ikm, ChildIndex, SecretKey}; struct TestVector { context_string: &'static [u8], @@ -141,7 +141,7 @@ mod tests { } // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/zip_0032_arbitrary.py - const TEST_VECTORS: &'static [TestVector] = &[ + const TEST_VECTORS: &[TestVector] = &[ TestVector { context_string: &[ 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, @@ -239,26 +239,89 @@ mod tests { 0xac, 0x19, 0x84, 0x29, ], }, + TestVector { + context_string: &[ + 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, + ], + seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + ikm: None, + path: &[2147483680], + sk: [ + 0xc4, 0x30, 0xc4, 0xde, 0xfd, 0x03, 0xd7, 0x57, 0x8b, 0x2b, 0xb0, 0x9e, 0x58, 0x13, + 0x5c, 0xdd, 0x1d, 0x7b, 0x7c, 0x97, 0x5f, 0x01, 0xa8, 0x90, 0x84, 0x7e, 0xe0, 0xb5, + 0xc4, 0x68, 0xbc, 0x98, + ], + c: [ + 0x0f, 0x47, 0x37, 0x89, 0xfe, 0x7d, 0x55, 0x85, 0xb7, 0x9a, 0xd5, 0xf7, 0xe0, 0xa4, + 0x69, 0xd9, 0xa3, 0x01, 0x46, 0x64, 0x77, 0x64, 0x48, 0x51, 0x50, 0xdb, 0x78, 0xd7, + 0x20, 0x9d, 0xcb, 0x30, + ], + }, + TestVector { + context_string: &[ + 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, + ], + seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + ikm: None, + path: &[2147483680, 2147483781], + sk: [ + 0x43, 0xe5, 0x48, 0x46, 0x79, 0xfd, 0xfa, 0x0f, 0x61, 0x76, 0xae, 0x86, 0x79, 0x5d, + 0x0d, 0x44, 0xc4, 0x0e, 0x14, 0x9e, 0xf4, 0xba, 0x1b, 0x0e, 0x2e, 0xbd, 0x88, 0x3c, + 0x71, 0xf4, 0x91, 0x87, + ], + c: [ + 0xdb, 0x42, 0xc3, 0xb7, 0x25, 0xf3, 0x24, 0x59, 0xb2, 0xcf, 0x82, 0x15, 0x41, 0x8b, + 0x8e, 0x8f, 0x8e, 0x7b, 0x1b, 0x3f, 0x4a, 0xba, 0x2f, 0x5b, 0x5e, 0x81, 0x29, 0xe6, + 0xf0, 0x57, 0x57, 0x84, + ], + }, + TestVector { + context_string: &[ + 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, + ], + seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + ikm: None, + path: &[2147483680, 2147483781, 2147483648], + sk: [ + 0xbf, 0x60, 0x07, 0x83, 0x62, 0xa0, 0x92, 0x34, 0xfc, 0xbc, 0x6b, 0xf6, 0xc8, 0xa8, + 0x7b, 0xde, 0x9f, 0xc7, 0x37, 0x76, 0xbf, 0x93, 0xf3, 0x7a, 0xdb, 0xcc, 0x43, 0x9a, + 0x85, 0x57, 0x4a, 0x9a, + ], + c: [ + 0x2b, 0x65, 0x7e, 0x08, 0xf6, 0x7a, 0x57, 0x0c, 0x53, 0xb9, 0xed, 0x30, 0x61, 0x1e, + 0x6a, 0x2f, 0x82, 0x26, 0x62, 0xb4, 0x88, 0x7a, 0x8c, 0xfb, 0x46, 0x9e, 0x9d, 0x0d, + 0x98, 0x17, 0x01, 0x1a, + ], + }, ]; #[test] fn test_vectors() { let context_string = b"Zcash test vectors"; - let full_path = [ - ChildIndex::hardened(1), - ChildIndex::hardened(2), - ChildIndex::hardened(3), - ]; - for (i, tv) in TEST_VECTORS.iter().enumerate() { + for tv in TEST_VECTORS { assert_eq!(tv.context_string, context_string); let path = tv .path - .into_iter() + .iter() .map(|i| ChildIndex::from_index(*i).expect("hardened")) .collect::>(); - assert_eq!(&full_path[..i], &path); // The derived master key should be identical to the key at the empty path. if let Some(mut tv_ikm) = tv.ikm { diff --git a/src/hardened_only.rs b/src/hardened_only.rs index f2a3b54f..0f8a2f41 100644 --- a/src/hardened_only.rs +++ b/src/hardened_only.rs @@ -1,22 +1,25 @@ //! Generic framework for hardened-only key derivation. //! -//! Defined in [ZIP32: Hardened-only key derivation][hkd]. +//! Defined in [ZIP 32: 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`]. +//! Any usage of the types in this module needs to have a corresponding ZIP (except via +//! [`arbitrary::SecretKey`] but that is [NOT RECOMMENDED for new protocols][adhockd]). //! //! [hkd]: https://zips.z.cash/zip-0032#specification-hardened-only-key-derivation +//! [adhockd]: https://zips.z.cash/zip-0032#specification-ad-hoc-key-derivation-deprecated //! [`arbitrary::SecretKey`]: crate::arbitrary::SecretKey use core::marker::PhantomData; use blake2b_simd::Params as Blake2bParams; use subtle::{Choice, ConstantTimeEq}; -use zcash_spec::PrfExpand; +use zcash_spec::{PrfExpand, VariableLengthSlice}; use crate::{ChainCode, ChildIndex}; +pub(crate) type HardenedOnlyCkdDomain = + PrfExpand<([u8; 32], [u8; 4], [u8; 1], VariableLengthSlice)>; + /// The context in which hardened-only key derivation is instantiated. pub trait Context { /// A 16-byte domain separator used during master key generation. @@ -25,12 +28,12 @@ pub trait Context { /// protocols. const MKG_DOMAIN: [u8; 16]; /// The `PrfExpand` domain used during child key derivation. - const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4])>; + const CKD_DOMAIN: HardenedOnlyCkdDomain; } -/// An arbitrary extended secret key. +/// An arbitrary or registered extended secret key. /// -/// Defined in [ZIP32: Hardened-only key derivation][hkd]. +/// Defined in [ZIP 32: Hardened-only key derivation][hkd]. /// /// [hkd]: https://zips.z.cash/zip-0032#specification-hardened-only-key-derivation #[derive(Clone, Debug)] @@ -48,6 +51,17 @@ impl ConstantTimeEq for HardenedOnlyKey { #[allow(non_snake_case)] impl HardenedOnlyKey { + /// Constructs a hardened-only key from its parts. + /// + /// Crate-internal because we want this to only be used within specific contexts. + pub(crate) fn from_parts(sk: [u8; 32], chain_code: ChainCode) -> Self { + Self { + sk, + chain_code, + _context: PhantomData, + } + } + /// Exposes the parts of this key. pub fn parts(&self) -> (&[u8; 32], &ChainCode) { (&self.sk, &self.chain_code) @@ -60,7 +74,7 @@ impl HardenedOnlyKey { /// Generates the master key of a hardened-only extended secret key. /// - /// Defined in [ZIP32: Hardened-only master key generation][mkgh]. + /// Defined in [ZIP 32: Hardened-only master key generation][mkgh]. /// /// [mkgh]: https://zips.z.cash/zip-0032#hardened-only-master-key-generation pub fn master(ikm: &[&[u8]]) -> Self { @@ -73,48 +87,61 @@ impl HardenedOnlyKey { for input in ikm { I.update(input); } - I.finalize().as_bytes().try_into().unwrap() + I.finalize().as_bytes().try_into().expect("64-byte output") }; + Self::from_bytes(&I) + } - 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()); + /// Derives a child key from a parent key at a given index and empty tag. + /// + /// This is a convenience function equivalent to + /// `self.derive_child_with_tag(index, &[])`. + pub fn derive_child(&self, index: ChildIndex) -> Self { + self.derive_child_with_tag(index, &[]) + } - Self { - sk: sk_m, - chain_code: c_m, - _context: PhantomData, - } + /// Derives a child key from a parent key at a given index and (possibly empty) + /// tag. + /// + /// Defined in [ZIP 32: Hardened-only child key derivation][ckdh]. + /// + /// [ckdh]: https://zips.z.cash/zip-0032#hardened-only-child-key-derivation + pub fn derive_child_with_tag(&self, index: ChildIndex, tag: &[u8]) -> Self { + Self::from_bytes(&self.ckdh_internal(index, 0, tag)) } - /// Derives a child key from a parent key at a given index. + /// Defined in [ZIP 32: Hardened-only child key derivation][ckdh]. /// - /// Defined in [ZIP32: Hardened-only child key derivation][ckdh]. + /// This returns `I` rather than `(I_L, I_R)` so that we don't have + /// to re-concatenate the pieces, e.g. when using it in + /// [`crate::registered::SecretKey::derive_child_cryptovalue`]. /// /// [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( + pub(crate) fn ckdh_internal(&self, index: ChildIndex, lead: u8, tag: &[u8]) -> [u8; 64] { + // One of these depending on lead and tag: + // - I := PRF^Expand(c_par, [Context.CKDDomain] || sk_par || I2LEOSP(i)) + // - I := PRF^Expand(c_par, [Context.CKDDomain] || sk_par || I2LEOSP(i) || [lead] || tag) + C::CKD_DOMAIN.with( self.chain_code.as_bytes(), &self.sk, &index.index().to_le_bytes(), - ); + &[lead], + tag, + ) + } + fn from_bytes(I: &[u8; 64]) -> Self { 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_L is used as the spending key sk. + let sk = 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()); + // I_R is used as the chain code c. + let chain_code = ChainCode::new(I_R.try_into().unwrap()); Self { - sk: sk_i, - chain_code: c_i, + sk, + chain_code, _context: PhantomData, } } diff --git a/src/lib.rs b/src/lib.rs index 584e2bba..231df6e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; pub mod arbitrary; pub mod fingerprint; pub mod hardened_only; +pub mod registered; /// A type-safe wrapper for account identifiers. /// @@ -87,6 +88,24 @@ impl core::fmt::Display for TryFromIntError { #[cfg(feature = "std")] impl std::error::Error for TryFromIntError {} +// Helper function for arbitrary and registered master key generation. + +pub(crate) fn with_ikm(context_string: &[u8], seed: &[u8], f: F) -> T +where + F: FnOnce(&[&[u8]]) -> T, +{ + 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]; + + f(ikm) +} + // ZIP 32 structures /// A child index for a derived key. @@ -127,6 +146,9 @@ impl ChildIndex { pub fn index(&self) -> u32 { self.0 } + + /// A `ChildIndex` sometimes employed for private-use subtrees. + pub const PRIVATE_USE: Self = Self::hardened(0x7fff_ffff); } /// A value that is needed, in addition to a spending key, in order to derive descendant @@ -363,7 +385,7 @@ mod tests { let max_di = DiversifierIndex([ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ]); - assert_eq!(u128::try_from(max_di), Ok(0x00ff_ffff_ffff_ffff_ffff_ffff)); + assert_eq!(u128::from(max_di), 0x00ff_ffff_ffff_ffff_ffff_ffff); assert_eq!( DiversifierIndex::try_from(0x00ff_ffff_ffff_ffff_ffff_ffff_u128).unwrap(), max_di, diff --git a/src/registered.rs b/src/registered.rs new file mode 100644 index 00000000..380468dc --- /dev/null +++ b/src/registered.rs @@ -0,0 +1,422 @@ +//! Registered key derivation. +//! +//! In the context of a particular application protocol defined by a ZIP, there is +//! sometimes a need to define an HD subtree that will not collide with keys derived +//! for other protocols, as far as that is possible to assure by following the +//! [ZIP process]. +//! +//! Within this subtree, the application protocol may use derivation paths related to +//! those used for existing key material — for example, to derive an account-level key. +//! The instantiation of the [hardened key derivation framework] in this module may be +//! used for this purpose. +//! +//! It is strongly RECOMMENDED that implementors ensure that documentation of the +//! usage and derivation paths of the application protocol's key tree in the +//! corresponding ZIP is substantially complete, before public deployment of software +//! or hardware using this mechanism. The ZIP process allows for subsequent updates +//! and corrections. +//! +//! The functionality of this module is similar to that of the [`zip32::arbitrary`] +//! module, with the following improvements: +//! +//! - The key tree is associated with the ZIP that should document it, and cannot +//! collide with the tree for any other ZIP. +//! - Child indices may include byte sequence tags. +//! - A 64-bit cryptovalue can be derived at the same path as any node in the tree, +//! without any cryptographic unsafety. +//! +//! The keys derived by the functions in this module will be unrelated to any keys +//! derived by functions in the [`zip32::arbitrary`] module, even if the same context +//! string and seed are used. +//! +//! Defined in [ZIP 32: Registered key derivation][regkd]. +//! +//! [hardened key derivation framework]: crate::hardened_only +//! [regkd]: https://zips.z.cash/zip-0032#specification-registered-key-derivation +//! [ZIP process]: https://zips.z.cash/zip-0000 +//! [`zip32::arbitrary`]: `crate::arbitrary` + +use core::fmt::Display; + +use zcash_spec::PrfExpand; + +use crate::{ + hardened_only::{Context, HardenedOnlyCkdDomain, HardenedOnlyKey}, + ChainCode, ChildIndex, +}; + +use super::with_ikm; + +struct Registered; + +impl Context for Registered { + const MKG_DOMAIN: [u8; 16] = *b"ZIPRegistered_KD"; + const CKD_DOMAIN: HardenedOnlyCkdDomain = PrfExpand::REGISTERED_ZIP32_CHILD; +} + +/// An error that occurred in cryptovalue derivation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DerivationError { + /// The provided seed data was invalid. A seed must be between 32 and 252 bytes in length, + /// inclusive. + SeedInvalid, + /// The provided context string is invalid; context strings must be non-empty and no greater + /// than 252 bytes in length. + ContextStringInvalid, + /// The provided subpath was empty. Empty subpaths are not permitted by this API, as the + /// full-width cryptovalue at the empty subpath would be outside the allowed subtree + /// rooted at `m_{context} / zip_number'`. + SubpathEmpty, +} + +impl Display for DerivationError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + DerivationError::SeedInvalid => { + write!(f, "Seed must be between 32 and 252 bytes, inclusive.") + } + DerivationError::ContextStringInvalid => write!( + f, + "Context string must be between 1 and 252 bytes, inclusive." + ), + DerivationError::SubpathEmpty => write!( + f, + "ZIP 32 registered 64-byte cryptovalue subpaths must have at least one element." + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for DerivationError {} + +/// A ZIP 32 registered key derivation path element, consisting of a child index and an +/// optionally-empty tag value. +pub struct PathElement<'a> { + child_index: ChildIndex, + tag: &'a [u8], +} + +impl<'a> PathElement<'a> { + /// Constructs a new [`PathElement`] from its constituent parts. + pub fn new(child_index: ChildIndex, tag: &'a [u8]) -> Self { + Self { child_index, tag } + } + + /// Returns the index at which the child key will be derived. + pub fn child_index(&self) -> ChildIndex { + self.child_index + } + + /// Returns the tag that will be used in derivation of the child key. + pub fn tag(&self) -> &[u8] { + self.tag + } +} + +/// A registered extended secret key. +/// +/// Defined in [ZIP 32: Registered key derivation][regkd]. +/// +/// [regkd]: https://zips.z.cash/zip-0032#specification-registered-key-derivation +pub struct SecretKey { + inner: HardenedOnlyKey, +} + +impl SecretKey { + /// Derives a key for a registered application protocol at the given path from the + /// given seed. Each path element may consist of an index and (possibly empty) tag. + /// + /// - `context_string`: an identifier for the context in which this key will be used. It must + /// be globally unique, non-empty, and no more than 252 bytes in length. + /// - `seed`: the root seed. Must be between 32 bytes and 252 bytes in length, inclusive. + /// - `zip_number`: the number of the ZIP defining the application protocol. The corresponding + /// hardened index (with empty tag) will be prepended to the `subpath` to obtain the ZIP 32 + /// path. + /// - `subpath`: the path to the desired child element. + pub fn from_subpath( + context_string: &[u8], + seed: &[u8], + zip_number: u16, + subpath: &[PathElement<'_>], + ) -> Result { + if context_string.is_empty() || context_string.len() > 252 { + return Err(DerivationError::ContextStringInvalid); + } + if seed.len() < 32 || seed.len() > 252 { + return Err(DerivationError::SeedInvalid); + } + + let mut xsk = Self::master(context_string, seed) + .derive_child(ChildIndex::hardened(u32::from(zip_number))); + + for elem in subpath { + xsk = xsk.derive_child_with_tag(elem.child_index, elem.tag); + } + Ok(xsk) + } + + /// Constructs a key for a registered application protocol from its constituent parts. + /// + /// This is a low-level API. The constructor must only be called with parts that were + /// obtained from previous calls to [`key.data()`][`Self::data`] and + /// [`key.chain_code()`][`Self::chain_code`] for some `key: registered::SecretKey`. + pub fn from_parts(sk: [u8; 32], chain_code: ChainCode) -> Self { + Self { + inner: HardenedOnlyKey::from_parts(sk, chain_code), + } + } + + /// Generates the master key of a registered extended secret key. + /// This should not be exposed directly. It is defined as an intermediate + /// value in [ZIP 32: Registered subtree root key generation][regroot]. + /// + /// [regroot]: https://zips.z.cash/zip-0032#registered-subtree-root-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 { + with_ikm(context_string, seed, |ikm| Self { + inner: HardenedOnlyKey::master(ikm), + }) + } + + /// Derives a child key from a parent key at a given index and empty tag. + /// + /// This is a convenience function equivalent to + /// `self.derive_child_with_tag(index, &[])`. + pub fn derive_child(&self, index: ChildIndex) -> Self { + self.derive_child_with_tag(index, &[]) + } + + /// Derives a child key from a parent key at a given index and (possibly empty) tag. + /// + /// Defined in [ZIP 32: Registered child key derivation][regckd]. + /// + /// [regckd]: https://zips.z.cash/zip-0032#registered-child-key-derivation + pub fn derive_child_with_tag(&self, index: ChildIndex, tag: &[u8]) -> Self { + Self { + inner: self.inner.derive_child_with_tag(index, tag), + } + } + + /// Derives a 64-byte child cryptovalue from a parent key at a given index + /// and (possibly empty) tag. + /// + /// Defined in [ZIP 32: Full-width child cryptovalue derivation][fwccd]. + /// + /// [fwccd]: https://zips.z.cash/zip-0032#full-width-child-cryptovalue-derivation + pub fn derive_child_cryptovalue(&self, index: ChildIndex, tag: &[u8]) -> [u8; 64] { + self.inner.ckdh_internal(index, 1, tag) + } + + /// Returns the key material for this key. + pub fn data(&self) -> &[u8; 32] { + self.inner.parts().0 + } + + /// Returns the chain code for this key. + pub fn chain_code(&self) -> &ChainCode { + self.inner.parts().1 + } +} + +/// Derives a 64-byte cryptovalue (for use as key material for example), for a registered +/// application protocol at the given non-empty subpath from the given seed. Each subpath element +/// may consist of an index and a (possibly empty) tag. +/// +/// - `context_string`: an identifier for the context in which this key will be used. It must be +/// globally unique, non-empty, and no more than 252 bytes in length. +/// - `seed`: the root seed. Must be between 32 bytes and 252 bytes in length, inclusive. +/// - `zip_number`: the number of the ZIP defining the application protocol. The corresponding +/// hardened index (with empty tag) will be prepended to the `subpath` to obtain the ZIP 32 path. +/// - `subpath`: the path to the desired child element. A non-empty path is required, in order +/// to ensure that the resulting full-width cryptovalue is within the allowed subtree rooted +/// at `m_{context} / zip_number'`. +pub fn cryptovalue_from_subpath( + context_string: &[u8], + seed: &[u8], + zip_number: u16, + subpath: &[PathElement<'_>], +) -> Result<[u8; 64], DerivationError> { + if context_string.is_empty() || context_string.len() > 252 { + return Err(DerivationError::ContextStringInvalid); + } + if seed.len() < 32 || seed.len() > 252 { + return Err(DerivationError::SeedInvalid); + } + // We can't use NonEmpty because it requires allocation. + if subpath.is_empty() { + return Err(DerivationError::SubpathEmpty); + } + + let mut xsk = SecretKey::master(context_string, seed) + .derive_child(ChildIndex::hardened(u32::from(zip_number))); + + for elem in subpath.iter().take(subpath.len() - 1) { + xsk = xsk.derive_child_with_tag(elem.child_index, elem.tag); + } + let elem = subpath.last().expect("nonempty"); + Ok(xsk.derive_child_cryptovalue(elem.child_index, elem.tag)) +} + +#[cfg(test)] +mod tests { + use crate::registered::PathElement; + + use super::{cryptovalue_from_subpath, ChildIndex, DerivationError, SecretKey}; + + #[test] + fn test_cryptovalue_from_empty_subpath_errors() { + assert_eq!( + cryptovalue_from_subpath(&[0], &[0; 32], 32, &[]), + Err(DerivationError::SubpathEmpty), + ); + } + + struct TestVector { + context_string: &'static [u8], + seed: [u8; 32], + zip_number: u16, + subpath: &'static [(u32, &'static [u8])], + sk: [u8; 32], + c: [u8; 32], + full_width: Option<[u8; 64]>, + } + + // From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/zip_0032_registered.py + const TEST_VECTORS: &[TestVector] = &[ + TestVector { + context_string: &[ + 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, + ], + seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + zip_number: 1, + subpath: &[], + sk: [ + 0x53, 0xa7, 0x15, 0x07, 0xe6, 0xdf, 0xda, 0x58, 0x8b, 0xc1, 0xe1, 0x38, 0xc2, 0x65, + 0x7c, 0x92, 0x69, 0xe5, 0x5f, 0x5d, 0x9b, 0x99, 0xe3, 0x88, 0x7c, 0x13, 0x40, 0x08, + 0x19, 0x3a, 0x2f, 0x47, + ], + c: [ + 0x08, 0xbb, 0x26, 0xaa, 0xe2, 0x1d, 0x4e, 0xfd, 0xc3, 0x24, 0x9b, 0x95, 0x57, 0xfc, + 0xd9, 0x13, 0x1e, 0x8b, 0x98, 0x27, 0x24, 0x1d, 0x9f, 0x61, 0xd0, 0xd7, 0x74, 0xbb, + 0x4f, 0xed, 0x3d, 0xe6, + ], + full_width: None, + }, + TestVector { + context_string: &[ + 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, + ], + seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + zip_number: 1, + subpath: &[( + 2147483650, + &[ + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x73, 0x20, + 0x61, 0x72, 0x65, 0x20, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x20, 0x72, 0x69, 0x67, + 0x68, 0x74, 0x73, + ], + )], + sk: [ + 0x02, 0xdc, 0x25, 0xcc, 0x40, 0x31, 0x0e, 0xed, 0x08, 0xb0, 0x28, 0xe0, 0x7f, 0xae, + 0x9a, 0xdb, 0xee, 0x2f, 0xbe, 0x56, 0xa4, 0x69, 0x4d, 0xef, 0x04, 0x01, 0xe6, 0x56, + 0xdf, 0xae, 0x02, 0x11, + ], + c: [ + 0xd8, 0xf9, 0xd8, 0xa1, 0xf8, 0x1d, 0x1b, 0x5d, 0x55, 0x06, 0xb5, 0xff, 0x94, 0x2d, + 0x2f, 0xf3, 0xda, 0xe7, 0xa6, 0x3f, 0x57, 0xd6, 0xb8, 0xc7, 0xfb, 0xe5, 0x81, 0x49, + 0x82, 0x3c, 0xc6, 0xec, + ], + full_width: Some([ + 0x25, 0x5d, 0x75, 0xb5, 0xf9, 0x7d, 0xd8, 0x80, 0xa1, 0x44, 0x60, 0xab, 0x0a, 0x28, + 0x93, 0x8e, 0x7b, 0xa4, 0x97, 0xce, 0xb1, 0x45, 0x7f, 0xff, 0x29, 0x92, 0xe9, 0x01, + 0x5a, 0x84, 0x03, 0xf8, 0xc0, 0x81, 0x12, 0xb7, 0xa9, 0x4c, 0xf5, 0x39, 0xc2, 0x1c, + 0x9d, 0xa7, 0xee, 0x99, 0x89, 0x7b, 0xe9, 0x47, 0x6b, 0x68, 0x13, 0x53, 0x2e, 0xe2, + 0x2c, 0x89, 0x47, 0xd7, 0x53, 0xb7, 0x2b, 0xdf, + ]), + }, + TestVector { + context_string: &[ + 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, + ], + seed: [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, + ], + zip_number: 1, + subpath: &[ + ( + 2147483650, + &[ + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x73, + 0x20, 0x61, 0x72, 0x65, 0x20, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x20, 0x72, + 0x69, 0x67, 0x68, 0x74, 0x73, + ], + ), + (2147483651, &[]), + ], + sk: [ + 0xa1, 0x27, 0xdb, 0x66, 0x62, 0x8b, 0x25, 0x6e, 0x5b, 0x66, 0x4d, 0x54, 0x05, 0x0c, + 0x1e, 0x6b, 0x02, 0x89, 0x63, 0xae, 0xa2, 0x2b, 0x04, 0xd1, 0xbc, 0x6f, 0x48, 0x12, + 0x36, 0x74, 0xed, 0x82, + ], + c: [ + 0x34, 0x00, 0x84, 0x03, 0x36, 0x05, 0xed, 0xca, 0x11, 0x46, 0x3f, 0xfe, 0xc5, 0x6b, + 0xf0, 0xca, 0xc4, 0x25, 0xc4, 0x10, 0xe9, 0x53, 0x62, 0x86, 0x71, 0xce, 0xc6, 0xa6, + 0x51, 0x4c, 0x32, 0xa8, + ], + full_width: Some([ + 0x7f, 0x85, 0x3e, 0xef, 0x00, 0x1b, 0x1b, 0xc5, 0xa1, 0xa5, 0xe6, 0x7f, 0x5d, 0xfd, + 0x0e, 0x90, 0x42, 0x75, 0x96, 0xd4, 0x84, 0x2f, 0x5b, 0x10, 0xa1, 0x11, 0xe9, 0x7c, + 0x40, 0x73, 0x20, 0x3c, 0xed, 0xf6, 0xb8, 0x0a, 0x85, 0x14, 0x5e, 0x50, 0x61, 0xac, + 0xd2, 0x9b, 0xc5, 0xa4, 0xe3, 0x49, 0xb1, 0x4f, 0x85, 0x57, 0xa7, 0x03, 0x3e, 0x23, + 0xb0, 0x66, 0xb7, 0xce, 0x24, 0x09, 0xd9, 0x73, + ]), + }, + ]; + + #[test] + fn test_vectors() { + for tv in TEST_VECTORS { + let subpath = tv + .subpath + .iter() + .map(|(i, tag)| { + PathElement::new(ChildIndex::from_index(*i).expect("hardened"), tag) + }) + .collect::>(); + + let sk = SecretKey::from_subpath(tv.context_string, &tv.seed, tv.zip_number, &subpath) + .unwrap(); + assert_eq!(sk.data(), &tv.sk); + assert_eq!(sk.chain_code().as_bytes(), &tv.c); + + let fw = (!subpath.is_empty()).then(|| { + cryptovalue_from_subpath(tv.context_string, &tv.seed, tv.zip_number, &subpath) + .unwrap() + }); + assert_eq!(&fw, &tv.full_width); + if let Some(fw) = fw { + assert_ne!(&fw[..32], &tv.sk); + assert_ne!(&fw[32..], &tv.c); + } + } + } +}