Fix issuance key derivation (#74)

Updated constants for master (extended) issuance key according to ZIP
227. Previously, we used the same personalization for the master
extended spending key and the master extended issuance key, as well as
the same purpose constant for the spending master key and the issuance
master key.

Now, the following updates have been made:
- Personalization for the master extended issuance key: ZIP32ZSAIssue_V1
- Purpose constant for the issuance master key: 227"
This commit is contained in:
Constance Beguier 2023-06-20 20:35:57 +02:00 committed by GitHub
parent 5b003f8b53
commit aa1d89561c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 78 deletions

View File

@ -586,7 +586,8 @@ mod tests {
};
use crate::issuance::{verify_issue_bundle, IssueAction, Signed};
use crate::keys::{
FullViewingKey, IssuanceAuthorizingKey, IssuanceValidatingKey, Scope, SpendingKey,
FullViewingKey, IssuanceAuthorizingKey, IssuanceKey, IssuanceValidatingKey, Scope,
SpendingKey,
};
use crate::note::{AssetBase, Nullifier};
use crate::value::{NoteValue, ValueSum};
@ -605,8 +606,8 @@ mod tests {
) {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let isk: IssuanceAuthorizingKey = (&sk).into();
let sk_iss = IssuanceKey::random(&mut rng);
let isk: IssuanceAuthorizingKey = (&sk_iss).into();
let ik: IssuanceValidatingKey = (&isk).into();
let fvk = FullViewingKey::from(&SpendingKey::random(&mut rng));
@ -876,7 +877,7 @@ mod tests {
)
.unwrap();
let wrong_isk: IssuanceAuthorizingKey = (&SpendingKey::random(&mut OsRng)).into();
let wrong_isk: IssuanceAuthorizingKey = (&IssuanceKey::random(&mut OsRng)).into();
let err = bundle
.prepare([0; 32])
@ -1108,7 +1109,7 @@ mod tests {
)
.unwrap();
let wrong_isk: IssuanceAuthorizingKey = (&SpendingKey::random(&mut rng)).into();
let wrong_isk: IssuanceAuthorizingKey = (&IssuanceKey::random(&mut rng)).into();
let mut signed = bundle.prepare(sighash).sign(rng, &isk).unwrap();
@ -1203,8 +1204,8 @@ mod tests {
let mut signed = bundle.prepare(sighash).sign(rng, &isk).unwrap();
let incorrect_sk = SpendingKey::random(&mut rng);
let incorrect_isk: IssuanceAuthorizingKey = (&incorrect_sk).into();
let incorrect_sk_iss = IssuanceKey::random(&mut rng);
let incorrect_isk: IssuanceAuthorizingKey = (&incorrect_sk_iss).into();
let incorrect_ik: IssuanceValidatingKey = (&incorrect_isk).into();
// Add "bad" note

View File

@ -11,7 +11,7 @@ use group::{
prime::PrimeCurveAffine,
Curve, GroupEncoding,
};
use pasta_curves::pallas;
use pasta_curves::{pallas, pallas::Scalar};
use rand::{CryptoRng, RngCore};
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption};
use zcash_note_encryption::EphemeralKeyBytes;
@ -24,11 +24,15 @@ use crate::{
to_scalar, NonIdentityPallasPoint, NonZeroPallasBase, NonZeroPallasScalar,
PreparedNonIdentityBase, PreparedNonZeroScalar, PrfExpand,
},
zip32::{self, ChildIndex, ExtendedSpendingKey},
zip32::{
self, ChildIndex, ExtendedSpendingKey, ZIP32_ORCHARD_PERSONALIZATION,
ZIP32_ORCHARD_PERSONALIZATION_FOR_ISSUANCE,
},
};
const KDF_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_OrchardKDF";
const ZIP32_PURPOSE: u32 = 32;
const ZIP32_PURPOSE_FOR_ISSUANCE: u32 = 227;
/// A spending key, from which all key material is derived.
///
@ -99,7 +103,8 @@ impl SpendingKey {
ChildIndex::try_from(coin_type)?,
ChildIndex::try_from(account)?,
];
ExtendedSpendingKey::from_path(seed, path).map(|esk| esk.sk())
ExtendedSpendingKey::from_path(seed, path, ZIP32_ORCHARD_PERSONALIZATION)
.map(|esk| esk.sk())
}
}
@ -132,13 +137,17 @@ impl From<&SpendingKey> for SpendAuthorizingKey {
// SpendingKey cannot be constructed such that this assertion would fail.
assert!(!bool::from(ask.is_zero()));
// TODO: Add TryFrom<S::Scalar> for SpendAuthorizingKey.
let ret = SpendAuthorizingKey(ask.to_repr().try_into().unwrap());
// If the last bit of repr_P(ak) is 1, negate ask.
if (<[u8; 32]>::from(SpendValidatingKey::from(&ret).0)[31] >> 7) == 1 {
SpendAuthorizingKey((-ask).to_repr().try_into().unwrap())
} else {
ret
}
SpendAuthorizingKey(conditionally_negate(ask))
}
}
// If the last bit of repr_P(ak) is 1, negate ask.
fn conditionally_negate<T: redpallas::SigType>(scalar: Scalar) -> redpallas::SigningKey<T> {
let ret = redpallas::SigningKey::<T>(scalar.to_repr().try_into().unwrap());
if (<[u8; 32]>::from(redpallas::VerificationKey::<T>::from(&ret).0)[31] >> 7) == 1 {
redpallas::SigningKey::<T>((-scalar).to_repr().try_into().unwrap())
} else {
ret
}
}
@ -178,7 +187,7 @@ impl SpendValidatingKey {
self.0.randomize(randomizer)
}
/// Converts this issuance validating key to its serialized form,
/// Converts this spend key to its serialized form,
/// I2LEOSP_256(ak).
pub(crate) fn to_bytes(&self) -> [u8; 32] {
// This is correct because the wrapped point must have ỹ = 0, and
@ -194,9 +203,87 @@ impl SpendValidatingKey {
}
}
/// A function to check structural validity of the validating keys for authorizing transfers and
/// issuing assets
/// Structural validity checks for ak_P or ik_P:
/// - The point must not be the identity (which for Pallas is canonically encoded as all-zeroes).
/// - The compressed y-coordinate bit must be 0.
fn check_structural_validity(
verification_key_bytes: [u8; 32],
) -> Option<VerificationKey<SpendAuth>> {
if verification_key_bytes != [0; 32] && verification_key_bytes[31] & 0x80 == 0 {
<redpallas::VerificationKey<SpendAuth>>::try_from(verification_key_bytes).ok()
} else {
None
}
}
/// We currently use `SpendAuth` as the `IssuanceAuth`.
type IssuanceAuth = SpendAuth;
/// An issuance key, from which all key material is derived.
///
/// $\mathsf{sk}$ as defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug, Copy, Clone)]
pub struct IssuanceKey([u8; 32]);
impl From<SpendingKey> for IssuanceKey {
fn from(sk: SpendingKey) -> Self {
IssuanceKey(*sk.to_bytes())
}
}
impl ConstantTimeEq for IssuanceKey {
fn ct_eq(&self, other: &Self) -> Choice {
self.to_bytes().ct_eq(other.to_bytes())
}
}
impl IssuanceKey {
/// Generates a random issuance key.
///
/// This is only used when generating a random AssetBase.
/// Real issuance keys should be derived according to [ZIP 32].
///
/// [ZIP 32]: https://zips.z.cash/zip-0032
pub(crate) fn random(rng: &mut impl RngCore) -> Self {
SpendingKey::random(rng).into()
}
/// Constructs an Orchard issuance key from uniformly-random bytes.
///
/// Returns `None` if the bytes do not correspond to a valid Orchard issuance key.
pub fn from_bytes(sk_iss: [u8; 32]) -> CtOption<Self> {
let sk_iss = IssuanceKey(sk_iss);
// If isk = 0 (A scalar value), discard this key.
let isk = IssuanceAuthorizingKey::derive_inner(&sk_iss);
CtOption::new(sk_iss, !isk.is_zero())
}
/// Returns the raw bytes of the issuance key.
pub fn to_bytes(&self) -> &[u8; 32] {
&self.0
}
/// Derives the Orchard issuance key for the given seed, coin type, and account.
pub fn from_zip32_seed(
seed: &[u8],
coin_type: u32,
account: u32,
) -> Result<Self, zip32::Error> {
// Call zip32 logic
let path = &[
ChildIndex::try_from(ZIP32_PURPOSE_FOR_ISSUANCE)?,
ChildIndex::try_from(coin_type)?,
ChildIndex::try_from(account)?,
];
ExtendedSpendingKey::from_path(seed, path, ZIP32_ORCHARD_PERSONALIZATION_FOR_ISSUANCE)
.map(|esk| esk.sk().into())
}
}
/// An issuance authorizing key, used to create issuance authorization signatures.
/// This type enforces that the corresponding public point (ik^) has ỹ = 0.
///
@ -208,9 +295,9 @@ type IssuanceAuth = SpendAuth;
pub struct IssuanceAuthorizingKey(redpallas::SigningKey<IssuanceAuth>);
impl IssuanceAuthorizingKey {
/// Derives isk from sk. Internal use only, does not enforce all constraints.
fn derive_inner(sk: &SpendingKey) -> pallas::Scalar {
to_scalar(PrfExpand::ZsaIsk.expand(&sk.0))
/// Derives isk from sk_iss. Internal use only, does not enforce all constraints.
fn derive_inner(sk_iss: &IssuanceKey) -> pallas::Scalar {
to_scalar(PrfExpand::ZsaIsk.expand(&sk_iss.0))
}
/// Sign the provided message using the `IssuanceAuthorizingKey`.
@ -223,18 +310,12 @@ impl IssuanceAuthorizingKey {
}
}
impl From<&SpendingKey> for IssuanceAuthorizingKey {
fn from(sk: &SpendingKey) -> Self {
let isk = Self::derive_inner(sk);
impl From<&IssuanceKey> for IssuanceAuthorizingKey {
fn from(sk_iss: &IssuanceKey) -> Self {
let isk = IssuanceAuthorizingKey::derive_inner(sk_iss);
// IssuanceAuthorizingKey cannot be constructed such that this assertion would fail.
assert!(!bool::from(isk.is_zero()));
let ret = IssuanceAuthorizingKey(isk.to_repr().try_into().unwrap());
// If the last bit of repr_P(ik) is 1, negate isk.
if (<[u8; 32]>::from(IssuanceValidatingKey::from(&ret).0)[31] >> 7) == 1 {
IssuanceAuthorizingKey((-isk).to_repr().try_into().unwrap())
} else {
ret
}
IssuanceAuthorizingKey(conditionally_negate(isk))
}
}
@ -297,21 +378,6 @@ impl IssuanceValidatingKey {
}
}
/// A function to check structural validity of the validating keys for authorizing transfers and
/// issuing assets
/// Structural validity checks for ak_P or ik_P:
/// - The point must not be the identity (which for Pallas is canonically encoded as all-zeroes).
/// - The compressed y-coordinate bit must be 0.
fn check_structural_validity(
verification_key_bytes: [u8; 32],
) -> Option<VerificationKey<SpendAuth>> {
if verification_key_bytes != [0; 32] && verification_key_bytes[31] & 0x80 == 0 {
<redpallas::VerificationKey<SpendAuth>>::try_from(verification_key_bytes).ok()
} else {
None
}
}
/// A key used to derive [`Nullifier`]s from [`Note`]s.
///
/// $\mathsf{nk}$ as defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
@ -1050,7 +1116,7 @@ impl SharedSecret {
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use super::{
DiversifierIndex, DiversifierKey, EphemeralSecretKey, IssuanceAuthorizingKey,
DiversifierIndex, DiversifierKey, EphemeralSecretKey, IssuanceAuthorizingKey, IssuanceKey,
IssuanceValidatingKey, SpendingKey,
};
use proptest::prelude::*;
@ -1070,6 +1136,20 @@ pub mod testing {
}
}
prop_compose! {
/// Generate a uniformly distributed Orchard issuance key.
pub fn arb_issuance_key()(
key in prop::array::uniform32(prop::num::u8::ANY)
.prop_map(IssuanceKey::from_bytes)
.prop_filter(
"Values must correspond to valid Orchard issuance keys.",
|opt| bool::from(opt.is_some())
)
) -> IssuanceKey {
key.unwrap()
}
}
prop_compose! {
/// Generate a uniformly distributed Orchard ephemeral secret key.
pub fn arb_esk()(
@ -1106,7 +1186,7 @@ pub mod testing {
/// Generate a uniformly distributed RedDSA issuance authorizing key.
pub fn arb_issuance_authorizing_key()(rng_seed in prop::array::uniform32(prop::num::u8::ANY)) -> IssuanceAuthorizingKey {
let mut rng = StdRng::from_seed(rng_seed);
IssuanceAuthorizingKey::from(&SpendingKey::random(&mut rng))
IssuanceAuthorizingKey::from(&IssuanceKey::random(&mut rng))
}
}
@ -1187,7 +1267,9 @@ mod tests {
let ask: SpendAuthorizingKey = (&sk).into();
assert_eq!(<[u8; 32]>::from(&ask.0), tv.ask);
let isk: IssuanceAuthorizingKey = (&sk).into();
let sk_iss = IssuanceKey::from_bytes(tv.sk).unwrap();
let isk: IssuanceAuthorizingKey = (&sk_iss).into();
assert_eq!(<[u8; 32]>::from(&isk.0), tv.isk);
let ak: SpendValidatingKey = (&ask).into();

View File

@ -10,7 +10,7 @@ use subtle::{Choice, ConstantTimeEq, CtOption};
use crate::constants::fixed_bases::{
NATIVE_ASSET_BASE_V_BYTES, VALUE_COMMITMENT_PERSONALIZATION, ZSA_ASSET_BASE_PERSONALIZATION,
};
use crate::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey, SpendingKey};
use crate::keys::{IssuanceAuthorizingKey, IssuanceKey, IssuanceValidatingKey};
/// Note type identifier.
#[derive(Clone, Copy, Debug, Eq)]
@ -92,8 +92,8 @@ impl AssetBase {
///
/// This is only used in tests.
pub(crate) fn random(rng: &mut impl RngCore) -> Self {
let sk = SpendingKey::random(rng);
let isk = IssuanceAuthorizingKey::from(&sk);
let sk_iss = IssuanceKey::random(rng);
let isk = IssuanceAuthorizingKey::from(&sk_iss);
let ik = IssuanceValidatingKey::from(&isk);
let asset_descr = "zsa_asset";
AssetBase::derive(&ik, asset_descr)
@ -126,13 +126,13 @@ pub mod testing {
use proptest::prelude::*;
use crate::keys::{testing::arb_spending_key, IssuanceAuthorizingKey, IssuanceValidatingKey};
use crate::keys::{testing::arb_issuance_key, IssuanceAuthorizingKey, IssuanceValidatingKey};
prop_compose! {
/// Generate a uniformly distributed note type
pub fn arb_asset_id()(
is_native in prop::bool::ANY,
sk in arb_spending_key(),
sk in arb_issuance_key(),
str in "[A-Za-z]{255}",
) -> AssetBase {
if is_native {
@ -155,10 +155,10 @@ pub mod testing {
prop_compose! {
/// Generate an asset ID
pub fn arb_zsa_asset_id()(
sk in arb_spending_key(),
sk_iss in arb_issuance_key(),
str in "[A-Za-z]{255}"
) -> AssetBase {
let isk = IssuanceAuthorizingKey::from(&sk);
let isk = IssuanceAuthorizingKey::from(&sk_iss);
AssetBase::derive(&IssuanceValidatingKey::from(&isk), &str)
}
}
@ -166,10 +166,10 @@ pub mod testing {
prop_compose! {
/// Generate an asset ID using a specific description
pub fn zsa_asset_id(asset_desc: String)(
sk in arb_spending_key(),
sk_iss in arb_issuance_key(),
) -> AssetBase {
assert!(super::is_asset_desc_of_valid_size(&asset_desc));
let isk = IssuanceAuthorizingKey::from(&sk);
let isk = IssuanceAuthorizingKey::from(&sk_iss);
AssetBase::derive(&IssuanceValidatingKey::from(&isk), &asset_desc)
}
}

View File

@ -23,7 +23,7 @@ impl SigType for Binding {}
/// A RedPallas signing key.
#[derive(Clone, Copy, Debug)]
pub struct SigningKey<T: SigType>(reddsa::SigningKey<T>);
pub struct SigningKey<T: SigType>(pub(crate) reddsa::SigningKey<T>);
impl<T: SigType> From<SigningKey<T>> for [u8; 32] {
fn from(sk: SigningKey<T>) -> [u8; 32] {
@ -63,7 +63,7 @@ impl<T: SigType> SigningKey<T> {
/// A RedPallas verification key.
#[derive(Clone, Debug)]
pub struct VerificationKey<T: SigType>(reddsa::VerificationKey<T>);
pub struct VerificationKey<T: SigType>(pub(crate) reddsa::VerificationKey<T>);
impl<T: SigType> From<VerificationKey<T>> for [u8; 32] {
fn from(vk: VerificationKey<T>) -> [u8; 32] {

View File

@ -80,10 +80,10 @@ mod tests {
use super::*;
fn create_test_asset(asset_desc: &str) -> AssetBase {
use crate::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey, SpendingKey};
use crate::keys::{IssuanceAuthorizingKey, IssuanceKey, IssuanceValidatingKey};
let sk = SpendingKey::from_bytes([0u8; 32]).unwrap();
let isk: IssuanceAuthorizingKey = (&sk).into();
let sk_iss = IssuanceKey::from_bytes([0u8; 32]).unwrap();
let isk: IssuanceAuthorizingKey = (&sk_iss).into();
AssetBase::derive(&IssuanceValidatingKey::from(&isk), asset_desc)
}

View File

@ -10,8 +10,11 @@ use crate::{
spec::PrfExpand,
};
const ZIP32_ORCHARD_PERSONALIZATION: &[u8; 16] = b"ZcashIP32Orchard";
const ZIP32_ORCHARD_FVFP_PERSONALIZATION: &[u8; 16] = b"ZcashOrchardFVFP";
/// Personalization for the master extended spending key
pub const ZIP32_ORCHARD_PERSONALIZATION: &[u8; 16] = b"ZcashIP32Orchard";
/// Personalization for the master extended issuance key
pub const ZIP32_ORCHARD_PERSONALIZATION_FOR_ISSUANCE: &[u8; 16] = b"ZIP32ZSAIssue_V1";
/// Errors produced in derivation of extended spending keys
#[derive(Debug, PartialEq, Eq)]
@ -117,8 +120,12 @@ impl ExtendedSpendingKey {
/// # Panics
///
/// Panics if seed results in invalid spending key.
pub fn from_path(seed: &[u8], path: &[ChildIndex]) -> Result<Self, Error> {
let mut xsk = Self::master(seed)?;
pub fn from_path(
seed: &[u8],
path: &[ChildIndex],
personalization: &[u8; 16],
) -> Result<Self, Error> {
let mut xsk = Self::master(seed, personalization)?;
for i in path {
xsk = xsk.derive_child(*i)?;
}
@ -134,13 +141,13 @@ impl ExtendedSpendingKey {
/// # Panics
///
/// Panics if the seed is shorter than 32 bytes or longer than 252 bytes.
fn master(seed: &[u8]) -> Result<Self, Error> {
fn master(seed: &[u8], personalization: &[u8; 16]) -> Result<Self, Error> {
assert!(seed.len() >= 32 && seed.len() <= 252);
// I := BLAKE2b-512("ZcashIP32Orchard", seed)
let I: [u8; 64] = {
let mut I = Blake2bParams::new()
.hash_length(64)
.personal(ZIP32_ORCHARD_PERSONALIZATION)
.personal(personalization)
.to_state();
I.update(seed);
I.finalize().as_bytes().try_into().unwrap()
@ -213,7 +220,7 @@ mod tests {
#[test]
fn derive_child() {
let seed = [0; 32];
let xsk_m = ExtendedSpendingKey::master(&seed).unwrap();
let xsk_m = ExtendedSpendingKey::master(&seed, ZIP32_ORCHARD_PERSONALIZATION).unwrap();
let i_5 = 5;
let xsk_5 = xsk_m.derive_child(i_5.try_into().unwrap());
@ -224,20 +231,28 @@ mod tests {
#[test]
fn path() {
let seed = [0; 32];
let xsk_m = ExtendedSpendingKey::master(&seed).unwrap();
let xsk_m = ExtendedSpendingKey::master(&seed, ZIP32_ORCHARD_PERSONALIZATION).unwrap();
let xsk_5h = xsk_m.derive_child(5.try_into().unwrap()).unwrap();
assert!(bool::from(
ExtendedSpendingKey::from_path(&seed, &[5.try_into().unwrap()])
.unwrap()
.ct_eq(&xsk_5h)
ExtendedSpendingKey::from_path(
&seed,
&[5.try_into().unwrap()],
ZIP32_ORCHARD_PERSONALIZATION
)
.unwrap()
.ct_eq(&xsk_5h)
));
let xsk_5h_7 = xsk_5h.derive_child(7.try_into().unwrap()).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,
&[5.try_into().unwrap(), 7.try_into().unwrap()],
ZIP32_ORCHARD_PERSONALIZATION
)
.unwrap()
.ct_eq(&xsk_5h_7)
));
}
}

View File

@ -13,7 +13,10 @@ use orchard::{
builder::Builder,
bundle::Flags,
circuit::{ProvingKey, VerifyingKey},
keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey, SpendingKey},
keys::{
FullViewingKey, IssuanceKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey,
SpendingKey,
},
value::NoteValue,
Address, Anchor, Bundle, Note,
};
@ -58,7 +61,8 @@ fn prepare_keys() -> Keychain {
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32, Scope::External);
let isk = IssuanceAuthorizingKey::from(&sk);
let sk_iss = IssuanceKey::from_bytes([0; 32]).unwrap();
let isk = IssuanceAuthorizingKey::from(&sk_iss);
let ik = IssuanceValidatingKey::from(&isk);
Keychain {
pk,