From 3d96f7143d22ecbe653bf77522cd32a078106abb Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Sat, 13 Jul 2024 19:54:34 +0200 Subject: [PATCH] feat: upstreamed to email-verifier+ tests --- packages/circuits/email-verifier.circom | 17 +- .../circuits/tests/email-verifier.test.ts | 343 ++++++++++-------- .../email-verifier-no-body-test.circom | 2 +- .../test-circuits/email-verifier-test.circom | 2 +- .../email-verifier-with-mask-test.circom | 5 + packages/helpers/src/input-generators.ts | 7 + 6 files changed, 227 insertions(+), 149 deletions(-) create mode 100644 packages/circuits/tests/test-circuits/email-verifier-with-mask-test.circom diff --git a/packages/circuits/email-verifier.circom b/packages/circuits/email-verifier.circom index f6388434b..5dad0d04c 100644 --- a/packages/circuits/email-verifier.circom +++ b/packages/circuits/email-verifier.circom @@ -9,6 +9,7 @@ include "./lib/sha.circom"; include "./utils/array.circom"; include "./utils/regex.circom"; include "./utils/hash.circom"; +include "./helpers/body-masker.circom"; /// @title EmailVerifier @@ -20,6 +21,7 @@ include "./utils/hash.circom"; /// @param n Number of bits per chunk the RSA key is split into. Recommended to be 121. /// @param k Number of chunks the RSA key is split into. Recommended to be 17. /// @param ignoreBodyHashCheck Set 1 to skip body hash check in case data to prove/extract is only in the headers. +/// @param turnOnBodyMasking Set 1 to turn on body masking. /// @input emailHeader[maxHeadersLength] Email headers that are signed (ones in `DKIM-Signature` header) as ASCII int[], padded as per SHA-256 block size. /// @input emailHeaderLength Length of the email header including the SHA-256 padding. /// @input pubkey[k] RSA public key split into k chunks of n bits each. @@ -28,8 +30,10 @@ include "./utils/hash.circom"; /// @input emailBodyLength Length of the email body including the SHA-256 padding. /// @input bodyHashIndex Index of the body hash `bh` in the emailHeader. /// @input precomputedSHA[32] Precomputed SHA-256 hash of the email body till the bodyHashIndex. +/// @input mask[maxBodyLength] Mask for the email body. /// @output pubkeyHash Poseidon hash of the pubkey - Poseidon(n/2)(n/2 chunks of pubkey with k*2 bits per chunk). -template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck) { +/// @output maskedBody[maxBodyLength] Masked email body. +template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, turnOnBodyMasking) { assert(maxHeadersLength % 64 == 0); assert(maxBodyLength % 64 == 0); assert(n * k > 2048); // to support 2048 bit RSA @@ -122,8 +126,17 @@ template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashChec } computedBodyHashInts[i].out === headerBodyHash[i]; } - } + if (turnOnBodyMasking == 1) { + signal input mask[maxBodyLength]; + signal output maskedBody[maxBodyLength]; + component bodyMasker = BodyMasker(maxBodyLength); + + bodyMasker.body <== emailBody; + bodyMasker.mask <== mask; + maskedBody <== bodyMasker.masked_body; + } + } // Calculate the Poseidon hash of DKIM public key as output // This can be used to verify (by verifier/contract) the pubkey used in the proof without needing the full key diff --git a/packages/circuits/tests/email-verifier.test.ts b/packages/circuits/tests/email-verifier.test.ts index b87a02235..e70196df4 100644 --- a/packages/circuits/tests/email-verifier.test.ts +++ b/packages/circuits/tests/email-verifier.test.ts @@ -9,210 +9,263 @@ import { poseidonLarge } from "@zk-email/helpers/src/hash"; describe("EmailVerifier", () => { - jest.setTimeout(10 * 60 * 1000); // 10 minutes + jest.setTimeout(10 * 60 * 1000); // 10 minutes - let dkimResult: DKIMVerificationResult; - let circuit: any; + let dkimResult: DKIMVerificationResult; + let circuit: any; - beforeAll(async () => { + beforeAll(async () => { const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml")); - dkimResult = await verifyDKIMSignature(rawEmail); - - circuit = await wasm_tester( - path.join(__dirname, "./test-circuits/email-verifier-test.circom"), - { - // @dev During development recompile can be set to false if you are only making changes in the tests. - // This will save time by not recompiling the circuit every time. - // Compile: circom "./tests/email-verifier-test.circom" --r1cs --wasm --sym --c --wat --output "./tests/compiled-test-circuits" - recompile: true, - include: path.join(__dirname, "../../../node_modules"), - output: path.join(__dirname, "./compiled-test-circuits"), - } - ); - }); - - it("should verify email without any SHA precompute selector", async function () { + dkimResult = await verifyDKIMSignature(rawEmail); + + circuit = await wasm_tester( + path.join(__dirname, "./test-circuits/email-verifier-test.circom"), + { + // @dev During development recompile can be set to false if you are only making changes in the tests. + // This will save time by not recompiling the circuit every time. + // Compile: circom "./tests/email-verifier-test.circom" --r1cs --wasm --sym --c --wat --output "./tests/compiled-test-circuits" + recompile: true, + include: path.join(__dirname, "../../../node_modules"), + output: path.join(__dirname, "./compiled-test-circuits"), + } + ); + }); + + it("should verify email without any SHA precompute selector", async function () { const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, }); - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - }); + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + }); - it("should verify email with a SHA precompute selector", async function () { + it("should verify email with a SHA precompute selector", async function () { const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { - shaPrecomputeSelector: "How are", - maxHeadersLength: 640, - maxBodyLength: 768, + shaPrecomputeSelector: "How are", + maxHeadersLength: 640, + maxBodyLength: 768, }); - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - }); + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + }); - it("should fail if the rsa signature is wrong", async function () { - const invalidRSASignature = dkimResult.signature + 1n; + it("should fail if the rsa signature is wrong", async function () { + const invalidRSASignature = dkimResult.signature + 1n; const dkim = { ...dkimResult, signature: invalidRSASignature } const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, }); - expect.assertions(1); - try { - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - } catch (error) { - expect((error as Error).message).toMatch("Assert Failed"); - } - }); + expect.assertions(1); + try { + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + } catch (error) { + expect((error as Error).message).toMatch("Assert Failed"); + } + }); - it("should fail if message is tampered", async function () { - const invalidHeader = Buffer.from(dkimResult.headers); - invalidHeader[0] = 1; + it("should fail if message is tampered", async function () { + const invalidHeader = Buffer.from(dkimResult.headers); + invalidHeader[0] = 1; const dkim = { ...dkimResult, headers: invalidHeader } const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, }); - expect.assertions(1); - try { - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - } catch (error) { - expect((error as Error).message).toMatch("Assert Failed"); - } - }); + expect.assertions(1); + try { + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + } catch (error) { + expect((error as Error).message).toMatch("Assert Failed"); + } + }); - it("should fail if message padding is tampered", async function () { + it("should fail if message padding is tampered", async function () { const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, + }); + emailVerifierInputs.emailHeader[640 - 1] = "1"; + + expect.assertions(1); + try { + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + } catch (error) { + expect((error as Error).message).toMatch("Assert Failed"); + } }); - emailVerifierInputs.emailHeader[640 - 1] = "1"; - - expect.assertions(1); - try { - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - } catch (error) { - expect((error as Error).message).toMatch("Assert Failed"); - } - }); - it("should fail if body is tampered", async function () { - const invalidBody = Buffer.from(dkimResult.body); - invalidBody[invalidBody.length - 1] = 1; + it("should fail if body is tampered", async function () { + const invalidBody = Buffer.from(dkimResult.body); + invalidBody[invalidBody.length - 1] = 1; const dkim = { ...dkimResult, body: invalidBody } const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, }); - expect.assertions(1); - try { - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - } catch (error) { - expect((error as Error).message).toMatch("Assert Failed"); - } - }); + expect.assertions(1); + try { + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + } catch (error) { + expect((error as Error).message).toMatch("Assert Failed"); + } + }); - it("should fail if body padding is tampered", async function () { + it("should fail if body padding is tampered", async function () { const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, + }); + emailVerifierInputs.emailBody![768 - 1] = "1"; + + expect.assertions(1); + try { + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + } catch (error) { + expect((error as Error).message).toMatch("Assert Failed"); + } }); - emailVerifierInputs.emailBody![768 - 1] = "1"; - - expect.assertions(1); - try { - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - } catch (error) { - expect((error as Error).message).toMatch("Assert Failed"); - } - }); - it("should fail if body hash is tampered", async function () { - const invalidBodyHash = dkimResult.bodyHash + "a"; + it("should fail if body hash is tampered", async function () { + const invalidBodyHash = dkimResult.bodyHash + "a"; const dkim = { ...dkimResult, bodyHash: invalidBodyHash } const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { - maxHeadersLength: 640, - maxBodyLength: 768, + maxHeadersLength: 640, + maxBodyLength: 768, }); - expect.assertions(1); - try { - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - } catch (error) { - expect((error as Error).message).toMatch("Assert Failed"); - } - }); + expect.assertions(1); + try { + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + } catch (error) { + expect((error as Error).message).toMatch("Assert Failed"); + } + }); - it("should produce dkim pubkey hash correctly", async function () { + it("should produce dkim pubkey hash correctly", async function () { const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { - shaPrecomputeSelector: "How are", - maxHeadersLength: 640, - maxBodyLength: 768, + shaPrecomputeSelector: "How are", + maxHeadersLength: 640, + maxBodyLength: 768, }); - // Calculate the Poseidon hash with pubkey chunked to 9*242 like in circuit - const poseidonHash = await poseidonLarge(dkimResult.publicKey, 9, 242); + // Calculate the Poseidon hash with pubkey chunked to 9*242 like in circuit + const poseidonHash = await poseidonLarge(dkimResult.publicKey, 9, 242); - // Calculate the hash using the circuit - const witness = await circuit.calculateWitness(emailVerifierInputs); + // Calculate the hash using the circuit + const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.assertOut(witness, { - pubkeyHash: poseidonHash, + await circuit.assertOut(witness, { + pubkeyHash: poseidonHash, + }); }); - }); }); describe("EmailVerifier : Without body check", () => { - jest.setTimeout(10 * 60 * 1000); // 10 minutes + jest.setTimeout(10 * 60 * 1000); // 10 minutes - let dkimResult: DKIMVerificationResult; - let circuit: any; + let dkimResult: DKIMVerificationResult; + let circuit: any; - beforeAll(async () => { - const rawEmail = fs.readFileSync( - path.join(__dirname, "./test-emails/test.eml"), - "utf8" - ); - dkimResult = await verifyDKIMSignature(rawEmail); + beforeAll(async () => { + const rawEmail = fs.readFileSync( + path.join(__dirname, "./test-emails/test.eml"), + "utf8" + ); + dkimResult = await verifyDKIMSignature(rawEmail); - circuit = await wasm_tester( + circuit = await wasm_tester( path.join(__dirname, "./test-circuits/email-verifier-no-body-test.circom"), - { - recompile: true, - include: path.join(__dirname, "../../../node_modules"), - // output: path.join(__dirname, "./compiled-test-circuits"), - } - ); - }); - - it("should verify email when ignore_body_hash_check is true", async function () { - // The result wont have shaPrecomputeSelector, maxHeadersLength, maxBodyLength, ignoreBodyHashCheck + { + recompile: true, + include: path.join(__dirname, "../../../node_modules"), + // output: path.join(__dirname, "./compiled-test-circuits"), + } + ); + }); + + it("should verify email when ignore_body_hash_check is true", async function () { + // The result wont have shaPrecomputeSelector, maxHeadersLength, maxBodyLength, ignoreBodyHashCheck const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { - maxHeadersLength: 640, - maxBodyLength: 768, - ignoreBodyHashCheck: true, + maxHeadersLength: 640, + maxBodyLength: 768, + ignoreBodyHashCheck: true, + }); + + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); }); +}); - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - }); +describe("EmailVerifier : With body masking", () => { + jest.setTimeout(10 * 60 * 1000); // 10 minutes + + let dkimResult: DKIMVerificationResult; + let circuit: any; + + beforeAll(async () => { + const rawEmail = fs.readFileSync( + path.join(__dirname, "./test-emails/test.eml") + ); + dkimResult = await verifyDKIMSignature(rawEmail); + + circuit = await wasm_tester( + path.join( + __dirname, + "./test-circuits/email-verifier-with-mask-test.circom" + ), + { + recompile: true, + include: path.join(__dirname, "../../../node_modules"), + output: path.join(__dirname, "./compiled-test-circuits"), + } + ); + }); + + it("should verify email with body masking", async function () { + const mask = Array.from({ length: 768 }, (_, i) => + i > 25 && i < 50 ? 1 : 0 + ); + + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { + maxHeadersLength: 640, + maxBodyLength: 768, + ignoreBodyHashCheck: false, + turnOnBodyMasking: true, + mask, + } + ); + + const expectedMaskedBody = emailVerifierInputs.emailBody!.map( + (byte, i) => (mask[i] === 1 ? byte : 0) + ); + + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + await circuit.assertOut(witness, { + maskedBody: expectedMaskedBody, + }); + }); }); diff --git a/packages/circuits/tests/test-circuits/email-verifier-no-body-test.circom b/packages/circuits/tests/test-circuits/email-verifier-no-body-test.circom index 9ff2f64b0..622de30f4 100644 --- a/packages/circuits/tests/test-circuits/email-verifier-no-body-test.circom +++ b/packages/circuits/tests/test-circuits/email-verifier-no-body-test.circom @@ -2,4 +2,4 @@ pragma circom 2.1.6; include "../../email-verifier.circom"; -component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 1); +component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 1, 0); diff --git a/packages/circuits/tests/test-circuits/email-verifier-test.circom b/packages/circuits/tests/test-circuits/email-verifier-test.circom index 58772343e..f027d66de 100644 --- a/packages/circuits/tests/test-circuits/email-verifier-test.circom +++ b/packages/circuits/tests/test-circuits/email-verifier-test.circom @@ -2,4 +2,4 @@ pragma circom 2.1.6; include "../../email-verifier.circom"; -component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 0); +component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 0, 0); diff --git a/packages/circuits/tests/test-circuits/email-verifier-with-mask-test.circom b/packages/circuits/tests/test-circuits/email-verifier-with-mask-test.circom new file mode 100644 index 000000000..a785c0d91 --- /dev/null +++ b/packages/circuits/tests/test-circuits/email-verifier-with-mask-test.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.6; + +include "../../email-verifier.circom"; + +component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 0, 1); diff --git a/packages/helpers/src/input-generators.ts b/packages/helpers/src/input-generators.ts index 42cd26c9b..65262b99c 100644 --- a/packages/helpers/src/input-generators.ts +++ b/packages/helpers/src/input-generators.ts @@ -12,13 +12,16 @@ type CircuitInput = { emailBodyLength?: string; precomputedSHA?: string[]; bodyHashIndex?: string; + mask?: number[]; }; type InputGenerationArgs = { ignoreBodyHashCheck?: boolean; + turnOnBodyMasking?: boolean; shaPrecomputeSelector?: string; maxHeadersLength?: number; // Max length of the email header including padding maxBodyLength?: number; // Max length of the email body after shaPrecomputeSelector including padding + mask?: number[]; }; /** @@ -97,6 +100,10 @@ export function generateEmailVerifierInputsFromDKIMResult( circuitInputs.precomputedSHA = Uint8ArrayToCharArray(precomputedSha); circuitInputs.bodyHashIndex = bodyHashIndex.toString(); circuitInputs.emailBody = Uint8ArrayToCharArray(bodyRemaining); + + if (params.turnOnBodyMasking) { + circuitInputs.mask = params.mask; + } } return circuitInputs;