You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Update README to document trusted DKIM key binding
The contract now stores trusted DKIM public key hashes and verifies them
against proof outputs, preventing forgery with self-generated keypairs.
Update the security model (4 → 5 constraints), constructor signature
(3 → 5 params), verify_email steps, diagram, and troubleshooting to
match the actual contract code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: zkemail_verification/README.md
+34-17Lines changed: 34 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,14 +6,27 @@ Built with the [zkemail.nr](https://github.com/zkemail/zkemail.nr) library on Az
6
6
7
7
## Security Model
8
8
9
-
The contract enforces four constraints on each email proof:
9
+
The contract enforces five constraints on each email proof:
10
10
11
-
1.**Recipient binding** — The email's `to` address must hash to the `authorized_email_hash` stored at deployment. Only emails sent to the account owner's address are accepted.
12
-
2.**Intent binding** — The email's `subject` field is hashed and compared against a caller-provided `expected_intent_hash`. This ties the email to a specific action (e.g., a Poseidon hash of calldata placed in the subject).
13
-
3.**Single use** — The DKIM signature is hashed into a nullifier and pushed on-chain. Replaying the same email proof fails because the nullifier already exists.
14
-
4.**Freshness** — The DKIM `t=` timestamp is extracted and checked in a public function against the block timestamp. Emails older than `max_email_age` seconds are rejected.
11
+
1.**DKIM key binding** — The proof's DKIM public key hash (`public_inputs[0]`, `[1]`) must match the trusted key hashes stored at deployment. Without this, an attacker could generate their own RSA keypair, forge a DKIM signature, and produce a valid proof.
12
+
2.**Recipient binding** — The email's `to` address must hash to the `authorized_email_hash` stored at deployment. Only emails sent to the account owner's address are accepted.
13
+
3.**Intent binding** — The email's `subject` field is hashed and compared against a caller-provided `expected_intent_hash`. This ties the email to a specific action (e.g., a Poseidon hash of calldata placed in the subject).
14
+
4.**Single use** — The DKIM signature is hashed into a nullifier and pushed on-chain. Replaying the same email proof fails because the nullifier already exists.
15
+
5.**Freshness** — The DKIM `t=` timestamp is extracted and checked in a public function against the block timestamp. Emails older than `max_email_age` seconds are rejected.
15
16
16
-
The sender's domain is also verified to be `icloud.com` via DKIM.
17
+
The sender's domain is also verified to be `icloud.com` via DKIM in the circuit.
18
+
19
+
### DKIM Key Rotation Caveat
20
+
21
+
This example pins the trusted DKIM public key hash at contract deployment time. In practice, mail providers rotate their DKIM signing keys periodically — a domain can publish a new key under the same selector, or switch selectors entirely. When that happens, proofs generated with the old key will still verify, but proofs using the new key will be rejected by the contract (the key hash won't match).
22
+
23
+
A production system should replace the static key hash with a **DKIM key registry** that tracks current keys per `(domain, selector)` pair. The two main approaches:
24
+
25
+
1.**DNSSEC-aware proof** — The prover includes the full DNSSEC chain from the DNS root to the DKIM TXT record. The circuit or a dedicated verifier contract validates the chain, proving the key was authentically published in DNS. This is the strongest trust model but requires in-circuit DNSSEC signature verification (multiple RSA/ECDSA checks across the delegation chain) and handling of signature validity windows.
26
+
27
+
2.**Narrowly-scoped DNSSEC oracle** — An off-chain service resolves the DKIM TXT record with full DNSSEC validation and submits signed `(domain, selector, key_hash, expires_at)` attestations to an on-chain registry. The email verifier contract checks the proof's key against the registry. This is simpler to implement and sufficient when the oracle's trust boundary is acceptable.
28
+
29
+
In either case, the contract should check key expiry and support updates without redeployment.
17
30
18
31
## How It Works
19
32
@@ -25,11 +38,11 @@ Raw Email ──> [Noir Circuit] ──> │ 6 public outputs: │
25
38
│ from domain │ [2] email_nullifier │──> [Aztec Contract]
│ all auth data is in DKIM-signed headers, │ check freshness
33
46
│ avoiding extra SHA256 hashing cost │
34
47
```
35
48
@@ -47,13 +60,14 @@ Body verification is omitted since all authorization-relevant data lives in the
47
60
48
61
### Contract (`contract/src/main.nr`)
49
62
50
-
-`constructor(vk_hash, authorized_email_hash, max_email_age)` — Stores the verification key hash, the Poseidon2 hash of the authorized recipient email, and the maximum email age in seconds.
63
+
-`constructor(vk_hash, trusted_dkim_key_hash_0, trusted_dkim_key_hash_1, authorized_email_hash, max_email_age)` — Stores the verification key hash, the trusted DKIM public key hashes (modulus and redc), the Poseidon2 hash of the authorized recipient email, and the maximum email age in seconds.
│ └── Nargo.toml # Depends on aztec-nr and bb_proof_verification
97
111
├── scripts/
@@ -181,6 +195,9 @@ Run `yarn ccc` to compile the contract and generate TypeScript bindings.
181
195
**"Cannot find module '../data.json'"**
182
196
Run `yarn data` to generate the proof data.
183
197
198
+
**"DKIM public key does not match trusted key"**
199
+
The proof was generated with a DKIM key that doesn't match the trusted key hashes stored at deployment. This can happen after a DKIM key rotation. Redeploy the contract with the current key hashes from the proof's `public_inputs[0]` and `[1]`.
200
+
184
201
**"Email recipient does not match authorized address"**
185
202
The `to` address in the email doesn't match the `authorized_email_hash` stored at deployment. Ensure the proof was generated with the correct test email.
0 commit comments