Skip to content

Commit c346973

Browse files
PR471 review: Fix issuance key derivation to use dedicated ZIP-32 context (#229)
Address the following review comments: r2542655700 r2542668676 This PR introduces a dedicated ZIP-227 issuance key derivation using hardened-only ZIP-32, separate from Orchard spending keys.
1 parent dc861af commit c346973

File tree

3 files changed

+86
-47
lines changed

3 files changed

+86
-47
lines changed

src/issuance/auth.rs

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,83 @@
2020
2121
use alloc::vec::Vec;
2222
use core::{
23+
fmt,
2324
fmt::{Debug, Formatter},
2425
mem::size_of_val,
2526
};
2627

2728
use rand_core::CryptoRngCore;
2829
use secp256k1::{schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey};
2930

30-
use crate::{
31-
issuance::Error,
32-
zip32::{self, ExtendedSpendingKey},
33-
};
31+
use crate::issuance::Error;
3432

35-
// Preserve '::' which specifies the EXTERNAL 'zip32' crate
36-
#[rustfmt::skip]
37-
pub use ::zip32::{AccountId, ChildIndex, DiversifierIndex, Scope, hardened_only};
33+
pub use ::zip32::{
34+
hardened_only, hardened_only::HardenedOnlyKey, AccountId, ChildIndex, DiversifierIndex, Scope,
35+
};
36+
use zcash_spec::{PrfExpand, VariableLengthSlice};
3837

3938
const ZIP32_PURPOSE_FOR_ISSUANCE: u32 = 227;
39+
const ZIP32_ORCHARD_ISSUANCE_PERSONALIZATION: &[u8; 16] = b"ZcashSA_Issue_V1";
40+
41+
/// Errors produced in derivation of extended issuance keys
42+
#[derive(Debug, PartialEq, Eq)]
43+
pub enum Zip32Error {
44+
/// A seed resulted in an invalid issuance key
45+
InvalidIssuanceKey,
46+
/// A non zero account when deriving an Orchard-ZSA issuance key
47+
NonZeroAccount,
48+
}
49+
50+
impl fmt::Display for Zip32Error {
51+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52+
match self {
53+
Zip32Error::InvalidIssuanceKey => {
54+
write!(f, "Seed produced an invalid issuance authorizing key.")
55+
}
56+
Zip32Error::NonZeroAccount => {
57+
write!(
58+
f,
59+
"A non zero account when deriving an Orchard-ZSA issuance key"
60+
)
61+
}
62+
}
63+
}
64+
}
65+
66+
#[derive(Clone, Copy, Debug)]
67+
struct Issuance;
68+
69+
impl hardened_only::Context for Issuance {
70+
const MKG_DOMAIN: [u8; 16] = *ZIP32_ORCHARD_ISSUANCE_PERSONALIZATION;
71+
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4], [u8; 1], VariableLengthSlice)> =
72+
PrfExpand::ORCHARD_ZIP32_CHILD;
73+
}
74+
75+
/// An extended issuance key.
76+
///
77+
/// Defined in [ZIP227: Issuance of Zcash Shielded Assets][issuancekeyderivation].
78+
///
79+
/// [issuancekeyderivation]: https://zips.z.cash/zip-0227#issuance-key-derivation
80+
#[derive(Debug, Clone)]
81+
pub(crate) struct ExtendedIssuanceKey {
82+
inner: HardenedOnlyKey<Issuance>,
83+
}
84+
85+
impl ExtendedIssuanceKey {
86+
/// Derives an issuance extended key from a ZIP-32 seed and a hardened derivation path.
87+
pub fn from_path(seed: &[u8], path: &[ChildIndex]) -> Self {
88+
let mut key = HardenedOnlyKey::<Issuance>::master(&[seed]);
89+
for index in path {
90+
key = key.derive_child(*index);
91+
}
92+
Self { inner: key }
93+
}
94+
95+
/// Returns the raw 32-byte sk.
96+
pub fn sk_bytes(&self) -> [u8; 32] {
97+
*self.inner.parts().0
98+
}
99+
}
40100

41101
/// Trait that defines the common interface for issuance authorization signature schemes.
42102
pub trait IssueAuthSigScheme {
@@ -157,13 +217,9 @@ impl IssueAuthKey<ZSASchnorr> {
157217
}
158218

159219
/// Derives the Orchard-ZSA issuance key for the given seed, coin type, and account.
160-
pub fn from_zip32_seed(
161-
seed: &[u8],
162-
coin_type: u32,
163-
account: u32,
164-
) -> Result<Self, zip32::Error> {
220+
pub fn from_zip32_seed(seed: &[u8], coin_type: u32, account: u32) -> Result<Self, Zip32Error> {
165221
if account != 0 {
166-
return Err(zip32::Error::NonZeroAccount);
222+
return Err(Zip32Error::NonZeroAccount);
167223
}
168224

169225
// Call zip32 logic
@@ -173,12 +229,9 @@ impl IssueAuthKey<ZSASchnorr> {
173229
ChildIndex::hardened(account),
174230
];
175231

176-
// we are reusing zip32 logic for deriving the key, zip32 should be updated as discussed
177-
let &isk_bytes = ExtendedSpendingKey::<zip32::Issuance>::from_path(seed, path)?
178-
.sk()
179-
.to_bytes();
232+
let isk_bytes = ExtendedIssuanceKey::from_path(seed, path).sk_bytes();
180233

181-
Self::from_bytes(&isk_bytes).ok_or(zip32::Error::InvalidSpendingKey)
234+
Self::from_bytes(&isk_bytes).ok_or(Zip32Error::InvalidIssuanceKey)
182235
}
183236
}
184237

src/keys.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use alloc::vec::Vec;
44
use core::fmt::Debug;
55
use core2::io::{self, Read, Write};
66

7-
use ::zip32::{hardened_only, ChildIndex};
7+
use ::zip32::ChildIndex;
88
use aes::Aes256;
99
use blake2b_simd::{Hash as Blake2bHash, Params};
1010
use fpe::ff1::{BinaryNumeralString, FF1};
@@ -107,7 +107,7 @@ impl SpendingKey {
107107
ChildIndex::hardened(coin_type),
108108
ChildIndex::hardened(account.into()),
109109
];
110-
ExtendedSpendingKey::<zip32::Orchard>::from_path(seed, path).map(|esk| esk.sk())
110+
ExtendedSpendingKey::from_path(seed, path).map(|esk| esk.sk())
111111
}
112112
}
113113

@@ -322,8 +322,8 @@ impl From<&SpendingKey> for FullViewingKey {
322322
}
323323
}
324324

325-
impl<C: hardened_only::Context> From<&ExtendedSpendingKey<C>> for FullViewingKey {
326-
fn from(extsk: &ExtendedSpendingKey<C>) -> Self {
325+
impl From<&ExtendedSpendingKey> for FullViewingKey {
326+
fn from(extsk: &ExtendedSpendingKey) -> Self {
327327
(&extsk.sk()).into()
328328
}
329329
}

src/zip32.rs

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ use crate::{
1717

1818
pub use zip32::ChildIndex;
1919

20-
/// Personalization for the master extended spending key
2120
const ZIP32_ORCHARD_PERSONALIZATION: &[u8; 16] = b"ZcashIP32Orchard";
22-
/// Personalization for the master extended issuance key
23-
const ZIP32_ORCHARD_ISSUANCE_PERSONALIZATION: &[u8; 16] = b"ZcashSA_Issue_V1";
2421
const ZIP32_ORCHARD_FVFP_PERSONALIZATION: &[u8; 16] = b"ZcashOrchardFVFP";
2522

2623
/// Errors produced in derivation of extended spending keys
@@ -30,8 +27,6 @@ pub enum Error {
3027
InvalidSpendingKey,
3128
/// A child index in a derivation path exceeded 2^31
3229
InvalidChildIndex(u32),
33-
/// A non zero account when deriving an Orchard-ZSA issuance key
34-
NonZeroAccount,
3530
}
3631

3732
impl fmt::Display for Error {
@@ -126,37 +121,28 @@ impl KeyIndex {
126121
}
127122

128123
#[derive(Clone, Copy, Debug)]
129-
pub(crate) struct Orchard;
124+
struct Orchard;
130125

131126
impl hardened_only::Context for Orchard {
132127
const MKG_DOMAIN: [u8; 16] = *ZIP32_ORCHARD_PERSONALIZATION;
133128
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4], [u8; 1], VariableLengthSlice)> =
134129
PrfExpand::ORCHARD_ZIP32_CHILD;
135130
}
136131

137-
#[derive(Clone, Copy, Debug)]
138-
pub(crate) struct Issuance;
139-
140-
impl hardened_only::Context for Issuance {
141-
const MKG_DOMAIN: [u8; 16] = *ZIP32_ORCHARD_ISSUANCE_PERSONALIZATION;
142-
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4], [u8; 1], VariableLengthSlice)> =
143-
PrfExpand::ORCHARD_ZIP32_CHILD;
144-
}
145-
146132
/// An Orchard extended spending key.
147133
///
148134
/// Defined in [ZIP32: Orchard extended keys][orchardextendedkeys].
149135
///
150136
/// [orchardextendedkeys]: https://zips.z.cash/zip-0032#orchard-extended-keys
151137
#[derive(Debug, Clone)]
152-
pub(crate) struct ExtendedSpendingKey<C: hardened_only::Context> {
138+
pub(crate) struct ExtendedSpendingKey {
153139
depth: u8,
154140
parent_fvk_tag: FvkTag,
155141
child_index: KeyIndex,
156-
inner: HardenedOnlyKey<C>,
142+
inner: HardenedOnlyKey<Orchard>,
157143
}
158144

159-
impl<C: hardened_only::Context> ConstantTimeEq for ExtendedSpendingKey<C> {
145+
impl ConstantTimeEq for ExtendedSpendingKey {
160146
fn ct_eq(&self, rhs: &Self) -> Choice {
161147
self.depth.ct_eq(&rhs.depth)
162148
& self.parent_fvk_tag.0.ct_eq(&rhs.parent_fvk_tag.0)
@@ -166,7 +152,7 @@ impl<C: hardened_only::Context> ConstantTimeEq for ExtendedSpendingKey<C> {
166152
}
167153

168154
#[allow(non_snake_case)]
169-
impl<C: hardened_only::Context> ExtendedSpendingKey<C> {
155+
impl ExtendedSpendingKey {
170156
/// Returns the spending key of the child key corresponding to
171157
/// the path derived from the master key
172158
///
@@ -250,7 +236,7 @@ mod tests {
250236
#[test]
251237
fn derive_child() {
252238
let seed = [0; 32];
253-
let xsk_m = ExtendedSpendingKey::<Orchard>::master(&seed).unwrap();
239+
let xsk_m = ExtendedSpendingKey::master(&seed).unwrap();
254240

255241
let i_5 = ChildIndex::hardened(5);
256242
let xsk_5 = xsk_m.derive_child(i_5);
@@ -261,18 +247,18 @@ mod tests {
261247
#[test]
262248
fn path() {
263249
let seed = [0; 32];
264-
let xsk_m = ExtendedSpendingKey::<Orchard>::master(&seed).unwrap();
250+
let xsk_m = ExtendedSpendingKey::master(&seed).unwrap();
265251

266252
let xsk_5h = xsk_m.derive_child(ChildIndex::hardened(5)).unwrap();
267253
assert!(bool::from(
268-
ExtendedSpendingKey::<Orchard>::from_path(&seed, &[ChildIndex::hardened(5)],)
254+
ExtendedSpendingKey::from_path(&seed, &[ChildIndex::hardened(5)],)
269255
.unwrap()
270256
.ct_eq(&xsk_5h)
271257
));
272258

273259
let xsk_5h_7 = xsk_5h.derive_child(ChildIndex::hardened(7)).unwrap();
274260
assert!(bool::from(
275-
ExtendedSpendingKey::<Orchard>::from_path(
261+
ExtendedSpendingKey::from_path(
276262
&seed,
277263
&[ChildIndex::hardened(5), ChildIndex::hardened(7)],
278264
)
@@ -294,9 +280,9 @@ mod tests {
294280
let i2h = ChildIndex::hardened(2);
295281
let i3h = ChildIndex::hardened(3);
296282

297-
let m = ExtendedSpendingKey::<Orchard>::master(&seed).unwrap();
283+
let m = ExtendedSpendingKey::master(&seed).unwrap();
298284
let m_1h = m.derive_child(i1h).unwrap();
299-
let m_1h_2h = ExtendedSpendingKey::<Orchard>::from_path(&seed, &[i1h, i2h]).unwrap();
285+
let m_1h_2h = ExtendedSpendingKey::from_path(&seed, &[i1h, i2h]).unwrap();
300286
let m_1h_2h_3h = m_1h_2h.derive_child(i3h).unwrap();
301287

302288
let xsks = [m, m_1h, m_1h_2h, m_1h_2h_3h];

0 commit comments

Comments
 (0)