Circuit 4 — Incident notification SLA (DKIM-anchored)
Status: v1 shipping. The circuit with the most interesting trust-anchor story in the bundle — the mail provider, not the vendor, is the signer of the evidence.
SSCoP principles: 4.3 (incident customer notification). CRA provisions: Article 14(1)–(2) — report actively exploited vulnerabilities and notify affected users. NIST SSDF practices: RV.1 (adjacent — identify and confirm vulnerabilities on an ongoing basis). DCC scope: Level 2 and Level 3 supply-chain controls.
The claim
A DKIM-signed email was sent to an approved recipient, naming a specific incident ID, with a mail-provider-signed send timestamp, from the vendor's authoritative sender domain.
One proof is one notification email. The completeness claim — "every customer in the committed customer set received a notification within the SLA window of the incident" — is assembled at the bundle level by aggregating one Circuit 4 proof per recipient under Circuit 5's policy composition. Circuit 4 itself proves the per-email facts the completeness aggregation needs:
- the email was validly DKIM-signed (so the mail-provider signature vouches for the send)
- the recipient is in the vendor's buyer-committed approved-recipient set (so the notification went somewhere real)
- the sender domain on the From: header is the same domain the DKIM signature's
d=tag names (so a shared-key or relay attack cannot fake the sender) - the email carries the incident ID in a header (so a random unrelated mail cannot be reused as notification evidence)
- the send timestamp is extracted from the DKIM
t=tag and exposed as a public input (so the SLA check at bundle level has a vendor-independent timestamp to reason about)
What the proof does not claim
- It does not say the notification window was met. Circuit 4 exposes
send_timestamp_unixas a public input; the SLA check is performed by Circuit 5 (or by the bundle verifier) against the incident's anchor timestamp and the buyer's SLA parameters. - It does not say every customer was notified. Per-customer completeness is the aggregation layer's job.
- It does not say the notification content was meaningful — only that the email exists, is DKIM-signed, and names the incident ID. This is intentional; the content stays private.
- It does not verify the DKIM key's DNS-publication status inside the circuit. The SDK fetches the DKIM key from DNS off-circuit, commits to it as
dkim_pubkey_hash, and feeds the commitment into the proof. Key rotation becomes a policy concern at bundle level.
Why this problem needs more than "we emailed them"
Incident notification is the part of supply-chain compliance that has historically run on trust and a spreadsheet. The existing mechanisms fail in sharp ways:
- Vendor-asserted send logs. The vendor emails the buyer a CSV of "notifications sent to X on date Y". The buyer has no cryptographic check. After an incident, a vendor under pressure can retroactively backdate the log.
- Published send logs. The vendor publishes notification events. The customer list is now public — this is contractually forbidden for most defence, regulated fintech, and closed-source ISVs.
- Third-party witness services. A third-party service receives a copy of every notification and attests delivery. Adds an integration point, adds a trust root, and the service itself becomes a target for the same adversary modelling the vendor.
DKIM is already in the vendor's outbound stack. Every notification email is already mail-provider-signed. Circuit 4 turns that signature into zero-knowledge evidence without adding any infrastructure the vendor doesn't already run.
Construction
Signed inputs
| Input | Source | Signature |
|---|---|---|
| Notification email (one per recipient) | Vendor's mail provider (Postmark / SendGrid / Mailgun / AWS SES) | DKIM RSA-2048 signature over canonicalised headers + body hash; public key published in DNS TXT record |
| Mail provider's DKIM public key | DNS TXT record at {selector}._domainkey.{vendor-domain} | DNS — pinned at proof time by the SDK; committed to as dkim_pubkey_hash |
The trust root is mail-provider-published. The vendor does not control the DKIM key; the vendor does not sign the evidence. This is unusual in the bundle — Circuits 1, 2, 3 all rely on vendor-originating signatures — and is the property that makes Circuit 4 independently credible under a "vendor compromised" threat model.
Public inputs
dkim_pubkey_hash— Poseidon2 commitment over the 36-field (18 modulus + 18 Barrett-reduction limbs) representation of the mail provider's RSA-2048 pubkeysigned_digest_commitment— Poseidon2 commitment over the SHA-256 of the canonicalised header block the RSA signature coverssender_domain_hash— commitment to the sender domain (bound both to the From: header's domain portion and to the DKIMd=tag; see "Double-binding" below)send_timestamp_unix— the DKIMt=tag value, parsed as a Unix timestamp. Public so SLA checks at bundle level have a mail-provider-attested timestamp rather than a vendor-asserted onerecipient_identity_hash— commitment to the recipient's email addressapproved_recipient_root— Merkle root of the buyer-committed approved-recipient setincident_id_hash— commitment to the incident identifier named in the email'sX-Colofon-Incident-Idheader
What the circuit checks
- DKIM RSA-2048 signature verification. Using
noir_rsa::verify_sha256_pkcs1v15, the RSA signature validates under the pinned DKIM pubkey oversha256(canonicalised_header).RSA_EXPONENT = 65537,RSA_NUM_LIMBS = 18. - Body-hash chain. The
bh=tag in the DKIM-Signature header contains the base64-encoded SHA-256 of the email body. The circuit base64-decodes the tag, then asserts it equals the SHA-256 of the body bytes the prover supplied. This closes the gap between "signature covers the header" and "the content is what we say it is". - Body hashing. v1 hashes the full body in-circuit via
sha256_var, bounded byMAX_PARTIAL_BODY_BYTES = 192(≈3 SHA-256 blocks; cost is dwarfed by the RSA verify above). The witness shape includes apartial_body_hashinput reserved for a future partial-SHA precompute path — that pattern is what ZK Email'szkemail.nruses for the same problem, and we re-implement on top of bare RSA primitives (see Further reading). The precompute path lifts the body ceiling by pre-hashing a block-aligned prefix off-circuit and finalising the final block in-circuit; activating it in Colofon requires a Noir SHA-256 library that supports non-block-aligned final-block finalisation, whichnoir-sha2560.3.0 does not. Until then, the v1 192-byte ceiling is the operative bound. - From-domain extraction. The prover hints the offset of the domain portion of the From: header inside the canonicalised header. The circuit asserts the byte at
offset - 1is@, extracts the domain bytes, and hashes them into a candidatesender_domain_hash. - DKIM
d=double-binding. The prover also hints the offset of thed=tag value in the DKIM-Signature header. The circuit extracts those bytes and hashes them intosender_domain_hashas well. Both independently-extracted byte ranges must hash to the same public commitment. Scheme B's fixed-size preimage means equal hashes imply equal bytes — which is what closes the shared-key / relay gap: an adversary with a valid DKIM signature for domain A cannot substitute domain B in the From: header without breaking this check. - DKIM
t=timestamp parsing. Thet=tag value (ASCII decimal digits, up to 19 bytes — Unix-time range) is extracted and parsed intosend_timestamp_unix. Public so SLA reasoning is vendor-independent. - Recipient extraction + Merkle membership. The
To:header recipient is extracted, hashed intorecipient_identity_hash, and the prover exhibits a Merkle opening of that hash in the approved-recipient tree rooted atapproved_recipient_root. The Merkle check carries the real soundness: even with a weak byte-level anchor, an arbitrary substring is overwhelmingly unlikely to hash into the vendor's approved-recipient set unless it is a real approved recipient. - Incident ID extraction. The
X-Colofon-Incident-Idheader value is extracted (pinned by the 24-byte\r\nx-colofon-incident-id:prefix) and hashed intoincident_id_hash. - DKIM-Signature envelope bounding. The DKIM-Signature header's extent (from the
\r\ndkim-signature:prefix to the next CRLF header boundary) is validated, and all three of thebh=,d=,t=offsets are asserted to fall inside this extent. Prevents a prover from pulling ad=tag from an unrelatedX-Foo:header that happened to contain the right bytes.
The double-binding on sender domain
The most interesting soundness property in the circuit.
An adversary with access to a shared DKIM key (for example, a relay service signing for many domains) could otherwise produce: a valid RSA signature over a header block where the DKIM-Signature's d= names domain A (the relay service's signing domain), but the From: header names domain B (a victim vendor's domain). The signature verifies; the From: looks like it is from the vendor.
The double-binding prevents this. The circuit asserts that the From-domain bytes and the DKIM-d bytes both hash to the same public sender_domain_hash. If the bytes differ, the hashes differ, and the proof fails. The attack requires the adversary to construct a DKIM-Signature where d= equals the victim's domain — which a shared-key relay service will not do.
The same pattern recurs for incident ID: the bytes are pinned to a specific signed-header offset, and the hash is the public commitment; the content is private but the commitment is reproducible at bundle level for cross-proof incident identification.
Cross-circuit binding and completeness
Circuit 4 produces per-email facts. Turning those into the completeness claim ("every customer received a notification within SLA") is done by Circuit 5 / the bundle verifier:
- Incident identification. Every Circuit 4 proof for the same incident shares the same
incident_id_hash. The bundle proves that every proof in a Circuit 4 batch names the incident the buyer is auditing. - Recipient-set coverage. Every
recipient_identity_hashfrom a Circuit 4 proof in the batch is present in the committed customer set — and every customer in the committed set has a correspondingrecipient_identity_hashin the batch. The "every customer has a notification" claim is an inclusion-and-count aggregation over the batch's public inputs. - SLA window. The incident anchor timestamp is a public input at the bundle level (committed by the vendor in a prior Rekor entry). Circuit 5 asserts
send_timestamp_unix - incident_timestamp <= sla_windowfor every Circuit 4 proof.
The committed customer-set root is the governance discipline this circuit requires of the vendor. v1 expects vendors to publish their customer-set Merkle root to Rekor on a quarterly cadence (or on each customer-list change). This is the "small amount of vendor governance discipline" referenced in the plan — the circuit gives the buyer the cryptographic evidence; the vendor retains the obligation to commit honestly.
What the proof carries — and what it does not
The proof publishes:
- the mail provider's DKIM pubkey commitment (a commitment; does not reveal the DNS-TXT contents, though those are public anyway)
- the signed-header digest commitment (commitment; does not reveal the header's other fields)
- the sender domain hash (commitment; reveals that the sender domain is consistent with the DKIM signature, not what the domain is)
- the send timestamp (revealed; public by design for the SLA check)
- the recipient hash (commitment)
- the approved-recipient Merkle root (commitment; does not reveal the customer set)
- the incident ID hash (commitment; does not reveal the incident contents or natural-language title)
An adversary who obtains the proof cannot derive:
- the customer list (the single most sensitive artefact for most vendors; enumeration requires access to the approved-recipient set itself, not the root)
- the natural-language incident identifier (only the hash is public)
- the email body content (the body-hash chain binds the body to the signature without exposing the body)
- which specific customers received this notification versus any other — each proof is independent; correlation across proofs requires access to the recipient set
- the volume of the vendor's customer base (the Merkle root is fixed-depth regardless of actual population)
- the cadence of prior incidents (each incident ID is independently hashed; no relational structure leaks)
The which customer got notified confidentiality is Circuit 4's central commercial property. A buyer can verify the vendor met the SLA for them specifically — because the buyer knows they are in the set and can verify their own recipient-identity hash — while being unable to enumerate the vendor's other customers. This is what unlocks notification compliance in sectors where the customer list is itself sensitive.
Honest caveats
- DKIM key fetching is trust-on-first-use-adjacent. The SDK pins the mail provider's DKIM pubkey at witness time by fetching the DNS TXT record. DNS is not cryptographically strong without DNSSEC; in practice the vendor's mail provider uses large mail services (Postmark / SendGrid / Mailgun / AWS SES) whose DKIM selectors are publicly documented and stable, but a DNSSEC-attested fetch is on the v1.5 roadmap.
- Anchor-based header parsing is sharp. Extraction of From-domain,
d=,t=, recipient, and incident ID from the canonicalised header block uses byte-level offset hints from the prover, with literal byte anchors verified by the circuit (for example\r\nx-colofon-incident-id:). The Merkle membership and double-binding checks carry the soundness the byte anchors do not — an arbitrary substring is overwhelmingly unlikely to satisfy the Merkle check against the buyer's approved-recipient root, or to match thed=tag's hash. Structural parsing (full RFC 5322 header tokeniser in-circuit) is out of scope for v1 on cost grounds. - Completeness depends on honest committed customer sets. If the vendor commits to a customer-set root that excludes customers it does not want to prove notification for, Circuit 4's inclusion aggregation will report "everyone in the committed set was notified" — which is true but potentially not sufficient. External audit against the vendor's operational customer list is the check; this is the "governance discipline" referenced above and is a limitation the whitepaper §3 acknowledges.
- Partial-SHA optimisation is soundness-critical. The pre-hash prefix must be a legitimate SHA-256 intermediate state of a multiple-of-64-byte prefix of the real body. The witness builder is responsible for producing this correctly; a bug there produces proofs that verify against a body that differs from the real one. Cryptographic review of the witness builder's SHA-256 streaming code is pre-launch audit scope.
- Body size cap.
MAX_PARTIAL_BODY_BYTES = 192is a hard cap on total body size in v1, because v1 hashes the full body in-circuit. The witness includes a reservedpartial_body_hashinput for a future precompute path that lifts this cap; activating it requires a Noir SHA-256 library with non-block-aligned final-block finalisation. Notification emails comfortably fit under the v1 cap.
Parameters and performance
| Parameter | Value | Notes |
|---|---|---|
| RSA_NUM_LIMBS | 18 | RSA-2048, 128-bit limbs |
| RSA_EXPONENT | 65537 | Standard DKIM public-exponent choice |
| MAX_CANONICALISED_HEADER_LENGTH | 1024 | Covers typical headers after RFC 6376 relaxed canonicalisation |
| MAX_DOMAIN_BYTES | 253 | Presentation-format FQDN max (RFC 1035 §2.3.4 wire format is 255 octets including length bytes) |
| MAX_EMAIL_BYTES | 320 | Theoretical max derived from RFC 5321 §4.5.3.1.1 (64-octet local-part) + §4.5.3.1.2 (255-octet domain). RFC 5321 §4.5.3.1.3 separately caps the SMTP path at 256 octets, which constrains envelope addresses below the theoretical address max in practice. |
| MAX_INCIDENT_ID_BYTES | 64 | Incident-ID header value ceiling (generous for UUID + prefix) |
| MAX_PARTIAL_BODY_BYTES | 192 | v1 full-body cap (hashed entirely in-circuit; precompute path reserved in witness) |
| APPROVED_RECIPIENT_TREE_HEIGHT | 10 | 1024 leaves; matches the approved-builder / authorised-signer tree sizes |
Proving time is dominated by RSA-2048 verification. Single-email Circuit 4 proves in the range of 20–30s; batching many recipients under Circuit 5 amortises the Merkle-membership checks but keeps the RSA per-email.
Further reading
- Whitepaper §3 — Whitepaper
- Source circuit —
colofonhq/colofon-circuits/dkim_sla/src/main.nr - zkemail.nr (ZK Email project) — pioneered DKIM verification in Noir; v1 audited by Consensys Diligence (Dec 2024). Informs Circuit 4's design; we don't import it because v2.0.0's Noir-API expectations don't align with our 1.0.0-beta.19 toolchain. https://github.com/zkemail/zkemail.nr
- RFC 6376 (DKIM) — https://datatracker.ietf.org/doc/html/rfc6376
- RFC 5322 (Internet Message Format) — https://datatracker.ietf.org/doc/html/rfc5322
- Circuit 5 (policy composition — aggregates one Circuit 4 proof per recipient) — Circuit 5