From 68290a1a58a35718dde1319885260a3d88b09140 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 10 Jan 2024 22:41:16 +0000 Subject: [PATCH] Migrate to `zip32 0.1` Closes zcash/orchard#410. --- CHANGELOG.md | 5 +++ Cargo.lock | 12 +++++++ Cargo.toml | 1 + src/keys.rs | 55 +++++++++-------------------- src/zip32.rs | 98 +++++++++++++++++++++++++++++++++++----------------- 5 files changed, 100 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93aed4520..4976deaf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ and this project adheres to Rust's notion of - `orchard::tree::Anchor::empty_tree` ### Changed +- Migrated to the `zip32` crate. The following types have been replaced by the + equivalent ones in that crate are now re-exported from there: + - `orchard::keys::DiversifierIndex` + - `orchard::zip32::ChildIndex` - `orchard::builder`: - `Builder::new` now takes the bundle type to be used in bundle construction, instead of taking the flags and anchor separately. @@ -40,6 +44,7 @@ and this project adheres to Rust's notion of - `AnchorMismatch` - `SpendInfo::new` now returns a `Result` instead of an `Option`. +- `orchard::keys::SpendingKey::from_zip32_seed` now takes a `zip32::AccountId`. ### Removed - `orchard::bundle::Flags::from_parts` diff --git a/Cargo.lock b/Cargo.lock index 1ebe810ed..dce2a53cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1442,6 +1442,7 @@ dependencies = [ "tracing", "zcash_note_encryption", "zcash_spec", + "zip32", ] [[package]] @@ -2523,6 +2524,17 @@ dependencies = [ "syn 2.0.31", ] +[[package]] +name = "zip32" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d724a63be4dfb50b7f3617e542984e22e4b4a5b8ca5de91f55613152885e6b22" +dependencies = [ + "blake2b_simd", + "memuse", + "subtle", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index f54971a89..502af385a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ subtle = "2.3" zcash_note_encryption = "0.4" incrementalmerkletree = "0.5" zcash_spec = "0.1" +zip32 = "0.1" # Logging tracing = "0.1" diff --git a/src/keys.rs b/src/keys.rs index e2408dd7b..8100a22c1 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,8 +1,8 @@ //! Key structures for Orchard. -use core::mem; use std::io::{self, Read, Write}; +use ::zip32::{AccountId, ChildIndex}; use aes::Aes256; use blake2b_simd::{Hash as Blake2bHash, Params}; use fpe::ff1::{BinaryNumeralString, FF1}; @@ -24,9 +24,11 @@ use crate::{ to_scalar, NonIdentityPallasPoint, NonZeroPallasBase, NonZeroPallasScalar, PreparedNonIdentityBase, PreparedNonZeroScalar, PrfExpand, }, - zip32::{self, ChildIndex, ExtendedSpendingKey}, + zip32::{self, ExtendedSpendingKey}, }; +pub use ::zip32::DiversifierIndex; + const KDF_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_OrchardKDF"; const ZIP32_PURPOSE: u32 = 32; @@ -91,13 +93,17 @@ impl SpendingKey { pub fn from_zip32_seed( seed: &[u8], coin_type: u32, - account: u32, + account: AccountId, ) -> Result { + if coin_type >= (1 << 31) { + return Err(zip32::Error::InvalidChildIndex(coin_type)); + } + // Call zip32 logic let path = &[ - ChildIndex::try_from(ZIP32_PURPOSE)?, - ChildIndex::try_from(coin_type)?, - ChildIndex::try_from(account)?, + ChildIndex::hardened(ZIP32_PURPOSE), + ChildIndex::hardened(coin_type), + ChildIndex::hardened(account.into()), ]; ExtendedSpendingKey::from_path(seed, path).map(|esk| esk.sk()) } @@ -481,44 +487,15 @@ impl FullViewingKey { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct DiversifierKey([u8; 32]); -/// The index for a particular diversifier. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct DiversifierIndex([u8; 11]); - -macro_rules! di_from { - ($n:ident) => { - impl From<$n> for DiversifierIndex { - fn from(j: $n) -> Self { - let mut j_bytes = [0; 11]; - j_bytes[..mem::size_of::<$n>()].copy_from_slice(&j.to_le_bytes()); - DiversifierIndex(j_bytes) - } - } - }; -} -di_from!(u32); -di_from!(u64); -di_from!(usize); - -impl From<[u8; 11]> for DiversifierIndex { - fn from(j_bytes: [u8; 11]) -> Self { - DiversifierIndex(j_bytes) - } -} - -impl DiversifierIndex { - /// Returns the raw bytes of the diversifier index. - pub fn to_bytes(&self) -> &[u8; 11] { - &self.0 - } -} - impl DiversifierKey { /// Returns the diversifier at the given index. pub fn get(&self, j: impl Into) -> Diversifier { let ff = FF1::::new(&self.0, 2).expect("valid radix"); let enc = ff - .encrypt(&[], &BinaryNumeralString::from_bytes_le(&j.into().0[..])) + .encrypt( + &[], + &BinaryNumeralString::from_bytes_le(j.into().as_bytes()), + ) .unwrap(); Diversifier(enc.to_bytes_le().try_into().unwrap()) } diff --git a/src/zip32.rs b/src/zip32.rs index 6dc44b0d7..31f318b11 100644 --- a/src/zip32.rs +++ b/src/zip32.rs @@ -3,13 +3,16 @@ use core::fmt; use blake2b_simd::Params as Blake2bParams; -use subtle::{Choice, ConstantTimeEq}; +use subtle::{Choice, ConstantTimeEq, CtOption}; +use zip32::ChainCode; use crate::{ keys::{FullViewingKey, SpendingKey}, spec::PrfExpand, }; +pub use zip32::ChildIndex; + const ZIP32_ORCHARD_PERSONALIZATION: &[u8; 16] = b"ZcashIP32Orchard"; const ZIP32_ORCHARD_FVFP_PERSONALIZATION: &[u8; 16] = b"ZcashOrchardFVFP"; @@ -64,27 +67,55 @@ impl FvkTag { } } -/// A hardened child index for a derived key. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ChildIndex(u32); +/// The derivation index associated with a key. +/// +/// Master keys are never derived via the ZIP 32 child derivation process, but they have +/// an index in their encoding. This type allows the encoding to be represented, while +/// also enabling the derivation methods to only accept [`ChildIndex`]. +#[derive(Clone, Copy, Debug)] +struct KeyIndex(CtOption); + +impl ConstantTimeEq for KeyIndex { + fn ct_eq(&self, other: &Self) -> Choice { + // We use a `CtOption` above instead of an enum so that we can implement this. + self.0.ct_eq(&other.0) + } +} -impl TryFrom for ChildIndex { - type Error = Error; +impl PartialEq for KeyIndex { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for KeyIndex {} - /// `index` must be less than 2^31 - fn try_from(index: u32) -> Result { - if index < (1 << 31) { - Ok(Self(index + (1 << 31))) +impl KeyIndex { + fn master() -> Self { + Self(CtOption::new(ChildIndex::hardened(0), 0.into())) + } + + fn child(i: ChildIndex) -> Self { + Self(CtOption::new(i, 1.into())) + } + + fn new(depth: u8, i: u32) -> Option { + match (depth == 0, i) { + (true, 0) => Some(KeyIndex::master()), + (false, _) => ChildIndex::from_index(i).map(KeyIndex::child), + _ => None, + } + } + + fn index(&self) -> u32 { + if self.0.is_some().into() { + self.0.unwrap().index() } else { - Err(Error::InvalidChildIndex(32)) + 0 } } } -/// The chain code forming the second half of an Orchard extended key. -#[derive(Debug, Copy, Clone, PartialEq)] -struct ChainCode([u8; 32]); - /// An Orchard extended spending key. /// /// Defined in [ZIP32: Orchard extended keys][orchardextendedkeys]. @@ -94,7 +125,7 @@ struct ChainCode([u8; 32]); pub(crate) struct ExtendedSpendingKey { depth: u8, parent_fvk_tag: FvkTag, - child_index: ChildIndex, + child_index: KeyIndex, chain_code: ChainCode, sk: SpendingKey, } @@ -103,8 +134,8 @@ impl ConstantTimeEq for ExtendedSpendingKey { fn ct_eq(&self, rhs: &Self) -> Choice { self.depth.ct_eq(&rhs.depth) & self.parent_fvk_tag.0.ct_eq(&rhs.parent_fvk_tag.0) - & self.child_index.0.ct_eq(&rhs.child_index.0) - & self.chain_code.0.ct_eq(&rhs.chain_code.0) + & self.child_index.ct_eq(&rhs.child_index) + & self.chain_code.ct_eq(&rhs.chain_code) & self.sk.ct_eq(&rhs.sk) } } @@ -153,13 +184,13 @@ impl ExtendedSpendingKey { let sk_m = sk_m.unwrap(); // I_R is used as the master chain code c_m. - let c_m = ChainCode(I[32..].try_into().unwrap()); + let c_m = ChainCode::new(I[32..].try_into().unwrap()); // For the master extended spending key, depth is 0, parent_fvk_tag is 4 zero bytes, and i is 0. Ok(Self { depth: 0, parent_fvk_tag: FvkTag([0; 4]), - child_index: ChildIndex(0), + child_index: KeyIndex::master(), chain_code: c_m, sk: sk_m, }) @@ -175,9 +206,9 @@ impl ExtendedSpendingKey { fn derive_child(&self, index: ChildIndex) -> Result { // I := PRF^Expand(c_par, [0x81] || sk_par || I2LEOSP(i)) let I: [u8; 64] = PrfExpand::ORCHARD_ZIP32_CHILD.with( - &self.chain_code.0, + self.chain_code.as_bytes(), self.sk.to_bytes(), - &index.0.to_le_bytes(), + &index.index().to_le_bytes(), ); // I_L is used as the child spending key sk_i. @@ -188,14 +219,14 @@ impl ExtendedSpendingKey { let sk_i = sk_i.unwrap(); // I_R is used as the child chain code c_i. - let c_i = ChainCode(I[32..].try_into().unwrap()); + let c_i = ChainCode::new(I[32..].try_into().unwrap()); let fvk: FullViewingKey = self.into(); Ok(Self { depth: self.depth + 1, parent_fvk_tag: FvkFingerprint::from(&fvk).tag(), - child_index: index, + child_index: KeyIndex::child(index), chain_code: c_i, sk: sk_i, }) @@ -216,8 +247,8 @@ mod tests { let seed = [0; 32]; let xsk_m = ExtendedSpendingKey::master(&seed).unwrap(); - let i_5 = 5; - let xsk_5 = xsk_m.derive_child(i_5.try_into().unwrap()); + let i_5 = ChildIndex::hardened(5); + let xsk_5 = xsk_m.derive_child(i_5); assert!(xsk_5.is_ok()); } @@ -227,18 +258,21 @@ mod tests { let seed = [0; 32]; let xsk_m = ExtendedSpendingKey::master(&seed).unwrap(); - let xsk_5h = xsk_m.derive_child(5.try_into().unwrap()).unwrap(); + let xsk_5h = xsk_m.derive_child(ChildIndex::hardened(5)).unwrap(); assert!(bool::from( - ExtendedSpendingKey::from_path(&seed, &[5.try_into().unwrap()]) + ExtendedSpendingKey::from_path(&seed, &[ChildIndex::hardened(5)]) .unwrap() .ct_eq(&xsk_5h) )); - let xsk_5h_7 = xsk_5h.derive_child(7.try_into().unwrap()).unwrap(); + let xsk_5h_7 = xsk_5h.derive_child(ChildIndex::hardened(7)).unwrap(); assert!(bool::from( - ExtendedSpendingKey::from_path(&seed, &[5.try_into().unwrap(), 7.try_into().unwrap()]) - .unwrap() - .ct_eq(&xsk_5h_7) + ExtendedSpendingKey::from_path( + &seed, + &[ChildIndex::hardened(5), ChildIndex::hardened(7)] + ) + .unwrap() + .ct_eq(&xsk_5h_7) )); } }