Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Reveal Substring Template #207

Merged
merged 32 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d85b22e
feat: match substring impl
shreyas-londhe Aug 1, 2024
b9008c4
test: added tests for substring-match template
shreyas-londhe Aug 1, 2024
d4980c1
feat: computing r like fiat-shamir
shreyas-londhe Aug 3, 2024
b48152e
fix: remove-soft-line-break at the beginning
shreyas-londhe Aug 2, 2024
116d8f9
feat: added body masking template
shreyas-londhe Jul 13, 2024
20b4c98
feat: upstreamed to email-verifier+ tests
shreyas-londhe Jul 13, 2024
fcbc96c
fix: added a assert-bit check in body-masker
shreyas-londhe Jul 13, 2024
ca5714f
fix: updated mask type
shreyas-londhe Jul 13, 2024
f6bdb5d
chore: turnOnBodyMasking -> enableBodyMasking name change
shreyas-londhe Aug 2, 2024
7eb8e6f
chore: minor refactoring
shreyas-londhe Aug 2, 2024
95e92c7
bump to 6.1.4 and eslint and published to npm
Divide-By-0 Aug 20, 2024
9b0d51d
Bug fixes
jayden-sudo Aug 21, 2024
f9a81f3
Using both Google and Cloudflare
jayden-sudo Aug 21, 2024
0432fff
chore: bumped to version 6.1.5
javiersuweijie Aug 21, 2024
48ef098
Apply suggestions from code review
jayden-sudo Aug 22, 2024
7a2202b
Feat/fix select regex reveal (#214)
SoraSuegami Aug 29, 2024
b628939
chore: renamed template
shreyas-londhe Aug 3, 2024
acae1e3
feat: added CountSubstringOccurences template
shreyas-londhe Sep 12, 2024
49cddd9
test: added CheckSubstringMatch tests
shreyas-londhe Sep 12, 2024
1f02068
test: added CountSubstringOccurrences tests
shreyas-londhe Sep 12, 2024
6200617
feat: added RevealSubstring template
shreyas-londhe Sep 13, 2024
2219b57
test: added RevealSubstring tests
shreyas-londhe Sep 13, 2024
6fae5aa
feat: added header masking
shreyas-londhe Sep 6, 2024
62b72d9
fix: removed unnecessary output in remove-soft-line-breaks
shreyas-londhe Sep 6, 2024
4e587b7
chore: refactor email-verifier tests
shreyas-londhe Sep 6, 2024
29f00af
chore: increased test timeout for email-verifier
shreyas-londhe Sep 13, 2024
ab907e3
chore: minor change
shreyas-londhe Sep 13, 2024
eaa79de
chore: minor change
shreyas-londhe Sep 13, 2024
8297549
fix: updated test script
shreyas-londhe Sep 13, 2024
1b8a128
fix: removed range checks from RevealSubstring
shreyas-londhe Sep 14, 2024
21e1e47
fix: made uniqueness check optional for RevealSubstring
shreyas-londhe Sep 14, 2024
800b9a1
feat: added range checks to RevealSubstring template
shreyas-londhe Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "6.1.3",
"version": "6.1.5",
"license": "MIT",
"private": true,
"scripts": {
Expand Down
51 changes: 50 additions & 1 deletion packages/circuits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ include "@zk-email/circuits/email-verifier.circom";
- `n`: Number of bits per chunk the RSA key is split into. Recommended to be 121.
- `k`: Number of chunks the RSA key is split into. Recommended to be 17.
- `ignoreBodyHashCheck`: Set 1 to skip body hash check in case data to prove/extract is only in the headers.
- `enableHeaderMasking`: Set 1 to turn on header masking.
- `enableBodyMasking`: Set 1 to turn on body masking.
- `removeSoftLineBreaks`: Set 1 to remove soft line breaks (`=\r\n`) from the email body.

`Note`: We use these values for n and k because their product (n * k) needs to be more than 2048 (RSA constraint) and n has to be less than half of 255 to fit in a circom signal.

Expand All @@ -41,10 +44,14 @@ include "@zk-email/circuits/email-verifier.circom";
- `emailBodyLength`: Length of the email body including the SHA-256 padding.
- `bodyHashIndex`: Index of the body hash `bh` in the `emailHeader`.
- `precomputedSHA[32]`: Precomputed SHA-256 hash of the email body till the bodyHashIndex.
- `headerMask[maxHeadersLength]`: Mask to be applied on the `emailHeader`.
- `bodyMask[maxBodyLength]`: Mask to be applied on the `emailBody`.
- `decodedEmailBody[maxBodyLength]`: Decoded email body after removing soft line breaks.

**Output Signal**
- `pubkeyHash`: Poseidon hash of the pubkey - Poseidon(n/2)(n/2 chunks of pubkey with k*2 bits per chunk).

- `maskedHeader[maxHeadersLength]`: Masked email header.
- `maskedBody[maxBodyLength]`: Masked email body.
<br/>

## **Libraries**
Expand Down Expand Up @@ -257,6 +264,33 @@ DigitBytesToInt: Converts a byte array representing digits to an integer.
- `out`: The output integer after conversion.
</details>

<details>
<summary>
AssertBit: Asserts that a given input is binary.
</summary>

- **[Source](utils/bytes.circom#L1-L7)**
- **Inputs**:
- `in`: An input signal, expected to be 0 or 1.
- **Outputs**:
- None. This template will throw an assertion error if the input is not binary.

</details>

<details>
<summary>
ByteMask: Masks an input array using a binary mask array.
</summary>

- **[Source](utils/bytes.circom#L9-L25)**
- **Parameters**:
- `maxLength`: The maximum length of the input and mask arrays.
- **Inputs**:
- `in`: An array of signals representing the body to be masked.
- `mask`: An array of signals representing the binary mask.
- **Outputs**:
- `out`: An array of signals representing the masked input.
</details>

### `utils/constants.circom`

Expand Down Expand Up @@ -359,5 +393,20 @@ EmailNullifier: Calculates the email nullifier using Poseidon hash.
- `out`: The email nullifier.
</details>

### `helpers/remove-soft-line-breaks.circom`

<details>
<summary>
RemoveSoftLineBreaks: Verifies the removal of soft line breaks from an encoded input string.
</summary>

- **[Source](helpers/remove-soft-line-breaks.circom)**
- **Parameters**:
- `maxLength`: The maximum length of the input strings.
- **Inputs**:
- `encoded[maxLength]`: An array of ASCII values representing the input string with potential soft line breaks.
- `decoded[maxLength]`: An array of ASCII values representing the expected output after removing soft line breaks.
- **Outputs**:
- `isValid`: A signal that is 1 if the decoded input correctly represents the encoded input with soft line breaks removed, 0 otherwise.

</details>
29 changes: 25 additions & 4 deletions packages/circuits/email-verifier.circom
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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";


Expand All @@ -21,6 +22,8 @@ include "./helpers/remove-soft-line-breaks.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 enableHeaderMasking Set 1 to turn on header masking.
/// @param enableBodyMasking Set 1 to turn on body masking.
/// @param removeSoftLineBreaks Set 1 to remove soft line breaks from the email body.
/// @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.
Expand All @@ -31,9 +34,12 @@ 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 maskedHeader[maxHeadersLength] Masked email header.
/// @output maskedBody[maxBodyLength] Masked email body.
template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck, enableHeaderMasking, enableBodyMasking, removeSoftLineBreaks) {
assert(maxHeadersLength % 64 == 0);
assert(maxBodyLength % 64 == 0);
assert(n * k > 2048); // to support 2048 bit RSA
Expand Down Expand Up @@ -85,6 +91,15 @@ template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashChec
rsaVerifier.modulus <== pubkey;
rsaVerifier.signature <== signature;

if (enableHeaderMasking == 1) {
signal input headerMask[maxHeadersLength];
signal output maskedHeader[maxHeadersLength];
component byteMask = ByteMask(maxHeadersLength);

byteMask.in <== emailHeader;
byteMask.mask <== headerMask;
maskedHeader <== byteMask.out;
}

// Calculate the SHA256 hash of the body and verify it matches the hash in the header
if (ignoreBodyHashCheck != 1) {
Expand Down Expand Up @@ -129,19 +144,25 @@ template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashChec

if (removeSoftLineBreaks == 1) {
signal input decodedEmailBodyIn[maxBodyLength];
signal output decodedEmailBodyOut[maxBodyLength];
component qpEncodingChecker = RemoveSoftLineBreaks(maxBodyLength);

qpEncodingChecker.encoded <== emailBody;
qpEncodingChecker.decoded <== decodedEmailBodyIn;

qpEncodingChecker.isValid === 1;
}

decodedEmailBodyOut <== qpEncodingChecker.decoded;
if (enableBodyMasking == 1) {
signal input bodyMask[maxBodyLength];
signal output maskedBody[maxBodyLength];
component byteMask = ByteMask(maxBodyLength);

byteMask.in <== emailBody;
byteMask.mask <== bodyMask;
maskedBody <== byteMask.out;
}
}


// 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
// Since PoseidonLarge concatenates nearby values its important to use same n/k (recommended 121*17) to produce uniform hashes
Expand Down
13 changes: 9 additions & 4 deletions packages/circuits/helpers/remove-soft-line-breaks.circom
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ template RemoveSoftLineBreaks(maxLength) {
}

// Calculate powers of r for encoded
rEnc[0] <== 1;
muxEnc[0] = Mux1();
muxEnc[0].c[0] <== r;
muxEnc[0].c[1] <== 1;
muxEnc[0].s <== shouldZero[0];
rEnc[0] <== muxEnc[0].out;

for (var i = 1; i < maxLength; i++) {
muxEnc[i] = Mux1();
muxEnc[i].c[0] <== rEnc[i - 1] * r;
Expand All @@ -99,19 +104,19 @@ template RemoveSoftLineBreaks(maxLength) {
}

// Calculate powers of r for decoded
rDec[0] <== 1;
rDec[0] <== r;
for (var i = 1; i < maxLength; i++) {
rDec[i] <== rDec[i - 1] * r;
}

// Calculate rlc for processed
sumEnc[0] <== processed[0];
sumEnc[0] <== rEnc[0] * processed[0];
for (var i = 1; i < maxLength; i++) {
sumEnc[i] <== sumEnc[i - 1] + rEnc[i] * processed[i];
}

// Calculate rlc for decoded
sumDec[0] <== decoded[0];
sumDec[0] <== rDec[0] * decoded[0];
for (var i = 1; i < maxLength; i++) {
sumDec[i] <== sumDec[i - 1] + rDec[i] * decoded[i];
}
Expand Down
50 changes: 50 additions & 0 deletions packages/circuits/helpers/reveal-substring.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
pragma circom 2.1.6;

include "circomlib/circuits/comparators.circom";
include "../utils/array.circom";

/// @title RevealSubstring
/// @notice This circuit reveals a substring from an input array and verifies its uniqueness
/// @dev Ensures the revealed substring occurs exactly once in the input
/// @dev Note: This circuit assumes that the consuming circuit handles input validation
/// (e.g., checking that substringStartIndex and substringLength are within valid ranges)
/// @param maxLength The maximum length of the input array
/// @param maxSubstringLength The maximum length of the substring to be revealed
template RevealSubstring(maxLength, maxSubstringLength, shouldCheckUniqueness) {
assert(maxSubstringLength < maxLength);

signal input in[maxLength];
signal input substringStartIndex;
signal input substringLength;

signal output substring[maxSubstringLength];

// Substring start index should be less than maxLength
signal isSubstringStartIndexValid <== LessThan(log2Ceil(maxLength))([substringStartIndex, maxLength]);
isSubstringStartIndexValid === 1;

// Substring length should be less than maxSubstringLength + 1
signal isSubstringLengthValid <== LessThan(log2Ceil(maxSubstringLength + 1))([substringLength, maxSubstringLength + 1]);
isSubstringLengthValid === 1;

// substring index + substring length should be less than maxLength + 1
signal sum <== substringStartIndex + substringLength;
signal isSumValid <== LessThan(log2Ceil(maxLength + 1))([sum, maxLength + 1]);
isSumValid === 1;

// Extract the substring
component selectSubArray = SelectSubArray(maxLength, maxSubstringLength);
selectSubArray.in <== in;
selectSubArray.startIndex <== substringStartIndex;
selectSubArray.length <== substringLength;

if (shouldCheckUniqueness) {
// Check if the substring occurs exactly once in the input
component countSubstringOccurrences = CountSubstringOccurrences(maxLength, maxSubstringLength);
countSubstringOccurrences.in <== in;
countSubstringOccurrences.substring <== selectSubArray.out;
countSubstringOccurrences.count === 1;
}

substring <== selectSubArray.out;
}
4 changes: 2 additions & 2 deletions packages/circuits/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@zk-email/circuits",
"version": "6.1.3",
"version": "6.1.5",
"license": "MIT",
"scripts": {
"publish": "yarn npm publish --access=public",
"test": "NODE_OPTIONS=--max_old_space_size=8192 jest tests"
"test": "NODE_OPTIONS=--max_old_space_size=8192 jest --runInBand --detectOpenHandles --forceExit --verbose tests"
},
"dependencies": {
"@zk-email/zk-regex-circom": "^2.1.0",
Expand Down
105 changes: 52 additions & 53 deletions packages/circuits/tests/base64.test.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
import { wasm } from "circom_tester";
import path from "path";


describe("Base64 Lookup", () => {
jest.setTimeout(10 * 60 * 1000); // 10 minutes

let circuit: any;

beforeAll(async () => {
circuit = await wasm(
path.join(__dirname, "./test-circuits/base64-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
}
);
});

it("should decode valid base64 chars", async function () {
const inputs = [
[65, 0], // A
[90, 25], // Z
[97, 26], // a
[122, 51], // z
[48, 52], // 0
[57, 61], // 9
[43, 62], // +
[47, 63], // /
[61, 0], // =
]

for (const [input, output] of inputs) {
const witness = await circuit.calculateWitness({
in: input
});
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { out: output })
}
});

it("should fail with invalid chars", async function () {
const inputs = [34, 64, 91, 44];

expect.assertions(inputs.length);
for (const input of inputs) {
try {
const witness = await circuit.calculateWitness({
in: input
});
await circuit.checkConstraints(witness);
} catch (error) {
expect((error as Error).message).toMatch("Assert Failed");
}
}
});
jest.setTimeout(30 * 60 * 1000); // 30 minutes

let circuit: any;

beforeAll(async () => {
circuit = await wasm(
path.join(__dirname, "./test-circuits/base64-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
}
);
});

it("should decode valid base64 chars", async function () {
const inputs = [
[65, 0], // A
[90, 25], // Z
[97, 26], // a
[122, 51], // z
[48, 52], // 0
[57, 61], // 9
[43, 62], // +
[47, 63], // /
[61, 0], // =
];

for (const [input, output] of inputs) {
const witness = await circuit.calculateWitness({
in: input,
});
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { out: output });
}
});

it("should fail with invalid chars", async function () {
const inputs = [34, 64, 91, 44];

expect.assertions(inputs.length);
for (const input of inputs) {
try {
const witness = await circuit.calculateWitness({
in: input,
});
await circuit.checkConstraints(witness);
} catch (error) {
expect((error as Error).message).toMatch("Assert Failed");
}
}
});
});
Loading
Loading