From d743b0d197e0456539455b9d4ce01a2d8f87af85 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Sat, 13 Jul 2024 16:59:22 +0200 Subject: [PATCH 1/6] feat: added body masking template --- packages/circuits/helpers/body-masker.circom | 11 +++++++ packages/circuits/tests/body-masker.test.ts | 30 +++++++++++++++++++ .../test-circuits/body-masker-test.circom | 5 ++++ 3 files changed, 46 insertions(+) create mode 100644 packages/circuits/helpers/body-masker.circom create mode 100644 packages/circuits/tests/body-masker.test.ts create mode 100644 packages/circuits/tests/test-circuits/body-masker-test.circom diff --git a/packages/circuits/helpers/body-masker.circom b/packages/circuits/helpers/body-masker.circom new file mode 100644 index 00000000..f3178ddc --- /dev/null +++ b/packages/circuits/helpers/body-masker.circom @@ -0,0 +1,11 @@ +pragma circom 2.1.6; + +template BodyMasker(maxBodyLength) { + signal input body[maxBodyLength]; + signal input mask[maxBodyLength]; + signal output masked_body[maxBodyLength]; + + for (var i = 0; i < maxBodyLength; i++) { + masked_body[i] <== body[i] * mask[i]; + } +} \ No newline at end of file diff --git a/packages/circuits/tests/body-masker.test.ts b/packages/circuits/tests/body-masker.test.ts new file mode 100644 index 00000000..7e966a0c --- /dev/null +++ b/packages/circuits/tests/body-masker.test.ts @@ -0,0 +1,30 @@ +import { wasm as wasm_tester } from "circom_tester"; +import path from "path"; + +describe("BodyMasker Circuit", () => { + let circuit: any; + + beforeAll(async () => { + circuit = await wasm_tester( + path.join(__dirname, "./test-circuits/body-masker-test.circom"), + { + recompile: true, + include: path.join(__dirname, "../../../node_modules"), + output: path.join(__dirname, "./compiled-test-circuits"), + } + ); + }); + + it("should mask the body correctly", async () => { + const input = { + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + mask: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + }; + + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + await circuit.assertOut(witness, { + masked_body: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], + }); + }); +}); diff --git a/packages/circuits/tests/test-circuits/body-masker-test.circom b/packages/circuits/tests/test-circuits/body-masker-test.circom new file mode 100644 index 00000000..7066b248 --- /dev/null +++ b/packages/circuits/tests/test-circuits/body-masker-test.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.6; + +include "../../helpers/body-masker.circom"; + +component main = BodyMasker(10); \ No newline at end of file From c0a68f9d78ad637020e4649fd58d3b958791c251 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Sat, 13 Jul 2024 19:54:34 +0200 Subject: [PATCH 2/6] feat: upstreamed to email-verifier+ tests --- packages/circuits/email-verifier.circom | 17 +- .../circuits/tests/email-verifier.test.ts | 339 ++++++++++-------- .../email-verifier-no-body-test.circom | 2 +- .../test-circuits/email-verifier-test.circom | 2 +- .../email-verifier-with-mask-test.circom | 5 + ...verifier-with-soft-line-breaks-test.circom | 2 +- packages/helpers/src/input-generators.ts | 7 + 7 files changed, 226 insertions(+), 148 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 a74763cb..74c45d1c 100644 --- a/packages/circuits/email-verifier.circom +++ b/packages/circuits/email-verifier.circom @@ -10,6 +10,7 @@ include "./utils/array.circom"; include "./utils/regex.circom"; include "./utils/hash.circom"; include "./helpers/remove-soft-line-breaks.circom"; +include "./helpers/body-masker.circom"; /// @title EmailVerifier @@ -22,6 +23,7 @@ include "./helpers/remove-soft-line-breaks.circom"; /// @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 removeSoftLineBreaks Set 1 to remove soft line breaks from the email body. +/// @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. @@ -31,9 +33,11 @@ include "./helpers/remove-soft-line-breaks.circom"; /// @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 decodedEmailBodyIn[maxBodyLength] Decoded email body without soft line breaks. +/// @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). /// @output decodedEmailBodyOut[maxBodyLength] Decoded email body with soft line breaks removed. -template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, removeSoftLineBreaks) { +/// @output maskedBody[maxBodyLength] Masked email body. +template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, removeSoftLineBreaks, turnOnBodyMasking) { assert(maxHeadersLength % 64 == 0); assert(maxBodyLength % 64 == 0); assert(n * k > 2048); // to support 2048 bit RSA @@ -139,8 +143,17 @@ template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashChec decodedEmailBodyOut <== qpEncodingChecker.decoded; } - } + 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 6030d928..3298b659 100644 --- a/packages/circuits/tests/email-verifier.test.ts +++ b/packages/circuits/tests/email-verifier.test.ts @@ -9,212 +9,265 @@ 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"), - } - ); - }); + 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 () { + 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"), - } - ); - }); + { + 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 + 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, + }); + }); }); describe('EmailVerifier : With soft line breaks', () => { 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 622de30f..39c19114 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, 0); +component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 1, 0, 0); diff --git a/packages/circuits/tests/test-circuits/email-verifier-test.circom b/packages/circuits/tests/test-circuits/email-verifier-test.circom index f027d66d..858a5443 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, 0); +component main { public [ pubkey ] } = EmailVerifier(640, 768, 121, 17, 0, 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 00000000..66181b39 --- /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, 0, 1); diff --git a/packages/circuits/tests/test-circuits/email-verifier-with-soft-line-breaks-test.circom b/packages/circuits/tests/test-circuits/email-verifier-with-soft-line-breaks-test.circom index f7605912..be74589d 100644 --- a/packages/circuits/tests/test-circuits/email-verifier-with-soft-line-breaks-test.circom +++ b/packages/circuits/tests/test-circuits/email-verifier-with-soft-line-breaks-test.circom @@ -2,4 +2,4 @@ pragma circom 2.1.6; include "../../email-verifier.circom"; -component main { public [ pubkey ] } = EmailVerifier(640, 1408, 121, 17, 0, 1); +component main { public [ pubkey ] } = EmailVerifier(640, 1408, 121, 17, 0, 1, 0); diff --git a/packages/helpers/src/input-generators.ts b/packages/helpers/src/input-generators.ts index 0a79b8bc..5b720b6a 100644 --- a/packages/helpers/src/input-generators.ts +++ b/packages/helpers/src/input-generators.ts @@ -13,14 +13,17 @@ type CircuitInput = { precomputedSHA?: string[]; bodyHashIndex?: string; decodedEmailBodyIn?: 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 removeSoftLineBreaks?: boolean; + mask?: number[]; }; function removeSoftLineBreaks(body: string[]): string[] { @@ -125,6 +128,10 @@ export function generateEmailVerifierInputsFromDKIMResult( if (params.removeSoftLineBreaks) { circuitInputs.decodedEmailBodyIn = removeSoftLineBreaks(circuitInputs.emailBody); } + + if (params.turnOnBodyMasking) { + circuitInputs.mask = params.mask; + } } return circuitInputs; From e0bff6005745b309016021eefcb13825223dc15a Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Sun, 14 Jul 2024 01:22:31 +0200 Subject: [PATCH 3/6] fix: added a assert-bit check in body-masker --- packages/circuits/helpers/body-masker.circom | 26 ++++++++++++++++++++ packages/circuits/tests/body-masker.test.ts | 17 +++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/circuits/helpers/body-masker.circom b/packages/circuits/helpers/body-masker.circom index f3178ddc..474aa237 100644 --- a/packages/circuits/helpers/body-masker.circom +++ b/packages/circuits/helpers/body-masker.circom @@ -1,11 +1,37 @@ pragma circom 2.1.6; +// Asserts that a given input is binary. +// +// Inputs: +// - in: an input signal, expected to be 0 or 1. +template AssertBit() { + signal input in; + in * (in - 1) === 0; +} + +// The BodyMasker template masks an input body array using a binary mask array. +// Each element in the body array is multiplied by the corresponding element in the mask array. +// The mask array is validated to ensure all elements are binary (0 or 1). +// +// Parameters: +// - maxBodyLength: The maximum length of the body and mask arrays. +// +// Inputs: +// - body: An array of signals representing the body to be masked. +// - mask: An array of signals representing the binary mask. +// +// Outputs: +// - masked_body: An array of signals representing the masked body. template BodyMasker(maxBodyLength) { signal input body[maxBodyLength]; signal input mask[maxBodyLength]; signal output masked_body[maxBodyLength]; + component bit_check[maxBodyLength]; + for (var i = 0; i < maxBodyLength; i++) { + bit_check[i] = AssertBit(); + bit_check[i].in <== mask[i]; masked_body[i] <== body[i] * mask[i]; } } \ No newline at end of file diff --git a/packages/circuits/tests/body-masker.test.ts b/packages/circuits/tests/body-masker.test.ts index 7e966a0c..aea8f3a7 100644 --- a/packages/circuits/tests/body-masker.test.ts +++ b/packages/circuits/tests/body-masker.test.ts @@ -27,4 +27,21 @@ describe("BodyMasker Circuit", () => { masked_body: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], }); }); + + it("should fail if mask has non-bit numbers", async () => { + const input = { + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + mask: [1, 2, 1, 0, 1, 0, 1, 0, 1, 0], // Mask with non-bit number (2) + }; + + try { + const witness = await circuit.calculateWitness(input); + await circuit.checkConstraints(witness); + await circuit.assertOut(witness, { + masked_body: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], + }); + } catch (error) { + expect(error).toBeTruthy(); + } + }); }); From f7cb95884a9e6b3dc09c1f2de73080ed01dc2ca1 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Sun, 14 Jul 2024 01:31:32 +0200 Subject: [PATCH 4/6] fix: updated mask type --- packages/circuits/tests/email-verifier.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/circuits/tests/email-verifier.test.ts b/packages/circuits/tests/email-verifier.test.ts index 3298b659..3426b6dc 100644 --- a/packages/circuits/tests/email-verifier.test.ts +++ b/packages/circuits/tests/email-verifier.test.ts @@ -254,7 +254,7 @@ describe("EmailVerifier : With body masking", () => { maxBodyLength: 768, ignoreBodyHashCheck: false, turnOnBodyMasking: true, - mask, + mask: mask.map((value) => value ? 1 : 0), } ); From 52cc15dde6e67cf890842c571ed850dd1ca6706a Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Fri, 2 Aug 2024 09:41:29 +0530 Subject: [PATCH 5/6] chore: turnOnBodyMasking -> enableBodyMasking name change --- packages/circuits/email-verifier.circom | 6 +- .../circuits/tests/email-verifier.test.ts | 173 ++++++++------- packages/helpers/src/input-generators.ts | 197 +++++++++--------- 3 files changed, 205 insertions(+), 171 deletions(-) diff --git a/packages/circuits/email-verifier.circom b/packages/circuits/email-verifier.circom index 74c45d1c..e67bbb99 100644 --- a/packages/circuits/email-verifier.circom +++ b/packages/circuits/email-verifier.circom @@ -23,7 +23,7 @@ include "./helpers/body-masker.circom"; /// @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 removeSoftLineBreaks Set 1 to remove soft line breaks from the email body. -/// @param turnOnBodyMasking Set 1 to turn on body masking. +/// @param enableBodyMasking 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. @@ -37,7 +37,7 @@ include "./helpers/body-masker.circom"; /// @output pubkeyHash Poseidon hash of the pubkey - Poseidon(n/2)(n/2 chunks of pubkey with k*2 bits per chunk). /// @output decodedEmailBodyOut[maxBodyLength] Decoded email body with soft line breaks removed. /// @output maskedBody[maxBodyLength] Masked email body. -template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, removeSoftLineBreaks, turnOnBodyMasking) { +template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, removeSoftLineBreaks, enableBodyMasking) { assert(maxHeadersLength % 64 == 0); assert(maxBodyLength % 64 == 0); assert(n * k > 2048); // to support 2048 bit RSA @@ -144,7 +144,7 @@ template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashChec decodedEmailBodyOut <== qpEncodingChecker.decoded; } - if (turnOnBodyMasking == 1) { + if (enableBodyMasking == 1) { signal input mask[maxBodyLength]; signal output maskedBody[maxBodyLength]; component bodyMasker = BodyMasker(maxBodyLength); diff --git a/packages/circuits/tests/email-verifier.test.ts b/packages/circuits/tests/email-verifier.test.ts index 3426b6dc..e5a26bf9 100644 --- a/packages/circuits/tests/email-verifier.test.ts +++ b/packages/circuits/tests/email-verifier.test.ts @@ -7,7 +7,6 @@ import { generateEmailVerifierInputsFromDKIMResult } from "@zk-email/helpers/src import { verifyDKIMSignature } from "@zk-email/helpers/src/dkim"; import { poseidonLarge } from "@zk-email/helpers/src/hash"; - describe("EmailVerifier", () => { jest.setTimeout(10 * 60 * 1000); // 10 minutes @@ -15,7 +14,9 @@ describe("EmailVerifier", () => { let circuit: any; beforeAll(async () => { - const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml")); + const rawEmail = fs.readFileSync( + path.join(__dirname, "./test-emails/test.eml") + ); dkimResult = await verifyDKIMSignature(rawEmail); circuit = await wasm_tester( @@ -32,21 +33,27 @@ describe("EmailVerifier", () => { }); it("should verify email without any SHA precompute selector", async function () { - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); const witness = await circuit.calculateWitness(emailVerifierInputs); await circuit.checkConstraints(witness); }); it("should verify email with a SHA precompute selector", async function () { - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { shaPrecomputeSelector: "How are", maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); const witness = await circuit.calculateWitness(emailVerifierInputs); await circuit.checkConstraints(witness); @@ -54,12 +61,15 @@ describe("EmailVerifier", () => { it("should fail if the rsa signature is wrong", async function () { const invalidRSASignature = dkimResult.signature + 1n; - const dkim = { ...dkimResult, signature: invalidRSASignature } + const dkim = { ...dkimResult, signature: invalidRSASignature }; - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkim, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); expect.assertions(1); try { @@ -74,12 +84,15 @@ describe("EmailVerifier", () => { const invalidHeader = Buffer.from(dkimResult.headers); invalidHeader[0] = 1; - const dkim = { ...dkimResult, headers: invalidHeader } + const dkim = { ...dkimResult, headers: invalidHeader }; - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkim, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); expect.assertions(1); try { @@ -91,10 +104,13 @@ describe("EmailVerifier", () => { }); it("should fail if message padding is tampered", async function () { - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); emailVerifierInputs.emailHeader[640 - 1] = "1"; expect.assertions(1); @@ -110,12 +126,15 @@ describe("EmailVerifier", () => { const invalidBody = Buffer.from(dkimResult.body); invalidBody[invalidBody.length - 1] = 1; - const dkim = { ...dkimResult, body: invalidBody } + const dkim = { ...dkimResult, body: invalidBody }; - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkim, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); expect.assertions(1); try { @@ -127,10 +146,13 @@ describe("EmailVerifier", () => { }); it("should fail if body padding is tampered", async function () { - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); emailVerifierInputs.emailBody![768 - 1] = "1"; expect.assertions(1); @@ -145,12 +167,15 @@ describe("EmailVerifier", () => { it("should fail if body hash is tampered", async function () { const invalidBodyHash = dkimResult.bodyHash + "a"; - const dkim = { ...dkimResult, bodyHash: invalidBodyHash } + const dkim = { ...dkimResult, bodyHash: invalidBodyHash }; - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkim, + { maxHeadersLength: 640, maxBodyLength: 768, - }); + } + ); expect.assertions(1); try { @@ -162,11 +187,14 @@ describe("EmailVerifier", () => { }); it("should produce dkim pubkey hash correctly", async function () { - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { 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); @@ -180,7 +208,6 @@ describe("EmailVerifier", () => { }); }); - describe("EmailVerifier : Without body check", () => { jest.setTimeout(10 * 60 * 1000); // 10 minutes @@ -195,7 +222,10 @@ describe("EmailVerifier : Without body check", () => { dkimResult = await verifyDKIMSignature(rawEmail); circuit = await wasm_tester( - path.join(__dirname, "./test-circuits/email-verifier-no-body-test.circom"), + path.join( + __dirname, + "./test-circuits/email-verifier-no-body-test.circom" + ), { recompile: true, include: path.join(__dirname, "../../../node_modules"), @@ -206,11 +236,14 @@ describe("EmailVerifier : Without body check", () => { 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, { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { maxHeadersLength: 640, maxBodyLength: 768, ignoreBodyHashCheck: true, - }); + } + ); const witness = await circuit.calculateWitness(emailVerifierInputs); await circuit.checkConstraints(witness); @@ -253,8 +286,8 @@ describe("EmailVerifier : With body masking", () => { maxHeadersLength: 640, maxBodyLength: 768, ignoreBodyHashCheck: false, - turnOnBodyMasking: true, - mask: mask.map((value) => value ? 1 : 0), + enableBodyMasking: true, + mask: mask.map((value) => (value ? 1 : 0)), } ); @@ -270,44 +303,44 @@ describe("EmailVerifier : With body masking", () => { }); }); -describe('EmailVerifier : With soft line breaks', () => { - jest.setTimeout(10 * 60 * 1000); // 10 minutes - - let dkimResult: DKIMVerificationResult; - let circuit: any; - - beforeAll(async () => { - const rawEmail = fs.readFileSync( - path.join(__dirname, './test-emails/lorem_ipsum.eml'), - 'utf8' - ); - dkimResult = await verifyDKIMSignature(rawEmail); - - circuit = await wasm_tester( - path.join( - __dirname, - './test-circuits/email-verifier-with-soft-line-breaks-test.circom' - ), - { - recompile: true, - include: path.join(__dirname, '../../../node_modules'), - output: path.join(__dirname, "./compiled-test-circuits"), - } - ); - }); - - it('should verify email when removeSoftLineBreaks is true', async function () { - const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( - dkimResult, - { - maxHeadersLength: 640, - maxBodyLength: 1408, - ignoreBodyHashCheck: false, - removeSoftLineBreaks: true, - } - ); - - const witness = await circuit.calculateWitness(emailVerifierInputs); - await circuit.checkConstraints(witness); - }); +describe("EmailVerifier : With soft line breaks", () => { + jest.setTimeout(10 * 60 * 1000); // 10 minutes + + let dkimResult: DKIMVerificationResult; + let circuit: any; + + beforeAll(async () => { + const rawEmail = fs.readFileSync( + path.join(__dirname, "./test-emails/lorem_ipsum.eml"), + "utf8" + ); + dkimResult = await verifyDKIMSignature(rawEmail); + + circuit = await wasm_tester( + path.join( + __dirname, + "./test-circuits/email-verifier-with-soft-line-breaks-test.circom" + ), + { + recompile: true, + include: path.join(__dirname, "../../../node_modules"), + output: path.join(__dirname, "./compiled-test-circuits"), + } + ); + }); + + it("should verify email when removeSoftLineBreaks is true", async function () { + const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult( + dkimResult, + { + maxHeadersLength: 640, + maxBodyLength: 1408, + ignoreBodyHashCheck: false, + removeSoftLineBreaks: true, + } + ); + + const witness = await circuit.calculateWitness(emailVerifierInputs); + await circuit.checkConstraints(witness); + }); }); diff --git a/packages/helpers/src/input-generators.ts b/packages/helpers/src/input-generators.ts index 5b720b6a..13a7407b 100644 --- a/packages/helpers/src/input-generators.ts +++ b/packages/helpers/src/input-generators.ts @@ -1,51 +1,54 @@ -import { Uint8ArrayToCharArray, toCircomBigIntBytes } from './binary-format'; -import { MAX_BODY_PADDED_BYTES, MAX_HEADER_PADDED_BYTES } from './constants'; -import { DKIMVerificationResult, verifyDKIMSignature } from './dkim'; -import { generatePartialSHA, sha256Pad } from './sha-utils'; +import { Uint8ArrayToCharArray, toCircomBigIntBytes } from "./binary-format"; +import { MAX_BODY_PADDED_BYTES, MAX_HEADER_PADDED_BYTES } from "./constants"; +import { DKIMVerificationResult, verifyDKIMSignature } from "./dkim"; +import { generatePartialSHA, sha256Pad } from "./sha-utils"; type CircuitInput = { - emailHeader: string[]; - emailHeaderLength: string; - pubkey: string[]; - signature: string[]; - emailBody?: string[]; - emailBodyLength?: string; - precomputedSHA?: string[]; - bodyHashIndex?: string; - decodedEmailBodyIn?: string[]; - mask?: number[]; + emailHeader: string[]; + emailHeaderLength: string; + pubkey: string[]; + signature: string[]; + emailBody?: string[]; + emailBodyLength?: string; + precomputedSHA?: string[]; + bodyHashIndex?: string; + decodedEmailBodyIn?: 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 - removeSoftLineBreaks?: boolean; - mask?: number[]; + ignoreBodyHashCheck?: boolean; + enableBodyMasking?: 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 + removeSoftLineBreaks?: boolean; + mask?: number[]; }; function removeSoftLineBreaks(body: string[]): string[] { - const result = []; - let i = 0; - while (i < body.length) { - if (i + 2 < body.length && - body[i] === '61' && // '=' character - body[i + 1] === '13' && // '\r' character - body[i + 2] === '10') { // '\n' character - // Skip the soft line break sequence - i += 3; // Move past the soft line break - } else { - result.push(body[i]); - i++; + const result = []; + let i = 0; + while (i < body.length) { + if ( + i + 2 < body.length && + body[i] === "61" && // '=' character + body[i + 1] === "13" && // '\r' character + body[i + 2] === "10" + ) { + // '\n' character + // Skip the soft line break sequence + i += 3; // Move past the soft line break + } else { + result.push(body[i]); + i++; + } } - } - // Pad the result with zeros to make it the same length as the body - while (result.length < body.length) { - result.push('0'); - } - return result; + // Pad the result with zeros to make it the same length as the body + while (result.length < body.length) { + result.push("0"); + } + return result; } /** @@ -56,15 +59,12 @@ function removeSoftLineBreaks(body: string[]): string[] { * @returns Circuit inputs for the EmailVerifier circuit */ export async function generateEmailVerifierInputs( - rawEmail: Buffer | string, - params: InputGenerationArgs = {}, + rawEmail: Buffer | string, + params: InputGenerationArgs = {} ) { - const dkimResult = await verifyDKIMSignature(rawEmail); + const dkimResult = await verifyDKIMSignature(rawEmail); - return generateEmailVerifierInputsFromDKIMResult( - dkimResult, - params, - ); + return generateEmailVerifierInputsFromDKIMResult(dkimResult, params); } /** @@ -75,64 +75,65 @@ export async function generateEmailVerifierInputs( * @returns Circuit inputs for the EmailVerifier circuit */ export function generateEmailVerifierInputsFromDKIMResult( - dkimResult: DKIMVerificationResult, - params: InputGenerationArgs = {}, + dkimResult: DKIMVerificationResult, + params: InputGenerationArgs = {} ): CircuitInput { - const { - headers, body, bodyHash, publicKey, signature, - } = dkimResult; - - // SHA add padding - const [messagePadded, messagePaddedLen] = sha256Pad( - headers, - params.maxHeadersLength || MAX_HEADER_PADDED_BYTES, - ); - - const circuitInputs: CircuitInput = { - emailHeader: Uint8ArrayToCharArray(messagePadded), // Packed into 1 byte signals - emailHeaderLength: messagePaddedLen.toString(), - pubkey: toCircomBigIntBytes(publicKey), - signature: toCircomBigIntBytes(signature), - }; - - if (!params.ignoreBodyHashCheck) { - if (!body || !bodyHash) { - throw new Error( - 'body and bodyHash are required when ignoreBodyHashCheck is false', - ); - } - - const bodyHashIndex = headers.toString().indexOf(bodyHash); - const maxBodyLength = params.maxBodyLength || MAX_BODY_PADDED_BYTES; + const { headers, body, bodyHash, publicKey, signature } = dkimResult; - // 65 comes from the 64 at the end and the 1 bit in the start, then 63 comes from the formula to round it up to the nearest 64. - // see sha256algorithm.com for a more full explanation of padding length - const bodySHALength = Math.floor((body.length + 63 + 65) / 64) * 64; - const [bodyPadded, bodyPaddedLen] = sha256Pad( - body, - Math.max(maxBodyLength, bodySHALength), + // SHA add padding + const [messagePadded, messagePaddedLen] = sha256Pad( + headers, + params.maxHeadersLength || MAX_HEADER_PADDED_BYTES ); - const { precomputedSha, bodyRemaining, bodyRemainingLength } = generatePartialSHA({ - body: bodyPadded, - bodyLength: bodyPaddedLen, - selectorString: params.shaPrecomputeSelector, - maxRemainingBodyLength: maxBodyLength, - }); + const circuitInputs: CircuitInput = { + emailHeader: Uint8ArrayToCharArray(messagePadded), // Packed into 1 byte signals + emailHeaderLength: messagePaddedLen.toString(), + pubkey: toCircomBigIntBytes(publicKey), + signature: toCircomBigIntBytes(signature), + }; - circuitInputs.emailBodyLength = bodyRemainingLength.toString(); - circuitInputs.precomputedSHA = Uint8ArrayToCharArray(precomputedSha); - circuitInputs.bodyHashIndex = bodyHashIndex.toString(); - circuitInputs.emailBody = Uint8ArrayToCharArray(bodyRemaining); + if (!params.ignoreBodyHashCheck) { + if (!body || !bodyHash) { + throw new Error( + "body and bodyHash are required when ignoreBodyHashCheck is false" + ); + } - if (params.removeSoftLineBreaks) { - circuitInputs.decodedEmailBodyIn = removeSoftLineBreaks(circuitInputs.emailBody); - } - - if (params.turnOnBodyMasking) { - circuitInputs.mask = params.mask; + const bodyHashIndex = headers.toString().indexOf(bodyHash); + const maxBodyLength = params.maxBodyLength || MAX_BODY_PADDED_BYTES; + + // 65 comes from the 64 at the end and the 1 bit in the start, then 63 comes from the formula to round it up to the nearest 64. + // see sha256algorithm.com for a more full explanation of padding length + const bodySHALength = Math.floor((body.length + 63 + 65) / 64) * 64; + const [bodyPadded, bodyPaddedLen] = sha256Pad( + body, + Math.max(maxBodyLength, bodySHALength) + ); + + const { precomputedSha, bodyRemaining, bodyRemainingLength } = + generatePartialSHA({ + body: bodyPadded, + bodyLength: bodyPaddedLen, + selectorString: params.shaPrecomputeSelector, + maxRemainingBodyLength: maxBodyLength, + }); + + circuitInputs.emailBodyLength = bodyRemainingLength.toString(); + circuitInputs.precomputedSHA = Uint8ArrayToCharArray(precomputedSha); + circuitInputs.bodyHashIndex = bodyHashIndex.toString(); + circuitInputs.emailBody = Uint8ArrayToCharArray(bodyRemaining); + + if (params.removeSoftLineBreaks) { + circuitInputs.decodedEmailBodyIn = removeSoftLineBreaks( + circuitInputs.emailBody + ); + } + + if (params.enableBodyMasking) { + circuitInputs.mask = params.mask; + } } - } - return circuitInputs; + return circuitInputs; } From f6a0698197973c9f7175e5fc6049a94f0a34de58 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Fri, 2 Aug 2024 09:51:33 +0530 Subject: [PATCH 6/6] chore: minor refactoring --- packages/circuits/email-verifier.circom | 10 ++--- packages/circuits/helpers/body-masker.circom | 37 ------------------- packages/circuits/tests/body-masker.test.ts | 6 +-- .../test-circuits/body-masker-test.circom | 4 +- packages/circuits/utils/bytes.circom | 36 ++++++++++++++++++ 5 files changed, 46 insertions(+), 47 deletions(-) delete mode 100644 packages/circuits/helpers/body-masker.circom diff --git a/packages/circuits/email-verifier.circom b/packages/circuits/email-verifier.circom index e67bbb99..51a495b9 100644 --- a/packages/circuits/email-verifier.circom +++ b/packages/circuits/email-verifier.circom @@ -9,8 +9,8 @@ include "./lib/sha.circom"; include "./utils/array.circom"; include "./utils/regex.circom"; include "./utils/hash.circom"; +include "./utils/bytes.circom"; include "./helpers/remove-soft-line-breaks.circom"; -include "./helpers/body-masker.circom"; /// @title EmailVerifier @@ -147,11 +147,11 @@ template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashChec if (enableBodyMasking == 1) { signal input mask[maxBodyLength]; signal output maskedBody[maxBodyLength]; - component bodyMasker = BodyMasker(maxBodyLength); + component byteMask = ByteMask(maxBodyLength); - bodyMasker.body <== emailBody; - bodyMasker.mask <== mask; - maskedBody <== bodyMasker.masked_body; + byteMask.body <== emailBody; + byteMask.mask <== mask; + maskedBody <== byteMask.maskedBody; } } diff --git a/packages/circuits/helpers/body-masker.circom b/packages/circuits/helpers/body-masker.circom deleted file mode 100644 index 474aa237..00000000 --- a/packages/circuits/helpers/body-masker.circom +++ /dev/null @@ -1,37 +0,0 @@ -pragma circom 2.1.6; - -// Asserts that a given input is binary. -// -// Inputs: -// - in: an input signal, expected to be 0 or 1. -template AssertBit() { - signal input in; - in * (in - 1) === 0; -} - -// The BodyMasker template masks an input body array using a binary mask array. -// Each element in the body array is multiplied by the corresponding element in the mask array. -// The mask array is validated to ensure all elements are binary (0 or 1). -// -// Parameters: -// - maxBodyLength: The maximum length of the body and mask arrays. -// -// Inputs: -// - body: An array of signals representing the body to be masked. -// - mask: An array of signals representing the binary mask. -// -// Outputs: -// - masked_body: An array of signals representing the masked body. -template BodyMasker(maxBodyLength) { - signal input body[maxBodyLength]; - signal input mask[maxBodyLength]; - signal output masked_body[maxBodyLength]; - - component bit_check[maxBodyLength]; - - for (var i = 0; i < maxBodyLength; i++) { - bit_check[i] = AssertBit(); - bit_check[i].in <== mask[i]; - masked_body[i] <== body[i] * mask[i]; - } -} \ No newline at end of file diff --git a/packages/circuits/tests/body-masker.test.ts b/packages/circuits/tests/body-masker.test.ts index aea8f3a7..c1fd513b 100644 --- a/packages/circuits/tests/body-masker.test.ts +++ b/packages/circuits/tests/body-masker.test.ts @@ -1,7 +1,7 @@ import { wasm as wasm_tester } from "circom_tester"; import path from "path"; -describe("BodyMasker Circuit", () => { +describe("ByteMask Circuit", () => { let circuit: any; beforeAll(async () => { @@ -24,7 +24,7 @@ describe("BodyMasker Circuit", () => { const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); await circuit.assertOut(witness, { - masked_body: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], + maskedBody: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], }); }); @@ -38,7 +38,7 @@ describe("BodyMasker Circuit", () => { const witness = await circuit.calculateWitness(input); await circuit.checkConstraints(witness); await circuit.assertOut(witness, { - masked_body: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], + maskedBody: [1, 0, 3, 0, 5, 0, 7, 0, 9, 0], }); } catch (error) { expect(error).toBeTruthy(); diff --git a/packages/circuits/tests/test-circuits/body-masker-test.circom b/packages/circuits/tests/test-circuits/body-masker-test.circom index 7066b248..217241eb 100644 --- a/packages/circuits/tests/test-circuits/body-masker-test.circom +++ b/packages/circuits/tests/test-circuits/body-masker-test.circom @@ -1,5 +1,5 @@ pragma circom 2.1.6; -include "../../helpers/body-masker.circom"; +include "../../utils/bytes.circom"; -component main = BodyMasker(10); \ No newline at end of file +component main = ByteMask(10); \ No newline at end of file diff --git a/packages/circuits/utils/bytes.circom b/packages/circuits/utils/bytes.circom index d4d96027..a394ce2e 100644 --- a/packages/circuits/utils/bytes.circom +++ b/packages/circuits/utils/bytes.circom @@ -147,3 +147,39 @@ template SplitBytesToWords (l,n,k) { out[i] <== bits2num[i].out; } } + +// Asserts that a given input is binary. +// +// Inputs: +// - in: an input signal, expected to be 0 or 1. +template AssertBit() { + signal input in; + in * (in - 1) === 0; +} + +// The ByteMask template masks an input body array using a binary mask array. +// Each element in the body array is multiplied by the corresponding element in the mask array. +// The mask array is validated to ensure all elements are binary (0 or 1). +// +// Parameters: +// - maxBodyLength: The maximum length of the body and mask arrays. +// +// Inputs: +// - body: An array of signals representing the body to be masked. +// - mask: An array of signals representing the binary mask. +// +// Outputs: +// - maskedBody: An array of signals representing the masked body. +template ByteMask(maxBodyLength) { + signal input body[maxBodyLength]; + signal input mask[maxBodyLength]; + signal output maskedBody[maxBodyLength]; + + component bit_check[maxBodyLength]; + + for (var i = 0; i < maxBodyLength; i++) { + bit_check[i] = AssertBit(); + bit_check[i].in <== mask[i]; + maskedBody[i] <== body[i] * mask[i]; + } +} \ No newline at end of file