Skip to content

Commit 7665ab3

Browse files
critesjoshclaude
andcommitted
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>
1 parent 95d39c7 commit 7665ab3

1 file changed

Lines changed: 34 additions & 17 deletions

File tree

zkemail_verification/README.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,27 @@ Built with the [zkemail.nr](https://github.com/zkemail/zkemail.nr) library on Az
66

77
## Security Model
88

9-
The contract enforces four constraints on each email proof:
9+
The contract enforces five constraints on each email proof:
1010

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.
1516

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.
1730

1831
## How It Works
1932

@@ -25,11 +38,11 @@ Raw Email ──> [Noir Circuit] ──> │ 6 public outputs: │
2538
│ from domain │ [2] email_nullifier │──> [Aztec Contract]
2639
│ to address │ [3] to_address_hash │ │
2740
│ subject │ [4] intent_hash │ │ verify_honk_proof()
28-
│ timestamp │ [5] dkim_timestamp │ │ check recipient
29-
│ └──────────────────────────┘ │ check intent
30-
│ │ push nullifier
31-
│ Body verification omitted — │ check freshness
32-
│ all auth data is in DKIM-signed headers, │
41+
│ timestamp │ [5] dkim_timestamp │ │ check DKIM key
42+
│ └──────────────────────────┘ │ check recipient
43+
│ │ check intent
44+
│ Body verification omitted — │ push nullifier
45+
│ all auth data is in DKIM-signed headers, │ check freshness
3346
│ avoiding extra SHA256 hashing cost │
3447
```
3548

@@ -47,13 +60,14 @@ Body verification is omitted since all authorization-relevant data lives in the
4760

4861
### Contract (`contract/src/main.nr`)
4962

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.
5164
- `verify_email(expected_intent_hash, vk, proof, public_inputs)`**Private function** that:
5265
1. Verifies the UltraHonk proof against the stored VK hash
53-
2. Asserts `public_inputs[3]` (to address hash) matches `authorized_email_hash`
54-
3. Asserts `public_inputs[4]` (subject hash) matches `expected_intent_hash`
55-
4. Pushes `public_inputs[2]` (email nullifier) to prevent replay
56-
5. Enqueues a public call to check `public_inputs[5]` (timestamp) is fresh
66+
2. Asserts `public_inputs[0]` and `[1]` (DKIM key hashes) match the trusted key
67+
3. Asserts `public_inputs[3]` (to address hash) matches `authorized_email_hash`
68+
4. Asserts `public_inputs[4]` (subject hash) matches `expected_intent_hash`
69+
5. Pushes `public_inputs[2]` (email nullifier) to prevent replay
70+
6. Enqueues a public call to check `public_inputs[5]` (timestamp) is fresh
5771
- `_check_email_freshness(email_timestamp, max_age)`**Public function** that checks `block.timestamp - email_timestamp <= max_age`
5872
- `get_authorized_email_hash()`**View function** that returns the stored authorized email hash
5973
- `get_max_email_age()`**View function** that returns the stored maximum email age
@@ -91,7 +105,7 @@ nargo --version # 1.0.0-beta.18
91105
│ ├── src/main.nr # DKIM verify + address/subject/timestamp extraction
92106
│ └── Nargo.toml # Depends on zkemail.nr
93107
├── contract/ # Aztec smart contract
94-
│ ├── src/main.nr # verify_honk_proof + nullifier + timestamp check
108+
│ ├── src/main.nr # verify_honk_proof + DKIM key + nullifier + timestamp
95109
│ ├── artifacts/ # Generated TypeScript bindings
96110
│ └── Nargo.toml # Depends on aztec-nr and bb_proof_verification
97111
├── scripts/
@@ -181,6 +195,9 @@ Run `yarn ccc` to compile the contract and generate TypeScript bindings.
181195
**"Cannot find module '../data.json'"**
182196
Run `yarn data` to generate the proof data.
183197

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+
184201
**"Email recipient does not match authorized address"**
185202
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.
186203

0 commit comments

Comments
 (0)