Compare commits

...

14 Commits

Author SHA1 Message Date
Kris Nuttycombe bbd4e9c243
Merge pull request #26 from nuttycom/add_const_accountid_ctr
Add `AccountId::const_from_u32`
2025-05-15 14:25:14 -06:00
Kris Nuttycombe 372a4887f0 Add `AccountId::const_from_u32` 2025-04-15 11:53:10 -06:00
Kris Nuttycombe 92aec1f9f4
Merge pull request #25 from zcash/release/v0.2.0
Release zip32 version 0.2.0
2025-02-20 14:03:25 -07:00
Kris Nuttycombe b9787dfe2c Release zip32 version 0.2.0 2025-02-20 13:51:44 -07:00
Kris Nuttycombe 6632f2c1b5
Merge pull request #24 from daira/support-tagged-child-derivation
Support tagged ZIP 32 child derivation for registered application protocols
2025-02-20 13:47:33 -07:00
Kris Nuttycombe 2c177cb81c Remove potential panics from public API of registered key derivation.
Co-authored-by: Kris Nuttycombe <kris@nutty.land>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
2025-02-20 19:41:06 +00:00
Daira-Emma Hopwood 49c65fc46b Update documentation, adapt to changes in `zcash_spec` API, and add
`registered::SecretKey::{derive_child, derive_child_cryptovalue}`.

Co-authored-by: Jack Grigg <jack@electriccoin.co>
Co-authored-by: Kris Nuttycombe <kris@nutty.land>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
2025-02-20 19:37:28 +00:00
Jack Grigg fa7805dd7d Add additional required methods to the public API 2025-02-18 18:14:22 +13:00
Jack Grigg bd9652155c Fix variable name 2025-02-18 18:14:01 +13:00
Daira-Emma Hopwood bcbfd01921 Rename a private marker type from `Arbitrary` to `Adhoc`.
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
2025-02-18 00:43:19 +00:00
Daira-Emma Hopwood 6cb332b3a1 Documentation link fixes.
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
2025-02-18 00:39:18 +00:00
Daira-Emma Hopwood e5b4ea2a43 Adapt to APIs changes in `zcash_spec`.
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
2025-02-18 00:25:28 +00:00
Daira-Emma Hopwood c7442d7d36 Support tagged ZIP 32 child derivation for application protocols specified
in a ZIP.

Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
2025-02-17 21:22:23 +00:00
Kris Nuttycombe 9ed8d60b2e
Merge pull request #23 from zcash/no-std-fix
Fix no-std usage
2024-12-13 17:47:15 -07:00
7 changed files with 661 additions and 95 deletions

View File

@ -7,6 +7,26 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zip32::AccountId::const_from_u32`
## [0.2.0] - 2025-02-20
### 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
### Fixed

6
Cargo.lock generated
View File

@ -51,16 +51,16 @@ 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",
]
[[package]]
name = "zip32"
version = "0.1.3"
version = "0.2.0"
dependencies = [
"assert_matches",
"blake2b_simd",

View File

@ -1,6 +1,6 @@
[package]
name = "zip32"
version = "0.1.3"
version = "0.2.0"
authors = [
"Jack Grigg <jack@electriccoin.co>",
"Kris Nuttycombe <kris@electriccoin.co>",
@ -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"

View File

@ -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;
impl Context for Arbitrary {
struct Adhoc;
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<Arbitrary>,
}
fn with_ikm<F, T>(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<Adhoc>,
}
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::<alloc::vec::Vec<_>>();
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 {

View File

@ -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<C: Context> ConstantTimeEq for HardenedOnlyKey<C> {
#[allow(non_snake_case)]
impl<C: Context> HardenedOnlyKey<C> {
/// 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<C: Context> HardenedOnlyKey<C> {
/// 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<C: Context> HardenedOnlyKey<C> {
for input in ikm {
I.update(input);
}
I.finalize().as_bytes().try_into().unwrap()
I.finalize().as_bytes().try_into().expect("64-byte output")
};
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,
}
Self::from_bytes(&I)
}
/// Derives a child key from a parent key at a given index.
/// Derives a child key from a parent key at a given index and empty tag.
///
/// Defined in [ZIP32: Hardened-only child key derivation][ckdh].
/// 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: 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(
pub fn derive_child_with_tag(&self, index: ChildIndex, tag: &[u8]) -> Self {
Self::from_bytes(&self.ckdh_internal(index, 0, tag))
}
/// Defined in [ZIP 32: 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(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,
}
}

View File

@ -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.
///
@ -72,6 +73,18 @@ impl AccountId {
pub fn next(&self) -> Option<Self> {
Self::try_from(self.0 + 1).ok()
}
/// Constant function to construct an account ID from a u32.
///
/// # Panics
/// Panics if the provided value is >= 2^31
pub const fn const_from_u32(value: u32) -> Self {
if value < (1 << 31) {
Self(value)
} else {
panic!("Account IDs must be in the range 0..2^31");
}
}
}
/// The error type returned when a checked integral type conversion fails.
@ -87,6 +100,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<F, T>(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 +158,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 +397,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,

422
src/registered.rs Normal file
View File

@ -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<Registered>,
}
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<Self, 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);
}
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::<alloc::vec::Vec<_>>();
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);
}
}
}
}