ZSA note encryption in Orchard crate (#3)

* Circleci project setup (#1)

* Added .circleci/config.yml

* Added NoteType to Notes

* reformated file

* updated `derive` for NoteType

* added note_type to value commit derivation

* rustfmt

* updated ci config

* updated ci config

* updated ci config

* updated derive for note_type

* added test for arb note_type

* added test for `native` note type

* zsa-note-encryption: introduce AssetType and encode and decode it in note plaintexts

* zsa-note-encryption: extend the size of compact notes to include asset_type

* fixed clippy warrnings

* rustfmt

* zsa-note-encryption: document parsing requirement

* zsa-note-encryption: revert support of ZSA compact action

* zsa_value: add NoteType method is_native

* zsa-note-encryption: remove dependency on changes in the other crate

* zsa-note-encryption: extract memo of ZSA notes

* zsa-note-encryption: tests (zcash_test_vectors 77c73492)

* zsa-note-encryption: simplify roundtrip test

* zsa-note-encryption: more test vectors (zcash_test_vectors c10da464)

* Circleci project setup (#1)

* Added .circleci/config.yml

* issuer keys implementation (#5)

Implements the issuer keys as

    IssuerAuthorizingKey -> isk
    IssuerVerifyingKey -> ik

Test vectors generated with zcash_test_vectors repo

* Added NoteType to Notes (#2)

* Added NoteType to Notes
* Added NoteType to value commitment derivation

* zsa-note-encryption: use both native and ZSA in proptests

* zsa-note-encryption: test vector commit 51398c93

* zsa-note-encryption: fix after merge

Co-authored-by: Paul <3682187+PaulLaux@users.noreply.github.com>
Co-authored-by: Paul <lauxpaul@protonmail.com>
Co-authored-by: Aurélien Nicolas <info@nau.re>
Co-authored-by: Daniel Benarroch <danielbenarroch92@gmail.com>
This commit is contained in:
naure 2022-07-20 13:08:58 +02:00 committed by GitHub
parent 5ae51075db
commit ac71bc7528
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 3059 additions and 763 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
**/*.rs.bk
Cargo.lock
.vscode
.idea

View File

@ -34,6 +34,9 @@ pub const VALUE_COMMITMENT_R_BYTES: [u8; 1] = *b"r";
/// SWU hash-to-curve personalization for the note commitment generator
pub const NOTE_COMMITMENT_PERSONALIZATION: &str = "z.cash:Orchard-NoteCommit";
/// SWU hash-to-curve personalization for the ZSA note commitment generator
pub const NOTE_ZSA_COMMITMENT_PERSONALIZATION: &str = "z.cash:ZSA-NoteCommit";
/// SWU hash-to-curve personalization for the IVK commitment generator
pub const COMMIT_IVK_PERSONALIZATION: &str = "z.cash:Orchard-CommitIvk";

View File

@ -235,6 +235,7 @@ impl Note {
g_d.to_bytes(),
self.recipient.pk_d().to_bytes(),
self.value,
self.note_type,
self.rho.0,
self.rseed.psi(&self.rho),
self.rseed.rcm(&self.rho),

View File

@ -7,7 +7,11 @@ use pasta_curves::pallas;
use subtle::{ConstantTimeEq, CtOption};
use crate::{
constants::{fixed_bases::NOTE_COMMITMENT_PERSONALIZATION, L_ORCHARD_BASE},
constants::{
fixed_bases::{NOTE_COMMITMENT_PERSONALIZATION, NOTE_ZSA_COMMITMENT_PERSONALIZATION},
L_ORCHARD_BASE,
},
note::note_type::NoteType,
spec::extract_p,
value::NoteValue,
};
@ -41,22 +45,46 @@ impl NoteCommitment {
g_d: [u8; 32],
pk_d: [u8; 32],
v: NoteValue,
note_type: NoteType,
rho: pallas::Base,
psi: pallas::Base,
rcm: NoteCommitTrapdoor,
) -> CtOption<Self> {
let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION);
domain
.commit(
iter::empty()
.chain(BitArray::<_, Lsb0>::new(g_d).iter().by_vals())
.chain(BitArray::<_, Lsb0>::new(pk_d).iter().by_vals())
.chain(v.to_le_bits().iter().by_vals())
.chain(rho.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
.chain(psi.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
&rcm.0,
)
.map(NoteCommitment)
let g_d_bits = BitArray::<_, Lsb0>::new(g_d);
let pk_d_bits = BitArray::<_, Lsb0>::new(pk_d);
let v_bits = v.to_le_bits();
let rho_bits = rho.to_le_bits();
let psi_bits = psi.to_le_bits();
let zec_note_bits = iter::empty()
.chain(g_d_bits.iter().by_vals())
.chain(pk_d_bits.iter().by_vals())
.chain(v_bits.iter().by_vals())
.chain(rho_bits.iter().by_vals().take(L_ORCHARD_BASE))
.chain(psi_bits.iter().by_vals().take(L_ORCHARD_BASE));
// TODO: make this constant-time.
if note_type.is_native().into() {
// Commit to ZEC notes as per the Orchard protocol.
Self::commit(NOTE_COMMITMENT_PERSONALIZATION, zec_note_bits, rcm)
} else {
// Commit to non-ZEC notes as per the ZSA protocol.
// Append the note type to the Orchard note encoding.
let type_bits = BitArray::<_, Lsb0>::new(note_type.to_bytes());
let zsa_note_bits = zec_note_bits.chain(type_bits.iter().by_vals());
// Commit in a different domain than Orchard notes.
Self::commit(NOTE_ZSA_COMMITMENT_PERSONALIZATION, zsa_note_bits, rcm)
}
}
fn commit(
personalization: &str,
bits: impl Iterator<Item = bool>,
rcm: NoteCommitTrapdoor,
) -> CtOption<Self> {
let domain = sinsemilla::CommitDomain::new(personalization);
domain.commit(bits, &rcm.0).map(NoteCommitment)
}
}

View File

@ -1,7 +1,7 @@
use group::GroupEncoding;
use halo2_proofs::arithmetic::CurveExt;
use pasta_curves::pallas;
use subtle::CtOption;
use subtle::{Choice, ConstantTimeEq, CtOption};
use crate::constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_V_BYTES};
use crate::keys::IssuerValidatingKey;
@ -15,7 +15,7 @@ const MAX_ASSET_DESCRIPTION_SIZE: usize = 512;
// the hasher used to derive the assetID
#[allow(non_snake_case)]
fn assetID_hasher(msg: Vec<u8>) -> pallas::Point {
// TODO(zsa) replace personalization, will require circuit change?
// TODO(zsa) replace personalization
pallas::Point::hash_to_curve(VALUE_COMMITMENT_PERSONALIZATION)(&msg)
}
@ -50,6 +50,16 @@ impl NoteType {
pub fn native() -> Self {
NoteType(assetID_hasher(VALUE_COMMITMENT_V_BYTES.to_vec()))
}
/// The base point used in value commitments.
pub fn cv_base(&self) -> pallas::Point {
self.0
}
/// Whether this note represents a native or ZSA asset.
pub fn is_native(&self) -> Choice {
self.0.ct_eq(&Self::native().0)
}
}
/// Generators for property testing.
@ -58,20 +68,25 @@ impl NoteType {
pub mod testing {
use proptest::prelude::*;
use super::NoteType;
use crate::keys::{testing::arb_spending_key, IssuerAuthorizingKey, IssuerValidatingKey};
use super::NoteType;
prop_compose! {
/// Generate a uniformly distributed note type
pub fn arb_note_type()(
is_native in prop::bool::ANY,
sk in arb_spending_key(),
bytes32a in prop::array::uniform32(prop::num::u8::ANY),
bytes32b in prop::array::uniform32(prop::num::u8::ANY),
) -> NoteType {
let bytes64 = [bytes32a, bytes32b].concat();
let isk = IssuerAuthorizingKey::from(&sk);
NoteType::derive(&IssuerValidatingKey::from(&isk), bytes64)
if is_native {
NoteType::native()
} else {
let bytes64 = [bytes32a, bytes32b].concat();
let isk = IssuerAuthorizingKey::from(&sk);
NoteType::derive(&IssuerValidatingKey::from(&isk), bytes64)
}
}
}
}

View File

@ -1,8 +1,7 @@
//! In-band secret distribution for Orchard bundles.
use core::fmt;
use blake2b_simd::{Hash, Params};
use core::fmt;
use group::ff::PrimeField;
use zcash_note_encryption::{
BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes,
@ -25,6 +24,15 @@ use crate::{
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
/// The size of the encoding of a ZSA asset type.
const ZSA_TYPE_SIZE: usize = 32;
/// The size of the ZSA variant of COMPACT_NOTE_SIZE.
const COMPACT_ZSA_NOTE_SIZE: usize = COMPACT_NOTE_SIZE + ZSA_TYPE_SIZE;
/// The size of the memo.
const MEMO_SIZE: usize = NOTE_PLAINTEXT_SIZE - COMPACT_NOTE_SIZE;
/// The size of the ZSA variant of the memo.
const ZSA_MEMO_SIZE: usize = NOTE_PLAINTEXT_SIZE - COMPACT_ZSA_NOTE_SIZE;
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
///
/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
@ -50,6 +58,8 @@ pub(crate) fn prf_ock_orchard(
)
}
/// Domain-specific requirements:
/// - If the note version is 3, the `plaintext` must contain a valid encoding of a ZSA asset type.
fn orchard_parse_note_plaintext_without_memo<F>(
domain: &OrchardDomain,
plaintext: &[u8],
@ -61,9 +71,8 @@ where
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
// Check note plaintext version
if plaintext[0] != 0x02 {
return None;
}
// and parse the asset type accordingly.
let note_type = parse_version_and_asset_type(plaintext)?;
// The unwraps below are guaranteed to succeed by the assertion above
let diversifier = Diversifier::from_bytes(plaintext[1..12].try_into().unwrap());
@ -76,11 +85,25 @@ where
let pk_d = get_validated_pk_d(&diversifier)?;
let recipient = Address::from_parts(diversifier, pk_d);
// TODO: add note_type
let note = Note::from_parts(recipient, value, NoteType::native(), domain.rho, rseed);
let note = Note::from_parts(recipient, value, note_type, domain.rho, rseed);
Some((note, recipient))
}
fn parse_version_and_asset_type(plaintext: &[u8]) -> Option<NoteType> {
// TODO: make this constant-time?
match plaintext[0] {
0x02 => Some(NoteType::native()),
0x03 if plaintext.len() >= COMPACT_ZSA_NOTE_SIZE => {
let bytes = &plaintext[COMPACT_NOTE_SIZE..COMPACT_ZSA_NOTE_SIZE]
.try_into()
.unwrap();
NoteType::from_bytes(bytes).into()
}
_ => None,
}
}
/// Orchard-specific note encryption logic.
#[derive(Debug)]
pub struct OrchardDomain {
@ -114,7 +137,7 @@ impl Domain for OrchardDomain {
type ValueCommitment = ValueCommitment;
type ExtractedCommitment = ExtractedNoteCommitment;
type ExtractedCommitmentBytes = [u8; 32];
type Memo = [u8; 512]; // TODO use a more interesting type
type Memo = [u8; MEMO_SIZE]; // TODO use a more interesting type
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
Some(note.esk())
@ -154,13 +177,23 @@ impl Domain for OrchardDomain {
_: &Self::Recipient,
memo: &Self::Memo,
) -> NotePlaintextBytes {
let is_native: bool = note.note_type().is_native().into();
let mut np = [0; NOTE_PLAINTEXT_SIZE];
np[0] = 0x02;
np[0] = if is_native { 0x02 } else { 0x03 };
np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
np[12..20].copy_from_slice(&note.value().to_bytes());
// todo: add note_type
np[20..52].copy_from_slice(note.rseed().as_bytes());
np[52..].copy_from_slice(memo);
if is_native {
np[52..].copy_from_slice(memo);
} else {
let zsa_type = note.note_type().to_bytes();
np[52..84].copy_from_slice(&zsa_type);
let short_memo = &memo[0..memo.len() - ZSA_TYPE_SIZE];
np[84..].copy_from_slice(short_memo);
// TODO: handle full-size memo or make short_memo explicit.
};
NotePlaintextBytes(np)
}
@ -227,9 +260,20 @@ impl Domain for OrchardDomain {
}
fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
.try_into()
.unwrap()
let mut memo = [0; MEMO_SIZE];
match get_note_version(plaintext) {
0x02 => {
let full_memo = &plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
memo.copy_from_slice(full_memo);
}
0x03 => {
// ZSA note plaintext have a shorter memo.
let short_memo = &plaintext.0[COMPACT_ZSA_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
memo[..ZSA_MEMO_SIZE].copy_from_slice(short_memo);
}
_ => {}
};
memo
}
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
@ -257,6 +301,10 @@ impl BatchDomain for OrchardDomain {
}
}
fn get_note_version(plaintext: &NotePlaintextBytes) -> u8 {
plaintext.0[0]
}
/// Implementation of in-band secret distribution for Orchard bundles.
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
@ -279,7 +327,7 @@ pub struct CompactAction {
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: [u8; 52],
enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
}
impl fmt::Debug for CompactAction {
@ -294,7 +342,7 @@ impl<T> From<&Action<T>> for CompactAction {
nullifier: *action.nullifier(),
cmx: *action.cmx(),
ephemeral_key: action.ephemeral_key(),
enc_ciphertext: action.encrypted_note().enc_ciphertext[..52]
enc_ciphertext: action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE]
.try_into()
.unwrap(),
}
@ -339,13 +387,13 @@ impl CompactAction {
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rand::rngs::OsRng;
use zcash_note_encryption::{
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk,
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk, Domain,
EphemeralKeyBytes,
};
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
use crate::note::NoteType;
use crate::{
action::Action,
@ -353,12 +401,55 @@ mod tests {
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
OutgoingViewingKey,
},
note::{ExtractedNoteCommitment, Nullifier, RandomSeed, TransmittedNoteCiphertext},
note::{
testing::arb_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
TransmittedNoteCiphertext,
},
primitives::redpallas,
value::{NoteValue, ValueCommitment},
Address, Note,
};
use super::{get_note_version, orchard_parse_note_plaintext_without_memo};
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
proptest! {
#[test]
fn test_encoding_roundtrip(
note in arb_note(NoteValue::from_raw(10)),
) {
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
// Encode.
let plaintext = OrchardDomain::note_plaintext_bytes(&note, &note.recipient(), memo);
// Decode.
let domain = OrchardDomain { rho: note.rho() };
let parsed_version = get_note_version(&plaintext);
let parsed_memo = domain.extract_memo(&plaintext);
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &plaintext.0,
|diversifier| {
assert_eq!(diversifier, &note.recipient().diversifier());
Some(*note.recipient().pk_d())
}
).expect("Plaintext parsing failed");
// Check.
assert_eq!(parsed_note, note);
assert_eq!(parsed_recipient, note.recipient());
if parsed_note.note_type().is_native().into() {
assert_eq!(parsed_version, 0x02);
assert_eq!(&parsed_memo, memo);
} else {
assert_eq!(parsed_version, 0x03);
let mut short_memo = *memo;
short_memo[512 - 32..].copy_from_slice(&[0; 32]);
assert_eq!(parsed_memo, short_memo);
}
}
}
#[test]
fn test_vectors() {
let test_vectors = crate::test_vectors::note_encryption::test_vectors();
@ -400,7 +491,13 @@ mod tests {
assert_eq!(ock.as_ref(), tv.ock);
let recipient = Address::from_parts(d, pk_d);
let note = Note::from_parts(recipient, value, NoteType::native(), rho, rseed);
let note_type = match tv.note_type {
None => NoteType::native(),
Some(type_bytes) => NoteType::from_bytes(&type_bytes).unwrap(),
};
let note = Note::from_parts(recipient, value, note_type, rho, rseed);
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
let action = Action::from_parts(
@ -439,7 +536,8 @@ mod tests {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, recipient);
}
None => panic!("Compact note decryption failed"),
None => assert!(tv.note_type.is_some(), "Compact note decryption failed"),
// Ignore that ZSA notes are not detected in compact decryption.
}
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {

File diff suppressed because it is too large Load Diff

View File

@ -302,7 +302,7 @@ impl ValueCommitment {
pallas::Scalar::from(abs_value)
};
let V_zsa = note_type.0;
let V_zsa = note_type.cv_base();
ValueCommitment(V_zsa * value + R * rcv.0)
}