mirror of
https://github.com/zkemail/zk-email-verify.git
synced 2026-01-09 13:38:03 -05:00
feat: upstreamed to email-verifier+ tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user