Add explicit scoping for viewing keys and addresses

Co-authored-by: Jack Grigg <jack@electriccoin.co>
Co-authored-by: Daira Hopwood <daira@jacaranda.org>
This commit is contained in:
therealyingtong 2022-03-30 19:53:46 +08:00
parent cbe032f3d4
commit a0424984c6
10 changed files with 155 additions and 65 deletions

View File

@ -7,6 +7,30 @@ and this project adheres to Rust's notion of
## [Unreleased]
### Added
- `orchard::keys`:
- `Scope` enum, for distinguishing external and internal scopes for viewing
keys and addresses.
- `FullViewingKey::{to_ivk, to_ovk}`, which each take a `Scope` argument.
- `FullViewingKey::scope_for_address`
### Changed
- `orchard::builder`:
- `Builder::add_spend` now requires that the `FullViewingKey` matches the
given `Note`, and handles any scoping itself (instead of requiring the
caller to pass the `FullViewingKey` for the correct scope).
- `orchard::keys`:
- `FullViewingKey::{address, address_at}` now each take a `Scope` argument.
### Removed
- `orchard::keys`:
- `FullViewingKey::derive_internal`
- `impl From<&FullViewingKey> for IncomingViewingKey` (use
`FullViewingKey::to_ivk` instead).
- `impl From<&FullViewingKey> for OutgoingViewingKey` (use
`FullViewingKey::to_ovk` instead).
## [0.1.0-beta.2] - 2022-03-22
### Added
- `orchard::keys`:
- `DiversifierIndex::to_bytes`
- `FullViewingKey::derive_internal`

View File

@ -10,7 +10,7 @@ use orchard::{
builder::Builder,
bundle::Flags,
circuit::{ProvingKey, VerifyingKey},
keys::{FullViewingKey, SpendingKey},
keys::{FullViewingKey, Scope, SpendingKey},
value::NoteValue,
Anchor, Bundle,
};
@ -20,7 +20,7 @@ fn criterion_benchmark(c: &mut Criterion) {
let rng = OsRng;
let sk = SpendingKey::from_bytes([7; 32]).unwrap();
let recipient = FullViewingKey::from(&sk).address_at(0u32);
let recipient = FullViewingKey::from(&sk).address_at(0u32, Scope::External);
let vk = VerifyingKey::build();
let pk = ProvingKey::build();

View File

@ -5,7 +5,7 @@ use orchard::{
builder::Builder,
bundle::Flags,
circuit::ProvingKey,
keys::{FullViewingKey, IncomingViewingKey, SpendingKey},
keys::{FullViewingKey, Scope, SpendingKey},
note_encryption::{CompactAction, OrchardDomain},
value::NoteValue,
Anchor, Bundle,
@ -21,8 +21,8 @@ fn bench_note_decryption(c: &mut Criterion) {
let pk = ProvingKey::build();
let fvk = FullViewingKey::from(&SpendingKey::from_bytes([7; 32]).unwrap());
let valid_ivk = IncomingViewingKey::from(&fvk);
let recipient = fvk.address_at(0u32);
let valid_ivk = fvk.to_ivk(Scope::External);
let recipient = valid_ivk.address_at(0u32);
// Compact actions don't have the full AEAD ciphertext, so ZIP 307 trial-decryption
// relies on an invalid ivk resulting in random noise for which the note commitment
@ -40,7 +40,8 @@ fn bench_note_decryption(c: &mut Criterion) {
.map(|i| {
let mut sk = [0; 32];
sk[..4].copy_from_slice(&i.to_le_bytes());
IncomingViewingKey::from(&FullViewingKey::from(&SpendingKey::from_bytes(sk).unwrap()))
let fvk = FullViewingKey::from(&SpendingKey::from_bytes(sk).unwrap());
fvk.to_ivk(Scope::External)
})
.collect();

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion};
use orchard::keys::{FullViewingKey, SpendingKey};
use orchard::keys::{FullViewingKey, Scope, SpendingKey};
fn key_derivation(c: &mut Criterion) {
// Meaningless random spending key.
@ -12,7 +12,9 @@ fn key_derivation(c: &mut Criterion) {
let fvk = FullViewingKey::from(&sk);
c.bench_function("derive_fvk", |b| b.iter(|| FullViewingKey::from(&sk)));
c.bench_function("default_address", |b| b.iter(|| fvk.address_at(0u32)));
c.bench_function("default_address", |b| {
b.iter(|| fvk.address_at(0u32, Scope::External))
});
}
criterion_group!(benches, key_derivation);

View File

@ -12,10 +12,10 @@ use crate::{
/// # Examples
///
/// ```
/// use orchard::keys::{SpendingKey, FullViewingKey};
/// use orchard::keys::{SpendingKey, FullViewingKey, Scope};
///
/// let sk = SpendingKey::from_bytes([7; 32]).unwrap();
/// let address = FullViewingKey::from(&sk).address_at(0u32);
/// let address = FullViewingKey::from(&sk).address_at(0u32, Scope::External);
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Address {
@ -73,7 +73,7 @@ pub mod testing {
use crate::keys::{
testing::{arb_diversifier_index, arb_spending_key},
FullViewingKey,
FullViewingKey, Scope,
};
use super::Address;
@ -82,7 +82,7 @@ pub mod testing {
/// Generates an arbitrary payment address.
pub(crate) fn arb_address()(sk in arb_spending_key(), j in arb_diversifier_index()) -> Address {
let fvk = FullViewingKey::from(&sk);
fvk.address_at(j)
fvk.address_at(j, Scope::External)
}
}
}

View File

@ -15,7 +15,8 @@ use crate::{
bundle::{Action, Authorization, Authorized, Bundle, Flags},
circuit::{Circuit, Instance, Proof, ProvingKey},
keys::{
FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey,
FullViewingKey, OutgoingViewingKey, Scope, SpendAuthorizingKey, SpendValidatingKey,
SpendingKey,
},
note::{Note, TransmittedNoteCiphertext},
note_encryption::OrchardNoteEncryption,
@ -55,6 +56,7 @@ impl From<value::OverflowError> for Error {
struct SpendInfo {
dummy_sk: Option<SpendingKey>,
fvk: FullViewingKey,
scope: Scope,
note: Note,
merkle_path: MerklePath,
}
@ -70,6 +72,9 @@ impl SpendInfo {
SpendInfo {
dummy_sk: Some(sk),
fvk,
// We use external scope to avoid unnecessary derivations, because the dummy
// note's spending key is random and thus scoping is irrelevant.
scope: Scope::External,
note,
merkle_path,
}
@ -91,7 +96,7 @@ impl RecipientInfo {
/// [orcharddummynotes]: https://zips.z.cash/protocol/nu5.pdf#orcharddummynotes
fn dummy(rng: &mut impl RngCore) -> Self {
let fvk: FullViewingKey = (&SpendingKey::random(rng)).into();
let recipient = fvk.address_at(0u32);
let recipient = fvk.address_at(0u32, Scope::External);
RecipientInfo {
ovk: None,
@ -191,7 +196,7 @@ impl ActionInfo {
alpha: Some(alpha),
ak: Some(ak),
nk: Some(*self.spend.fvk.nk()),
rivk: Some(*self.spend.fvk.rivk()),
rivk: Some(self.spend.fvk.rivk(self.spend.scope)),
g_d_new_star: Some((*note.recipient().g_d()).to_bytes()),
pk_d_new_star: Some(note.recipient().pk_d().to_bytes()),
v_new: Some(note.value()),
@ -246,9 +251,15 @@ impl Builder {
return Err("All anchors must be equal.");
}
// Check if note is internal or external.
let scope = fvk
.scope_for_address(&note.recipient())
.ok_or("FullViewingKey does not correspond to the given note")?;
self.spends.push(SpendInfo {
dummy_sk: None,
fvk,
scope,
note,
merkle_path,
});
@ -591,10 +602,7 @@ pub mod testing {
address::testing::arb_address,
bundle::{Authorized, Bundle, Flags},
circuit::ProvingKey,
keys::{
testing::arb_spending_key, FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey,
SpendingKey,
},
keys::{testing::arb_spending_key, FullViewingKey, SpendAuthorizingKey, SpendingKey},
note::testing::arb_note,
tree::{Anchor, MerkleHashOrchard, MerklePath},
value::{testing::arb_positive_note_value, NoteValue, MAX_NOTE_VALUE},
@ -624,7 +632,6 @@ pub mod testing {
/// Create a bundle from the set of arbitrary bundle inputs.
fn into_bundle<V: TryFrom<i64>>(mut self) -> Bundle<Authorized, V> {
let fvk = FullViewingKey::from(&self.sk);
let ovk = OutgoingViewingKey::from(&fvk);
let flags = Flags::from_parts(true, true);
let mut builder = Builder::new(flags, self.anchor);
@ -633,6 +640,9 @@ pub mod testing {
}
for (addr, value) in self.recipient_amounts.into_iter() {
let scope = fvk.scope_for_address(&addr).unwrap();
let ovk = fvk.to_ovk(scope);
builder
.add_recipient(Some(ovk.clone()), addr, value, None)
.unwrap();
@ -720,7 +730,7 @@ mod tests {
bundle::{Authorized, Bundle, Flags},
circuit::ProvingKey,
constants::MERKLE_DEPTH_ORCHARD,
keys::{FullViewingKey, SpendingKey},
keys::{FullViewingKey, Scope, SpendingKey},
tree::EMPTY_ROOTS,
value::NoteValue,
};
@ -732,7 +742,7 @@ mod tests {
let sk = SpendingKey::random(&mut rng);
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32);
let recipient = fvk.address_at(0u32, Scope::External);
let mut builder = Builder::new(
Flags::from_parts(true, true),

View File

@ -906,7 +906,7 @@ mod tests {
let sender_address = spent_note.recipient();
let nk = *fvk.nk();
let rivk = *fvk.rivk();
let rivk = fvk.rivk(fvk.scope_for_address(&spent_note.recipient()).unwrap());
let nf_old = spent_note.nullifier(&fvk);
let ak: SpendValidatingKey = fvk.into();
let alpha = pallas::Scalar::random(&mut rng);

View File

@ -1,5 +1,6 @@
//! Key structures for Orchard.
use std::array;
use std::convert::{TryFrom, TryInto};
use std::io::{self, Read, Write};
use std::mem;
@ -277,6 +278,24 @@ impl CommitIvkRandomness {
}
}
/// The scope of a viewing key or address.
///
/// A "scope" narrows the visibility or usage to a level below "full".
///
/// Consistent usage of `Scope` enables the user to provide consistent views over a wallet
/// to other people. For example, a user can give an external [`IncomingViewingKey`] to a
/// merchant terminal, enabling it to only detect "real" transactions from customers and
/// not internal transactions from the wallet.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scope {
/// A scope used for wallet-external operations, namely deriving addresses to give to
/// other users in order to receive funds.
External,
/// A scope used for wallet-internal operations, such as creating change notes,
/// auto-shielding, and note management.
Internal,
}
/// A key that provides the capability to view incoming and outgoing transactions.
///
/// This key is useful anywhere you need to maintain accurate balance, but do not want the
@ -319,17 +338,19 @@ impl FullViewingKey {
&self.nk
}
pub(crate) fn rivk(&self) -> &CommitIvkRandomness {
&self.rivk
}
pub(crate) fn rivk_internal(&self) -> CommitIvkRandomness {
let k = self.rivk.0.to_repr();
let ak = self.ak.to_bytes();
let nk = self.nk.to_bytes();
CommitIvkRandomness(to_scalar(
PrfExpand::OrchardRivkInternal.with_ad_slices(&k, &[&ak, &nk]),
))
/// Returns either `rivk` or `rivk_internal` based on `scope`.
pub(crate) fn rivk(&self, scope: Scope) -> CommitIvkRandomness {
match scope {
Scope::External => self.rivk,
Scope::Internal => {
let k = self.rivk.0.to_repr();
let ak = self.ak.to_bytes();
let nk = self.nk.to_bytes();
CommitIvkRandomness(to_scalar(
PrfExpand::OrchardRivkInternal.with_ad_slices(&k, &[&ak, &nk]),
))
}
}
}
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
@ -346,14 +367,25 @@ impl FullViewingKey {
}
/// Returns the payment address for this key at the given index.
pub fn address_at(&self, j: impl Into<DiversifierIndex>) -> Address {
IncomingViewingKey::from(self).address_at(j)
pub fn address_at(&self, j: impl Into<DiversifierIndex>, scope: Scope) -> Address {
self.to_ivk(scope).address_at(j)
}
/// Returns the payment address for this key corresponding to the given diversifier.
pub fn address(&self, d: Diversifier) -> Address {
pub fn address(&self, d: Diversifier, scope: Scope) -> Address {
// Shortcut: we don't need to derive DiversifierKey.
KeyAgreementPrivateKey::from(self).address(d)
match scope {
Scope::External => KeyAgreementPrivateKey::from_fvk(self),
Scope::Internal => KeyAgreementPrivateKey::from_fvk(&self.derive_internal()),
}
.address(d)
}
/// Returns the scope of the given address, or `None` if the address is not derived
/// from this full viewing key.
pub fn scope_for_address(&self, address: &Address) -> Option<Scope> {
array::IntoIter::new([Scope::External, Scope::Internal])
.find(|scope| self.to_ivk(*scope).diversifier_index(address).is_some())
}
/// Serializes the full viewing key as specified in [Zcash Protocol Spec § 5.6.4.4: Orchard Raw Full Viewing Keys][orchardrawfullviewingkeys]
@ -404,14 +436,31 @@ impl FullViewingKey {
Some(FullViewingKey { ak, nk, rivk })
}
/// Derives an internal full viewing key from a full viewing key, as specified in [ZIP32][orchardinternalfullviewingkey]
/// Derives an internal full viewing key from a full viewing key, as specified in
/// [ZIP32][orchardinternalfullviewingkey]. Internal use only.
///
/// [orchardinternalfullviewingkey]: https://zips.z.cash/zip-0032#orchard-internal-key-derivation
pub fn derive_internal(&self) -> Self {
fn derive_internal(&self) -> Self {
FullViewingKey {
ak: self.ak.clone(),
nk: self.nk,
rivk: self.rivk_internal(),
rivk: self.rivk(Scope::Internal),
}
}
/// Derives an `IncomingViewingKey` for this full viewing key.
pub fn to_ivk(&self, scope: Scope) -> IncomingViewingKey {
match scope {
Scope::External => IncomingViewingKey::from_fvk(self),
Scope::Internal => IncomingViewingKey::from_fvk(&self.derive_internal()),
}
}
/// Derives an `OutgoingViewingKey` for this full viewing key.
pub fn to_ovk(&self, scope: Scope) -> OutgoingViewingKey {
match scope {
Scope::External => OutgoingViewingKey::from_fvk(self),
Scope::Internal => OutgoingViewingKey::from_fvk(&self.derive_internal()),
}
}
}
@ -527,8 +576,13 @@ impl Diversifier {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct KeyAgreementPrivateKey(NonZeroPallasScalar);
impl From<&FullViewingKey> for KeyAgreementPrivateKey {
fn from(fvk: &FullViewingKey) -> Self {
impl KeyAgreementPrivateKey {
/// Derives `KeyAgreementPrivateKey` from fvk.
///
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/protocol.pdf#orchardkeycomponents
fn from_fvk(fvk: &FullViewingKey) -> Self {
// KeyAgreementPrivateKey cannot be constructed such that this unwrap would fail.
let ivk = KeyAgreementPrivateKey::derive_inner(fvk).unwrap();
KeyAgreementPrivateKey(ivk.into())
@ -588,11 +642,12 @@ pub struct IncomingViewingKey {
ivk: KeyAgreementPrivateKey,
}
impl From<&FullViewingKey> for IncomingViewingKey {
fn from(fvk: &FullViewingKey) -> Self {
impl IncomingViewingKey {
/// Helper method.
fn from_fvk(fvk: &FullViewingKey) -> Self {
IncomingViewingKey {
dk: fvk.derive_dk_ovk().0,
ivk: fvk.into(),
ivk: KeyAgreementPrivateKey::from_fvk(fvk),
}
}
}
@ -653,8 +708,9 @@ impl IncomingViewingKey {
#[derive(Debug, Clone)]
pub struct OutgoingViewingKey([u8; 32]);
impl From<&FullViewingKey> for OutgoingViewingKey {
fn from(fvk: &FullViewingKey) -> Self {
impl OutgoingViewingKey {
/// Helper method.
fn from_fvk(fvk: &FullViewingKey) -> Self {
fvk.derive_dk_ovk().1
}
}
@ -934,7 +990,7 @@ mod tests {
esk in arb_esk(),
j in arb_diversifier_index(),
) {
let ivk = IncomingViewingKey::from(&(&sk).into());
let ivk = IncomingViewingKey::from_fvk(&(&sk).into());
let addr = ivk.address_at(j);
let epk = esk.derive_public(addr.g_d());
@ -978,12 +1034,12 @@ mod tests {
assert_eq!(fvk.nk().0.to_repr(), tv.nk);
assert_eq!(fvk.rivk.0.to_repr(), tv.rivk);
let ivk: KeyAgreementPrivateKey = (&fvk).into();
assert_eq!(ivk.0.to_repr(), tv.ivk);
let external_ivk = fvk.to_ivk(Scope::External);
assert_eq!(external_ivk.ivk.0.to_repr(), tv.ivk);
let diversifier = Diversifier(tv.default_d);
let addr = fvk.address(diversifier);
let addr = fvk.address(diversifier, Scope::External);
assert_eq!(&addr.pk_d().to_bytes(), &tv.default_pk_d);
let rho = Nullifier::from_bytes(&tv.note_rho).unwrap();
@ -999,17 +1055,14 @@ mod tests {
assert_eq!(note.nullifier(&fvk).to_bytes(), tv.note_nf);
let internal_rivk = fvk.rivk_internal();
let internal_rivk = fvk.rivk(Scope::Internal);
assert_eq!(internal_rivk.0.to_repr(), tv.internal_rivk);
let internal_fvk = fvk.derive_internal();
assert_eq!(internal_rivk, *internal_fvk.rivk());
let internal_ivk = fvk.to_ivk(Scope::Internal);
assert_eq!(internal_ivk.ivk.0.to_repr(), tv.internal_ivk);
assert_eq!(internal_ivk.dk.0, tv.internal_dk);
let internal_ivk: KeyAgreementPrivateKey = (&internal_fvk).into();
assert_eq!(internal_ivk.0.to_repr(), tv.internal_ivk);
let (internal_dk, internal_ovk) = internal_fvk.derive_dk_ovk();
assert_eq!(internal_dk.0, tv.internal_dk);
let internal_ovk = fvk.to_ovk(Scope::Internal);
assert_eq!(internal_ovk.0, tv.internal_ovk);
}
}

View File

@ -5,7 +5,7 @@ use rand::RngCore;
use subtle::CtOption;
use crate::{
keys::{EphemeralSecretKey, FullViewingKey, SpendingKey},
keys::{EphemeralSecretKey, FullViewingKey, Scope, SpendingKey},
spec::{to_base, to_scalar, NonZeroPallasScalar, PrfExpand},
value::NoteValue,
Address,
@ -155,7 +155,7 @@ impl Note {
) -> (SpendingKey, FullViewingKey, Self) {
let sk = SpendingKey::random(rng);
let fvk: FullViewingKey = (&sk).into();
let recipient = fvk.address_at(0u32);
let recipient = fvk.address_at(0u32, Scope::External);
let note = Note::new(
recipient,

View File

@ -5,7 +5,7 @@ use orchard::{
builder::Builder,
bundle::{Authorized, Flags},
circuit::{ProvingKey, VerifyingKey},
keys::{FullViewingKey, IncomingViewingKey, SpendAuthorizingKey, SpendingKey},
keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey},
note::ExtractedNoteCommitment,
note_encryption::OrchardDomain,
tree::{MerkleHashOrchard, MerklePath},
@ -36,7 +36,7 @@ fn bundle_chain() {
let sk = SpendingKey::from_bytes([0; 32]).unwrap();
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32);
let recipient = fvk.address_at(0u32, Scope::External);
// Create a shielding bundle.
let shielding_bundle: Bundle<_, i64> = {
@ -59,7 +59,7 @@ fn bundle_chain() {
// Create a shielded bundle spending the previous output.
let shielded_bundle: Bundle<_, i64> = {
let ivk = IncomingViewingKey::from(&fvk);
let ivk = fvk.to_ivk(Scope::External);
let (note, _, _) = shielding_bundle
.actions()
.iter()