Kuba Opoczka Signature verification instrument RFC 6376

Don’t trust it. Break it.

A genuine DKIM-signed email. Two independent implementations. No cryptography library. Change one byte and watch the proof fail.

Signing domain
example.com
Selector
sel2026
Algorithm
rsa-sha256
Key size
2048-bit
Canonicalisation
relaxed/relaxed
Public exponent
65537

Verification procedure executing in this browser
01

Hash the body, compare against the signature’s claim

The signature header carries a bh= tag: the SHA-256 hash of the message body after relaxed canonicalisation (collapse runs of whitespace, strip trailing whitespace per line, drop trailing empty lines). If the body changed by one byte after signing, this hash stops matching.

Message body / editable change anything and the proof collapses
bh= claimed
bh computed
checking
02

Rebuild the signed header set, hash it

The h= tag lists which headers were signed, in order. Each is canonicalised (name lower-cased, folding removed, whitespace collapsed), then the DKIM-Signature header itself is appended with its own b= value emptied because you cannot sign the signature. This exact byte string is what the RSA signature covers.

Reconstructed signed data / 324 bytes
SHA-256
03

Recover the padding block with pow(s, e, n)

RSA verification is one modular exponentiation: raise the signature to the public exponent, modulo the public key. Nothing here is a comparison against a stored answer: the digest is recovered out of the signature, then matched against the one computed in step 02.

What comes back has a shape. Two marker bytes, a long wall of FF padding, a 00 separator, a DER header naming the hash algorithm, and finally the digest. That shape is drawn below from the actual recovered bytes.

Recovered block structure derived from the bytes, not assumed
marker & recovered digest DER DigestInfo FF padding
Recovered
Computed
checking

Verifying…

Running SHA-256 and a 2048-bit modular exponentiation.

Now verify one of your own

Open any email. In Gmail that’s the More menu → Show original; in Outlook, View source. Copy the whole thing and paste it below. The same code that ran above will fetch the sender’s public key from DNS and check their signature.

Your email never leaves this page. Parsing, hashing and the RSA operation all happen in your browser. The one thing that goes out is a DNS-over-HTTPS lookup for the signing domain and selector (never your address or the message), because the public key has to come from somewhere. That request goes to Cloudflare, then Google as a fallback.

Raw message source headers and body, exactly as copied

The whole verification is eight lines

This is the function, unedited, from analyzer/dkim.py. No imports beyond the standard library; the RSA operation is Python’s built-in pow() with three arguments, and the padding check is a byte comparison against a block we rebuild ourselves.

def _rsa_verify(sig: bytes, digest: bytes, n: int, e: int, hashname: str) -> bool:
    if int.from_bytes(sig, "big") >= n:
        return False
    k = (n.bit_length() + 7) // 8
    em = pow(int.from_bytes(sig, "big"), e, n).to_bytes(k, "big")
    t = _DIGEST_PREFIX[hashname] + digest
    expected = b"\x00\x01" + b"\xff" * (k - len(t) - 3) + b"\x00" + t
    return em == expected

It sits inside DAEMON, an email threat-intelligence console: 28 modules, 130 passing tests, standard library at the core. The verifier was validated non-circularly: signed by a third-party tool, verified by this code, then re-checked against a deliberately tampered copy.


How this was built, honestly

An AI wrote most of the code on this page. I think you should know that, and I think it matters less than what happened next.

I decided what it had to do

Not “build me a DKIM verifier”. I wrote a specification. No cryptography library. Must recover the padding block rather than compare against a stored answer. Must fail in a way that distinguishes a tampered body from a bad signature, because those are different problems. The constraints are the design work.

Then I assumed it was wrong

Code that runs is not code that is right, and AI is unusually good at producing the first while looking like the second. A signature verifier that returns true for everything passes a naive test perfectly.

So I made it prove itself twice

The Python and the JavaScript here are two separate implementations of the same specification. I ran them against each other on the cases most likely to break: folded headers, repeated headers, tabs and trailing whitespace, a signed header that is missing, an empty body. Seven cases, byte-identical output. Then the key parser against two real published keys (2048-bit and 1024-bit): same modulus, same exponent.

And I checked it can fail

A verifier that never rejects anything is worthless. This one verifies a message signed by an entirely separate third-party tool, and rejects a deliberately tampered copy. Passing the real one is half the proof; failing the fake one is the other half.

The same logic runs over my CV: every claim I make is either checked live against GitHub and npm, open to inspection, or labelled as unverifiable. That is the whole method, and it is not really about cryptography. On a browser extension I built the same way, a review pass found three bugs I had already shipped: a global overwritten instead of chained, a tree-walker miscounting its own depth, a licence check calling the wrong endpoint. None of them crashed. All of them failed silently and looked fine. Anyone can prompt an agent into working code. The job is building the thing that catches it when it quietly lies to you.