From 70ce551dedd973453827068a57b91d7366fac4d8 Mon Sep 17 00:00:00 2001 From: Jack Gilcrest Date: Sat, 19 Oct 2024 18:48:27 -0600 Subject: [PATCH 1/4] p --- README.md | 71 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 3941c38..a2f3088 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,21 @@ ZKEmail written in [NoirLang](https://noir-lang.org/) In your Nargo.toml file, add the version of this library you would like to install under dependency: -``` +```toml [dependencies] -zkemail = { tag = "v0.3.1", git = "https://github.com/zkemail/zkemail.nr", directory = "lib" } +zkemail = { tag = "v0.3.2", git = "https://github.com/zkemail/zkemail.nr", directory = "lib" } ``` The library exports the following functions: -- `verify_dkim_1024` and `verify_dkim_2048` - for verifying DKIM signatures over an email header. This is needed for all email verifications. -- `get_body_hash_by_index` - to get the body hash from the header. -- `body_hash_base64_decode` - to decode the body hash from the header. -- Above two methods are needed to verify the body hash of an email. This is only needed if you want to contrain something over the email body. +- `dkim::RSAPubkey::verify_dkim_signature` - for verifying DKIM signatures over an email header. This is needed for all email verifications. +- `headers::body_hash::get_body_hash` - constrained access and decoding of the body hash from the header +- `headers::email_address::get_email_address` - constrained extraction of to or from email addresses +- `partial_hash::partial_sha256_var_end` - finish a precomputed sha256 hash over the body +- `masking::mask_text` - apply a byte mask to the header or body to selectively reveal parts of the entire email - `standard_outputs` - returns the hash of the DKIM pubkey and a nullifier for the email (`hash(signature)`) -``` +```rust use dep::zkemail::{ KEY_LIMBS_1024, dkim::verify_dkim_1024, get_body_hash_by_index, base64::body_hash_base64_decode, standard_outputs @@ -42,11 +43,11 @@ use dep::std::hash::sha256_var; ### Usage with partial SHA -You can use partial hashing technique for email with large body when the part you want to contrain in the body is towards the end. +You can use partial hashing technique for email with large body when the part you want to constrain in the body is towards the end. Since SHA works in chunks of 64 bytes, we can hash the body up to the chunk from where we want to extract outside of the circuit and do the remaining hash in the circuit. This will save a lot of constraints as SHA is very expensive in circuit. -``` +```rust use dep::zkemail::{ KEY_LIMBS_2048, dkim::verify_dkim_2048, get_body_hash_by_index, partial_hash::partial_sha256_var_end, base64::body_hash_base64_decode, @@ -70,25 +71,37 @@ use dep::zkemail::{ ... ``` -Find more examples in the [examples](./examples) folder. +### Extracting Email Addresses + +In the header, email addresses can appear in a variety of formats: + * `"name" ` + * `name ` + * `name local-part@domain.com` + * `local-part@domain.com` +Without regex, we take a slightly different approach. + + +Find examples of each implementation in the [examples](./examples) folder. ## Using the Input Generation JS Library Install the library: -``` +```console yarn add @mach-34/zkemail-nr ``` ### Usage +See the [witness simulation](./js/tests/circuits.test.ts) and [proving](./js/tests/proving.test.ts) tests for an in-depth demonstration of each use case. -``` +```js +// example of generating inputs for a partial hash import { generateEmailVerifierInputs } from "@mach-34/zkemail-nr"; const zkEmailInputs = await generateEmailVerifierInputs(emailContent, { - maxBodyLength: 1280, // Same as MAX_PARTIAL_EMAIL_BODY_LENGTH in circuit - maxHeadersLength: 1408, // Same as MAX_EMAIL_HEADER_LENGTH in circuit - shaPrecomputeSelector: "some string in body up to which you want to hash outside circuit", // if you want to use partial hashing + maxBodyLength: 1280, + maxHeadersLength: 1408, + shaPrecomputeSelector: "some string in body up to which you want to hash outside circuit", }); ``` @@ -102,13 +115,27 @@ TODO ## Todo - Negative Unit Testing - Expected InputGen testing - - Robust from/ to string search implementation - - Contract/ testing for UltraPlonk reintegrated - EVM Contract tests for email integration - Aztec Contract tests for email integration - 1024-bit key demo eml (current one is sensitive and cannot be provided in public repo) - - DKIM Key pedersen hash function - - Handle optional inputs (partial hashing, no body check, etc) gracefully again - - Partial SHA256 hashing - - Implementation with Regex - - test does not exit on completion? \ No newline at end of file + - Implementation with Regex + - Add constraint estimations and benchmarking + - Add native proving scripts + - Macro Impl + +### Proposed Macro Impl +```rust +// zkemail attribute would automatically inject fields +// header: BoundedVec, +// pubkey: RSAPubkey, +// signature: [Field; KEY_LIMBS] +// and add range check header.len() + pubkey::verify_rsa_signature +// it then would add additional necessary fields for each trait, add the +#[zkemail(PartialHash, From, DKIMHash, Nullifier)] +let Input {}; +//fn main(input: Input) { +// input.verify() +//} +``` + +By [Mach-34](https://mach34.space) \ No newline at end of file From c7f59b81d3870864d75f9399aca94c103d66631a Mon Sep 17 00:00:00 2001 From: Jack Gilcrest Date: Mon, 21 Oct 2024 02:40:14 -0600 Subject: [PATCH 2/4] negative nr testing for all but email access --- js/src/utils.ts | 6 +- js/tests/circuits.test.ts | 15 +- js/tests/test-data/email-header-tampered.eml | 18 ++ lib/src/lib.nr | 9 +- lib/src/partial_hash.nr | 136 ++++++----- lib/src/tests/mod.nr | 230 +++++++++++++++++++ lib/src/tests/test_inputs.nr | 79 +++++++ 7 files changed, 416 insertions(+), 77 deletions(-) create mode 100644 js/tests/test-data/email-header-tampered.eml create mode 100644 lib/src/tests/mod.nr create mode 100644 lib/src/tests/test_inputs.nr diff --git a/js/src/utils.ts b/js/src/utils.ts index 0d017f7..2a65f1a 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -46,7 +46,11 @@ export function toProverToml(inputs: any): string { } else { let values = ""; for (const [k, v] of Object.entries(value!)) { - values = values.concat(`${k} = '${v}'\n`); + if (Array.isArray(v)) { + values = values.concat(`${k} = [${v.map((val) => `'${val}'`).join(", ")}]\n`); + } else { + values = values.concat(`${k} = '${v}'\n`); + } } structs.push(`[${key}]\n${values}`); } diff --git a/js/tests/circuits.test.ts b/js/tests/circuits.test.ts index 1ca51b5..52bc6af 100644 --- a/js/tests/circuits.test.ts +++ b/js/tests/circuits.test.ts @@ -2,7 +2,7 @@ import fs from "fs"; import path from "path"; import { ZKEmailProver } from "../src/prover"; import { generateEmailVerifierInputs } from "../src/index"; -import { makeEmailAddressCharTable } from "../src/utils"; +import { makeEmailAddressCharTable, toProverToml } from "../src/utils"; // import circuit1024 from "../../examples/verify_email_1024_bit_dkim/target/verify_email_1024_bit_dkim.json"; import circuit2048 from "../../examples/verify_email_2048_bit_dkim/target/verify_email_2048_bit_dkim.json"; import circuitPartialHash from "../../examples/partial_hash/target/partial_hash.json"; @@ -14,6 +14,12 @@ const emails = { large: fs.readFileSync( path.join(__dirname, "./test-data/email-good-large.eml") ), + tamperedBody: fs.readFileSync( + path.join(__dirname, "./test-data/email-body-tampered.eml") + ), + tamperedHeader: fs.readFileSync( + path.join(__dirname, "./test-data/email-header-tampered.eml") + ), }; // default header/ body lengths to use for input gen @@ -55,7 +61,7 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { describe("Successful Cases", () => { it("2048-bit DKIM", async () => { const inputs = await generateEmailVerifierInputs( - emails.small, + emails.large, inputParams ); await prover2048.simulateWitness(inputs); @@ -102,7 +108,7 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { expect(expectedMaskedBody).toEqual(acutalMaskedBody); }); it("Extract Sender/ Recipient", async () => { - const inputs = await generateEmailVerifierInputs(emails.small, { + const inputs = await generateEmailVerifierInputs(emails.large, { extractFrom: true, extractTo: true, ...inputParams, @@ -137,6 +143,9 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { ); expect(expectedFrom).toEqual(actualFrom); expect(expectedTo).toEqual(actualTo); + + console.log(toProverToml(inputs)) }); }); + }); diff --git a/js/tests/test-data/email-header-tampered.eml b/js/tests/test-data/email-header-tampered.eml new file mode 100644 index 0000000..52d0698 --- /dev/null +++ b/js/tests/test-data/email-header-tampered.eml @@ -0,0 +1,18 @@ +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD + M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx + VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR + 2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ + wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5 + Ry43lwp1/3+sA== +from: president@whitehouse.gov +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\)) +Subject: Hello +Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com> +Date: Sat, 26 Aug 2023 12:25:22 +0400 +to: zkewtest@gmail.com + +Hello, + +How are you? \ No newline at end of file diff --git a/lib/src/lib.nr b/lib/src/lib.nr index 7dfe5f4..1fd8917 100644 --- a/lib/src/lib.nr +++ b/lib/src/lib.nr @@ -1,11 +1,12 @@ use dep::std::{hash::pedersen_hash, collections::bounded_vec::BoundedVec}; use crate::dkim::RSAPubkey; -mod dkim; -mod headers; -mod partial_hash; -mod masking; +pub mod dkim; +pub mod headers; +pub mod partial_hash; +pub mod masking; // mod macro; +mod tests; global RSA_EXPONENT: u32 = 65537; global KEY_BYTES_1024: u32 = 128; diff --git a/lib/src/partial_hash.nr b/lib/src/partial_hash.nr index 0a18934..82bcb95 100644 --- a/lib/src/partial_hash.nr +++ b/lib/src/partial_hash.nr @@ -110,6 +110,49 @@ pub fn partial_sha256_var_start(msg: [u8; N]) -> [u32; 8] { h } +/** + * Given some state of a partially computed sha256 hash and part of the preimage, continue hashing + * @notice used for complex/ recursive offloading of post-partial hashing + * + * @param N - the maximum length of the message to hash + * @param h - the intermediate hash state + * @param msg - the preimage to hash + * @return the intermediate hash state after compressing in msg to h + */ +pub fn partial_sha256_var_interstitial(mut h: [u32; 8], msg: [u8; N]) -> [u32; 8] { + let num_blocks = N / BLOCK_SIZE; + let mut msg_block: [u8; BLOCK_SIZE] = [0; BLOCK_SIZE]; + let mut msg_byte_ptr = 0; // Pointer into msg_block + + for i in 0..num_blocks { + let msg_start = BLOCK_SIZE * i; + let (new_msg_block, new_msg_byte_ptr) = unsafe { + build_msg_block_iter(msg, N, msg_start) + }; + if msg_start < N { + msg_block = new_msg_block; + } + + if !is_unconstrained() { + // Verify the block we are compressing was appropriately constructed + let new_msg_byte_ptr = verify_msg_block(msg, N, msg_block, msg_start); + if msg_start < N { + msg_byte_ptr = new_msg_byte_ptr; + } + } else if msg_start < N { + msg_byte_ptr = new_msg_byte_ptr; + } + + // If the block is filled, compress it. + // An un-filled block is handled after this loop. + if (msg_start < N) & (msg_byte_ptr == BLOCK_SIZE) { + h = sha256_compression(msg_u8_to_u32(msg_block), h); + } + } + + h +} + // https://github.com/noir-lang/noir/blob/76eec710ff73e5e45fdddcd41ae2cd74e879cfa5/noir_stdlib/src/hash/sha256.nr#L85 /** * Given some state of a partially computed sha256 hash and remaining preimage, complete the hash @@ -298,7 +341,7 @@ fn hash_final_block(msg_block: [u8; BLOCK_SIZE], mut state: [u32; 8]) -> [u8; 32 out_h } -// #[test] +#[test] fn test_partial_hash() { let data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, @@ -327,75 +370,30 @@ fn test_partial_hash() { } #[test] -fn test_partial_hash_email_body() { - let body = [ - 84, 104, 101, 32, 84, 105, 109, 101, 115, 32, 48, 51, 47, 74, 97, - 110, 47, 50, 48, 48, 57, 32, 67, 104, 97, 110, 99, 101, 108, 108, - 111, 114, 32, 111, 110, 32, 98, 114, 105, 110, 107, 32, 111, 102, - 32, 115, 101, 99, 111, 110, 100, 32, 98, 97, 105, 108, 111, 117, - 116, 32, 102, 111, 114, 32, 98, 97, 110, 107, 115, 13, 10, 13, 10, - 49, 53, 32, 121, 101, 97, 114, 115, 32, 97, 103, 111, 44, 32, 83, - 97, 116, 111, 115, 104, 105, 32, 109, 105, 110, 101, 100, 32, 116, - 104, 101, 32, 102, 105, 114, 115, 116, 32, 98, 108, 111, 99, 107, - 32, 111, 102, 32, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, - 110, 32, 98, 108, 111, 99, 107, 99, 104, 97, 105, 110, 32, 61, - 13, 10, 65, 102, 116, 101, 114, 32, 116, 104, 101, 32, 66, 105, - 116, 99, 111, 105, 110, 32, 119, 104, 105, 116, 101, 32, 112, 97, - 112, 101, 114, 32, 97, 112, 112, 101, 97, 114, 101, 100, 32, 111, - 110, 32, 79, 99, 116, 111, 98, 101, 114, 32, 51, 49, 44, 32, 50, - 48, 48, 56, 44, 32, 111, 110, 32, 97, 32, 61, 13, 10, 99, 114, - 121, 112, 116, 111, 103, 114, 97, 112, 104, 121, 32, 109, 97, 105, - 108, 105, 110, 103, 32, 108, 105, 115, 116, 44, 32, 116, 104, 101, - 32, 71, 101, 110, 101, 115, 105, 115, 32, 66, 108, 111, 99, 107, - 32, 61, 69, 50, 61, 56, 48, 61, 57, 52, 32, 116, 104, 101, 32, 102, - 105, 114, 115, 116, 32, 98, 105, 116, 99, 111, 105, 110, 32, 61, - 13, 10, 98, 108, 111, 99, 107, 32, 97, 110, 100, 32, 116, 104, 101, - 32, 98, 97, 115, 105, 115, 32, 111, 102, 32, 116, 104, 101, 32, - 101, 110, 116, 105, 114, 101, 32, 66, 105, 116, 99, 111, 105, 110, - 32, 116, 114, 97, 100, 105, 110, 103, 32, 115, 121, 115, 116, 101, - 109, 32, 105, 110, 32, 112, 108, 97, 99, 101, 32, 116, 111, 32, - 61, 13, 10, 116, 104, 105, 115, 32, 100, 97, 121, 32, 61, 69, 50, - 61, 56, 48, 61, 57, 52, 32, 119, 97, 115, 32, 109, 105, 110, 101, - 100, 32, 111, 110, 32, 74, 97, 110, 117, 97, 114, 121, 32, 51, 44, - 32, 50, 48, 48, 57, 46, 61, 50, 48, 13, 10, 13, 10, 84, 104, 101, - 32, 71, 101, 110, 101, 115, 105, 115, 32, 66, 108, 111, 99, 107, 32, - 105, 115, 32, 97, 108, 115, 111, 32, 107, 110, 111, 119, 110, 32, - 97, 115, 32, 66, 108, 111, 99, 107, 32, 48, 32, 111, 114, 32, 66, - 108, 111, 99, 107, 32, 49, 44, 32, 97, 110, 100, 32, 105, 115, 32, - 115, 116, 105, 108, 108, 32, 105, 110, 32, 61, 13, 10, 116, 104, - 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 110, 101, 116, 119, - 111, 114, 107, 44, 32, 119, 104, 101, 114, 101, 32, 105, 116, 32, - 119, 105, 108, 108, 32, 114, 101, 109, 97, 105, 110, 32, 97, 115, - 32, 108, 111, 110, 103, 32, 97, 115, 32, 116, 104, 101, 114, 101, - 32, 105, 115, 32, 97, 32, 99, 111, 109, 112, 117, 116, 101, 114, - 32, 61, 13, 10, 114, 117, 110, 110, 105,110, 103, 32, 116, 104, - 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 115, 111, 102, 116, - 119, 97, 114, 101, 46, 61, 50, 48, 13, 10, 13, 10, 65, 108, 108, - 32, 110, 111, 100, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, - 66, 105, 116, 99, 111, 105, 110, 32, 110, 101, 116, 119, 111, 114, - 107, 32, 99, 97, 110, 32, 99, 111, 110, 115, 117, 108, 116, 32, 105, - 116, 44, 32, 101, 118, 101, 110, 32, 105, 102, 32, 105, 116, 32, - 105, 115, 32, 97, 116, 32, 116, 104, 101, 32, 61, 13, 10, 111, 116, - 104, 101, 114, 32, 101, 110, 100, 32, 111, 102, 32, 116, 104, 101, - 32, 110, 101, 116, 119, 111, 114, 107, 32, 119, 105, 116, 104, 32, - 104, 117, 110, 100, 114, 101, 100, 115, 32, 111, 102, 32, 116, 104, - 111, 117, 115, 97, 110, 100, 115, 32, 111, 102, 32, 98, 108, 111, - 99, 107, 115, 46, 13, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +fn test_partial_hash_interstitial() { + let data = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, + 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, + 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, + 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, + 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, + 185, 186, 187, 188, 189, 190, 191 ]; - let actual_message_size = 740; - let mut precomputed: [u8; 576] = [0; 576]; - for i in 0..576 { - precomputed[i] = body[i]; - } - let mut remaining: [u8; 192] = [0; 192]; - for i in 0..192 { - remaining[i] = body[precomputed.len() + i]; + let mut data0 = [0; 64]; + let mut data1 = [0; 64]; + let mut data2 = [0; 64]; + for i in 0..data0.len() { + data0[i] = data[i]; + data1[i] = data[64 + i]; + data2[i] = data[128 + i]; } - let remaining_len = actual_message_size - precomputed.len(); - let state = partial_sha256_var_start(precomputed); - let hash = partial_sha256_var_end(state, remaining, remaining_len as u64, actual_message_size as u64); - // let correct_hash = std::hash::sha256_var(body, 572); - let correct_hash = std::hash::sha256::sha256_var(body, actual_message_size as u64); + let pre_hash = partial_sha256_var_start(data0); + let interstitial_hash = partial_sha256_var_interstitial(pre_hash, data1); + let hash = partial_sha256_var_end(interstitial_hash, data2, data2.len() as u64, data.len() as u64); + let correct_hash = std::hash::sha256_var(data, data.len() as u64); assert_eq(hash, correct_hash); } diff --git a/lib/src/tests/mod.nr b/lib/src/tests/mod.nr new file mode 100644 index 0000000..f601262 --- /dev/null +++ b/lib/src/tests/mod.nr @@ -0,0 +1,230 @@ +mod test_inputs; + +mod test_success { + + use crate::{ + headers::body_hash::get_body_hash, tests::test_inputs::EmailLarge, + partial_hash::partial_sha256_var_end + }; + use std::hash::sha256_var; + + #[test] + fn test_dkim_signature() { + EmailLarge::PUBKEY.verify_dkim_signature(EmailLarge::HEADER, EmailLarge::SIGNATURE); + } + + #[test] + fn test_body_hash() { + // get the body hash from the header + let signed_body_hash = get_body_hash( + EmailLarge::HEADER, + EmailLarge::DKIM_HEADER_SEQUENCE, + EmailLarge::BODY_HASH_INDEX + ); + // compute the body hash + let computed_body_hash: [u8; 32] = sha256_var(EmailLarge::BODY.storage, EmailLarge::BODY.len() as u64); + // compare the body hashes + assert( + signed_body_hash == computed_body_hash, "SHA256 hash computed over body does not match body hash found in DKIM-signed header" + ); + } + + #[test] + fn test_partial_hash() { + // get the body hash from the header + let signed_body_hash = get_body_hash( + EmailLarge::HEADER, + EmailLarge::DKIM_HEADER_SEQUENCE, + EmailLarge::BODY_HASH_INDEX + ); + // finish the partial hash + let computed_body_hash = partial_sha256_var_end( + EmailLarge::PARTIAL_BODY_HASH, + EmailLarge::PARTIAL_BODY.storage, + EmailLarge::PARTIAL_BODY.len() as u64, + EmailLarge::PARTIAL_BODY_REAL_LENGTH as u64 + ); + // compare the body hashes + assert( + signed_body_hash == computed_body_hash, "Sha256 hash computed over body does not match DKIM-signed header" + ); + } +} + +mod test_tampered_hash { + use crate::{headers::body_hash::get_body_hash, tests::test_inputs::EmailLarge}; + use std::hash::sha256_var; + + // no reasonable message to constrain here + #[test(should_fail)] + fn test_tampered_header() { + // get tampered header + let mut tampered_header = EmailLarge::tampered_header(); + // attempt to verify the DKIM signature + EmailLarge::PUBKEY.verify_dkim_signature(tampered_header, EmailLarge::SIGNATURE); + } + + #[test] + fn test_tampered_body() { + // get the body hash from the header + let signed_body_hash = get_body_hash( + EmailLarge::HEADER, + EmailLarge::DKIM_HEADER_SEQUENCE, + EmailLarge::BODY_HASH_INDEX + ); + // get tampered body + let mut tampered_body = EmailLarge::tampered_body(); + // compute the body hash + let tampered_body_hash: [u8; 32] = sha256_var(tampered_body.storage, tampered_body.len() as u64); + // compare the body hashes + assert(signed_body_hash != tampered_body_hash, "SHA256 hash should not match tampered body hash"); + } +} + +mod header_field_access { + + use crate::{headers::body_hash::get_body_hash, Sequence, tests::test_inputs::EmailLarge}; + + #[test(should_fail_with = "No 'bh=' prefix found at asserted bh index")] + fn test_bad_body_hash_off_one_minus() { + // attempt to get body hash with one off index + let _ = get_body_hash( + EmailLarge::HEADER, + EmailLarge::DKIM_HEADER_SEQUENCE, + EmailLarge::BODY_HASH_INDEX - 1 + ); + } + + #[test(should_fail_with = "No 'bh=' prefix found at asserted bh index")] + fn test_bad_body_hash_off_one_plus() { + // attempt to get body hash with one off index + let _ = get_body_hash( + EmailLarge::HEADER, + EmailLarge::DKIM_HEADER_SEQUENCE, + EmailLarge::BODY_HASH_INDEX + 1 + ); + } + + #[test(should_fail_with = "Header field name does not match")] + fn test_bad_body_hash_not_in_dkim_field() { + // create header field for malicious bh + let mut dkim_field: BoundedVec = BoundedVec::new(); + dkim_field.len = EmailLarge::HEADER.len(); + // craft a malicious "to" field where attacker tries to put bh in display name + let mut malicious_to: [u8; 78] = comptime { + "\r\nto:\"bh=2JsdK4BMzzt9w4Zlz2TdyVCFc+l7vNyT5aAgGDYf7fM=;\" \r\n".as_bytes() + }; + let mut malicious_sequence = Sequence { + index: 8, // 8 to make it check for crlf on both sides (could be anything > 2) + length: malicious_to.len() - 4 // 4 is the crlf on each end + }; + for i in 0..malicious_to.len() { + let index = malicious_sequence.index + i - 2; + dkim_field.storage[index] = malicious_to[i]; + } + let malicious_body_hash_index = 15; + // copy the body hash to the beginning of the + // attempt to get body hash + let _ = get_body_hash(dkim_field, malicious_sequence, malicious_body_hash_index); + } + + #[test(should_fail_with = "Header field must end with CRLF")] + fn test_header_field_sequence_overflow_end() { + // make sequence extend beyond the end of the header field + let mut overflowed_sequence = EmailLarge::DKIM_HEADER_SEQUENCE; + overflowed_sequence.length = overflowed_sequence.length + 1; + // set header len to be a bit longer so it doesn't overflow + let mut longer_header = EmailLarge::HEADER; + longer_header.len = longer_header.len + 2; + // attempt to get body hash + let _ = get_body_hash( + longer_header, + overflowed_sequence, + EmailLarge::BODY_HASH_INDEX + ); + } + + #[test(should_fail_with = "Header field must start with CRLF")] + fn test_header_field_sequence_overflow_start() { + // make sequence extend beyond the start of the header field + let mut overflowed_sequence = EmailLarge::DKIM_HEADER_SEQUENCE; + overflowed_sequence.index = overflowed_sequence.index - 1; + // attempt to get body hash + let _ = get_body_hash( + EmailLarge::HEADER, + overflowed_sequence, + EmailLarge::BODY_HASH_INDEX + ); + } + + #[test(should_fail_with = "Header field must end with CRLF")] + fn test_header_field_sequence_underflow_end() { + // make sequence extend beyond the end of the header field + let mut underflowed_sequence = EmailLarge::DKIM_HEADER_SEQUENCE; + underflowed_sequence.length = underflowed_sequence.length - 1; + // attempt to get body hash + let _ = get_body_hash( + EmailLarge::HEADER, + underflowed_sequence, + EmailLarge::BODY_HASH_INDEX + ); + } + + #[test(should_fail_with = "Header field must start with CRLF")] + fn test_header_field_sequence_underflow_start() { + // make sequence extend beyond the end of the header field + let mut underflowed_sequence = EmailLarge::DKIM_HEADER_SEQUENCE; + underflowed_sequence.index = underflowed_sequence.length + 1; + // attempt to get body hash + let _ = get_body_hash( + EmailLarge::HEADER, + underflowed_sequence, + EmailLarge::BODY_HASH_INDEX + ); + } + + #[test(should_fail_with = "Header field must not contain newlines")] + fn test_header_field_multiple_fields() { + // combine to and dkim-signature fields together + let mut tampered_header: BoundedVec = BoundedVec::new(); + let combined_sequence = Sequence { + index: 2, + length: EmailLarge::TO_HEADER_SEQUENCE.length + EmailLarge::DKIM_HEADER_SEQUENCE.length + 2 // 2 for crlf in middle + }; + tampered_header.len = combined_sequence.length + 4; + // copy dkim-signature field + for i in 0..EmailLarge::DKIM_HEADER_SEQUENCE.length + 2 { + tampered_header.storage[i] = EmailLarge::HEADER.storage[EmailLarge::DKIM_HEADER_SEQUENCE.index + i - 2]; + } + tampered_header.storage[EmailLarge::DKIM_HEADER_SEQUENCE.length + 2] = "\r".as_bytes()[0]; + tampered_header.storage[EmailLarge::DKIM_HEADER_SEQUENCE.length + 3] = "\n".as_bytes()[0]; + // copy to field + for i in 0..EmailLarge::TO_HEADER_SEQUENCE.length + 2 { + let index = EmailLarge::DKIM_HEADER_SEQUENCE.length + 4; + tampered_header.storage[index + i] = EmailLarge::HEADER.storage[EmailLarge::TO_HEADER_SEQUENCE.index + i]; + } + // set crlf at end + tampered_header.storage[combined_sequence.length + 2] = "\r".as_bytes()[0]; + tampered_header.storage[combined_sequence.length + 3] = "\n".as_bytes()[0]; + let tampered_body_hash_index = 93; // just manually setting this + // attempt to get body hash + let _ = get_body_hash( + tampered_header, + combined_sequence, + tampered_body_hash_index + ); + } + + #[test(should_fail_with = "Header field out of bounds")] + fn test_header_field_outside_header() { + let mut shortened_header = EmailLarge::HEADER; + // shorten header to be just under the end of the dkim field + shortened_header.len = EmailLarge::DKIM_HEADER_SEQUENCE.index + EmailLarge::DKIM_HEADER_SEQUENCE.length - 1; + // attempt to get body hash + let _ = get_body_hash( + shortened_header, + EmailLarge::DKIM_HEADER_SEQUENCE, + EmailLarge::BODY_HASH_INDEX + ); + } +} diff --git a/lib/src/tests/test_inputs.nr b/lib/src/tests/test_inputs.nr new file mode 100644 index 0000000..6667f26 --- /dev/null +++ b/lib/src/tests/test_inputs.nr @@ -0,0 +1,79 @@ +mod EmailLarge { + + use crate::{Sequence, KEY_LIMBS_2048, dkim::RSAPubkey}; + use std::collections::bounded_vec::BoundedVec; + + // regular inputs + global EMAIL_LARGE_MAX_HEADER_LENGTH: u32 = 512; + global EMAIL_LARGE_MAX_BODY_LENGTH: u32 = 1024; + global HEADER: BoundedVec = BoundedVec { + storage: [102, 114, 111, 109, 58, 114, 117, 110, 110, 105, 101, 114, 46, 108, 101, 97, 103, 117, 101, 115, 46, 48, 106, 64, 105, 99, 108, 111, 117, 100, 46, 99, 111, 109, 13, 10, 99, 111, 110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 58, 116, 101, 120, 116, 47, 112, 108, 97, 105, 110, 59, 32, 99, 104, 97, 114, 115, 101, 116, 61, 117, 116, 102, 45, 56, 13, 10, 109, 105, 109, 101, 45, 118, 101, 114, 115, 105, 111, 110, 58, 49, 46, 48, 32, 40, 77, 97, 99, 32, 79, 83, 32, 88, 32, 77, 97, 105, 108, 32, 49, 54, 46, 48, 32, 92, 40, 51, 55, 51, 49, 46, 53, 48, 48, 46, 50, 51, 49, 92, 41, 41, 13, 10, 115, 117, 98, 106, 101, 99, 116, 58, 66, 105, 116, 99, 111, 105, 110, 13, 10, 109, 101, 115, 115, 97, 103, 101, 45, 105, 100, 58, 60, 49, 50, 56, 48, 48, 65, 57, 48, 45, 52, 69, 67, 67, 45, 52, 53, 49, 51, 45, 57, 50, 57, 56, 45, 65, 51, 51, 53, 52, 54, 49, 50, 50, 57, 50, 48, 64, 109, 101, 46, 99, 111, 109, 62, 13, 10, 100, 97, 116, 101, 58, 87, 101, 100, 44, 32, 51, 32, 65, 112, 114, 32, 50, 48, 50, 52, 32, 49, 54, 58, 50, 51, 58, 52, 56, 32, 43, 48, 53, 51, 48, 13, 10, 116, 111, 58, 122, 107, 101, 119, 116, 101, 115, 116, 64, 103, 109, 97, 105, 108, 46, 99, 111, 109, 13, 10, 100, 107, 105, 109, 45, 115, 105, 103, 110, 97, 116, 117, 114, 101, 58, 118, 61, 49, 59, 32, 97, 61, 114, 115, 97, 45, 115, 104, 97, 50, 53, 54, 59, 32, 99, 61, 114, 101, 108, 97, 120, 101, 100, 47, 114, 101, 108, 97, 120, 101, 100, 59, 32, 100, 61, 105, 99, 108, 111, 117, 100, 46, 99, 111, 109, 59, 32, 115, 61, 49, 97, 49, 104, 97, 105, 59, 32, 116, 61, 49, 55, 49, 50, 49, 52, 49, 54, 52, 52, 59, 32, 98, 104, 61, 50, 74, 115, 100, 75, 52, 66, 77, 122, 122, 116, 57, 119, 52, 90, 108, 122, 50, 84, 100, 121, 86, 67, 70, 99, 43, 108, 55, 118, 78, 121, 84, 53, 97, 65, 103, 71, 68, 89, 102, 55, 102, 77, 61, 59, 32, 104, 61, 102, 114, 111, 109, 58, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112, 101, 58, 77, 105, 109, 101, 45, 86, 101, 114, 115, 105, 111, 110, 58, 83, 117, 98, 106, 101, 99, 116, 58, 77, 101, 115, 115, 97, 103, 101, 45, 73, 100, 58, 68, 97, 116, 101, 58, 116, 111, 59, 32, 98, 61, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + len: 470 + }; + global BODY: BoundedVec = BoundedVec { + storage: [84, 104, 101, 32, 84, 105, 109, 101, 115, 32, 48, 51, 47, 74, 97, 110, 47, 50, 48, 48, 57, 32, 67, 104, 97, 110, 99, 101, 108, 108, 111, 114, 32, 111, 110, 32, 98, 114, 105, 110, 107, 32, 111, 102, 32, 115, 101, 99, 111, 110, 100, 32, 98, 97, 105, 108, 111, 117, 116, 32, 102, 111, 114, 32, 98, 97, 110, 107, 115, 13, 10, 13, 10, 49, 53, 32, 121, 101, 97, 114, 115, 32, 97, 103, 111, 44, 32, 83, 97, 116, 111, 115, 104, 105, 32, 109, 105, 110, 101, 100, 32, 116, 104, 101, 32, 102, 105, 114, 115, 116, 32, 98, 108, 111, 99, 107, 32, 111, 102, 32, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 98, 108, 111, 99, 107, 99, 104, 97, 105, 110, 32, 61, 13, 10, 65, 102, 116, 101, 114, 32, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 119, 104, 105, 116, 101, 32, 112, 97, 112, 101, 114, 32, 97, 112, 112, 101, 97, 114, 101, 100, 32, 111, 110, 32, 79, 99, 116, 111, 98, 101, 114, 32, 51, 49, 44, 32, 50, 48, 48, 56, 44, 32, 111, 110, 32, 97, 32, 61, 13, 10, 99, 114, 121, 112, 116, 111, 103, 114, 97, 112, 104, 121, 32, 109, 97, 105, 108, 105, 110, 103, 32, 108, 105, 115, 116, 44, 32, 116, 104, 101, 32, 71, 101, 110, 101, 115, 105, 115, 32, 66, 108, 111, 99, 107, 32, 61, 69, 50, 61, 56, 48, 61, 57, 52, 32, 116, 104, 101, 32, 102, 105, 114, 115, 116, 32, 98, 105, 116, 99, 111, 105, 110, 32, 61, 13, 10, 98, 108, 111, 99, 107, 32, 97, 110, 100, 32, 116, 104, 101, 32, 98, 97, 115, 105, 115, 32, 111, 102, 32, 116, 104, 101, 32, 101, 110, 116, 105, 114, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 116, 114, 97, 100, 105, 110, 103, 32, 115, 121, 115, 116, 101, 109, 32, 105, 110, 32, 112, 108, 97, 99, 101, 32, 116, 111, 32, 61, 13, 10, 116, 104, 105, 115, 32, 100, 97, 121, 32, 61, 69, 50, 61, 56, 48, 61, 57, 52, 32, 119, 97, 115, 32, 109, 105, 110, 101, 100, 32, 111, 110, 32, 74, 97, 110, 117, 97, 114, 121, 32, 51, 44, 32, 50, 48, 48, 57, 46, 61, 50, 48, 13, 10, 13, 10, 84, 104, 101, 32, 71, 101, 110, 101, 115, 105, 115, 32, 66, 108, 111, 99, 107, 32, 105, 115, 32, 97, 108, 115, 111, 32, 107, 110, 111, 119, 110, 32, 97, 115, 32, 66, 108, 111, 99, 107, 32, 48, 32, 111, 114, 32, 66, 108, 111, 99, 107, 32, 49, 44, 32, 97, 110, 100, 32, 105, 115, 32, 115, 116, 105, 108, 108, 32, 105, 110, 32, 61, 13, 10, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 110, 101, 116, 119, 111, 114, 107, 44, 32, 119, 104, 101, 114, 101, 32, 105, 116, 32, 119, 105, 108, 108, 32, 114, 101, 109, 97, 105, 110, 32, 97, 115, 32, 108, 111, 110, 103, 32, 97, 115, 32, 116, 104, 101, 114, 101, 32, 105, 115, 32, 97, 32, 99, 111, 109, 112, 117, 116, 101, 114, 32, 61, 13, 10, 114, 117, 110, 110, 105, 110, 103, 32, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 115, 111, 102, 116, 119, 97, 114, 101, 46, 61, 50, 48, 13, 10, 13, 10, 65, 108, 108, 32, 110, 111, 100, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 110, 101, 116, 119, 111, 114, 107, 32, 99, 97, 110, 32, 99, 111, 110, 115, 117, 108, 116, 32, 105, 116, 44, 32, 101, 118, 101, 110, 32, 105, 102, 32, 105, 116, 32, 105, 115, 32, 97, 116, 32, 116, 104, 101, 32, 61, 13, 10, 111, 116, 104, 101, 114, 32, 101, 110, 100, 32, 111, 102, 32, 116, 104, 101, 32, 110, 101, 116, 119, 111, 114, 107, 32, 119, 105, 116, 104, 32, 104, 117, 110, 100, 114, 101, 100, 115, 32, 111, 102, 32, 116, 104, 111, 117, 115, 97, 110, 100, 115, 32, 111, 102, 32, 98, 108, 111, 99, 107, 115, 46, 13, 10, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + len: 740 + }; + global PUBKEY: RSAPubkey = RSAPubkey { + modulus: [0xe5cf995b5ef59ce9943d1f4209b6ab, 0xe0caf03235e91a2db27e9ed214bcc6, 0xafe1309f87414bd36ed296dacfade2, 0xbeff3f19046a43adce46c932514988, 0x324041af8736e87de4358860fff057, 0xadcc6669dfa346f322717851a8c22a, 0x8b2a193089e6bf951c553b5a6f71aa, 0x0a570fe582918c4f731a0002068df2, 0x39419a433d6bfdd1978356cbca4b60, 0x550d695a514d38b45c862320a00ea5, 0x1c56ac1dfbf1beea31e8a613c2a51f, 0x6a30c9f22d2e5cb6934263d0838809, 0x0a281f268a44b21a4f77a91a52f960, 0x5134dc3966c8e91402669a47cc8597, 0x71590781df114ec072e641cdc5d224, 0xa1bc0f0937489c806c1944fd029dc9, 0x911f6e47f84db3b64c3648ebb5a127, 0xd5], + redc: [0xa48a824e4ebc7e0f1059f3ecfa57c4, 0x05c1db23f3c7d47ad7e7d7cfda5189, 0x79bb6bbbd8facf011f022fa9051aec, 0x24faa4cef474bed639362ea71f7a21, 0x1503aa50b77e24b030841a7d061581, 0x5bbf4e62805e1860a904c0f66a5fad, 0x5cbd24b72442d2ce647dd7d0a44368, 0x074a8839a4460c169dce7138efdaef, 0x0f06e09e3191b995b08e5b45182f65, 0x51fad4a89f8369fe10e5d4b6e149a1, 0xdc778b15982d11ebf7fe23b4e15f10, 0xa09ff3a4567077510c474e4ac0a21a, 0xb37e69e5dbb77167b73065e4c5ad6a, 0xecf4774e22e7fe3a38642186f7ae74, 0x16e72b5eb4c813a3b37998083aab81, 0xa48e7050aa8abedce5a45c16985376, 0xdd3285e53b322b221f7bcf4f8f8ad8, 0x0132] + }; + global SIGNATURE: [Field; KEY_LIMBS_2048] = [0xf193c3300b7c9902e32861c38d0d2d, 0x9f6927fdb3df0b84092d8459654327, 0x8a0bea5e2fa82821e49c27b68d5a7b, 0xaa8c0acc1190f9fd845ef64f8e7ae9, 0xa7aeebb37f4395965543e6df69a5a7, 0x087ecef9921569cfba83331ca11c6b, 0x4589ed316ed20757e65ad221736011, 0x0835d8748f11dcc985700c3fea27b1, 0xe870d2493fb83b4a1d72350e5de926, 0x268b28eda0aac07625cfab32b60af1, 0xb41a164eae7ba1602eaec5b5a39fe6, 0x693cc5ec578422bee48eabe390fc37, 0xa29504dd504f14423f2ce65b2ac388, 0x6c3ac6310c084a0b126fcd5225c208, 0xab0903e48563e5f4a5365ac5cbd888, 0xf05bf2e5b6266c0ac88dfc733c414f, 0xf58f9e9669e0f4f3086cce1187fd44, 0xb9]; + global BODY_HASH_INDEX: u32 = 361; + global DKIM_HEADER_SEQUENCE = Sequence { + index: 267, + length: 203 + }; + // partial hash inputs + global EMAIL_LARGE_MAX_PARTIAL_BODY_LENGTH: u32 = 192; + global PARTIAL_BODY_HASH: [u32; 8] = [616616639, 1838516336, 2875062989, 1377577374, 300896023, 2753786313, 2559427666, 979052520]; + global PARTIAL_BODY_REAL_LENGTH: u32 = BODY.len(); + global PARTIAL_BODY: BoundedVec = BoundedVec { + storage: [104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 115, 111, 102, 116, 119, 97, 114, 101, 46, 61, 50, 48, 13, 10, 13, 10, 65, 108, 108, 32, 110, 111, 100, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, 66, 105, 116, 99, 111, 105, 110, 32, 110, 101, 116, 119, 111, 114, 107, 32, 99, 97, 110, 32, 99, 111, 110, 115, 117, 108, 116, 32, 105, 116, 44, 32, 101, 118, 101, 110, 32, 105, 102, 32, 105, 116, 32, 105, 115, 32, 97, 116, 32, 116, 104, 101, 32, 61, 13, 10, 111, 116, 104, 101, 114, 32, 101, 110, 100, 32, 111, 102, 32, 116, 104, 101, 32, 110, 101, 116, 119, 111, 114, 107, 32, 119, 105, 116, 104, 32, 104, 117, 110, 100, 114, 101, 100, 115, 32, 111, 102, 32, 116, 104, 111, 117, 115, 97, 110, 100, 115, 32, 111, 102, 32, 98, 108, 111, 99, 107, 115, 46, 13, 10, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + len: 164 + }; + + // email address inputs + global FROM_HEADER_SEQUENCE = Sequence { + index: 0, + length: 34 + }; + global FROM_ADDRESS_SEQUENCE = Sequence { + index: 5, + length: 29 + }; + global TO_HEADER_SEQUENCE = Sequence { + index: 244, + length: 21 + }; + global TO_ADDRESS_SEQUENCE = Sequence { + index: 247, + length: 18 + }; + + // mutate inputs + + unconstrained pub fn tampered_header() -> BoundedVec { + let mut header = HEADER; + header.set(140, header.get_unchecked(140) + 1); + header + } + + unconstrained pub fn tampered_body() -> BoundedVec { + let mut body = BODY; + body.set(140, body.get_unchecked(140) + 1); + body + } + + unconstrained pub fn body_hash_outside_sequence() -> u32 { + let dkim_sequence_start = DKIM_HEADER_SEQUENCE.index; + dkim_sequence_start - 40 + } +} + +mod FromHeaderFields { + global EMAIL_ONLY: [u8; 38] = comptime { "\r\nfrom:runnier.leagues.0j@icloud.com\r\n".as_bytes() }; + global NO_BRACKETS: [u8; 51] = comptime { "\r\nfrom:ZKEmail Team runnier.leagues.0j@icloud.com\r\n".as_bytes() }; + global BRACKETS: [u8; 52] = comptime { "\r\nfrom:ZKEmail Team Date: Mon, 21 Oct 2024 03:21:08 -0600 Subject: [PATCH 3/4] email extraction tests --- js/tests/circuits.test.ts | 14 +-- lib/src/tests/mod.nr | 188 +++++++++++++++++++++++++++++++++-- lib/src/tests/test_inputs.nr | 44 +++++++- 3 files changed, 225 insertions(+), 21 deletions(-) diff --git a/js/tests/circuits.test.ts b/js/tests/circuits.test.ts index 52bc6af..acf01ac 100644 --- a/js/tests/circuits.test.ts +++ b/js/tests/circuits.test.ts @@ -14,12 +14,6 @@ const emails = { large: fs.readFileSync( path.join(__dirname, "./test-data/email-good-large.eml") ), - tamperedBody: fs.readFileSync( - path.join(__dirname, "./test-data/email-body-tampered.eml") - ), - tamperedHeader: fs.readFileSync( - path.join(__dirname, "./test-data/email-header-tampered.eml") - ), }; // default header/ body lengths to use for input gen @@ -61,10 +55,11 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { describe("Successful Cases", () => { it("2048-bit DKIM", async () => { const inputs = await generateEmailVerifierInputs( - emails.large, + emails.small, inputParams ); await prover2048.simulateWitness(inputs); + console.log(toProverToml(inputs)); }); it("Partial Hash", async () => { const inputs = await generateEmailVerifierInputs(emails.large, { @@ -108,7 +103,7 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { expect(expectedMaskedBody).toEqual(acutalMaskedBody); }); it("Extract Sender/ Recipient", async () => { - const inputs = await generateEmailVerifierInputs(emails.large, { + const inputs = await generateEmailVerifierInputs(emails.small, { extractFrom: true, extractTo: true, ...inputParams, @@ -143,9 +138,6 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { ); expect(expectedFrom).toEqual(actualFrom); expect(expectedTo).toEqual(actualTo); - - console.log(toProverToml(inputs)) }); }); - }); diff --git a/lib/src/tests/mod.nr b/lib/src/tests/mod.nr index f601262..511ea2c 100644 --- a/lib/src/tests/mod.nr +++ b/lib/src/tests/mod.nr @@ -3,8 +3,8 @@ mod test_inputs; mod test_success { use crate::{ - headers::body_hash::get_body_hash, tests::test_inputs::EmailLarge, - partial_hash::partial_sha256_var_end + headers::{email_address::get_email_address, body_hash::get_body_hash}, + tests::test_inputs::EmailLarge, partial_hash::partial_sha256_var_end, MAX_EMAIL_ADDRESS_LENGTH }; use std::hash::sha256_var; @@ -49,6 +49,33 @@ mod test_success { signed_body_hash == computed_body_hash, "Sha256 hash computed over body does not match DKIM-signed header" ); } + + #[test] + fn test_address_extraction() { + let from = comptime { + "from".as_bytes() + }; + let to = comptime { + "to".as_bytes() + }; + // 16k gate cost? has to be able to be brought down + let from_address = get_email_address( + EmailLarge::HEADER, + EmailLarge::FROM_HEADER_SEQUENCE, + EmailLarge::FROM_ADDRESS_SEQUENCE, + from + ); + let to_address = get_email_address( + EmailLarge::HEADER, + EmailLarge::TO_HEADER_SEQUENCE, + EmailLarge::TO_ADDRESS_SEQUENCE, + to + ); + let expected_from_address: BoundedVec = BoundedVec::from_array("runnier.leagues.0j@icloud.com".as_bytes()); + let expected_to_address: BoundedVec = BoundedVec::from_array("zkewtest@gmail.com".as_bytes()); + assert_eq(expected_from_address, from_address, "From address does not match expected address"); + assert_eq(expected_to_address, to_address, "To address does not match expected address"); + } } mod test_tampered_hash { @@ -208,11 +235,7 @@ mod header_field_access { tampered_header.storage[combined_sequence.length + 3] = "\n".as_bytes()[0]; let tampered_body_hash_index = 93; // just manually setting this // attempt to get body hash - let _ = get_body_hash( - tampered_header, - combined_sequence, - tampered_body_hash_index - ); + let _ = get_body_hash(tampered_header, combined_sequence, tampered_body_hash_index); } #[test(should_fail_with = "Header field out of bounds")] @@ -228,3 +251,154 @@ mod header_field_access { ); } } + +mod test_address_extraction { + // header field constrains are not checked here, should work same as tests in header_field_access + use crate::{headers::email_address::get_email_address, Sequence, tests::test_inputs::EmailAddresses}; + + #[test] + fn test_email_only() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::ADDRESS_ONLY); + let address = get_email_address( + header, + EmailAddresses::ADDRESS_ONLY_FIELD_SEQUENCE, + EmailAddresses::ADDRESS_ONLY_ADDRESS_SEQUENCE, + from + ); + assert_eq(address, EmailAddresses::ADDRESS); + } + + #[test] + fn test_no_brackets() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::NO_BRACKETS); + let address = get_email_address( + header, + EmailAddresses::NO_BRACKETS_FIELD_SEQUENCE, + EmailAddresses::NO_BRACKETS_ADDRESS_SEQUENCE, + from + ); + assert_eq(address, EmailAddresses::ADDRESS); + } + + #[test] + fn test_brackets() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::BRACKETS); + let address = get_email_address( + header, + EmailAddresses::BRACKETS_FIELD_SEQUENCE, + EmailAddresses::BRACKETS_ADDRESS_SEQUENCE, + from + ); + assert_eq(address, EmailAddresses::ADDRESS); + } + + #[test] + fn test_quotes() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::QUOTES); + let address = get_email_address( + header, + EmailAddresses::QUOTES_FIELD_SEQUENCE, + EmailAddresses::QUOTES_ADDRESS_SEQUENCE, + from + ); + assert_eq(address, EmailAddresses::ADDRESS); + } + + #[test(should_fail_with = "Email address must start immediately after '<' if bracket is present")] + fn test_malicious_display_name() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::QUOTES); + let _ = get_email_address( + header, + EmailAddresses::QUOTES_FIELD_SEQUENCE, + EmailAddresses::MALICIOUS_QUOTES_ADDRESS_SEQUENCE, + from + ); + } + + #[test(should_fail_with = "Email address must start immediately after '<' if bracket is present")] + fn test_not_full_address_bracket_start() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + // mutate index to remove "runnier." from address + let mut malicious_sequence = EmailAddresses::QUOTES_ADDRESS_SEQUENCE; + malicious_sequence.index = malicious_sequence.index + 8; + malicious_sequence.length = malicious_sequence.length - 8; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::QUOTES); + let _ = get_email_address( + header, + EmailAddresses::QUOTES_FIELD_SEQUENCE, + malicious_sequence, + from + ); + } + + #[test(should_fail_with = "Email address must end with an acceptable character")] + fn test_not_full_address_bracket_end() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + // mutate index to remove ".com" from address + let mut malicious_sequence = EmailAddresses::QUOTES_ADDRESS_SEQUENCE; + malicious_sequence.index = malicious_sequence.index; + malicious_sequence.length = malicious_sequence.length - 4; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::QUOTES); + let _ = get_email_address( + header, + EmailAddresses::QUOTES_FIELD_SEQUENCE, + malicious_sequence, + from + ); + } + + #[test(should_fail_with = "Email address must start with an acceptable character")] + fn test_not_full_address_start() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + // mutate index to remove "runnier." from address + let mut malicious_sequence = EmailAddresses::ADDRESS_ONLY_ADDRESS_SEQUENCE; + malicious_sequence.index = malicious_sequence.index + 8; + malicious_sequence.length = malicious_sequence.length - 8; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::ADDRESS_ONLY); + let _ = get_email_address( + header, + EmailAddresses::ADDRESS_ONLY_FIELD_SEQUENCE, + malicious_sequence, + from + ); + } + + #[test(should_fail_with = "Email address must end with an acceptable character")] + fn test_not_full_address_end() { + let from: [u8; 4] = comptime { + "from".as_bytes() + }; + // mutate index to remove ".com" from address + let mut malicious_sequence = EmailAddresses::ADDRESS_ONLY_ADDRESS_SEQUENCE; + malicious_sequence.index = malicious_sequence.index; + malicious_sequence.length = malicious_sequence.length - 4; + let header: BoundedVec = BoundedVec::from_array(EmailAddresses::ADDRESS_ONLY); + let _ = get_email_address( + header, + EmailAddresses::ADDRESS_ONLY_FIELD_SEQUENCE, + malicious_sequence, + from + ); + } +} diff --git a/lib/src/tests/test_inputs.nr b/lib/src/tests/test_inputs.nr index 6667f26..8a24c63 100644 --- a/lib/src/tests/test_inputs.nr +++ b/lib/src/tests/test_inputs.nr @@ -1,7 +1,6 @@ mod EmailLarge { use crate::{Sequence, KEY_LIMBS_2048, dkim::RSAPubkey}; - use std::collections::bounded_vec::BoundedVec; // regular inputs global EMAIL_LARGE_MAX_HEADER_LENGTH: u32 = 512; @@ -71,9 +70,48 @@ mod EmailLarge { } } -mod FromHeaderFields { - global EMAIL_ONLY: [u8; 38] = comptime { "\r\nfrom:runnier.leagues.0j@icloud.com\r\n".as_bytes() }; +mod EmailAddresses { + + use crate::{Sequence, MAX_EMAIL_ADDRESS_LENGTH}; + global ADDRESS: BoundedVec = BoundedVec::from_array("runnier.leagues.0j@icloud.com".as_bytes()); + global ADDRESS_ONLY: [u8; 38] = comptime { "\r\nfrom:runnier.leagues.0j@icloud.com\r\n".as_bytes() }; global NO_BRACKETS: [u8; 51] = comptime { "\r\nfrom:ZKEmail Team runnier.leagues.0j@icloud.com\r\n".as_bytes() }; global BRACKETS: [u8; 52] = comptime { "\r\nfrom:ZKEmail Team Date: Mon, 21 Oct 2024 03:53:27 -0600 Subject: [PATCH 4/4] update readme --- README.md | 100 ++++++++++++++++++++++++------------------------ js/package.json | 4 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index a2f3088..07bfc69 100644 --- a/README.md +++ b/README.md @@ -14,81 +14,97 @@ The library exports the following functions: - `dkim::RSAPubkey::verify_dkim_signature` - for verifying DKIM signatures over an email header. This is needed for all email verifications. - `headers::body_hash::get_body_hash` - constrained access and decoding of the body hash from the header - `headers::email_address::get_email_address` - constrained extraction of to or from email addresses +- `headers::constrain_header_field` - constrain an index/ length in the header to be the correct name, full, and uninterrupted - `partial_hash::partial_sha256_var_end` - finish a precomputed sha256 hash over the body - `masking::mask_text` - apply a byte mask to the header or body to selectively reveal parts of the entire email - `standard_outputs` - returns the hash of the DKIM pubkey and a nullifier for the email (`hash(signature)`) +Additionally, the `@zk-email/zkemail-nr` JS library exports an ergonomic API for easily deriving circuit inputs needed to utilize the Noir library. +For demonstrations of all functionality, see the [examples](./examples). + +### Basic Email Verification +A basic email verifier will often look like this: ```rust use dep::zkemail::{ - KEY_LIMBS_1024, dkim::verify_dkim_1024, get_body_hash_by_index, + KEY_LIMBS_1024, dkim::RSAPubkey, get_body_hash_by_index, base64::body_hash_base64_decode, standard_outputs }; use dep::std::hash::sha256_var; // Somewhere in your function ... - verify_dkim_1024(header, header_length, pubkey, pubkey_redc, signature); - - let body_hash_encoded = get_body_hash_by_index(header, body_hash_index); - let signed_body_hash: [u8; 32] = body_hash_base64_decode(body_hash_encoded); - - let computed_body_hash: [u8; 32] = sha256_var(body, body_length as u64); - + // verify the dkim signature over the asserted header + pubkey.verify_dkim_signature(header, signature); + // extract the body hash from the header + let signed_body_hash = get_body_hash(header, dkim_header_sequence, body_hash_index); + // compute the sha256 hash of the asserted body + let computed_body_hash: [u8; 32] = sha256_var(body.storage, body.len() as u64); + // constain the computed body hash to match the one found in the header assert( - signed_body_hash == computed_body_hash, "SHA256 hash computed over body does not match body hash found in DKIM-signed header" + signed_body_hash == computed_body_hash, + "SHA256 hash computed over body does not match body hash found in DKIM-signed header" ); ... ``` +From here, you can operate on the header or body with guarantees that the accessed text was signed by the DKIM key. + +You may also have an email where you need access to the header, but not the body. You can simply omit everything after `verify_dkim_signature` and proceed! ### Usage with partial SHA You can use partial hashing technique for email with large body when the part you want to constrain in the body is towards the end. -Since SHA works in chunks of 64 bytes, we can hash the body up to the chunk from where we want to extract outside of the circuit and do the remaining hash in the circuit. This will save a lot of constraints as SHA is very expensive in circuit. +Since SHA works in chunks of 64 bytes, we can hash the body up to the chunk from where we want to extract outside of the circuit and do the remaining hash in the circuit. This will save a lot of constraints as SHA is very expensive in circuit (~100 constraints/ byte). ```rust use dep::zkemail::{ - KEY_LIMBS_2048, dkim::verify_dkim_2048, get_body_hash_by_index, - partial_hash::partial_sha256_var_end, base64::body_hash_base64_decode, + KEY_LIMBS_2048, dkim::RSAPubkey, headers::body_hash::get_body_hash, + partial_hash::partial_sha256_var_end }; ... - // verify the dkim signature over the header - verify_dkim_2048(header, header_length, pubkey, pubkey_redc, signature); - - // manually extract the body hash from the header - let body_hash_encoded = get_body_hash_by_index(header, body_hash_index); - let signed_body_hash: [u8; 32] = body_hash_base64_decode(body_hash_encoded); - + // verify the dkim signature over the asserted header + pubkey.verify_dkim_signature(header, signature); + // extract the body hash from the header + let signed_body_hash = get_body_hash(header, dkim_header_sequence, body_hash_index); // finish the partial hash - let computed_body_hash = partial_sha256_var_end(partial_body_hash, body, partial_body_length as u64, body_length as u64); - - // check the body hashes match + let computed_body_hash = partial_sha256_var_end(partial_body_hash, body.storage(), body.len() as u64, partial_body_real_length); + // constain the computed body hash to match the one found in the header assert( - signed_body_hash == computed_body_hash, "Sha256 hash computed over body does not match DKIM-signed header" + signed_body_hash == computed_body_hash, + "SHA256 hash computed over body does not match body hash found in DKIM-signed header" ); ... ``` ### Extracting Email Addresses -In the header, email addresses can appear in a variety of formats: - * `"name" ` - * `name ` - * `name local-part@domain.com` - * `local-part@domain.com` -Without regex, we take a slightly different approach. - - -Find examples of each implementation in the [examples](./examples) folder. +To and from email addresses can be extracted from the header with `get_email_address` +```rust +use dep::zkemail::get_email_address; +... + // define the header field to access (set "to" or "from") + let to = comptime { "to".as_bytes() }; + // constrained retrieval of the email header + let to_address = get_email_address(header, to_header_sequence, to_address_sequence, to); +... +``` +`to_address` is a "BoundedVec", meaning the output of a parsed email address "zkemail@prove.email" would export +```json +{ + "storage": [122, 107, 101, 109, 97, 105, 108, 64, 112, 114, 111, 118, 101, 46, 101, 109, 97, 105, 108, 0, ..., 0], + "len": 19 +} +``` +which is easily interpreted with `Buffer.from(output.storage.slice(0, output.storage.len)).toString()`. You can additionally perform your own transformations or commitments in-circuit. ## Using the Input Generation JS Library Install the library: ```console -yarn add @mach-34/zkemail-nr +yarn add @zk-email/zkemail-nr ``` ### Usage @@ -96,7 +112,7 @@ See the [witness simulation](./js/tests/circuits.test.ts) and [proving](./js/tes ```js // example of generating inputs for a partial hash -import { generateEmailVerifierInputs } from "@mach-34/zkemail-nr"; +import { generateEmailVerifierInputs } from "@zk-email/zkemail-nr"; const zkEmailInputs = await generateEmailVerifierInputs(emailContent, { maxBodyLength: 1280, @@ -113,7 +129,6 @@ TODO TODO ## Todo - - Negative Unit Testing - Expected InputGen testing - EVM Contract tests for email integration - Aztec Contract tests for email integration @@ -123,19 +138,4 @@ TODO - Add native proving scripts - Macro Impl -### Proposed Macro Impl -```rust -// zkemail attribute would automatically inject fields -// header: BoundedVec, -// pubkey: RSAPubkey, -// signature: [Field; KEY_LIMBS] -// and add range check header.len() + pubkey::verify_rsa_signature -// it then would add additional necessary fields for each trait, add the -#[zkemail(PartialHash, From, DKIMHash, Nullifier)] -let Input {}; -//fn main(input: Input) { -// input.verify() -//} -``` - By [Mach-34](https://mach34.space) \ No newline at end of file diff --git a/js/package.json b/js/package.json index 14d5faf..4dae041 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { - "name": "@mach-34/zkemail-nr", - "version": "1.2.0", + "name": "@zk-email/zkemail-nr", + "version": "1.2.1", "main": "dist", "types": "dist", "license": "MIT",