Colofon
Book a demo →
Circuits

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_unix as 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

InputSourceSignature
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 keyDNS 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 pubkey
  • signed_digest_commitment — Poseidon2 commitment over the SHA-256 of the canonicalised header block the RSA signature covers
  • sender_domain_hash — commitment to the sender domain (bound both to the From: header's domain portion and to the DKIM d= tag; see "Double-binding" below)
  • send_timestamp_unix — the DKIM t= 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 one
  • recipient_identity_hash — commitment to the recipient's email address
  • approved_recipient_root — Merkle root of the buyer-committed approved-recipient set
  • incident_id_hash — commitment to the incident identifier named in the email's X-Colofon-Incident-Id header

What the circuit checks

  1. DKIM RSA-2048 signature verification. Using noir_rsa::verify_sha256_pkcs1v15, the RSA signature validates under the pinned DKIM pubkey over sha256(canonicalised_header). RSA_EXPONENT = 65537, RSA_NUM_LIMBS = 18.
  2. 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".
  3. Body hashing. v1 hashes the full body in-circuit via sha256_var, bounded by MAX_PARTIAL_BODY_BYTES = 192 (≈3 SHA-256 blocks; cost is dwarfed by the RSA verify above). The witness shape includes a partial_body_hash input reserved for a future partial-SHA precompute path — that pattern is what ZK Email's zkemail.nr uses 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, which noir-sha256 0.3.0 does not. Until then, the v1 192-byte ceiling is the operative bound.
  4. 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 - 1 is @, extracts the domain bytes, and hashes them into a candidate sender_domain_hash.
  5. DKIM d= double-binding. The prover also hints the offset of the d= tag value in the DKIM-Signature header. The circuit extracts those bytes and hashes them into sender_domain_hash as 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.
  6. DKIM t= timestamp parsing. The t= tag value (ASCII decimal digits, up to 19 bytes — Unix-time range) is extracted and parsed into send_timestamp_unix. Public so SLA reasoning is vendor-independent.
  7. Recipient extraction + Merkle membership. The To: header recipient is extracted, hashed into recipient_identity_hash, and the prover exhibits a Merkle opening of that hash in the approved-recipient tree rooted at approved_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.
  8. Incident ID extraction. The X-Colofon-Incident-Id header value is extracted (pinned by the 24-byte \r\nx-colofon-incident-id: prefix) and hashed into incident_id_hash.
  9. 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 the bh=, d=, t= offsets are asserted to fall inside this extent. Prevents a prover from pulling a d= tag from an unrelated X-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_hash from a Circuit 4 proof in the batch is present in the committed customer set — and every customer in the committed set has a corresponding recipient_identity_hash in 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_window for 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 the d= 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 = 192 is a hard cap on total body size in v1, because v1 hashes the full body in-circuit. The witness includes a reserved partial_body_hash input 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

ParameterValueNotes
RSA_NUM_LIMBS18RSA-2048, 128-bit limbs
RSA_EXPONENT65537Standard DKIM public-exponent choice
MAX_CANONICALISED_HEADER_LENGTH1024Covers typical headers after RFC 6376 relaxed canonicalisation
MAX_DOMAIN_BYTES253Presentation-format FQDN max (RFC 1035 §2.3.4 wire format is 255 octets including length bytes)
MAX_EMAIL_BYTES320Theoretical 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_BYTES64Incident-ID header value ceiling (generous for UUID + prefix)
MAX_PARTIAL_BODY_BYTES192v1 full-body cap (hashed entirely in-circuit; precompute path reserved in witness)
APPROVED_RECIPIENT_TREE_HEIGHT101024 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