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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion noir-projects/aztec-nr/aztec/src/keys/ephemeral.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use std::embedded_curve_ops::{EmbeddedCurveScalar, fixed_base_scalar_mul};

use crate::protocol::{point::Point, scalar::Scalar};

use crate::oracle::random::random;
use crate::{oracle::random::random, utils::point::get_sign_of_point};

/// Generates a random ephemeral key pair.
pub fn generate_ephemeral_key_pair() -> (Scalar, Point) {
// @todo Need to draw randomness from the full domain of Fq not only Fr

Expand All @@ -20,3 +21,56 @@ pub fn generate_ephemeral_key_pair() -> (Scalar, Point) {

(eph_sk, eph_pk)
}

/// Generates a random ephemeral key pair with a positive y-coordinate.
///
/// Unlike [`generate_ephemeral_key_pair`], the y-coordinate of the public key is guaranteed to be a positive value
/// (i.e. [`crate::utils::point::get_sign_of_point`] will return `true`).
///
/// This is useful as it means it is possible to just broadcast the x-coordinate as a single `Field` and then
/// reconstruct the original public key using [`crate::utils::point::point_from_x_coord_and_sign`] with `sign: true`.
pub fn generate_positive_ephemeral_key_pair() -> (Scalar, Point) {
Copy link
Contributor Author

@nventuro nventuro Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new fn to avoid changing the other one, since we want to backport this and minimize breakage. There's a linear task to clean this up later on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a TODO with the linear task id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's done already 🙃

// Safety: we use the randomness to preserve the privacy of both the sender and recipient via encryption, so a
// malicious sender could use non-random values to reveal the plaintext. But they already know it themselves
// anyway, and so the recipient already trusts them to not disclose this information. We can therefore assume that
// the sender will cooperate in the random value generation.
let eph_sk = unsafe { generate_secret_key_for_positive_public_key() };
let eph_pk = fixed_base_scalar_mul(eph_sk);

assert(get_sign_of_point(eph_pk), "Got an ephemeral public key with a negative y coordinate");

(eph_sk, eph_pk)
}

unconstrained fn generate_secret_key_for_positive_public_key() -> EmbeddedCurveScalar {
let mut sk = std::mem::zeroed();

loop {
// We simply produce random secret keys until we find one that has results in a positive public key. About half
// of all public keys fulfill this condition, so this should only take a few iterations at most.

// @todo Need to draw randomness from the full domain of Fq not only Fr
sk = EmbeddedCurveScalar::from_field(random());
let pk = fixed_base_scalar_mul(sk);
if get_sign_of_point(pk) {
break;
}
}

sk
}

mod test {
use crate::utils::point::get_sign_of_point;
use super::generate_positive_ephemeral_key_pair;

#[test]
fn generate_positive_ephemeral_key_pair_produces_positive_keys() {
// About half of random points are negative, so testing just a couple gives us high confidence that
// `generate_positive_ephemeral_key_pair` is indeed producing positive ones.
for _ in 0..10 {
let (_, pk) = generate_positive_ephemeral_key_pair();
assert(get_sign_of_point(pk));
}
}
}
6 changes: 2 additions & 4 deletions noir-projects/aztec-nr/aztec/src/messages/encoding.nr
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@ pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 16;
pub(crate) global AES128_PKCS7_EXPANSION_IN_BYTES: u32 = 16;

pub global EPH_PK_X_SIZE_IN_FIELDS: u32 = 1;
pub global EPH_PK_SIGN_BYTE_SIZE_IN_BYTES: u32 = 1;

// (15 - 1) * 31 - 16 - 1 - 16 = 401. Note: We multiply by 31 because ciphertext bytes are stored in fields using
// (15 - 1) * 31 - 16 - 16 = 402. Note: We multiply by 31 because ciphertext bytes are stored in fields using
// bytes_to_fields, which packs 31 bytes per field (since a Field is ~254 bits and can safely store 31 whole bytes).
pub(crate) global MESSAGE_PLAINTEXT_SIZE_IN_BYTES: u32 = (MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31
- HEADER_CIPHERTEXT_SIZE_IN_BYTES
- EPH_PK_SIGN_BYTE_SIZE_IN_BYTES
- AES128_PKCS7_EXPANSION_IN_BYTES;
// The plaintext bytes represent Field values that were originally serialized using fields_to_bytes, which converts
// each Field to 32 bytes. To convert the plaintext bytes back to fields, we divide by 32. 401 / 32 = 12
// each Field to 32 bytes. To convert the plaintext bytes back to fields, we divide by 32. 402 / 32 = 12
pub global MESSAGE_PLAINTEXT_LEN: u32 = MESSAGE_PLAINTEXT_SIZE_IN_BYTES / 32;

pub global MESSAGE_EXPANDED_METADATA_LEN: u32 = 1;
Expand Down
41 changes: 18 additions & 23 deletions noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use crate::protocol::{
};

use crate::{
keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_ephemeral_key_pair},
keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair},
messages::{
encoding::{
EPH_PK_SIGN_BYTE_SIZE_IN_BYTES, EPH_PK_X_SIZE_IN_FIELDS, HEADER_CIPHERTEXT_SIZE_IN_BYTES,
MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN, MESSAGE_PLAINTEXT_SIZE_IN_BYTES,
EPH_PK_X_SIZE_IN_FIELDS, HEADER_CIPHERTEXT_SIZE_IN_BYTES, MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN,
MESSAGE_PLAINTEXT_SIZE_IN_BYTES,
},
encryption::message_encryption::MessageEncryption,
logs::arithmetic_generics_utils::{
Expand All @@ -25,7 +25,7 @@ use crate::{
bytes_to_fields::{bytes_from_fields, bytes_to_fields},
fields_to_bytes::{fields_from_bytes, fields_to_bytes},
},
point::{get_sign_of_point, point_from_x_coord_and_sign},
point::point_from_x_coord_and_sign,
random::get_random_bytes,
},
};
Expand Down Expand Up @@ -208,11 +208,11 @@ impl MessageEncryption for AES128 {
/// multiple of 31 so it divides evenly into fields:
///
/// ```text
/// +---------+------------+-------------------------+---------+
/// | pk sign | header ct | body ct | byte pad|
/// | 1 B | 16 B | PlaintextLen*32 + 16 B | (random)|
/// +---------+------------+-------------------------+---------+
/// |<----------- padded to a multiple of 31 B ------------->|
/// +------------+-------------------------+---------+
/// | header ct | body ct | byte pad|
/// | 16 B | PlaintextLen*32 + 16 B | (random)|
/// +------------+-------------------------+---------+
/// |<-------- padded to a multiple of 31 B -------->|
/// ```
///
/// **Step 2 -- Pack into fields.** The byte array is split into 31-byte chunks, each stored in one field. The
Expand Down Expand Up @@ -245,9 +245,7 @@ impl MessageEncryption for AES128 {
let plaintext_bytes = fields_to_bytes(plaintext);

// Derive ECDH shared secret with recipient using a fresh ephemeral keypair.
let (eph_sk, eph_pk) = generate_ephemeral_key_pair();

let eph_pk_sign_byte: u8 = get_sign_of_point(eph_pk) as u8;
let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair();

// (not to be confused with the tagging shared secret) TODO (#17158): Currently we unwrap the Option returned
// by derive_ecdh_shared_secret. We need to handle the case where the ephemeral public key is invalid to
Expand Down Expand Up @@ -309,7 +307,7 @@ impl MessageEncryption for AES128 {
);

// Assemble the message byte array:
// [eph_pk_sign (1B)] [header_ct (16B)] [body_ct] [padding to mult of 31]
// [header_ct (16B)] [body_ct] [padding to mult of 31]
let mut message_bytes_padding_to_mult_31 =
get_arr_of_size__message_bytes_padding__from_PT::<PlaintextLen * 32>();
// Safety: this randomness won't be constrained to be random. It's in the interest of the executor of this fn
Expand All @@ -323,8 +321,7 @@ impl MessageEncryption for AES128 {
"Unexpected error: message_bytes.len() should be divisible by 31, by construction.",
);

message_bytes[0] = eph_pk_sign_byte;
let mut offset = 1;
let mut offset = 0;
for i in 0..header_ciphertext_bytes.len() {
message_bytes[offset + i] = header_ciphertext_bytes[i];
}
Expand All @@ -346,7 +343,7 @@ impl MessageEncryption for AES128 {
// computation used to obtain the offset computes the expected value (which we _can_ do in a static check), and
// then add a cheap runtime check to also validate that the offset matches this.
std::static_assert(
1 + header_ciphertext_bytes.len() + ciphertext_bytes.len() + message_bytes_padding_to_mult_31.len()
header_ciphertext_bytes.len() + ciphertext_bytes.len() + message_bytes_padding_to_mult_31.len()
== message_bytes.len(),
"unexpected message length",
);
Expand Down Expand Up @@ -396,12 +393,10 @@ impl MessageEncryption for AES128 {
// Convert the ciphertext represented as fields to a byte representation (its original format)
let ciphertext_without_eph_pk_x = bytes_from_fields(ciphertext_without_eph_pk_x_fields);

// First byte of the ciphertext represents the ephemeral public key sign
let eph_pk_sign_bool = ciphertext_without_eph_pk_x.get(0) != 0;

// With the sign and the x-coordinate of the ephemeral public key, we can reconstruct the point. This may fail
// however, as not all x-coordinates are on the curve. In that case, we simply return `Option::none`.
point_from_x_coord_and_sign(eph_pk_x, eph_pk_sign_bool).map(|eph_pk| {
// With the x-coordinate of the ephemeral public key we can reconstruct the point as we know that the
// y-coordinate must be positive. This may fail however, as not all x-coordinates are on the curve. In that
// case, we simply return `Option::none`.
point_from_x_coord_and_sign(eph_pk_x, true).map(|eph_pk| {
// Derive shared secret
let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk);

Expand All @@ -413,7 +408,7 @@ impl MessageEncryption for AES128 {
let (header_sym_key, header_iv) = pairs[1];

// Extract the header ciphertext
let header_start = EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Skip eph_pk_sign byte
let header_start = 0;
let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] =
array::subarray(ciphertext_without_eph_pk_x.storage(), header_start);
// We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ fn get_arr_of_size__ciphertext<let FullPt: u32, let PtAesPadding: u32>(
[0; FullPt + PtAesPadding]
}

// Ok, so we have the following bytes: eph_pk_sign, header_ciphertext, ciphertext: Let mbwop = 1 +
// Ok, so we have the following bytes: header_ciphertext, ciphertext: Let mbwop =
// HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka message bytes without padding
fn get_arr_of_size__message_bytes_without_padding<let Ct: u32>(
_ct: [u8; Ct],
) -> [u8; 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct] {
[0; 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct]
) -> [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct] {
[0; HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct]
}

// Recall:
// mbwop := 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka message bytes without padding
// mbwop := HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka message bytes without padding
// We now want to pad b to the next multiple of 31, so as to "fill" fields. Let p be that padding. p = 31 * ceil(mbwop
// / 31) - mbwop
// = 31 * ((mbwop + 30) // 31) - mbwop
Expand All @@ -51,16 +51,16 @@ fn get_arr_of_size__message_bytes_padding<let Mbwop: u32>(
[0; (31 * ((Mbwop + 30) / 31)) - Mbwop]
}

// |message_bytes| = 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| + p // aka message bytes (with
// |message_bytes| = HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| + p // aka message bytes (with
// padding) Recall:
// mbwop := 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| p is the padding
// mbwop := HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| p is the padding
fn get_arr_of_size__message_bytes<let MBWOP: u32, let P: u32>(_mbwop: [u8; MBWOP], _p: [u8; P]) -> [u8; MBWOP + P] {
[0; MBWOP + P]
}

// The return type is pasted from the LSP's expectation, because it was too difficult to match its weird way of doing
// algebra. It doesn't know all rules of arithmetic. Pt is the plaintext length.
pub(crate) fn get_arr_of_size__message_bytes_padding__from_PT<let Pt: u32>() -> [u8; ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1))] {
pub(crate) fn get_arr_of_size__message_bytes_padding__from_PT<let Pt: u32>() -> [u8; ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES))] {
let full_pt = get_arr_of_size__full_plaintext::<Pt>();
let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt);
let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding);
Expand All @@ -71,7 +71,7 @@ pub(crate) fn get_arr_of_size__message_bytes_padding__from_PT<let Pt: u32>() ->

// The return type is pasted from the LSP's expectation, because it was too difficult to match its weird way of doing
// algebra. It doesn't know all rules of arithmetic.
pub(crate) fn get_arr_of_size__message_bytes__from_PT<let Pt: u32>() -> [u8; (((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1)))] {
pub(crate) fn get_arr_of_size__message_bytes__from_PT<let Pt: u32>() -> [u8; (((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES) + ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES)))] {
let full_pt = get_arr_of_size__full_plaintext::<Pt>();
let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt);
let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding);
Expand Down
Loading