The short answer
If your audit log is signed with Ed25519 and the records must stay checkable for ten years or more, add a second, post-quantum signature now. NIST’s draft transition plan, IR 8547, lists EdDSA, which covers Ed25519, as disallowed after 2035.1 A record written in 2026 and kept for twenty years will be checked in 2046. By then its Ed25519 signature alone may not be accepted, and once an algorithm can be forged, anyone holding a forgery can make a record that looks authentic.
The fix is not to swap algorithms. It’s to carry both: the Ed25519 signature every current tool reads, and an ML-DSA signature (FIPS 204) beside it. The hard part is doing that without letting someone strip the new signature off, and without breaking every record you already wrote.
The rules below apply to any signed log. Deedglass, the gate we build, implements them, so the commands and output are real.
Deedglass
A sealed record you can check offline
Record 1043
- What ran
- Medication order, Bed 12
- Approved by
- Dr. N. Shah
- Check
- Matches
- Time
- 09:12
- The request
- Not keptA fingerprint only: 77d2…c41a
Each record is linked to the one before
- Record 1041
- Record 1042
- Record 1043
Why signatures have a shelf life
Encryption and signatures fail differently. With encryption, the worry is “harvest now, decrypt later”: someone stores your traffic today and reads it once they can break the key. With signatures, nothing secret leaks. The risk is that the signature stops meaning anything. If Ed25519 can be forged in 2040, a signed record from 2026 no longer tells a reader in 2040 that it is the record you wrote.
Audit records are the worst case for this, because their whole job is to be checked long after they were written. The reader often arrives years later, with only the record and your public keys.
Three things now exist that make a second seal practical:
- The algorithm. ML-DSA is NIST’s standard post-quantum signature, FIPS 204.
- The wire format. RFC 9964 (May 2026) defines ML-DSA for JOSE and COSE: COSE algorithm −49 for ML-DSA-65, key type AKP with the public key in
pub, and an empty context string.2 - The library. Go 1.27’s standard library includes
crypto/mldsa, with 1,952-byte ML-DSA-65 public keys and 3,309-byte signatures.3 No new dependency.
Five rules for a second seal
Adding a second signature field is easy. Adding one that an attacker can’t simply delete takes rules. These are the five we wrote down, with attack cases, in the first pull request, before any of the new format’s code.
1. The signed body lists every seal it must carry. If the second signature sits only in the envelope, beside the body, nothing signed says it exists. Strip it, and you’re left with a record that looks like a valid old-format record. So the list of required seals goes inside the body, and every signature covers that list. In Deedglass, every seal of a v2 record names its algorithm, and the record lists every seal it must carry, so a removed seal is detected.4
2. Each algorithm signs a different, tagged input. Prefix the bytes with the format version and the algorithm name, so a signature made for one purpose is never accepted for another. In the spec, the Ed25519 seal signs deedglass/deed/v2/ed25519 plus the body hash, and the ML-DSA-65 seal signs deedglass/deed/v2/ml-dsa-65 plus the same hash.
3. Decide by the record’s date, not the verifier’s clock. A rule like “Ed25519 alone is fine until 2035” must read the date written in the record. Otherwise the same file gives one answer in 2026 and another in 2046, and nobody can reproduce an old check. From 2035-01-01 on, a record sealed with Ed25519 alone is refused (seal_missing); the date is read from the record’s issued_at, never from the verifier’s clock.4
4. An algorithm you can’t check is “cannot decide”, never a pass. A verifier from 2027 will meet records sealed with algorithms invented later. It shouldn’t call them altered, and it must not call them verified. It should say it can’t decide, and exit with a different code.
5. Every seal present must pass. One good seal never covers a bad one. If either signature fails, the record fails.
And one rule about the past: old records must keep verifying exactly as before. Records and bundles written before deed v2 verify as before: one bundle from every release since 0.1.0 is verified on every pull request.4 Every release keeps one sealed export bundle in the repo, and a required CI job verifies all of them with the CLI and the browser verifier, so a change that stops an old release’s records from verifying fails a required check.4 We call it the 2046 test.
What it looks like when it works
Here is deedglass verify from the v0.9.0 release, run against the published v2 test vectors. These are sealed with test keys, and the CLI says so. First, a dual-sealed record:
Code What the screen or file shows, copied as is.
$ deedglass verify testdata/deeds/v2/valid/03-dual-sealed.json --keys testdata/deeds/v2/keys.json
✓ 1 deed · seals intact · chain not checked · 2 seal keys
○ These deeds were sealed by Deedglass's published test keys. They mean nothing outside tests.
next Verify deeds from a real issuer against its published keys.
The same record with its ML-DSA-65 seal removed (rule 1):
Code What the screen or file shows, copied as is.
$ deedglass verify testdata/deeds/v2/invalid/pq-seal-stripped/deed.json --keys testdata/deeds/v2/keys.json
✕ Deed 1 is missing a seal it must carry (no ML-DSA-65 seal by sk_01m3xxf0m4wz8mwd5h76p09nn7).
deed deed_01m3xxj291vb6nxg25pympae2g (line 1)
next Treat this deed as altered, or ask the issuer for the deed as it was sealed.
A downgrade: rewritten as an old-format record and re-sealed with the Ed25519 key alone. It fails because the key set shows an ML-DSA-65 key that was active when the record says it was written:
Code What the screen or file shows, copied as is.
$ deedglass verify testdata/deeds/v2/invalid/pq-downgrade-to-v1/deed.json --keys testdata/deeds/v2/keys.json
✕ Deed 1 is missing a seal it must carry (the key set has a ML-DSA-65 key active at 2026-10-02T09:00:30.000Z, and this deed has no ML-DSA-65 seal).
deed deed_01m3xxj291vb6nxg25pympae2g (line 1)
next Treat this deed as altered, or ask the issuer for the deed as it was sealed.
Ed25519 alone, dated after the cut-off (rule 3):
Code What the screen or file shows, copied as is.
✕ Deed 1 is missing a seal it must carry (Ed25519 alone is accepted only for deeds issued before 2035-01-01T00:00:00.000Z).
And an algorithm this verifier doesn’t implement (rule 4). Exit code 2, not 1:
Code What the screen or file shows, copied as is.
○ Deed 1 is sealed with an algorithm this verifier can't check (SLH-DSA-SHA2-128s).
next Update deedglass, or check it with a verifier that supports that algorithm.
The same checks run in the browser. The browser verifier checks both seals offline with no runtime dependency.5 It checks ML-DSA-65 with its own dependency-free code and agrees with Go’s crypto/mldsa on every v2 vector and the 200-case v2 parity corpus. It is not a FIPS 140 validated module.6
What it costs
Post-quantum signatures are big and slower to check. Plan for both.
Size. An ML-DSA-65 signature is 3,309 bytes against Ed25519’s 64.3 Every record carries one, so storage grows by several kilobytes per record.
Speed. This is where we lose. Dual-sealed records are about 3.4 times slower to verify than Ed25519-only ones: 100,000 took 5.54 s against 1.63 s on an 8-core laptop. That misses our own 5 s budget for 100,000 records. Memory stays within 256 MiB.6 We publish the miss on our limitations page rather than relax the budget.
Operations. Turning it on is one command where the server’s database URL is set, then a restart of every instance with the new key file before the time it prints:
Code What the screen or file shows, copied as is.
$ deedglass keys rotate --alg ML-DSA-65
The Ed25519 key stays as it is (seal keys). Until you run that, nothing changes: an issuer with only an Ed25519 key keeps writing the old format, byte for byte, so older verifiers keep reading it.
What a second seal can’t do
Be clear about the limit. A date rule only helps against forgeries dated after your post-quantum key became active. Someone who can forge Ed25519 can also write any issued_at they like. So a forger in 2040 could write a fake record dated 2025, before your ML-DSA key existed, and the date rule alone would accept it.
What protects older history is the chain. In a hash-chained log, each record covers the hash of the one before it. A dual-sealed record therefore covers every record before it. A reader who keeps a recent dual-sealed head, or an outside timestamp, will see a forged earlier record as a broken link. That only works if the second seal starts early. Adding it in 2034 leaves eight years of records hanging on Ed25519 alone.
Two more limits. Our export-bundle manifest and the signed head we publish are still sealed with Ed25519 only, though the records inside a bundle are dual-sealed (limitations). And none of this says the action in a record was right or safe. A signature tells you a record is the one the issuer wrote. Whether the agent should have done it is a separate question, covered in why logs aren’t evidence.
A checklist
Whatever signs your logs, ask:
- How long must a record stay checkable? If the answer passes 2035, you need a second algorithm before then.
- Does the signed body list the seals it requires, so stripping one is detected?
- Does each algorithm sign a tagged input that names the format and the algorithm?
- Are date rules read from the record, never from the verifier’s clock?
- Is an unknown algorithm reported as “cannot decide”, separately from “altered”?
- Must every seal present pass?
- Is one record from every past release re-verified on every change?
- Have you measured verify time and storage with both seals, and written down the number?
If you can’t answer the first, start there.
To see records and their seals on a real Postgres, try the playground. To see which of your tables Deedglass could check, run npx deedglass scan --postgres-dsn-env DATABASE_URL (the grade, explained). The full rules are in the deed spec, and the agent action record glossary entry explains what a record holds.
Written with AI assistance and checked against our claims register.