-
Notifications
You must be signed in to change notification settings - Fork 82
/
Copy pathemail-verifier.circom
171 lines (138 loc) · 7.91 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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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";
include "./utils/bytes.circom";
include "./helpers/remove-soft-line-breaks.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.
/// @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.
/// @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.
/// @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.
/// @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
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;
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) {
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];
}
if (removeSoftLineBreaks == 1) {
signal input decodedEmailBodyIn[maxBodyLength];
component qpEncodingChecker = RemoveSoftLineBreaks(maxBodyLength);
qpEncodingChecker.encoded <== emailBody;
qpEncodingChecker.decoded <== decodedEmailBodyIn;
qpEncodingChecker.isValid === 1;
}
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
// https://zkrepl.dev/?gist=43ce7dce2466c63812f6efec5b13aa73 - This can be used to calculate the pubkey hash separately
pubkeyHash <== PoseidonLarge(n, k)(pubkey);
}