diff --git a/src/issuance.rs b/src/issuance.rs index d312b0dbe..e3c7e973b 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -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 diff --git a/src/keys.rs b/src/keys.rs index 6842d182d..7853d4920 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -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 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(scalar: Scalar) -> redpallas::SigningKey { + let ret = redpallas::SigningKey::(scalar.to_repr().try_into().unwrap()); + if (<[u8; 32]>::from(redpallas::VerificationKey::::from(&ret).0)[31] >> 7) == 1 { + redpallas::SigningKey::((-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> { + if verification_key_bytes != [0; 32] && verification_key_bytes[31] & 0x80 == 0 { + >::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 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 { + 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 { + // 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); 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> { - if verification_key_bytes != [0; 32] && verification_key_bytes[31] & 0x80 == 0 { - >::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(); diff --git a/src/note/asset_base.rs b/src/note/asset_base.rs index 63da3a5f8..500d84210 100644 --- a/src/note/asset_base.rs +++ b/src/note/asset_base.rs @@ -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) } } diff --git a/src/primitives/redpallas.rs b/src/primitives/redpallas.rs index 92fe165e4..a414bf285 100644 --- a/src/primitives/redpallas.rs +++ b/src/primitives/redpallas.rs @@ -23,7 +23,7 @@ impl SigType for Binding {} /// A RedPallas signing key. #[derive(Clone, Copy, Debug)] -pub struct SigningKey(reddsa::SigningKey); +pub struct SigningKey(pub(crate) reddsa::SigningKey); impl From> for [u8; 32] { fn from(sk: SigningKey) -> [u8; 32] { @@ -63,7 +63,7 @@ impl SigningKey { /// A RedPallas verification key. #[derive(Clone, Debug)] -pub struct VerificationKey(reddsa::VerificationKey); +pub struct VerificationKey(pub(crate) reddsa::VerificationKey); impl From> for [u8; 32] { fn from(vk: VerificationKey) -> [u8; 32] { diff --git a/src/supply_info.rs b/src/supply_info.rs index 6bd96df34..79e6b7d70 100644 --- a/src/supply_info.rs +++ b/src/supply_info.rs @@ -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) } diff --git a/src/zip32.rs b/src/zip32.rs index 3ba8bd571..12427bf8d 100644 --- a/src/zip32.rs +++ b/src/zip32.rs @@ -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 { - let mut xsk = Self::master(seed)?; + pub fn from_path( + seed: &[u8], + path: &[ChildIndex], + personalization: &[u8; 16], + ) -> Result { + 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 { + fn master(seed: &[u8], personalization: &[u8; 16]) -> Result { 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) )); } } diff --git a/tests/zsa.rs b/tests/zsa.rs index f0ee62c28..649d5e8cf 100644 --- a/tests/zsa.rs +++ b/tests/zsa.rs @@ -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,