-
Notifications
You must be signed in to change notification settings - Fork 77
/
email-verifier.circom
133 lines (105 loc) · 6.21 KB
/
email-verifier.circom
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
pragma circom 2.1.6;
include "circomlib/circuits/bitify.circom";
include "circomlib/circuits/poseidon.circom";
include "@zk-email/zk-regex-circom/circuits/common/body_hash_regex.circom";
include "./lib/base64.circom";
include "./lib/rsa.circom";
include "./lib/sha.circom";
include "./utils/array.circom";
include "./utils/regex.circom";
include "./utils/hash.circom";
/// @title EmailVerifier
/// @notice Circuit to verify email signature as per DKIM standard.
/// @notice Verifies the signature is valid for the given header and pubkey, and the hash of the body matches the hash in the header.
/// @notice This cicuit only verifies signature as per `rsa-sha256` algorithm.
/// @param maxHeadersLength Maximum length for the email header.
/// @param maxBodyLength Maximum length for the email body.
/// @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.
/// @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.
/// @input signature[k] RSA signature split into k chunks of n bits each.
/// @input emailBody[maxBodyLength] Email body after the precomputed SHA as ASCII int[], padded as per SHA-256 block size.
/// @input emailBodyLength Length of the email body including the SHA-256 padding.
/// @input bodyHashIndex Index of the body hash `bh` in the emailHeader.
/// @input precomputedSHA[32] Precomputed SHA-256 hash of the email body till the bodyHashIndex.
/// @output pubkeyHash Poseidon hash of the pubkey - Poseidon(n/2)(n/2 chunks of pubkey with k*2 bits per chunk).
template EmailVerifier(maxHeadersLength, maxBodyLength, n, k, ignoreBodyHashCheck) {
assert(maxHeadersLength % 64 == 0);
assert(maxBodyLength % 64 == 0);
assert(n * k > 2048); // to support 2048 bit RSA
assert(n < (255 \ 2)); // for multiplication to fit in the field (255 bits)
signal input emailHeader[maxHeadersLength];
signal input emailHeaderLength;
signal input pubkey[k];
signal input signature[k];
signal output pubkeyHash;
// Assert `emailHeaderLength` fits in `ceil(log2(maxHeadersLength))`
component n2bHeaderLength = Num2Bits(log2Ceil(maxHeadersLength));
n2bHeaderLength.in <== emailHeaderLength;
// Assert `emailHeader` data after `emailHeaderLength` are zeros
AssertZeroPadding(maxHeadersLength)(emailHeader, emailHeaderLength);
// Calculate SHA256 hash of the `emailHeader` - 506,670 constraints
signal output sha[256] <== Sha256Bytes(maxHeadersLength)(emailHeader, emailHeaderLength);
// Pack SHA output bytes to int[] for RSA input message
var rsaMessageSize = (256 + n) \ n;
component rsaMessage[rsaMessageSize];
for (var i = 0; i < rsaMessageSize; i++) {
rsaMessage[i] = Bits2Num(n);
}
for (var i = 0; i < 256; i++) {
rsaMessage[i \ n].in[i % n] <== sha[255 - i];
}
for (var i = 256; i < n * rsaMessageSize; i++) {
rsaMessage[i \ n].in[i % n] <== 0;
}
// Verify RSA signature - 149,251 constraints
component rsaVerifier = RSAVerifier65537(n, k);
for (var i = 0; i < rsaMessageSize; i++) {
rsaVerifier.message[i] <== rsaMessage[i].out;
}
for (var i = rsaMessageSize; i < k; i++) {
rsaVerifier.message[i] <== 0;
}
rsaVerifier.modulus <== pubkey;
rsaVerifier.signature <== signature;
// Calculate the SHA256 hash of the body and verify it matches the hash in the header
if (ignoreBodyHashCheck != 1) {
signal input bodyHashIndex;
signal input precomputedSHA[32];
signal input emailBody[maxBodyLength];
signal input emailBodyLength;
// Assert `emailBodyLength` fits in `ceil(log2(maxBodyLength))`
component n2bBodyLength = Num2Bits(log2Ceil(maxBodyLength));
n2bBodyLength.in <== emailBodyLength;
// Assert data after the body (`maxBodyLength - emailBody.length`) is all zeroes
AssertZeroPadding(maxBodyLength)(emailBody, emailBodyLength);
// Body hash regex - 617,597 constraints
// Extract the body hash from the header (i.e. the part after bh= within the DKIM-signature section)
signal (bhRegexMatch, bhReveal[maxHeadersLength]) <== BodyHashRegex(maxHeadersLength)(emailHeader);
bhRegexMatch === 1;
var shaB64Length = 44; // Length of SHA-256 hash when base64 encoded - ceil(32 / 3) * 4
signal bhBase64[shaB64Length] <== SelectRegexReveal(maxHeadersLength, shaB64Length)(bhReveal, bodyHashIndex);
signal headerBodyHash[32] <== Base64Decode(32)(bhBase64);
// Compute SHA256 of email body : 760,142 constraints (for maxBodyLength = 1536)
// We are using a technique to save constraints by precomputing the SHA hash of the body till the area we want to extract
// It doesn't have an impact on security since a user must have known the pre-image of a signed message to be able to fake it
signal computedBodyHash[256] <== Sha256BytesPartial(maxBodyLength)(emailBody, emailBodyLength, precomputedSHA);
// Ensure the bodyHash from the header matches the calculated body hash
component computedBodyHashInts[32];
for (var i = 0; i < 32; i++) {
computedBodyHashInts[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
computedBodyHashInts[i].in[7 - j] <== computedBodyHash[i * 8 + j];
}
computedBodyHashInts[i].out === headerBodyHash[i];
}
}
// 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
// https://zkrepl.dev/?gist=43ce7dce2466c63812f6efec5b13aa73 - This can be used to calculate the pubkey hash separately
pubkeyHash <== PoseidonLarge(n, k)(pubkey);
}