FreeSign

Post-quantum · ML-DSA / FIPS 204

The first post-quantum e-signature — on a PDF everyone can still open.

FreeSign puts an ML-DSA signature — the NIST post-quantum standard, FIPS 204, formerly CRYSTALS-Dilithium — alongside the classical signature inside the same signed PDF. Both cover the same bytes. The classical half is untouched, so Adobe Reader, openssl and pyHanko validate the file exactly as before. It is an ordinary free e-signature with a second, quantum-resistant signature riding along.

A “post-quantum PDF signature” that breaks Adobe Reader is not a feature — it is a file nobody can open. Everything below follows from refusing that trade.

1. What we claim — and what we don't

Post-quantum is the most over-claimed word in security marketing right now, so the boundaries come first, before the technical detail.

What we claim

  • FreeSign is the first electronic-signature service that signs with post-quantum cryptography. Every PDF signed here carries an ML-DSA signature alongside the classical one — in your browser, without an account, for free. Elsewhere in the ecosystem the post-quantum work so far has been about accepting such signatures: readers are learning to validate them, no signing product produces them.
  • The post-quantum signature is a real ML-DSA signature over the same DER-encoded CMS SignedAttributes the classical signature covers — not a hash pasted into a comment, not a metadata label.
  • The ML-DSA public key is bound to the signer by the classical signature, because its SHA-256 sits inside the signed attributes.
  • Adding it changes nothing for existing validators. A CI test asserts that openssl cms -verify still accepts a co-signed CMS.

What we do not claim

  • Not a post-quantum certificate. The ML-DSA key is bound by a signed commitment, not by an RFC 9881 certificate. There is no ML-DSA certificate authority behind it — see § 9.
  • Not a quantum-safe identity. Everything that establishes who signed — the leaf certificate, the browser session key, the passkey assertion — is still classical elliptic-curve cryptography. The post-quantum half proves the same attributes were signed, not who signed them, except through the classical binding.
  • Not a change of legal class. It is still not a Qualified Electronic Signature. See § 8.
  • Not retroactive. Documents signed before this shipped do not carry a post-quantum half — nothing can be added to a PDF after the fact without breaking its signature. /verify tells you which is which, per file.

2. Why now: the ten-year window

We are not going to tell you a quantum computer is about to break your NDA. Nobody honest knows the date, and the ones who claim to are selling something.

The actual reason is duller and firmer: a document signed today has to remain verifiable for as long as it matters legally, and that is measured in years. FreeSign retains signing evidence for about ten years — the period the privacy policy commits to — and commercial contracts, employment agreements and NDAs routinely outlive that. The question is not “will a quantum computer arrive tomorrow”; it is “if one arrives inside the lifetime of this document, does the signature still mean anything?”

You cannot retrofit a signature. Once a PDF is out in the world, the only cryptography protecting it is the cryptography that was in it when it was signed. So the layer has to go in before it is needed, or not at all — which is precisely why it is worth shipping now, and precisely why the addition must not cost you anything today.

3. Why a co-signature, not a replacement

The cryptographic standards below the PDF layer are ready. RFC 9881 defines ML-DSA in X.509 certificates; RFC 9882 defines it in CMS, the container a PDF signature lives in. The PDF layer is not ready: no shipping PDF validator accepts an ML-DSA SignerInfo today.

So the choice was between two options:

Replace the algorithm.

Swap ECDSA for ML-DSA in the SignerInfo. You gain a property nobody can check yet, and you lose Adobe Reader, pyHanko and openssl — every tool your counterparty actually uses. The recipient sees a broken file.

Add alongside it.

Leave the classical signature byte-for-byte as it was and carry the post-quantum one as extra CMS attributes. Conforming validators ignore attributes they do not recognise, so nothing changes for them — and the property is there the day anyone wants to check it.

FreeSign takes the second. The SignerInfo signature stays ECDSA P-256 and the profile stays PAdES-B-T (optionally B-LT). Everything on the trust page and in the openssl verification guide still holds, unchanged, on a co-signed file.

4. How it works: two attributes, one signed payload

When the seal runs, FreeSign generates an ephemeral ML-DSA keypair for that one signature — same lifetime as the ephemeral ECDSA leaf key, wiped from memory as soon as the signature exists, never written to disk or database. Two attributes then go into the CMS, under FreeSign's own IANA PEN arc 1.3.6.1.4.1.65834:

The post-quantum attribute pair inside the signature's CMS
 Key commitmentSignature
OID1.3.6.1.4.1.65834.1.31.3.6.1.4.1.65834.1.4
Where it livessignedAttrs — covered by the classical signatureunsignedAttrs — it cannot sign itself
ASN.1SEQUENCE { algorithm OID, publicKeyHash OCTET STRING }SEQUENCE { algorithm OID, publicKey OCTET STRING, signature OCTET STRING }
ContentsThe ML-DSA parameter-set OID and SHA-256 of the raw ML-DSA public keyThe raw FIPS 204 public key and the ML-DSA signature over the DER SignedAttributes
What it provesThat this signer, with today's unforgeable classical signature, named that post-quantum keyThat the holder of that key signed the very same attributes the classical signature covers

Why the split is load-bearing

This is the part worth understanding, because it is the difference between a real construction and decoration.

  • The commitment must be inside the signed attributes. Otherwise nothing connects the post-quantum key to the signer — anyone could append an unsigned attribute containing their key and their signature. Today's ECDSA signature, which is unforgeable today, is what binds the key to this signer's identity. Key generation therefore has to happen before the signed attributes are built.
  • The signature cannot be inside them. It covers those exact bytes; putting it in would require it to sign itself. So it rides as an unsigned attribute — bound in the other direction: it commits to the signed attributes, which commit to messageDigest = SHA-256(ByteRange), which is the entire signed revision of the PDF. Edit anything and it breaks.
  • The two halves check each other. A verifier must reject a signature attribute whose public-key hash does not match the signed commitment. FreeSign's verifier fails a swapped key, a bad signature, and a commitment whose signature was stripped out — that last one because the commitment is signed: the signer vouched for a post-quantum key that is no longer in the file. The mirror case is deliberately a caveat rather than a failure: a signature attribute with no commitment is an unbound key, and since anyone can append an unsigned attribute to anyone's PDF, failing on it would let a stranger turn a valid document red. It is reported and ignored. Absent on both sides is merely informational — most PDFs in the world carry neither.

The cost

ML-DSA-65 — the middle parameter set — adds a 1,952-byte public key and a 3,309-byte signature, about 5.3 KB DER-encoded, inside a 32 KB signature slot that has room to spare. Signing costs single-digit milliseconds. Nothing about the ceremony gets slower or larger in any way a signer would notice. All three parameter sets are supported:

ML-DSA parameter sets (FIPS 204), with the OIDs FreeSign writes into both attributes
Parameter setAlgorithm OIDPublic keySignature
ML-DSA-442.16.840.1.101.3.4.3.171,312 bytes2,420 bytes
ML-DSA-652.16.840.1.101.3.4.3.181,952 bytes3,309 bytes
ML-DSA-872.16.840.1.101.3.4.3.192,592 bytes4,627 bytes

Signing uses “pure” ML-DSA with an empty context string — the mode RFC 9882 mandates for CMS. The implementation is the audited pure-JavaScript @noble/post-quantum, because WebCrypto still has no ML-DSA in any shipping browser or in the Cloudflare Workers runtime.

5. The threat model: retroactive forgery

There is no “harvest now, decrypt later” for a signature. A signature keeps no secret — the document is right there in the open. Collecting signed PDFs today to decrypt in 2040 buys an attacker nothing, because there is nothing encrypted to collect.

The real risk has a different shape: retroactive forgery. An adversary with a cryptographically relevant quantum computer recovers a private key from a public one, forges an ECDSA signature over a document of their choosing, and claims it was signed years earlier — before anyone could have done that.

Defeating that needs two things, and FreeSign already had one of them.

  1. 1. A signature the quantum computer cannot forge

    That is the ML-DSA co-signature. Its security rests on lattice problems, not on the discrete logarithm that Shor's algorithm dismantles.

  2. 2. Proof that the key binding existed before such a machine did

    This part was already there. Every FreeSign signature carries an RFC 3161 timestamp and an OpenTimestamps proof that settles against public Bitcoin block headers. Both are hash-based, and hash functions are the part of classical cryptography that a quantum computer degrades rather than breaks — SHA-256 keeps a large security margin. So they remain meaningful evidence of when something existed even after Q-day.

    One refinement mattered enough to build: that OpenTimestamps proof dates the document, and an adversary who can forge signatures could mint a whole new signature structure over the same document — new certificate, new post-quantum key, new commitment — which the document proof would happily date. So every signature carries a second OpenTimestamps anchor, over SHA-256 of the signed attributes themselves (unsigned attribute 1.3.6.1.4.1.65834.1.5, embedded in the PDF like the first). Those attributes are where the key commitment lives, so this is the anchor that actually dates the binding. Like any OpenTimestamps proof it is a calendar commitment first and independent evidence once it reaches a Bitcoin block header, usually within an hour or two of signing; /verify says which state a given file is in.

Put together: the commitment names the post-quantum key inside the signed attributes; the classical signature — unforgeable today — binds that key to the signer; and the timestamps, the second anchor in particular, date that binding to today. After Q-day the classical binding is no longer trustworthy on its own, but the timestamped record that it existed beforehand is. That is the whole argument, and it is why the timestamps and the co-signature are worth more together than either is alone.

6. The standards this is built on

None of the cryptography here is home-made. Every piece is a published standard, which is what makes the signature checkable by anyone with off-the-shelf tools rather than only by us.

  • FIPS 204 — NIST's Module-Lattice-Based Digital Signature Standard, published August 2024, the algorithm formerly submitted as CRYSTALS-Dilithium. FreeSign signs with the ML-DSA-65 parameter set.
  • RFC 9881 defines how ML-DSA keys and signatures are expressed in X.509, and RFC 9882 does the same for CMS — including the algorithm identifiers this page quotes. Our attributes use those identifiers rather than inventing any.
  • ETSI EN 319 142 (PAdES) and RFC 5652 (CMS) are what the surrounding signature conforms to, unchanged by the post-quantum addition.

What is new here is not the algorithm — it is that you can use it. The standards have been available to every signing vendor since 2024; FreeSign is the first to put them behind a signing button. Drop a PDF on the home page, sign it, and the file you download carries a post-quantum signature: no account, no licence, no cost, and the document never leaves your browser.

7. Verify it yourself

“Verify it yourself” is this product's entire posture — the same posture as the trust page. A post-quantum claim you have to take on faith is worth nothing, so there are three independent ways to check it, none of which require trusting FreeSign.

  1. 1. In your browser, at /verify

    Drop the signed PDF. The post-quantum co-signature is the sixth check. It confirms that the embedded ML-DSA public key hashes to the value committed inside the signed attributes, and that the ML-DSA signature verifies over those same attribute bytes. The file is never uploaded — the check runs locally, like every other one on that page.

  2. 2. With the open-source verifier

    The exact client-side code served at /verify is published under the MIT licence at github.com/free-sign/verifier, and it runs under Node as well as in a browser. Read it, audit it, run it against our PDFs or anyone else's.

  3. 3. From a shell

    node tools/validate-sealed-pdf.mjs signed.pdf runs the whole validator ladder in one shot; the post-quantum verdict is step [7]. And the point that matters most for interoperability — openssl cms -verify and pyHanko sign validate accepting a co-signed file exactly as they accept a classical one — you can reproduce in a terminal in under a minute with the verification guide.

Tampering tells the same story from the other side, and this is pinned by continuous-integration tests: flip a bit in the ML-DSA signature, swap the embedded public key, or strip the unsigned attribute entirely, and only the post-quantum check fails — the classical CMS check stays green. Which is exactly why the sixth check has to exist as its own verdict rather than being folded into the first.

9. What we deliberately did not build

Each of these was a decision, not an oversight.

  • No post-quantum certificate authority. The ML-DSA key is bound by the signed commitment, not by an RFC 9881 certificate. A second, ML-DSA certificate chain is worth building once something in the ecosystem validates it; until then it would be complexity with no verifier.
  • No ML-DSA in the SignerInfo. For the reason in § 3: it would break every validator that exists. When ISO and Adobe formalise the PDF algorithms, that door opens.
  • The browser session key and the two envelope signatures stay ECDSA P-256. The session key is a minutes-long anti-replay binding with no long-term value, and it lives as a non-extractable CryptoKey in the browser. Moving it to ML-DSA would mean handling the private key as a plain byte array in JavaScript — a real security downgrade, today, for a benefit that does not apply to a key that expires in minutes.
  • Passkeys stay ES256. The algorithm is the authenticator's choice, not ours — no shipping platform authenticator offers a post-quantum option.

The OpenTimestamps anchor over the signed attributes described in § 5 was the next step on this list, and it shipped: the key commitment now has its own hash-based proof of existence rather than relying on the document anchor. Next, and only once the ecosystem can check them, come RFC 9881 certificates and eventually a native post-quantum signature.

Sign a PDF and check the sixth line yourself

Sign a document, then drop the result on the verifier. Both run entirely in your browser; neither ever sees the file.

Sign a PDF now →
Technical details for advanced users

The two attributes, in ASN.1

Both live in the signature's CMS SignerInfo, under FreeSign's IANA Private Enterprise Number arc 1.3.6.1.4.1.65834 — the same arc that carries the document OpenTimestamps proof (….1.1), the signing-ceremony evidence record (….1.2) and the OpenTimestamps proof over the signed attributes (….1.5).

FreeSignPqCommitment ::= SEQUENCE {   -- signedAttr, OID 1.3.6.1.4.1.65834.1.3
  algorithm      OBJECT IDENTIFIER,  -- id-ml-dsa-44 / -65 / -87
  publicKeyHash  OCTET STRING }      -- SHA-256(raw ML-DSA public key)

FreeSignPqSignature  ::= SEQUENCE {  -- unsignedAttr, OID 1.3.6.1.4.1.65834.1.4
  algorithm      OBJECT IDENTIFIER,
  publicKey      OCTET STRING,       -- raw FIPS 204 public key
  signature      OCTET STRING }      -- ML-DSA over the DER SignedAttributes

What the ML-DSA signature actually covers

The same octets the classical signature covers: the DER encoding of the SignedAttributes set, re-encoded with the SET OF tag as RFC 5652 requires for signing. Those attributes include messageDigest, which is SHA-256 of the PDF's /ByteRange — the whole signed revision minus the signature slot. So the post-quantum signature transitively covers the document, the signing time, the signingCertificateV2 binding to the leaf certificate, and its own key commitment. There is no separate message and no separate digest to argue about.

Verification order a third-party implementation should follow

  1. Locate the signed commitment attribute (…1.3) and the unsigned signature attribute (…1.4). Neither present → classical-only file, report informational, not a failure.
  2. Only the commitment present → fail. It is a stripped post-quantum half: the unsigned attribute sits in a mutable region and can be removed by anyone, but the commitment is signed, so the signer demonstrably vouched for a key that is no longer in the file.
  3. Only the signature present → report it, do not fail. It is an unbound key worth nothing — but attribute presence in the unsigned set is unauthenticated, and nothing inside the signed attributes promises a post-quantum half exists. Failing here would let anyone append this attribute to any signed PDF, FreeSign's or a QTSP's, and flip a valid document to invalid.
  4. Both present → the two algorithm OIDs must match each other, and the public key and signature lengths must match that parameter set exactly.
  5. SHA-256(publicKey) must equal the committed publicKeyHash. Mismatch means the key was swapped after signing → fail.
  6. Verify the ML-DSA signature over the DER SignedAttributes, pure mode, empty context.
  7. Report the classical verdict independently. A post-quantum signature that verifies while the classical one does not proves only that whoever held that ML-DSA key signed those attributes — the identity binding runs through the classical signature.

Reproducing the interoperability claim

The claim the whole design rests on is that adding these attributes changes nothing for existing validators. The quickest reproduction is pyHanko, which understands PAdES coverage formulas and so needs no manual ByteRange reconstruction:

# A co-signed file validates exactly like a classical one.
pyhanko sign validate --pretty-print signed.pdf
#   Integrity check:           OK
#   Document modification:     NONE

# Carve the newest signature's CMS blob out and look at the attributes.
grep -aoE '/Contents *<[0-9A-Fa-f]+>' signed.pdf | tail -1 \
  | sed -E 's/.*<//; s/>//' | xxd -r -p > cms.der
openssl asn1parse -inform DER -in cms.der | grep -B1 -A3 '65834'

For openssl cms -verify you also need the ByteRange-defined content reconstructed from the PDF; node tools/validate-sealed-pdf.mjs signed.pdf does that for you in step [2], and the openssl verification guide walks through it by hand. The CA certificate is published at /.well-known/free-sign-signing-ca.pem.

Failure semantics inside FreeSign

FreeSign signs with the ml-dsa-65 parameter set. The signing code accepts any of the three FIPS 204 sets (ml-dsa-44, ml-dsa-65, ml-dsa-87), and an unrecognised value is a hard error that refuses to seal rather than a silent fall-back to classical-only — a configuration typo must never quietly downgrade what users receive. The same principle governs the RFC 3161 timestamp: a timestamp failure is an error, never an unannounced drop to an untimestamped signature.

Machine-readable references

The attribute pair, the OIDs and the verifier semantics are documented for automated clients in /llms-full.txt (under the seal endpoint) and summarised in /llms.txt. The evidence record that shares the same OID arc has its own JSON Schema at /evidence/v2/schema.json and a reference page at the evidence JSON schema guide.