diff --git a/.gitignore b/.gitignore index 5308eb6..2b0ac3d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ target dist .tsbuildinfo zk-email-verify - +!*/email-*.eml diff --git a/examples/recipient_search/Nargo.toml b/examples/email_mask/Nargo.toml similarity index 62% rename from examples/recipient_search/Nargo.toml rename to examples/email_mask/Nargo.toml index deb3266..24a05d6 100644 --- a/examples/recipient_search/Nargo.toml +++ b/examples/email_mask/Nargo.toml @@ -1,8 +1,8 @@ [package] -name = "recipient_search" +name = "email_mask" type = "bin" authors = ["Mach 34"] -compiler_version = ">=0.34.0" +compiler_version = ">=0.35.0" [dependencies] zkemail = { path = "../../lib"} diff --git a/examples/email_mask/src/main.nr b/examples/email_mask/src/main.nr new file mode 100644 index 0000000..f02bbae --- /dev/null +++ b/examples/email_mask/src/main.nr @@ -0,0 +1,60 @@ +use dep::zkemail::{ + KEY_LIMBS_2048, dkim::RSAPubkey, headers::body_hash::get_body_hash, + standard_outputs, Sequence, masking::mask_text +}; +use dep::std::{collections::bounded_vec::BoundedVec, hash::sha256_var}; + +global MAX_EMAIL_HEADER_LENGTH: u32 = 512; +global MAX_EMAIL_BODY_LENGTH: u32 = 1024; + +/** + * Verify an arbitrary email signed by a 2048-bit RSA DKIM signature and mask outputs + * + * @param header - The email header, 0-padded at end to the MAX_EMAIL_HEADER_LENGTH + * @param body - The email body, 0-padded at end to the MAX_EMAIL_BODY_LENGTH + * @param pubkey - The DKIM RSA Public Key modulus and reduction parameter + * @param signature - The DKIM RSA Signature + * @param body_hash_index - The index of the body hash in the partial hash array + * @param dkim_header_sequence - The index and length of the DKIM header field + * @param header_mask - The mask for the header + * @param body_mask - The mask for the body + * @return - + * 0: Pedersen hash of DKIM public key (root of trust) + * 1: Pedersen hash of DKIM signature (email nullifier) + */ +fn main( + header: BoundedVec, + body: BoundedVec, + pubkey: RSAPubkey, + signature: [Field; KEY_LIMBS_2048], + body_hash_index: u32, + dkim_header_sequence: Sequence, + header_mask: [bool; MAX_EMAIL_HEADER_LENGTH], + body_mask: [bool; MAX_EMAIL_BODY_LENGTH] +) -> pub ([Field; 2], [u8; MAX_EMAIL_HEADER_LENGTH], [u8; MAX_EMAIL_BODY_LENGTH]) { + // check the body and header lengths are within bounds + assert(header.len() <= MAX_EMAIL_HEADER_LENGTH); + assert(body.len() <= MAX_EMAIL_BODY_LENGTH); + + // verify the dkim signature over the 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); + + // hash the asserted body + let computed_body_hash: [u8; 32] = sha256_var(body.storage, 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" + ); + + // mask the header and body + let masked_header = mask_text(header, header_mask); + let masked_body = mask_text(body, body_mask); + + // hash the pubkey and signature for the standard outputs + let standard_out = standard_outputs(pubkey.modulus, signature); + (standard_out, masked_header, masked_body) +} diff --git a/examples/extract_addresses/Nargo.toml b/examples/extract_addresses/Nargo.toml new file mode 100644 index 0000000..2a1967c --- /dev/null +++ b/examples/extract_addresses/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "extract_addresses" +type = "bin" +authors = ["Mach 34"] +compiler_version = ">=0.35.0" + +[dependencies] +zkemail = { path = "../../lib"} diff --git a/examples/extract_addresses/src/main.nr b/examples/extract_addresses/src/main.nr new file mode 100644 index 0000000..e15ed3d --- /dev/null +++ b/examples/extract_addresses/src/main.nr @@ -0,0 +1,55 @@ +use dep::zkemail::{ + KEY_LIMBS_2048, dkim::RSAPubkey, + headers::{body_hash::get_body_hash, email_address::get_email_address}, standard_outputs, Sequence, + MAX_EMAIL_ADDRESS_LENGTH +}; +use dep::std::{collections::bounded_vec::BoundedVec, hash::sha256_var}; + +global MAX_EMAIL_HEADER_LENGTH: u32 = 512; +global MAX_EMAIL_BODY_LENGTH: u32 = 1024; + +/** + * Verify an arbitrary email signed by a 2048-bit RSA DKIM signature and extract sender and recipient addresses + * @dev example of only constraining access to the header too + * + * @param header - The email header, 0-padded at end to the MAX_EMAIL_HEADER_LENGTH + * @param pubkey - The DKIM RSA Public Key modulus and reduction parameter + * @param signature - The DKIM RSA Signature + * @param from_header_sequence - The index and length of the "From" header field + * @param from_address_sequence - The index and length of the "From" email address + * @param to_header_sequence - The index and length of the "To" header field + * @param to_address_sequence - The index and length of the "To" email address + * @return - + * 0: Pedersen hash of DKIM public key (root of trust) + * 1: Pedersen hash of DKIM signature (email nullifier) + */ +fn main( + header: BoundedVec, + pubkey: RSAPubkey, + signature: [Field; KEY_LIMBS_2048], + from_header_sequence: Sequence, + from_address_sequence: Sequence, + to_header_sequence: Sequence, + to_address_sequence: Sequence +) -> pub ([Field; 2], BoundedVec, BoundedVec) { + // check the body and header lengths are within bounds + assert(header.len() <= MAX_EMAIL_HEADER_LENGTH); + + // verify the dkim signature over the header + pubkey.verify_dkim_signature(header, signature); + + // extract to and from email addresses + 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(header, from_header_sequence, from_address_sequence, from); + let to_address = get_email_address(header, to_header_sequence, to_address_sequence, to); + + // hash the pubkey and signature for the standard outputs + let standard_out = standard_outputs(pubkey.modulus, signature); + (standard_out, from_address, to_address) +} diff --git a/examples/partial_hash/Nargo.toml b/examples/partial_hash/Nargo.toml index 985a5f8..300e02c 100644 --- a/examples/partial_hash/Nargo.toml +++ b/examples/partial_hash/Nargo.toml @@ -2,7 +2,7 @@ name = "partial_hash" type = "bin" authors = ["Mach 34"] -compiler_version = ">=0.34.0" +compiler_version = ">=0.35.0" [dependencies] zkemail = { path = "../../lib"} diff --git a/examples/partial_hash/src/main.nr b/examples/partial_hash/src/main.nr index aa3a674..aa4c3aa 100644 --- a/examples/partial_hash/src/main.nr +++ b/examples/partial_hash/src/main.nr @@ -1,5 +1,5 @@ use dep::zkemail::{ - KEY_LIMBS_2048, dkim::RSAPubkey, headers::get_body_hash, + KEY_LIMBS_2048, dkim::RSAPubkey, headers::body_hash::get_body_hash, partial_hash::partial_sha256_var_end, standard_outputs, Sequence }; diff --git a/examples/recipient_search/src/main.nr b/examples/recipient_search/src/main.nr deleted file mode 100644 index 541cf72..0000000 --- a/examples/recipient_search/src/main.nr +++ /dev/null @@ -1,46 +0,0 @@ -// use dep::zkemail::{ -// KEY_LIMBS_2048, dkim::verify_dkim_2048, -// standard_outputs -// }; - -// global MAX_EMAIL_HEADER_LENGTH: u32 = 1024; -// global MAX_EMAIL_BODY_LENGTH: u32 = 1536; - -// /** -// * Verify an arbitrary email signed by a 1024-bit RSA DKIM signature -// * -// * @param body_hash_index - The index of the body hash in the partial hash array -// * @param header - The email header, 0-padded at end to the MAX_EMAIL_HEADER_LENGTH -// * @param header_length - The actual length of the email header -// * @param body - The email body, 0-padded at end to the MAX_EMAIL_BODY_LENGTH -// * @param body_length - The actual length of the email body -// * @param pubkey_modulus_limbs - The DKIM RSA Pubkey -// * @param redc_params_limbs - Barrett Reduction Parameter for Pubkey for efficient signature verification -// * @param signature - The DKIM RSA Signature -// * @return - -// * 0: Pedersen hash of DKIM public key (root of trust) -// * 1: Pedersen hash of DKIM signature (email nullifier) -// */ -// fn main( -// header: [u8; MAX_EMAIL_HEADER_LENGTH], -// header_length: u32, -// pubkey: [Field; KEY_LIMBS_2048], -// pubkey_redc: [Field; KEY_LIMBS_2048], -// signature: [Field; KEY_LIMBS_2048] -// ) -> pub [Field; 2] { -// // check the body and header lengths are within bounds -// assert(header_length <= MAX_EMAIL_HEADER_LENGTH); - -// // verify the dkim signature over the header -// verify_dkim_2048(header, header_length, pubkey, pubkey_redc, signature); - -// // check the recipient email address -// // todo - -// // hash the pubkey and signature for the standard outputs -// standard_outputs(pubkey, signature) -// } - -fn main() -> pub bool{ - 1 + 1 == 2 -} \ No newline at end of file diff --git a/examples/verify_email_1024_bit_dkim/Nargo.toml b/examples/verify_email_1024_bit_dkim/Nargo.toml index 890f993..8ea6699 100644 --- a/examples/verify_email_1024_bit_dkim/Nargo.toml +++ b/examples/verify_email_1024_bit_dkim/Nargo.toml @@ -2,7 +2,7 @@ name = "verify_email_1024_bit_dkim" type = "bin" authors = ["Mach 34"] -compiler_version = ">=0.34.0" +compiler_version = ">=0.35.0" [dependencies] zkemail = { path = "../../lib"} \ No newline at end of file diff --git a/examples/verify_email_1024_bit_dkim/src/main.nr b/examples/verify_email_1024_bit_dkim/src/main.nr index 348b74c..a759fab 100644 --- a/examples/verify_email_1024_bit_dkim/src/main.nr +++ b/examples/verify_email_1024_bit_dkim/src/main.nr @@ -1,5 +1,5 @@ use dep::zkemail::{ - KEY_LIMBS_1024, dkim::RSAPubkey, headers::get_body_hash, + KEY_LIMBS_1024, dkim::RSAPubkey, headers::body_hash::get_body_hash, standard_outputs, Sequence }; use dep::std::{collections::bounded_vec::BoundedVec, hash::sha256_var}; diff --git a/examples/verify_email_2048_bit_dkim/Nargo.toml b/examples/verify_email_2048_bit_dkim/Nargo.toml index 5c78077..d52803e 100644 --- a/examples/verify_email_2048_bit_dkim/Nargo.toml +++ b/examples/verify_email_2048_bit_dkim/Nargo.toml @@ -2,7 +2,7 @@ name = "verify_email_2048_bit_dkim" type = "bin" authors = ["Mach 34"] -compiler_version = ">=0.34.0" +compiler_version = ">=0.35.0" [dependencies] zkemail = { path = "../../lib"} \ No newline at end of file diff --git a/examples/verify_email_2048_bit_dkim/src/main.nr b/examples/verify_email_2048_bit_dkim/src/main.nr index 7588b19..76291c8 100644 --- a/examples/verify_email_2048_bit_dkim/src/main.nr +++ b/examples/verify_email_2048_bit_dkim/src/main.nr @@ -1,5 +1,5 @@ use dep::zkemail::{ - KEY_LIMBS_2048, dkim::RSAPubkey, headers::get_body_hash, + KEY_LIMBS_2048, dkim::RSAPubkey, headers::body_hash::get_body_hash, standard_outputs, Sequence }; use dep::std::{collections::bounded_vec::BoundedVec, hash::sha256_var}; diff --git a/js/src/index.ts b/js/src/index.ts index 5d31dab..a1f11f6 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -11,7 +11,7 @@ import { verifyDKIMSignature, } from "@zk-email/helpers/dist/dkim"; import * as NoirBignum from "@mach-34/noir-bignum-paramgen"; -import { u8ToU32, getHeaderSequence } from "./utils"; +import { u8ToU32, getHeaderSequence, getAddressHeaderSequence } from "./utils"; export { verifyDKIMSignature } from "@zk-email/helpers/dist/dkim"; // This file is essentially https://github.com/zkemail/zk-email-verify/blob/main/packages/helpers/src/input-generators.ts @@ -28,6 +28,7 @@ export type BoundedVec = { }; export type CircuitInput = { + // required inputs for all zkemail verifications header: BoundedVec; pubkey: { modulus: string[]; @@ -35,12 +36,20 @@ export type CircuitInput = { }; signature: string[]; dkim_header_sequence: Sequence; + // inputs used for verifying full or partial hash body?: BoundedVec; body_hash_index?: string; + // inputs used for only partial hash partial_body_real_length?: string; partial_body_hash?: string[]; - header_mask: string[]; - body_mask: string[]; + // inputs used for only masking + header_mask?: string[]; + body_mask?: string[]; + // inputs used for address extraction + from_header_sequence?: Sequence; + from_address_sequence?: Sequence; + to_header_sequence?: Sequence; + to_address_sequence?: Sequence; }; export type InputGenerationArgs = { @@ -51,6 +60,9 @@ export type InputGenerationArgs = { removeSoftLineBreaks?: boolean; headerMask?: number[]; bodyMask?: number[]; + // todo: probably move these out into a separate extended type? + extractFrom?: boolean; + extractTo?: boolean; }; // copied without modification, but not publicly exported in original @@ -193,6 +205,18 @@ export function generateEmailVerifierInputsFromDKIMResult( circuitInputs.header_mask = params.headerMask.map((x) => x.toString()); if (params.bodyMask) circuitInputs.body_mask = params.bodyMask.map((x) => x.toString()); + + // address extraction + if (params.extractFrom) { + const fromSequences = getAddressHeaderSequence(headers, "from"); + circuitInputs.from_header_sequence = fromSequences[0]; + circuitInputs.from_address_sequence = fromSequences[1]; + } + if (params.extractTo) { + const toSequences = getAddressHeaderSequence(headers, "to"); + circuitInputs.to_header_sequence = toSequences[0]; + circuitInputs.to_address_sequence = toSequences[1]; + } } return circuitInputs; diff --git a/js/src/utils.ts b/js/src/utils.ts index 4c9401f..41d96d5 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -69,6 +69,38 @@ export function getHeaderSequence( return { index: match.index!.toString(), length: match[0].length.toString() }; } +/** + * Get the index and length of a header field as well as the address in the field + * @dev only works for to, from. Not set up for cc + * + * @param header - the header to search for the field in + * @param headerField - the field name to search for + * @returns - the index and length of the field in the header and the index and length of the address in the field + */ +export function getAddressHeaderSequence( + header: Buffer, + headerField: string +) { + const regexPrefix = `[${headerField[0].toUpperCase()}${headerField[0].toLowerCase()}]${headerField + .slice(1) + .toLowerCase()}`; + const regex = new RegExp( + `${regexPrefix}:.*?<([^>]+)>|${regexPrefix}:.*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})` + ); + const headerStr = header.toString(); + const match = headerStr.match(regex); + if (match === null) + throw new Error(`Field "${headerField}" not found in header`); + if (match[1] === null && match[2] === null) + throw new Error(`Address not found in "${headerField}" field`); + const address = match[1] || match[2]; + const addressIndex = headerStr.indexOf(address); + return [ + { index: match.index!.toString(), length: match[0].length.toString() }, + { index: addressIndex.toString(), length: address.length.toString() }, + ] +} + /** * Build a ROM table for allowable email characters */ @@ -92,13 +124,13 @@ export function makeEmailAddressCharTable(): string { for (let i = 0; i < procedingChars.length; i++) { table[procedingChars.charCodeAt(i)] = 3; } - let tableStr = `global EMAIL_ADDRESS_CHAR_TABLE: [u8; ${tableLength}] = [\n` - console.log() + let tableStr = `global EMAIL_ADDRESS_CHAR_TABLE: [u8; ${tableLength}] = [\n`; + console.log(); for (let i = 0; i < table.length; i += 10) { const end = i + 10 < table.length ? i + 10 : table.length; tableStr += ` ${table.slice(i, end).join(", ")},\n`; } - return tableStr += "];"; + return (tableStr += "];"); } // export function computeStandardOutputs(email: Buffer): Promise<[bigint, bigint]> { diff --git a/js/tests/circuits.test.ts b/js/tests/circuits.test.ts index b26fe97..64892a7 100644 --- a/js/tests/circuits.test.ts +++ b/js/tests/circuits.test.ts @@ -7,6 +7,7 @@ import { makeEmailAddressCharTable } from "../src/utils"; 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"; import circuitEmailMask from "../../examples/email_mask/target/email_mask.json"; +import circuitExtractAddresses from "../../examples/extract_addresses/target/extract_addresses.json"; const emails = { small: fs.readFileSync(path.join(__dirname, "./test-data/email-good.eml")), @@ -28,6 +29,7 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { let prover2048: ZKEmailProver; let proverPartialHash: ZKEmailProver; let proverMasked: ZKEmailProver; + let proverExtractAddresses: ZKEmailProver; beforeAll(() => { //@ts-ignore @@ -38,6 +40,8 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { proverPartialHash = new ZKEmailProver(circuitPartialHash, "all"); //@ts-ignore proverMasked = new ZKEmailProver(circuitEmailMask, "all"); + //@ts-ignore + proverExtractAddresses = new ZKEmailProver(circuitExtractAddresses, "all"); }); afterAll(async () => { @@ -45,6 +49,7 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { await prover2048.destroy(); await proverPartialHash.destroy(); await proverMasked.destroy(); + await proverExtractAddresses.destroy(); }); describe("Successful Cases", () => { @@ -66,7 +71,7 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { }); await proverPartialHash.simulateWitness(inputs); }); - it("Masked Header/ Body", async () => { + xit("Masked Header/ Body", async () => { // make masks const headerMask = Array.from( { length: inputParams.maxHeadersLength }, @@ -99,5 +104,25 @@ describe("ZKEmail.nr Circuit Unit Tests", () => { expect(expectedMaskedHeader).toEqual(acutalMaskedHeader); expect(expectedMaskedBody).toEqual(acutalMaskedBody); }); + it("Extract Sender/ Recipient", async () => { + const inputs = await generateEmailVerifierInputs(emails.small, { + extractFrom: true, + extractTo: true, + ...inputParams, + }); + console.log() + // simulate witness + // const result = await proverExtractAddresses.simulateWitness(inputs); + // console.log("result: ", result.returnValue); + // // parse expected addresses + // const header = Buffer.from(inputs.header.storage.map((byte) => parseInt(byte))).toString(); + // const fromAddressStart = parseInt(inputs.from_adddres_sequence!.index); + // const fromAddressEnd = fromAddressStart + parseInt(inputs.from_adddres_sequence!.length); + // const from = header.slice(fromAddressStart, fromAddressEnd); + // const toAddressStart = parseInt(inputs.to_address_sequence!.index); + // const toAddressEnd = toAddressStart + parseInt(inputs.to_address_sequence!.length); + // const to = header.slice(toAddressStart, toAddressEnd); + + }); }); }); diff --git a/js/tests/proving.test.ts b/js/tests/proving.test.ts index 2ded927..d5ad506 100644 --- a/js/tests/proving.test.ts +++ b/js/tests/proving.test.ts @@ -40,25 +40,6 @@ describe("ZKEmail.nr E2E Tests", () => { await proverPartialHash.destroy(); }); - describe("Witness Generation", () => { - it("2048-bit DKIM", async () => { - const inputs = await generateEmailVerifierInputs( - emails.small, - inputParams - ); - await prover2048.simulateWitness(inputs); - }); - it("Partial Hash", async () => { - const selectorText = "All nodes in the Bitcoin network can consult it"; - const inputs = await generateEmailVerifierInputs(emails.large, { - shaPrecomputeSelector: selectorText, - maxHeadersLength: 512, - maxBodyLength: 192, - }); - await proverPartialHash.simulateWitness(inputs); - }) - }) - describe("UltraPlonk", () => { it("UltraPlonk::SmallEmail", async () => { const inputs = await generateEmailVerifierInputs( diff --git a/lib/src/headers/body_hash.nr b/lib/src/headers/body_hash.nr new file mode 100644 index 0000000..1f17df6 --- /dev/null +++ b/lib/src/headers/body_hash.nr @@ -0,0 +1,59 @@ +use dep::base64::base64_encode; +use dep::std::collections::bounded_vec::BoundedVec; +use crate::{Sequence, BODY_HASH_BASE64_LENGTH, MAX_DKIM_HEADER_FIELD_LENGTH, headers::constrain_header_field}; + +/** + * Constrained access to the body hash in the header + * + * @param MAX_HEADER_LENGTH - The maximum length of the email header + * @param header - The email header as validated in the DKIM signature + * @param dkim_header_field_sequence - The sequence of the DKIM header field + * @param body_hash_index - The index of the body hash in the header + */ +pub fn get_body_hash( + header: BoundedVec, + dkim_header_field_sequence: Sequence, + body_hash_index: u32 +) -> [u8; 32] { + // constrain the access of dkim signature field + let header_field_name: [u8; 14] = comptime { + "dkim-signature".as_bytes() + }; + constrain_header_field::(header, dkim_header_field_sequence, header_field_name); + // constrain access to the body hash + assert( + body_hash_index > dkim_header_field_sequence.index + & body_hash_index < dkim_header_field_sequence.index + dkim_header_field_sequence.length, "Body hash index accessed outside of DKIM header field" + ); + let bh_prefix: [u8; 3] = comptime { + "bh=".as_bytes() + }; + for i in 0..3 { + assert( + header.get_unchecked(body_hash_index - 3 + i) == bh_prefix[i], "No 'bh=' prefix found at asserted bh index" + ); + } + // get the body hash + get_body_hash_unsafe(header, body_hash_index) +} + +/** + * Get the body hash from the header without validating the access index + * + * @param MAX_HEADER_LENGTH - The maximum length of the email header + * @param header - The email header as validated in the DKIM signature + * @param body_hash_index - The asserted index to find the body hash at + */ +pub fn get_body_hash_unsafe( + header: BoundedVec, + body_hash_index: u32 +) -> [u8; 32] { + // get the body hash + let mut body_hash_encoded: [u8; BODY_HASH_BASE64_LENGTH] = [0; BODY_HASH_BASE64_LENGTH]; + for i in 0..BODY_HASH_BASE64_LENGTH { + body_hash_encoded[i] = header.get_unchecked(body_hash_index + i); + } + // return the decoded body hash + // idk why encode vs decode... + base64_encode::(body_hash_encoded) +} diff --git a/lib/src/headers/email_address.nr b/lib/src/headers/email_address.nr new file mode 100644 index 0000000..092589f --- /dev/null +++ b/lib/src/headers/email_address.nr @@ -0,0 +1,60 @@ +use dep::std::collections::bounded_vec::BoundedVec; +use crate::{ + Sequence, MAX_EMAIL_ADDRESS_LENGTH, EMAIL_ADDRESS_CHAR_TABLE, + headers::constrain_header_field_detect_last_angle_bracket +}; + +pub fn get_email_address( + header: BoundedVec, + from_header_field_sequence: Sequence, + email_address_sequence: Sequence, + header_field_name: [u8; HEADER_FIELD_NAME_LENGTH] +) -> BoundedVec { + // check field is uninterrupted and matches the expected field name + let last_angle_bracket = constrain_header_field_detect_last_angle_bracket::< + MAX_HEADER_LENGTH, + MAX_EMAIL_ADDRESS_LENGTH + HEADER_FIELD_NAME_LENGTH + 1, + HEADER_FIELD_NAME_LENGTH + >(header, from_header_field_sequence, header_field_name); + // if angle bracket found, assert index is +1 + if last_angle_bracket != 0 { + assert( + email_address_sequence.index == last_angle_bracket + 1, "Email address must start immediately after '<' if bracket is present" + ); + } + // check email sequence is within header field + assert( + email_address_sequence.index >= from_header_field_sequence.index + & email_address_sequence.index + email_address_sequence.length <= last_angle_bracket, "Email address sequence out of bounds" + ); + + // constrained get email address + parse_email_address(header, email_address_sequence) +} + +pub fn parse_email_address( + header: BoundedVec, + email_address_sequence: Sequence +) -> BoundedVec { + // check the sequence is proceeded by an acceptable character + assert( + EMAIL_ADDRESS_CHAR_TABLE[header.get_unchecked(email_address_sequence.index - 1)] == 2, "Email address must start with an acceptable character" + ); + assert( + EMAIL_ADDRESS_CHAR_TABLE[header.get_unchecked(email_address_sequence.index + email_address_sequence.length)] + == 3, "Email address must end with an acceptable character" + ); + // check the email address and assign + let mut email_address: BoundedVec = BoundedVec::new(); + for i in 0..MAX_EMAIL_ADDRESS_LENGTH { + let index = email_address_sequence.index + i; + assert( + EMAIL_ADDRESS_CHAR_TABLE[header.get_unchecked(index)] != 1, "Email address contains an unacceptable character" + ); + email_address.push(header.get_unchecked(index)); + } + // todo: should probably introduce a check for @ + + email_address +} + diff --git a/lib/src/headers.nr b/lib/src/headers/mod.nr similarity index 67% rename from lib/src/headers.nr rename to lib/src/headers/mod.nr index c127160..814515c 100644 --- a/lib/src/headers.nr +++ b/lib/src/headers/mod.nr @@ -1,73 +1,19 @@ -use dep::base64::base64_encode; -use dep::std::collections::bounded_vec::BoundedVec; -use crate::{Sequence, CR, LF, MAX_DKIM_HEADER_FIELD_LENGTH, BODY_HASH_BASE64_LENGTH}; +use crate::{Sequence, CR, LF}; + +pub mod body_hash; +pub mod email_address; + -/** - * Constrained access to the body hash in the header - * - * @param MAX_HEADER_LENGTH - The maximum length of the email header - * @param header - The email header as validated in the DKIM signature - * @param dkim_header_field_sequence - The sequence of the DKIM header field - * @param body_hash_index - The index of the body hash in the header - */ -pub fn get_body_hash( - header: BoundedVec, - dkim_header_field_sequence: Sequence, - body_hash_index: u32, -) -> [u8; 32] { - // constrain the access of dkim signature field - let header_field_name: [u8; 14] = comptime { "dkim-signature".as_bytes() }; - constrain_header_field::( - header, - dkim_header_field_sequence, - header_field_name, - ); - // constrain access to the body hash - assert( - body_hash_index > dkim_header_field_sequence.index - & body_hash_index < dkim_header_field_sequence.index + dkim_header_field_sequence.length, - "Body hash index accessed outside of DKIM header field" - ); - let bh_prefix: [u8; 3] = comptime { "bh=".as_bytes() }; - for i in 0..3 { - assert( - header.get_unchecked(body_hash_index - 3 + i) == bh_prefix[i], - "No 'bh=' prefix found at asserted bh index" - ); - } - // get the body hash - get_body_hash_unsafe(header, body_hash_index) -} /** - * Get the body hash from the header without validating the access index + * Constrain a sequence in a header to match the specific header field * * @param MAX_HEADER_LENGTH - The maximum length of the email header + * @param MAX_HEADER_FIELD_LENGTH - The maximum length of the header field + * @param HEADER_FIELD_NAME_LENGTH - The length of the header field name * @param header - The email header as validated in the DKIM signature - * @param body_hash_index - The asserted index to find the body hash at - */ -pub fn get_body_hash_unsafe( - header: BoundedVec, - body_hash_index: u32, -) -> [u8; 32] { - // get the body hash - let mut body_hash_encoded: [u8; BODY_HASH_BASE64_LENGTH] = [0; BODY_HASH_BASE64_LENGTH]; - for i in 0..BODY_HASH_BASE64_LENGTH { - body_hash_encoded[i] = header.get_unchecked(body_hash_index + i); - } - // return the decoded body hash - // idk why encode vs decode... - base64_encode::(body_hash_encoded) -} - -// pub fn get_email_address( -// header: BoundedVec, -// from_header_field_sequence: Sequence, - -// ) - -/** - * Constrains a header field to be within + * @param header_field_sequence - The sequence of the header field + * @param header_field_name - The name of the header field */ pub fn constrain_header_field< let MAX_HEADER_LENGTH: u32, @@ -128,7 +74,7 @@ pub fn constrain_header_field< } /** - * Constrains a header field to be within + * contrain_header_field with checks for the last occurence of "<" inside the loop to save constraints */ pub fn constrain_header_field_detect_last_angle_bracket< let MAX_HEADER_LENGTH: u32, diff --git a/lib/src/masking.nr b/lib/src/masking.nr index a09d40d..fb6ef8b 100644 --- a/lib/src/masking.nr +++ b/lib/src/masking.nr @@ -11,7 +11,7 @@ use dep::std::collections::bounded_vec::BoundedVec; */ pub fn mask_text( text: BoundedVec, - mask: [u8; TEXT_LEN], + mask: [bool; TEXT_LEN], ) -> [u8; TEXT_LEN] { let mut masked_text = [0; TEXT_LEN]; // probably fine to just iterate through whole thing without an if statement