The internet solved the problem of who is speaking. TLS authenticates the endpoint: the certificate chain ends at a domain you asked for, and the connection is encrypted in transit. But it leaves a quieter gap open. The server you talk to can serve whatever bytes it wants, and as long as they travel over the authenticated channel, they look exactly like the truth.

That gap is not hypothetical. A compromised server, a caching layer with too much power, a CDN that decides to rewrite a page, an intermediary with access to the endpoint, all of them can change the content you receive without breaking a single signature on the wire. TLS authenticates the route. Nothing authenticates the content.

TLS proves you reached the right door. It does not prove the right things came out of it.
· the problem htmlsign exists to close

Signing an HTML document is harder than signing a file, because the document is not one blob. It is a page assembled from blocks, some of which change constantly and most of which you did not write. What you want is to freeze the parts that must not change, a price, a biography, a legal notice, and leave the rest alone. That is what htmlsign does: it signs blocks of a document, not the whole page.

What a signed block looks like

You point htmlsign at a file and a CSS selector, and it writes a signature back into the document as an attribute:

$ hs sign index.html div.text
Signed 1 block(s) in index.html
  key fingerprint: 7f6a2c...c3d09b
  <div> signed 47 bytes

The result is a self-contained attribute on the element itself:

<div class="text" data-hs-signature="SHA3-256+ML-KEM-768+ML-DSA-65+BASE64:QWsd....">
  <p>Some text</p>
  <img src="image.jpg">
</div>

Three decisions are embedded in that string, and each one was deliberate. The signature is computed over the entire block, including the root element and every attribute except the signature itself, so nothing inside the block can change unnoticed. The public keys travel next to the signature, so verification is fully self-contained, no key database to consult. And the algorithm list is explicit in the payload, so a future reader can see exactly what was used and look it up.

Minification-proof by construction

The obvious objection is that nobody keeps a document byte-for-byte forever. Servers minify, reformat, and reflow HTML constantly. If the signature covered the raw bytes, the first deployment pipeline would break every signature on the page.

htmlsign signs a canonical form instead. Text whitespace is normalized before signing: runs collapse to a single space, leading and trailing whitespace is trimmed, and whitespace-only text nodes, indentation, line breaks, are dropped. A server can minify or reformat the block and every signature still validates.

The limits are the point. Whitespace inside <pre>, <textarea>, <script>, and <style> is preserved verbatim, because there it is semantically significant. Content, attribute, and structural changes always fail. The tool is relaxed about the things a machine may reasonably reformat and strict about the things a human actually wrote.

The cryptography underneath

The cryptographic stack is the same post-quantum pair I have come to trust elsewhere. ML-KEM (CRYSTALS-Kyber, FIPS 203) for key encapsulation and ML-DSA (CRYSTALS-Dilithium, FIPS 204) for signatures, both lattice-based and standardized. Symmetric material is protected with XChaCha20-Poly1305, and passphrases are stretched with Argon2id. Pure-Rust crates only; the pqcrypto umbrella crate was deliberately avoided because it is unmaintained.

Signing is hash-then-sign. The ML-DSA signature covers a 32-byte SHA3-256 digest of the block's canonical bytes, so the cost of signing does not grow with the block size. Keys are stored passphrase-encrypted and never in plaintext on disk, and the crate contains no unsafe.

What verification actually proves

Verification is the part that had to be honest about what a signature can and cannot claim. hs verify finds every signed block, recomputes its canonical bytes, and checks the embedded ML-DSA signature:

$ hs verify index.html
[0] <div> OK
      fingerprint: 7f6a2c...c3d09b
OK: 1 of 1 blocks verified.

A self-contained signature proves the block has not changed since the person holding the key signed it. That is a real and useful claim, but it does not yet answer who that person is. An attacker with their own key could sign altered content and it would verify just fine. So htmlsign offers a second mode: verify with a pinned key.

When you pass -k key.pub, every block must match that exact public key. And for remote verification over a URL, the pin lives in DNS. htmlsign reads the _hs_key.example.org TXT record, which holds the signing key's SHA3-256 fingerprint and the URL where the armored key is served. It downloads the key, checks the digest against the pin, and requires every block to match it.

TLS authenticates the endpoint. The DNS pin authenticates the key. The signature binds the content.
· the three-layer argument

That closes the loop in a way a plain signature cannot. A compromised server can swap the key it serves, but not the pin in DNS, not without the domain owner noticing, and the signature itself is computationally binding. The pieces are ordinary. The combination is what makes the claim end-to-end.

Why post-quantum

The same reasoning that pushed my mail tooling to lattice cryptography applies here, and it applies more strongly. Signatures are the most long-lived secret a system keeps. They authenticate content that is meant to be archived, republished, and cited for years. A classical signature harvested today and broken by a quantum computer tomorrow would retroactively invalidate every document it ever signed.

Hash-then-sign makes that failure slightly less catastrophic, because the digest commit is at least recorded. But the honest fix is to sign with algorithms that do not depend on the assumptions a quantum machine erases. ML-DSA keys are larger and signatures are bulkier than RSA, a real cost for pages with many signed blocks, but the content that needs signing is usually small and important, exactly the content whose integrity should outlast a generation of hardware.

The habit worth forming

I built htmlsign because I kept hitting the same uncomfortable fact: the parts of a page that matter most to a reader, the biography, the price, the claim, are exactly the parts with no integrity guarantee between the author and the reader. Certificates are getting worse at this, not better, as the web moves toward ever more intermediaries with ever more rewriting power.

The tool does not pretend to protect the entire page. It protects what you choose to freeze, and it makes that protection cheap enough to use: one command to sign, one command to verify, and a pipeline that exits non-zero the moment a signature fails. The discipline is the same as everywhere else on this site. Keep the invariant small, state it explicitly, and let a machine check it forever.

The source lives on GitHub. If you sign anything, sign the parts whose integrity outlives the connection they traveled over.