From 3e0553456535cd32743f7cf33e51ffd8a36ff75d Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Tue, 7 May 2024 13:06:09 +0100 Subject: [PATCH] feat: add ciphertext computation for log header (#6175) Fixes #5867 with the AES oracle on the noir side and a encrypt/decrypt tool on the typescript side as well. Changes the symmetric key derivation to use `GrumpkinPrivateKey` instead of the `GrumpkinScalar`, this changes the order of low/high. --- .../aztec-nr/aztec/src/encrypted_logs.nr | 1 + .../aztec/src/encrypted_logs/header.nr | 57 +++++++++++++++ .../aztec/src/keys/point_to_symmetric_key.nr | 10 +-- noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../contracts/test_contract/src/main.nr | 11 ++- yarn-project/aztec.js/src/index.ts | 1 + .../src/logs/encrypted_log_header.test.ts | 59 +++++++++++++++ .../src/logs/encrypted_log_header.ts | 72 +++++++++++++++++++ yarn-project/circuit-types/src/logs/index.ts | 1 + .../end-to-end/src/e2e_2_pxes.test.ts | 2 +- .../end-to-end/src/e2e_encryption.test.ts | 22 +++++- .../end-to-end/src/e2e_key_registry.test.ts | 2 +- .../server_world_state_synchronizer.test.ts | 2 + 13 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs.nr create mode 100644 noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr create mode 100644 yarn-project/circuit-types/src/logs/encrypted_log_header.test.ts create mode 100644 yarn-project/circuit-types/src/logs/encrypted_log_header.ts diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr new file mode 100644 index 00000000000..2ffdecb1b34 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr @@ -0,0 +1 @@ +mod header; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr new file mode 100644 index 00000000000..03b5a33e3d1 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr @@ -0,0 +1,57 @@ +use dep::protocol_types::{address::AztecAddress, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint}; + +use crate::oracle::encryption::aes128_encrypt; +use crate::keys::point_to_symmetric_key::point_to_symmetric_key; + +struct EncryptedLogHeader { + address: AztecAddress, +} + +impl EncryptedLogHeader { + fn new(address: AztecAddress) -> Self { + EncryptedLogHeader { address } + } + + // @todo Issue(#5901) Figure out if we return the bytes or fields for the log + fn compute_ciphertext(self, secret: GrumpkinPrivateKey, point: GrumpkinPoint) -> [u8; 32] { + let full_key = point_to_symmetric_key(secret, point); + let mut sym_key = [0; 16]; + let mut iv = [0; 16]; + let mut input = [0; 32]; + let input_slice = self.address.to_field().to_be_bytes(32); + + for i in 0..16 { + sym_key[i] = full_key[i]; + iv[i] = full_key[i + 16]; + + // We copy address on the following 2 lines in order to avoid having 2 loops + input[i] = input_slice[i]; + input[i + 16] = input_slice[i + 16]; + } + + // @todo Issue(#6172) This encryption is currently using an oracle. It is not actually constrained atm. + aes128_encrypt(input, iv, sym_key) + } +} + +// @todo Issue(#6172) This is to be run as a test. But it is currently using the AES oracle so will fail there. +fn test_encrypted_log_header() { + let address = AztecAddress::from_field(0xdeadbeef); + let header = EncryptedLogHeader::new(address); + let secret = GrumpkinPrivateKey::new( + 0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06, + 0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd + ); + let point = GrumpkinPoint::new( + 0x2688431c705a5ff3e6c6f2573c9e3ba1c1026d2251d0dbbf2d810aa53fd1d186, + 0x1e96887b117afca01c00468264f4f80b5bb16d94c1808a448595f115556e5c8e + ); + + let ciphertext = header.compute_ciphertext(secret, point); + + let expected_header_ciphertext = [ + 131, 119, 105, 129, 244, 32, 151, 205, 12, 99, 93, 62, 10, 180, 72, 21, 179, 36, 250, 95, 56, 167, 171, 16, 195, 164, 223, 57, 75, 5, 24, 119 + ]; + + assert_eq(ciphertext, expected_header_ciphertext); +} diff --git a/noir-projects/aztec-nr/aztec/src/keys/point_to_symmetric_key.nr b/noir-projects/aztec-nr/aztec/src/keys/point_to_symmetric_key.nr index b708d00e8bc..488df346e73 100644 --- a/noir-projects/aztec-nr/aztec/src/keys/point_to_symmetric_key.nr +++ b/noir-projects/aztec-nr/aztec/src/keys/point_to_symmetric_key.nr @@ -1,9 +1,9 @@ -use dep::protocol_types::{constants::GENERATOR_INDEX__SYMMETRIC_KEY, grumpkin_point::GrumpkinPoint, utils::arr_copy_slice}; +use dep::protocol_types::{constants::GENERATOR_INDEX__SYMMETRIC_KEY, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint, utils::arr_copy_slice}; use dep::std::{hash::sha256, grumpkin_scalar::GrumpkinScalar, scalar_mul::variable_base_embedded_curve}; // TODO(#5726): This function is called deriveAESSecret in TS. I don't like point_to_symmetric_key name much since // point is not the only input of the function. Unify naming with TS once we have a better name. -pub fn point_to_symmetric_key(secret: GrumpkinScalar, point: GrumpkinPoint) -> [u8; 32] { +pub fn point_to_symmetric_key(secret: GrumpkinPrivateKey, point: GrumpkinPoint) -> [u8; 32] { let shared_secret_fields = variable_base_embedded_curve(point.x, point.y, secret.low, secret.high); // TODO(https://github.com/AztecProtocol/aztec-packages/issues/6061): make the func return Point struct directly let shared_secret = GrumpkinPoint::new(shared_secret_fields[0], shared_secret_fields[1]); @@ -16,9 +16,9 @@ pub fn point_to_symmetric_key(secret: GrumpkinScalar, point: GrumpkinPoint) -> [ #[test] fn check_point_to_symmetric_key() { // Value taken from "derive shared secret" test in encrypt_buffer.test.ts - let secret = GrumpkinScalar::new( - 0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd, - 0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06 + let secret = GrumpkinPrivateKey::new( + 0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06, + 0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd ); let point = GrumpkinPoint::new( 0x2688431c705a5ff3e6c6f2573c9e3ba1c1026d2251d0dbbf2d810aa53fd1d186, diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 5043c7e5135..ce6504e1f74 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -10,4 +10,5 @@ mod oracle; mod state_vars; mod prelude; mod public_storage; +mod encrypted_logs; use dep::protocol_types; diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index 1a92cce5d60..430e0e21347 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -10,10 +10,12 @@ contract Test { use dep::aztec::protocol_types::{ abis::private_circuit_public_inputs::PrivateCircuitPublicInputs, - constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, CANONICAL_KEY_REGISTRY_ADDRESS}, traits::{Serialize, ToField, FromField}, - grumpkin_point::GrumpkinPoint + constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, CANONICAL_KEY_REGISTRY_ADDRESS}, + traits::{Serialize, ToField, FromField}, grumpkin_point::GrumpkinPoint, grumpkin_private_key::GrumpkinPrivateKey }; + use dep::aztec::encrypted_logs::header::EncryptedLogHeader; + use dep::aztec::note::constants::MAX_NOTES_PER_PAGE; use dep::aztec::state_vars::{shared_mutable::SharedMutablePrivateGetter, map::derive_storage_slot_in_map}; @@ -342,6 +344,11 @@ contract Test { aes128_encrypt(input, iv, key) } + #[aztec(private)] + fn compute_note_header_ciphertext(secret: GrumpkinPrivateKey, point: GrumpkinPoint) -> [u8; 32] { + EncryptedLogHeader::new(context.this_address()).compute_ciphertext(secret, point) + } + #[aztec(public)] fn assert_public_global_vars( chain_id: Field, diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index cb9179a84a7..cb64310e2ef 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -123,6 +123,7 @@ export { mockTx, Comparator, SiblingPath, + EncryptedLogHeader, } from '@aztec/circuit-types'; export { NodeInfo } from '@aztec/types/interfaces'; diff --git a/yarn-project/circuit-types/src/logs/encrypted_log_header.test.ts b/yarn-project/circuit-types/src/logs/encrypted_log_header.test.ts new file mode 100644 index 00000000000..78d02e31802 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/encrypted_log_header.test.ts @@ -0,0 +1,59 @@ +import { AztecAddress, GrumpkinScalar } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { updateInlineTestData } from '@aztec/foundation/testing'; + +import { EncryptedLogHeader } from './encrypted_log_header.js'; + +describe('encrypt log header', () => { + let grumpkin: Grumpkin; + + beforeAll(() => { + grumpkin = new Grumpkin(); + }); + + it('encrypt and decrypt a log header', () => { + const ephSecretKey = GrumpkinScalar.random(); + const viewingSecretKey = GrumpkinScalar.random(); + + const ephPubKey = grumpkin.mul(Grumpkin.generator, ephSecretKey); + const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey); + + const addr = AztecAddress.random(); + const header = new EncryptedLogHeader(addr); + + const encrypted = header.computeCiphertext(ephSecretKey, viewingPubKey); + + const recreated = EncryptedLogHeader.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); + + expect(recreated.toBuffer()).toEqual(addr.toBuffer()); + }); + + it('encrypt a log header, generate input for noir test', () => { + // The following 2 are arbitrary fixed values - fixed in order to test a match with Noir + const viewingSecretKey: GrumpkinScalar = new GrumpkinScalar( + 0x23b3127c127b1f29a7adff5cccf8fb06649e7ca01d9de27b21624098b897babdn, + ); + const ephSecretKey: GrumpkinScalar = new GrumpkinScalar( + 0x1fdd0dd8c99b21af8e00d2d130bdc263b36dadcbea84ac5ec9293a0660deca01n, + ); + + const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey); + + const addr = AztecAddress.fromBigInt(BigInt('0xdeadbeef')); + const header = new EncryptedLogHeader(addr); + + const encrypted = header.computeCiphertext(ephSecretKey, viewingPubKey); + + const byteArrayString = `[${encrypted + .toString('hex') + .match(/.{1,2}/g)! + .map(byte => parseInt(byte, 16))}]`; + + // Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data + updateInlineTestData( + 'noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr', + 'expected_header_ciphertext', + byteArrayString, + ); + }); +}); diff --git a/yarn-project/circuit-types/src/logs/encrypted_log_header.ts b/yarn-project/circuit-types/src/logs/encrypted_log_header.ts new file mode 100644 index 00000000000..055e3f6d695 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/encrypted_log_header.ts @@ -0,0 +1,72 @@ +import { AztecAddress, type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js'; +import { Aes128 } from '@aztec/circuits.js/barretenberg'; + +import { deriveAESSecret } from './l1_note_payload/encrypt_buffer.js'; + +/** + * An encrypted log header, containing the address of the log along with utility + * functions to compute and decrypt its ciphertext. + * + * Using AES-128-CBC for encryption. + * Can be used for both incoming and outgoing logs. + * + */ +export class EncryptedLogHeader { + constructor(public readonly address: AztecAddress) {} + + /** + * Serializes the log header to a buffer + * + * @returns The serialized log header + */ + public toBuffer(): Buffer { + return this.address.toBuffer(); + } + + public static fromBuffer(buf: Buffer): EncryptedLogHeader { + return new EncryptedLogHeader(AztecAddress.fromBuffer(buf)); + } + + /** + * Computes the ciphertext of the encrypted log header + * + * @param secret - An ephemeral secret key + * @param publicKey - The incoming or outgoing viewing key of the "recipient" of this log + * @returns The ciphertext of the encrypted log header + */ + public computeCiphertext(secret: GrumpkinPrivateKey, publicKey: PublicKey) { + const aesSecret = deriveAESSecret(secret, publicKey); + const key = aesSecret.subarray(0, 16); + const iv = aesSecret.subarray(16, 32); + + const aes128 = new Aes128(); + const buffer = this.address.toBuffer(); + + return aes128.encryptBufferCBC(buffer, iv, key); + } + + /** + * + * @param ciphertext - The ciphertext buffer + * @param secret - The private key matching the public key used in encryption + * @param publicKey - The public key generated with the ephemeral secret key used in encryption + * e.g., eph_sk * G + * @returns + */ + public static fromCiphertext( + ciphertext: Buffer | bigint[], + secret: GrumpkinPrivateKey, + publicKey: PublicKey, + ): EncryptedLogHeader { + const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); + + const aesSecret = deriveAESSecret(secret, publicKey); + const key = aesSecret.subarray(0, 16); + const iv = aesSecret.subarray(16, 32); + + const aes128 = new Aes128(); + const buffer = aes128.decryptBufferCBC(input, iv, key); + const address = AztecAddress.fromBuffer(buffer); + return new EncryptedLogHeader(address); + } +} diff --git a/yarn-project/circuit-types/src/logs/index.ts b/yarn-project/circuit-types/src/logs/index.ts index c333eb9fdae..58dbb93f7a9 100644 --- a/yarn-project/circuit-types/src/logs/index.ts +++ b/yarn-project/circuit-types/src/logs/index.ts @@ -10,3 +10,4 @@ export * from './l1_note_payload/index.js'; export * from './tx_l2_logs.js'; export * from './unencrypted_l2_log.js'; export * from './extended_unencrypted_l2_log.js'; +export * from './encrypted_log_header.js'; diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index 4f7f20cbe87..7e185f169e0 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -17,7 +17,7 @@ import { jest } from '@jest/globals'; import { expectsNumOfEncryptedLogsInTheLastBlockToBe, setup, setupPXEService } from './fixtures/utils.js'; -const TIMEOUT = 90_000; +const TIMEOUT = 120_000; describe('e2e_2_pxes', () => { jest.setTimeout(TIMEOUT); diff --git a/yarn-project/end-to-end/src/e2e_encryption.test.ts b/yarn-project/end-to-end/src/e2e_encryption.test.ts index a8d4e7b34be..6cb1e63eee4 100644 --- a/yarn-project/end-to-end/src/e2e_encryption.test.ts +++ b/yarn-project/end-to-end/src/e2e_encryption.test.ts @@ -1,5 +1,5 @@ -import { type Wallet } from '@aztec/aztec.js'; -import { Aes128 } from '@aztec/circuits.js/barretenberg'; +import { EncryptedLogHeader, GrumpkinScalar, type Wallet } from '@aztec/aztec.js'; +import { Aes128, Grumpkin } from '@aztec/circuits.js/barretenberg'; import { TestContract } from '@aztec/noir-contracts.js'; import { randomBytes } from 'crypto'; @@ -8,6 +8,7 @@ import { setup } from './fixtures/utils.js'; describe('e2e_encryption', () => { const aes128 = new Aes128(); + let grumpkin: Grumpkin; let wallet: Wallet; let teardown: () => Promise; @@ -17,7 +18,8 @@ describe('e2e_encryption', () => { beforeAll(async () => { ({ teardown, wallet } = await setup()); contract = await TestContract.deploy(wallet).send().deployed(); - }); + grumpkin = new Grumpkin(); + }, 120_000); afterAll(() => teardown()); @@ -52,4 +54,18 @@ describe('e2e_encryption', () => { expect(ciphertext).toEqual(expectedCiphertext); }); + + it('encrypts header', async () => { + const ephSecretKey = GrumpkinScalar.random(); + const viewingSecretKey = GrumpkinScalar.random(); + + const ephPubKey = grumpkin.mul(Grumpkin.generator, ephSecretKey); + const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey); + + const encrypted = await contract.methods.compute_note_header_ciphertext(ephSecretKey, viewingPubKey).simulate(); + + const recreated = EncryptedLogHeader.fromCiphertext(encrypted, viewingSecretKey, ephPubKey); + + expect(recreated.address).toEqual(contract.address); + }); }); diff --git a/yarn-project/end-to-end/src/e2e_key_registry.test.ts b/yarn-project/end-to-end/src/e2e_key_registry.test.ts index 48fb1fa90ab..c770ceaf9dd 100644 --- a/yarn-project/end-to-end/src/e2e_key_registry.test.ts +++ b/yarn-project/end-to-end/src/e2e_key_registry.test.ts @@ -8,7 +8,7 @@ import { jest } from '@jest/globals'; import { publicDeployAccounts, setup } from './fixtures/utils.js'; -const TIMEOUT = 100_000; +const TIMEOUT = 120_000; const SHARED_MUTABLE_DELAY = 5; diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts index 1ccdb134c64..c7bc0fc5cbf 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts @@ -27,6 +27,8 @@ const consumeNextBlocks = () => { const log = createDebugLogger('aztec:server_world_state_synchronizer_test'); describe('server_world_state_synchronizer', () => { + jest.setTimeout(30_000); + let db: AztecKVStore; let l1ToL2Messages: Fr[]; let inHash: Buffer;