Compare commits

...

29 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
Jack Grigg a355905c5e v0.1.3 2024-12-13 22:59:19 +00:00
Jack Grigg 7238aa4e1d Disable default features of dependencies to fix no-std support 2024-12-13 22:48:49 +00:00
Jack Grigg 56a4ea7d33
Merge pull request #22 from zcash/arb-keys-test-vectors
Test vectors for ZIP 32 arbitrary keys
2024-12-14 11:48:05 +13:00
Jack Grigg 4d4b38cdfb Update test vectors and fix test
I was using the wrong seed; we now get it from the test vectors.

Source: https://github.com/zcash/zcash-test-vectors
Revision: 4505cb843828209c0038cb83075ccbde818e9c34
2024-11-13 07:28:07 +00:00
Jack Grigg 4d72c475da Add test vectors
Source: https://github.com/zcash/zcash-test-vectors
Revision: 53cc8e25ea400244359cb572a6480528966d456a
2024-11-13 05:01:22 +00:00
Jack Grigg f4e1c2fe99
Merge pull request #19 from zcash/arb-keys
Implement ZIP 32 arbitrary key derivation
2024-10-22 21:45:40 +01:00
Jack Grigg 3c9086a118 v0.1.2 2024-10-22 20:26:36 +00:00
Jack Grigg b97f065fa0 Implement ZIP 32 arbitrary key derivation 2024-10-18 23:15:43 +00:00
Kris Nuttycombe ea6a62c891
Merge pull request #16 from zcash/diversifier_index_ord
Implement `{PartialOrd, Ord, Hash}` for `DiversifierIndex`
2024-09-11 09:16:05 -06:00
Kris Nuttycombe 38e39b7086 Implement `{PartialOrd, Ord, Hash}` for `DiversifierIndex` 2024-03-16 08:45:17 -06:00
Kris Nuttycombe 5805178fc7
Merge pull request #15 from zcash/release-0.1.1
Release v0.1.1
2024-03-14 16:25:01 -06:00
Kris Nuttycombe de85e1c690 v0.1.1 2024-03-14 16:19:43 -06:00
Kris Nuttycombe 05e23b77ce
Merge pull request #14 from zcash/seed_fingerprint_traits
Add Copy, Clone, Debug, Eq, Ord, and Hash for SeedFingerprint.
2024-03-14 16:17:01 -06:00
Kris Nuttycombe 3935950449 Add Copy, Clone, Debug, Eq, Ord, and Hash for SeedFingerprint. 2024-03-14 15:31:22 -06:00
str4d 916145e6ff
Merge pull request #13 from zcash/release-0.1.0
Release 0.1.0
2023-12-06 17:48:21 +00:00
8 changed files with 1067 additions and 10 deletions

View File

@ -7,5 +7,46 @@ 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
- Disabled default features of dependencies to fix no-std support.
## [0.1.2] - 2024-10-22
### 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
### Added
- `impl {Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash}` for
`zip32::fingerprint::SeedFingerprint`
- `zip32::fingerprint::SeedFingerprint::from_bytes`
## [0.1.0] - 2023-12-06
Initial release.

16
Cargo.lock generated
View File

@ -39,9 +39,9 @@ checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
[[package]]
name = "memuse"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2145869435ace5ea6ea3d35f59be559317ec9a0d04e1812d5f185a87b6d36f1a"
checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964"
[[package]]
name = "subtle"
@ -49,12 +49,22 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "zcash_spec"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915"
dependencies = [
"blake2b_simd",
]
[[package]]
name = "zip32"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"assert_matches",
"blake2b_simd",
"memuse",
"subtle",
"zcash_spec",
]

View File

@ -1,6 +1,6 @@
[package]
name = "zip32"
version = "0.1.0"
version = "0.2.0"
authors = [
"Jack Grigg <jack@electriccoin.co>",
"Kris Nuttycombe <kris@electriccoin.co>",
@ -14,13 +14,14 @@ edition = "2021"
rust-version = "1.60"
[dependencies]
blake2b_simd = "1"
memuse = "0.2.1"
subtle = "2.2.3"
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.2.1"
[dev-dependencies]
assert_matches = "1.5"
[features]
default = ["std"]
std = []
std = ["memuse/std"]

344
src/arbitrary.rs Normal file
View File

@ -0,0 +1,344 @@
//! Ad-hoc ("arbitrary") key derivation.
//!
//! 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].
//!
//! 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.
//!
//! 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, HardenedOnlyCkdDomain, HardenedOnlyKey},
ChainCode, ChildIndex,
};
use super::with_ikm;
struct Adhoc;
impl Context for Adhoc {
const MKG_DOMAIN: [u8; 16] = *b"ZcashArbitraryKD";
const CKD_DOMAIN: HardenedOnlyCkdDomain = PrfExpand::ADHOC_ZIP32_CHILD;
}
/// An ad-hoc extended secret key.
///
/// Defined in [ZIP 32: Ad-hoc key generation (deprecated)][adhockd].
///
/// [adhockd]: https://zips.z.cash/zip-0032#specification-ad-hoc-key-derivation-deprecated
pub struct SecretKey {
inner: HardenedOnlyKey<Adhoc>,
}
impl SecretKey {
/// 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.
///
/// # 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 ad-hoc extended secret key.
///
/// Defined in [ZIP 32: Ad-hoc master key generation (deprecated)][adhocmkg].
///
/// [adhocmkg]: https://zips.z.cash/zip-0032#ad-hoc-master-key-generation-deprecated
///
/// # 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.
///
/// Defined in [ZIP 32: Ad-hoc child key derivation (deprecated)][adhocckd].
///
/// [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 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
}
/// 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. 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.
let mut key = [0; 64];
key[..32].copy_from_slice(&sk);
key[32..].copy_from_slice(&c.0);
key
}
}
#[cfg(test)]
mod tests {
use super::{with_ikm, ChildIndex, SecretKey};
struct TestVector {
context_string: &'static [u8],
seed: [u8; 32],
ikm: Option<&'static [u8]>,
path: &'static [u32],
sk: [u8; 32],
c: [u8; 32],
}
// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/zip_0032_arbitrary.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,
],
ikm: Some(&[
0x12, 0x5a, 0x63, 0x61, 0x73, 0x68, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65,
0x63, 0x74, 0x6f, 0x72, 0x73, 0x20, 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,
]),
path: &[],
sk: [
0xe9, 0xda, 0x88, 0x06, 0x40, 0x9d, 0xc3, 0xc3, 0xeb, 0xd1, 0xfc, 0x2a, 0x71, 0xc8,
0x79, 0xc1, 0x3d, 0xd7, 0xaa, 0x93, 0xed, 0xe8, 0x03, 0xbf, 0x1a, 0x83, 0x41, 0x4b,
0x9d, 0x3b, 0x15, 0x8a,
],
c: [
0x65, 0xa7, 0x48, 0xf2, 0x90, 0x5f, 0x7a, 0x8a, 0xab, 0x9f, 0x3d, 0x02, 0xf1, 0xb2,
0x6c, 0x3d, 0x65, 0xc8, 0x29, 0x94, 0xce, 0x59, 0xa0, 0x86, 0xd4, 0xc6, 0x51, 0xd8,
0xa8, 0x1c, 0xec, 0x51,
],
},
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: &[2147483649],
sk: [
0xe8, 0x40, 0x9a, 0xaa, 0x83, 0x2c, 0xc2, 0x37, 0x8f, 0x2b, 0xad, 0xeb, 0x77, 0x15,
0x05, 0x62, 0x15, 0x37, 0x42, 0xfe, 0xe8, 0x76, 0xdc, 0xf4, 0x78, 0x3a, 0x6c, 0xcd,
0x11, 0x9d, 0xa6, 0x6a,
],
c: [
0xcc, 0x08, 0x49, 0x22, 0xa0, 0xea, 0xd2, 0xda, 0x53, 0x38, 0xbd, 0x82, 0x20, 0x0a,
0x19, 0x46, 0xbc, 0x85, 0x85, 0xb8, 0xd9, 0xee, 0x41, 0x6d, 0xf6, 0xa0, 0x9a, 0x71,
0xab, 0x0e, 0x5b, 0x58,
],
},
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: &[2147483649, 2147483650],
sk: [
0x46, 0x4f, 0x90, 0xa3, 0x64, 0xcf, 0xf8, 0x05, 0xfe, 0xe9, 0x3a, 0x85, 0xb7, 0x2f,
0x48, 0x94, 0xce, 0x4e, 0x13, 0x58, 0xdc, 0xdc, 0x1e, 0x61, 0xa3, 0xd4, 0x30, 0x30,
0x1c, 0x60, 0x91, 0x0e,
],
c: [
0xf9, 0xd2, 0x54, 0x4a, 0x55, 0x28, 0xae, 0x6b, 0xd9, 0xf0, 0x36, 0xf4, 0x2f, 0x9f,
0x05, 0xd8, 0x3d, 0xff, 0x50, 0x7a, 0xeb, 0x2a, 0x81, 0x41, 0xaf, 0x11, 0xd9, 0xf1,
0x67, 0xe2, 0x21, 0xae,
],
},
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: &[2147483649, 2147483650, 2147483651],
sk: [
0xfc, 0x4b, 0x6e, 0x93, 0xb0, 0xe4, 0x2f, 0x7a, 0x76, 0x2c, 0xa0, 0xc6, 0x52, 0x2c,
0xcd, 0x10, 0x45, 0xca, 0xb5, 0x06, 0xb3, 0x72, 0x45, 0x2a, 0xf7, 0x30, 0x6c, 0x87,
0x38, 0x9a, 0xb6, 0x2c,
],
c: [
0xe8, 0x9b, 0xf2, 0xed, 0x73, 0xf5, 0xe0, 0x88, 0x75, 0x42, 0xe3, 0x67, 0x93, 0xfa,
0xc8, 0x2c, 0x50, 0x8a, 0xb5, 0xd9, 0x91, 0x98, 0x57, 0x82, 0x27, 0xb2, 0x41, 0xfb,
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";
for tv in TEST_VECTORS {
assert_eq!(tv.context_string, context_string);
let path = tv
.path
.iter()
.map(|i| ChildIndex::from_index(*i).expect("hardened"))
.collect::<alloc::vec::Vec<_>>();
// The derived master key should be identical to the key at the empty path.
if let Some(mut tv_ikm) = tv.ikm {
with_ikm(tv.context_string, &tv.seed, |ikm| {
for part in ikm {
assert_eq!(*part, &tv_ikm[..part.len()]);
tv_ikm = &tv_ikm[part.len()..];
}
});
let sk = SecretKey::master(context_string, &tv.seed);
assert_eq!((sk.data(), sk.chain_code().as_bytes()), (&tv.sk, &tv.c));
}
let sk = SecretKey::from_path(tv.context_string, &tv.seed, &path);
assert_eq!(sk.data(), &tv.sk);
assert_eq!(sk.chain_code().as_bytes(), &tv.c);
}
}
}

View File

@ -10,8 +10,21 @@ const ZIP32_SEED_FP_PERSONALIZATION: &[u8; 16] = b"Zcash_HD_Seed_FP";
/// The fingerprint for a wallet's seed bytes, as defined in [ZIP 32].
///
/// [ZIP 32]: https://zips.z.cash/zip-0032#seed-fingerprints
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SeedFingerprint([u8; 32]);
impl ::core::fmt::Debug for SeedFingerprint {
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
write!(f, "SeedFingerprint(")?;
for i in self.0 {
write!(f, "{:02x}", i)?;
}
write!(f, ")")?;
Ok(())
}
}
impl SeedFingerprint {
/// Derives the fingerprint of the given seed bytes.
///
@ -38,6 +51,11 @@ impl SeedFingerprint {
}
}
/// Reconstructs the fingerprint from a buffer containing a previously computed fingerprint.
pub fn from_bytes(hash: [u8; 32]) -> Self {
Self(hash)
}
/// Returns the fingerprint as a byte array.
pub fn to_bytes(&self) -> [u8; 32] {
self.0
@ -49,6 +67,7 @@ fn test_seed_fingerprint() {
struct TestVector {
root_seed: [u8; 32],
fingerprint: [u8; 32],
fingerprint_str: &'static str,
}
let test_vectors = [TestVector {
@ -62,11 +81,16 @@ fn test_seed_fingerprint() {
0x46, 0xf2, 0xfd, 0x8d, 0x53, 0x89, 0xf7, 0x7, 0x25, 0x56, 0xdc, 0xb5, 0x55, 0xfd,
0xbe, 0x5e, 0x3a, 0xe3,
],
fingerprint_str: "deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3",
}];
for tv in test_vectors {
let fp = SeedFingerprint::from_seed(&tv.root_seed).expect("root_seed has valid length");
assert_eq!(&fp.to_bytes(), &tv.fingerprint[..]);
assert_eq!(
alloc::format!("{:?}", fp),
alloc::format!("SeedFingerprint({})", tv.fingerprint_str)
);
}
}
#[test]

148
src/hardened_only.rs Normal file
View File

@ -0,0 +1,148 @@
//! Generic framework for hardened-only key derivation.
//!
//! Defined in [ZIP 32: Hardened-only key derivation][hkd].
//!
//! 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, 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.
///
/// 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: HardenedOnlyCkdDomain;
}
/// An arbitrary or registered extended secret key.
///
/// Defined in [ZIP 32: 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> {
/// 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)
}
/// 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 [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 {
// 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().expect("64-byte output")
};
Self::from_bytes(&I)
}
/// 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: 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))
}
/// 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 spending key sk.
let sk = I_L.try_into().unwrap();
// I_R is used as the chain code c.
let chain_code = ChainCode::new(I_R.try_into().unwrap());
Self {
sk,
chain_code,
_context: PhantomData,
}
}
}

View File

@ -7,6 +7,9 @@
#![deny(unsafe_code)]
#![deny(rustdoc::broken_intra_doc_links)]
#[cfg(test)]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
@ -15,7 +18,10 @@ use core::mem;
use memuse::{self, DynamicUsage};
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.
///
@ -67,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.
@ -82,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.
@ -122,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
@ -149,7 +188,7 @@ impl ChainCode {
}
/// The index for a particular diversifier.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DiversifierIndex([u8; 11]);
impl Default for DiversifierIndex {
@ -214,6 +253,26 @@ impl From<DiversifierIndex> for u128 {
}
}
impl PartialOrd for DiversifierIndex {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DiversifierIndex {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.0
.iter()
.rev()
.zip(other.0.iter().rev())
.find_map(|(a, b)| match a.cmp(b) {
core::cmp::Ordering::Equal => None,
ineq => Some(ineq),
})
.unwrap_or(core::cmp::Ordering::Equal)
}
}
impl DiversifierIndex {
/// Constructs the zero index.
pub fn new() -> Self {
@ -338,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,
@ -375,4 +434,12 @@ mod tests {
assert_matches!(di.increment(), Err(_));
}
#[test]
fn diversifier_index_ord() {
assert!(DiversifierIndex::from(1u64) < DiversifierIndex::from(2u64));
assert!(DiversifierIndex::from(u64::MAX - 1) < DiversifierIndex::from(u64::MAX));
assert!(DiversifierIndex::from(3u64) == DiversifierIndex::from(3u64));
assert!(DiversifierIndex::from(u64::MAX) == DiversifierIndex::from(u64::MAX));
}
}

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);
}
}
}
}