diff --git a/noir-projects/aztec-nr/aztec/src/keys/ephemeral.nr b/noir-projects/aztec-nr/aztec/src/keys/ephemeral.nr index e8d59d42be8c..0e24bb42a526 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/ephemeral.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/ephemeral.nr @@ -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 @@ -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) { + // 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)); + } + } +} diff --git a/noir-projects/aztec-nr/aztec/src/messages/encoding.nr b/noir-projects/aztec-nr/aztec/src/messages/encoding.nr index d313a48a4764..dc484086cf8a 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encoding.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encoding.nr @@ -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; diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr index 58683f9c9217..e4b6a07b1a84 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr @@ -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::{ @@ -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, }, }; @@ -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 @@ -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 @@ -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::(); // Safety: this randomness won't be constrained to be random. It's in the interest of the executor of this fn @@ -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]; } @@ -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", ); @@ -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); @@ -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 diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/arithmetic_generics_utils.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/arithmetic_generics_utils.nr index f9b304e80cd4..6bd7e79a12e4 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/arithmetic_generics_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/arithmetic_generics_utils.nr @@ -31,16 +31,16 @@ fn get_arr_of_size__ciphertext( [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( _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 @@ -51,16 +51,16 @@ fn get_arr_of_size__message_bytes_padding( [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(_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() -> [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() -> [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::(); 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); @@ -71,7 +71,7 @@ pub(crate) fn get_arr_of_size__message_bytes_padding__from_PT() -> // 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() -> [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() -> [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::(); 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);