feat: upstreamed to email-verifier+ tests

This commit is contained in:
shreyas-londhe
2024-07-13 19:54:34 +02:00
parent d743b0d197
commit c0a68f9d78
7 changed files with 228 additions and 150 deletions

View File

@@ -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

View File

@@ -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);
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,
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"),
}
);
});
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 without any SHA precompute selector", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
shaPrecomputeSelector: "How are",
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 fail if the rsa signature is wrong", async function () {
const invalidRSASignature = dkimResult.signature + 1n;
it("should verify email with a SHA precompute selector", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
shaPrecomputeSelector: "How are",
maxHeadersLength: 640,
maxBodyLength: 768,
});
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;
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";
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");
}
});
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";
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");
}
});
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
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
maxHeadersLength: 640,
maxBodyLength: 768,
ignoreBodyHashCheck: true,
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
}
);
});
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);
});
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,
});
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', () => {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;