Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
32 changes: 32 additions & 0 deletions docs/PCZT_APDU.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ Packet sequence:
- `cv_net [u8; 32]`
- `nullifier [u8; 32]`
- `rk [u8; 32]`
- `spend_recipient [u8; 43]`, raw Orchard payment address
- `spend_value u64`
- `spend_rho [u8; 32]`
- `spend_rseed [u8; 32]`
- `alpha [u8; 32]`
- `zip32_derivation` packet:
- seed fingerprint `[u8; 32]`
Expand All @@ -122,9 +126,37 @@ Packet sequence:
- `out_ciphertext Vec<u8>` packet sequence:
- first packet: CompactSize byte length + ciphertext bytes
- continuation packets: ciphertext bytes only
- Output metadata packet:
- `recipient [u8; 43]`, raw Orchard payment address
- `value u64`
- `rseed [u8; 32]`
- `rcv [u8; 32]`

3. Bundle trailer packet, only when Orchard action count is greater than `0`:
- `flags u8`
- `value_sum` magnitude `u64`
- `value_sum` negative-sign flag `u8`
- `anchor [u8; 32]`

### Orchard validation requirements

The app does not trust host-supplied Orchard display fields directly. Before an
action is accepted:

- `rk` is recomputed from the signing key selected by `zip32_derivation` and the
disclosed `alpha`.
- `cv_net` must match `ValueCommitment(spend_value - value, rcv)`.
- `spend_recipient` must derive from the signing Orchard FVK's external or
internal IVK.
- `nullifier` is recomputed from the signing FVK's `nk`, `spend_recipient`,
`spend_value`, `spend_rho`, and `spend_rseed`.
- The output must decrypt with the prepared Orchard decipher keys. For
decryptable outputs, the decrypted value and raw Orchard receiver must match
`value` and `recipient`.
- A zero-valued undecryptable output is accepted only as a dummy output: the app
recomputes the Orchard note commitment from `recipient`, `value == 0`, the
action nullifier used as `rho`, and output `rseed`, and compares it with
`cmx`. Validated dummy outputs are omitted from the clear-sign review list.
- Non-zero undecryptable outputs are rejected.

Dummy spends are not represented by this compact APDU subset.
90 changes: 90 additions & 0 deletions ledger_zcash_crypto/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#![no_std]

extern crate alloc;

mod bytes;
mod hashtocurve;
mod montgomery;
pub mod orchard;
mod poseidon;
mod poseidon_fp;
pub mod redpallas;
mod sinsemilla;

Expand Down Expand Up @@ -43,6 +47,14 @@ const ORCHARD_ESK_DOMAIN_SEPARATOR: u8 = 0x04;
const ORCHARD_RCM_DOMAIN_SEPARATOR: u8 = 0x05;
const ORCHARD_PSI_DOMAIN_SEPARATOR: u8 = 0x09;
const PRF_EXPAND_BYTES: usize = 64;
const ORCHARD_VALUE_COMMITMENT_VALUE_BASEPOINT_BYTES: [u8; 32] = [
0x67, 0x43, 0xf9, 0x3a, 0x6e, 0xbd, 0xa7, 0x2a, 0x8c, 0x7c, 0x5a, 0x2b, 0x7f, 0xa3, 0x04, 0xfe,
0x32, 0xb2, 0x9b, 0x4f, 0x70, 0x6a, 0xa8, 0xf7, 0x42, 0x0f, 0x3d, 0x8e, 0x7a, 0x59, 0x70, 0x2f,
];
const ORCHARD_VALUE_COMMITMENT_RANDOMNESS_BASEPOINT_BYTES: [u8; 32] = [
0x91, 0x5a, 0x3c, 0x88, 0x68, 0xc6, 0xc3, 0x0e, 0x2f, 0x80, 0x90, 0xee, 0x45, 0xd7, 0x6e, 0x40,
0x48, 0x20, 0x8d, 0xea, 0x5b, 0x23, 0x66, 0x4f, 0xbb, 0x09, 0xa4, 0x0f, 0x55, 0x44, 0xf4, 0x07,
];

pub fn _debug_print(str: &str) {
debug!("{}", str);
Expand Down Expand Up @@ -169,6 +181,51 @@ pub fn orchard_pk_d(ivk: &[u8; 32], g_d: &[u8; 32]) -> Result<[u8; 32], Error> {
pallas_point_to_bytes(&pk_d)
}

/// Computes `ValueCommit^Orchard(value, rcv)` using precomputed Orchard value
/// commitment basepoints.
///
/// `value` is the signed net action value, `spend.value - output.value`.
/// `rcv` must be a canonical little-endian Pallas scalar encoding.
pub fn orchard_value_commitment_bytes(value: i64, rcv: &[u8; 32]) -> Result<[u8; 32], Error> {
let mut sum = value_commitment_value_term(value)?;

if *rcv != [0; 32] {
let rcv_be = canonical_scalar_bytes_be(rcv)?;
let rcv_term = pallas_basepoint_mul(
&ORCHARD_VALUE_COMMITMENT_RANDOMNESS_BASEPOINT_BYTES,
&rcv_be,
)?;
sum = match sum {
Some(value_term) => Some(pallas_point_add(&value_term, &rcv_term)?),
None => Some(rcv_term),
};
}

match sum {
Some(point) => pallas_point_to_bytes(&point),
None => Ok([0; 32]),
}
}

pub fn orchard_spend_nullifier_bytes(
nk: &[u8; 32],
raw_address: &[u8; orchard::ORCHARD_RAW_ADDRESS_SIZE],
value: u64,
rho: &[u8; 32],
rseed: &[u8; 32],
) -> Result<[u8; 32], Error> {
orchard::spend_nullifier_bytes(nk, raw_address, value, rho, rseed)
}

pub fn orchard_note_commitment_bytes(
raw_address: &[u8; orchard::ORCHARD_RAW_ADDRESS_SIZE],
value: u64,
rho: &[u8; 32],
rseed: &[u8; 32],
) -> Result<[u8; 32], Error> {
orchard::note_commitment_bytes(raw_address, value, rho, rseed)
}

/// Parses a compressed Pallas point encoding and rejects the identity.
///
/// Returns canonical little-endian affine coordinates on success.
Expand Down Expand Up @@ -407,6 +464,39 @@ fn canonical_scalar_bytes_be(bytes_le: &[u8; 32]) -> Result<[u8; 32], Error> {
)
}

fn value_commitment_value_term(value: i64) -> Result<Option<EcPoint>, Error> {
if value == 0 {
return Ok(None);
}

let mut scalar_be = [0u8; 32];
scalar_be[24..].copy_from_slice(&value.unsigned_abs().to_be_bytes());
let term = pallas_basepoint_mul(&ORCHARD_VALUE_COMMITMENT_VALUE_BASEPOINT_BYTES, &scalar_be)?;

if value.is_negative() {
let mut negated = pallas_point_to_bytes(&term)?;
negated[31] ^= 0x80;
Ok(Some(pallas_point_from_bytes(&negated)?))
} else {
Ok(Some(term))
}
}

fn pallas_basepoint_mul(
basepoint_bytes: &[u8; 32],
scalar_bytes_be: &[u8; 32],
) -> Result<EcPoint, Error> {
let mut point = pallas_point_from_bytes(basepoint_bytes)?;
point.rnd_scalarmul(scalar_bytes_be)?;
Ok(point)
}

fn pallas_point_add(lhs: &EcPoint, rhs: &EcPoint) -> Result<EcPoint, Error> {
let mut sum = EcPoint::new(CurvesId::Pallas)?;
sum.add(lhs, rhs)?;
Ok(sum)
}

fn reduce_uniform_le_bytes_mod_pallas(
uniform_le: &[u8; PRF_EXPAND_BYTES],
modulus_param: CurveDomainParam,
Expand Down
114 changes: 109 additions & 5 deletions ledger_zcash_crypto/src/orchard.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloc::{boxed::Box, vec};
use chacha20::{
ChaCha20,
cipher::{KeyIvInit, StreamCipher, StreamCipherSeek},
Expand All @@ -12,9 +13,11 @@ use pasta_curves::pallas;

use crate::{
Error, ORCHARD_ESK_DOMAIN_SEPARATOR, ORCHARD_PSI_DOMAIN_SEPARATOR,
ORCHARD_RCM_DOMAIN_SEPARATOR, PRF_EXPAND_BYTES, bytes::reverse_copy, pallas_base_from_repr,
pallas_point_from_bytes, pallas_point_to_bytes, pallas_scalar_from_repr,
prf_expand_with_domain_separator_and_inputs, sinsemilla::sinsemilla_short_commit,
ORCHARD_RCM_DOMAIN_SEPARATOR, PRF_EXPAND_BYTES,
bytes::reverse_copy,
pallas_base_from_repr, pallas_basepoint_mul, pallas_point_add, pallas_point_from_bytes,
pallas_point_to_bytes, pallas_scalar_from_repr, prf_expand_with_domain_separator_and_inputs,
sinsemilla::{extract_p, sinsemilla_short_commit, sinsemilla_short_commit_point},
to_pallas_base_bytes, to_pallas_scalar_bytes,
};

Expand All @@ -38,6 +41,10 @@ const NOTE_COMMITMENT_MESSAGE_BITS: usize = 32 * 8 + 32 * 8 + 64 + L_ORCHARD_BAS
const PRF_OCK_ORCHARD_PERSONALIZATION: [u8; 16] = *b"Zcash_Orchardock";
const KDF_ORCHARD_PERSONALIZATION: [u8; 16] = *b"Zcash_OrchardKDF";
const NOTE_COMMITMENT_PERSONALIZATION: &str = "z.cash:Orchard-NoteCommit";
const ORCHARD_NULLIFIER_K_BASEPOINT_BYTES: [u8; HASH_SIZE] = [
0x75, 0xca, 0x47, 0xe4, 0xa7, 0x6a, 0x6f, 0xd3, 0x9b, 0xdb, 0xb5, 0xcc, 0x92, 0xb1, 0x7e, 0x5e,
0xcf, 0xc9, 0xf4, 0xfa, 0x71, 0x55, 0x37, 0x2e, 0x8d, 0x19, 0xa8, 0x9c, 0x16, 0xaa, 0xe7, 0x25,
];

#[derive(Clone, Copy, Debug)]
pub struct OrchardCompactAction {
Expand All @@ -56,10 +63,12 @@ pub struct OrchardActionCiphertext<'a> {
pub out_ciphertext: [u8; ORCHARD_OUT_CIPHERTEXT_SIZE],
}

#[derive(Clone, Copy, Debug)]
#[derive(Debug)]
pub struct DecipheredOrchardOutput {
pub value: u64,
pub raw_address: [u8; ORCHARD_RAW_ADDRESS_SIZE],
// Keep the 512-byte memo off stack-sensitive Orchard return paths.
pub memo: Option<Box<[u8]>>,
}

pub fn decipher_value_with_ovk(
Expand All @@ -76,6 +85,67 @@ pub fn decipher_compact_value(
try_compact_note_decryption_with_ivk(ivk, compact)
}

pub fn spend_nullifier_bytes(
nk: &[u8; HASH_SIZE],
raw_address: &[u8; ORCHARD_RAW_ADDRESS_SIZE],
value: u64,
rho: &[u8; HASH_SIZE],
rseed: &[u8; HASH_SIZE],
) -> Result<[u8; HASH_SIZE], Error> {
let rho = pallas_base_from_repr(*rho)?;
let _esk = orchard_esk(rseed, &rho)?;

let mut diversifier = [0u8; DIVERSIFIER_SIZE];
diversifier.copy_from_slice(&raw_address[..DIVERSIFIER_SIZE]);

let mut pk_d = [0u8; HASH_SIZE];
pk_d.copy_from_slice(&raw_address[DIVERSIFIER_SIZE..]);
if !is_valid_nonidentity_pallas_point(&pk_d)? {
return Err(Error::MalformedPallasPoint);
}

let g_d = crate::diversify_hash_ledger(&diversifier)?;
let cm = note_commitment_point(&g_d, &pk_d, value, &rho, rseed)?;
let psi = pallas_base_from_repr(orchard_psi(rseed, &rho)?)?;
let nk = pallas_base_from_repr(*nk)?;
let prf_nf = crate::poseidon::p128pow5t3_hash_len2(nk, rho);
let nullifier_scalar = pallas_scalar_from_repr((prf_nf + psi).to_repr())?;

let nullifier_point = if bool::from(nullifier_scalar.is_zero()) {
cm
} else {
let nullifier_k = pallas_basepoint_mul(
&ORCHARD_NULLIFIER_K_BASEPOINT_BYTES,
&scalar_bytes_be(&nullifier_scalar),
)?;
pallas_point_add(&nullifier_k, &cm)?
};

Ok(extract_p(&nullifier_point)?.to_repr())
}

pub fn note_commitment_bytes(
raw_address: &[u8; ORCHARD_RAW_ADDRESS_SIZE],
value: u64,
rho: &[u8; HASH_SIZE],
rseed: &[u8; HASH_SIZE],
) -> Result<[u8; HASH_SIZE], Error> {
let rho = pallas_base_from_repr(*rho)?;
let _esk = orchard_esk(rseed, &rho)?;

let mut diversifier = [0u8; DIVERSIFIER_SIZE];
diversifier.copy_from_slice(&raw_address[..DIVERSIFIER_SIZE]);

let mut pk_d = [0u8; HASH_SIZE];
pk_d.copy_from_slice(&raw_address[DIVERSIFIER_SIZE..]);
if !is_valid_nonidentity_pallas_point(&pk_d)? {
return Err(Error::MalformedPallasPoint);
}

let g_d = crate::diversify_hash_ledger(&diversifier)?;
note_commitment(&g_d, &pk_d, value, &rho, rseed)
}

fn try_output_recovery_with_ovk(
ovk: &[u8; HASH_SIZE],
action: &OrchardActionCiphertext<'_>,
Expand Down Expand Up @@ -120,13 +190,16 @@ fn try_output_recovery_with_ovk(

let mut note_plaintext_prefix = [0u8; ORCHARD_NOTE_PLAINTEXT_PREFIX_SIZE];
note_plaintext_prefix.copy_from_slice(&note_plaintext[..ORCHARD_NOTE_PLAINTEXT_PREFIX_SIZE]);
let mut memo = vec![0u8; ORCHARD_MEMO_SIZE].into_boxed_slice();
memo.copy_from_slice(&note_plaintext[ORCHARD_NOTE_PLAINTEXT_PREFIX_SIZE..]);

parse_and_validate_note_plaintext(
&action.compact,
&note_plaintext_prefix,
&pk_d,
Some(&esk),
&rho,
Some(memo),
)
}

Expand Down Expand Up @@ -168,7 +241,7 @@ fn try_compact_note_decryption_with_ivk(
};
let pk_d = crate::orchard_pk_d(&ivk.to_repr(), &g_d)?;

parse_and_validate_note_plaintext(compact, &note_plaintext_prefix, &pk_d, None, &rho)
parse_and_validate_note_plaintext(compact, &note_plaintext_prefix, &pk_d, None, &rho, None)
}

fn parse_and_validate_note_plaintext(
Expand All @@ -177,6 +250,7 @@ fn parse_and_validate_note_plaintext(
pk_d: &[u8; HASH_SIZE],
expected_esk: Option<&[u8; HASH_SIZE]>,
rho: &pallas::Base,
memo: Option<Box<[u8]>>,
) -> Result<Option<DecipheredOrchardOutput>, Error> {
let Some(note_plaintext) = parse_note_plaintext_prefix(plaintext) else {
return Ok(None);
Expand Down Expand Up @@ -211,6 +285,7 @@ fn parse_and_validate_note_plaintext(
Ok(Some(DecipheredOrchardOutput {
value: note_plaintext.value,
raw_address,
memo,
}))
}

Expand Down Expand Up @@ -417,6 +492,35 @@ fn note_commitment(
Ok(cmx.to_repr())
}

fn note_commitment_point(
g_d: &[u8; HASH_SIZE],
pk_d: &[u8; HASH_SIZE],
value: u64,
rho: &pallas::Base,
rseed: &[u8; HASH_SIZE],
) -> Result<ledger_device_sdk::ecc::math::EcPoint, Error> {
let psi = orchard_psi(rseed, rho)?;
let rcm = orchard_rcm(rseed, rho)?;
let rcm = pallas_scalar_from_repr(rcm)?;

let mut message = [false; NOTE_COMMITMENT_MESSAGE_BITS];
let mut offset = 0;
append_le_bits(&mut message, &mut offset, g_d, 32 * 8);
append_le_bits(&mut message, &mut offset, pk_d, 32 * 8);
append_le_bits(&mut message, &mut offset, &value.to_le_bytes(), 64);
append_le_bits(&mut message, &mut offset, &rho.to_repr(), L_ORCHARD_BASE);
append_le_bits(&mut message, &mut offset, &psi, L_ORCHARD_BASE);

sinsemilla_short_commit_point(NOTE_COMMITMENT_PERSONALIZATION, &message, &rcm)?
.ok_or(Error::InvalidKeyDiscarded)
}

fn scalar_bytes_be(scalar: &pallas::Scalar) -> [u8; HASH_SIZE] {
let mut bytes_be = [0u8; HASH_SIZE];
reverse_copy(&mut bytes_be, &scalar.to_repr());
bytes_be
}

fn append_le_bits(message: &mut [bool], offset: &mut usize, bytes: &[u8], bit_len: usize) {
for bit_index in 0..bit_len {
message[*offset + bit_index] = ((bytes[bit_index / 8] >> (bit_index % 8)) & 1) == 1;
Expand Down
Loading
Loading