Don’t trust us. Check.Try to break a sealed record
Coriqo
Disclosure · seal defects

We broke our own seal three times.

If you rely on our record, you need a checker that says fail when it should and pass when it should. You should also hear about our mistakes from us. Before asking anyone else to attack the format, we wrote down exactly what it promises. Doing that showed us three defects in our own verifier. One of them was a full break under the rules of our own challenge. Each is described below, with what it did and did not affect.

Published 2 September 2026 · all three found internally, none reported to us · the fixes are in the shipped verifier

We run a challenge called Break the Seal. Download a sealed governance bundle and the verifier that checks it. Change what the bundle says. If the verifier still passes it, we publish your finding under your name.

Before asking anyone else to try, we wrote down the format. It is a specification precise enough that someone could build a second verifier in Rust and find where it disagrees with ours. That document is SPEC.md. Writing it is how we found three defects in our own implementation.

AttributionNobody reported these to us. All three came from our own code changes, and we found them while writing the specification. We say so plainly because taking credit for someone else's work would be the costliest mistake this challenge could make. If a finding ever comes in from outside, we publish it under the finder's name, not inside a post like this one.

An unsealed event verified as sealed

In practice, a proof could have shown a reviewer that an event was sealed when it never was.

Everything above the signature in a proof bundle is unsigned. The bundle repeats the Merkle root and the tree size at the top level for display. The Ed25519 signature covers only the signed body underneath. Our verifier checked the Merkle proof against the top-level values, which nobody had signed.

Here is how the attack worked. Keep a genuine signed checkpoint body and its untouched signature. Then supply the root and path of a tree you built by hand, containing an event that was never sealed. The Merkle walk succeeds against your own root. The signature verifies against our real body. The verifier reports that the event is "cryptographically proven to be part of" a real checkpoint.

Both checks passed. That is a full break.

The fix ties the bundle to the signature before anything else runs. The top-level root and size must equal the signed ones, and the inclusion check uses the signed values. Here is the same attack run now, with the signed body and signature identical to a genuine bundle:

[FAIL] proof fields match the signed checkpoint body
       merkle_root in the bundle (24424d8e…) disagrees with the signed
       checkpoint body (c44c5036…); tree_size in the bundle (2)
       disagrees with the signed checkpoint body (5161)
[FAIL] Merkle inclusion proof
[PASS] Ed25519 checkpoint signature

The signature check still passes, because the signature was always genuine. It just is not enough on its own any more.

No sealed record was ever altered by this, and no signature was ever forged. What was wrong was the conclusion our verifier drew from a valid signature over a real body.


The tree was not the tree we said it was

Two different sets of records could produce the same seal, so the seal did not pin down exactly what it covered. Using that required our signing key.

We took our hashing from RFC 6962, the Certificate Transparency specification for Merkle trees. The 0x00 and 0x01 prefixes were right. The shape of the tree was not.

RFC 6962 splits an n-leaf tree at the largest power of two below n. We duplicated the last node on odd levels, which is the construction behind Bitcoin's CVE-2012-2459. Distinct leaf sets produce an identical root:

leaves [a, b, c]     e9636069c740c9ff51625b01a0b040396d265a9b920cc6febdfa5ecc9f58ecce
leaves [a, b, c, c]  e9636069c740c9ff51625b01a0b040396d265a9b920cc6febdfa5ecc9f58ecce

A Merkle root is supposed to identify its leaf set. Ours did not.

This one was contained, not exploitable. The tree size sits inside the signed body, and the collisions only happen across different sizes, so anyone using one would have needed our signing key. We checked whether two leaf sets of the same size could collide by brute force over every set of up to seven leaves from a six-symbol alphabet, and none did.

But the containment depended on a rule in our own specification that every future implementer had to remember: check the proof depth against the declared size, or the size means nothing. A footnote that holds up a security property will eventually get skipped. And a comment in our source called this "the standard RFC 6962 construction", which was not true.

Checkpoints now seal under tree_version 2, which is RFC 6962's Merkle Tree Hash. Each list of records gives a different root, so the root identifies the records without help from a signed size field. Anyone can also check a proof with Certificate Transparency tools that nobody at Coriqo wrote.

Default statetree_version 2 is what every new checkpoint seals under. It is the default in the shipped code, so a customer does not have to turn it on. Checkpoints sealed before the change keep tree_version 1 permanently, and the verifier implements both.

We did not re-seal the old ones. A root is a signed commitment to a tree of one particular shape, so it cannot be recomputed under a different shape. Re-sealing an old range with today's code would also be the kind of after-the-fact claim this product exists to rule out. They stay as they are.

One detail for anyone writing their own: under RFC 6962 the audit-path length depends on the leaf's index, not only on the tree size. A 5,161-leaf tree has valid path lengths of 4, 7, 8, 12 and 13. The fixed-depth check the old construction needs will reject most honest proofs under the new one.


An honest record failed verification

This is the one worth reading twice: a record nobody had touched was reported as failing.

We ordered the records in each seal by (created_at, seq). Neither column is unique, and created_at is a business timestamp that can be earlier than the moment the row was saved. On a live account that gave 5,161 events but only 4,825 distinct sort keys. Two sealed checkpoints then drifted by 759 and 359 positions when later writes were dated before them. The account's continuity certificate reported not_established. The certificate we lead with, the one that says governance never stopped, did not verify.

This never allowed a forgery. It made a record nobody had altered fail, and that is a problem of its own. A verifier that says FAIL on an honest record makes FAIL meaningless. A product that ships one teaches its customers to ignore it.

Every governance event now carries an ordinal, a number assigned when the event is added and never changed. Each checkpoint records the highest ordinal it covers inside its signed body. A seal covers exactly the events at or below that number. Recomputing a checkpoint therefore reads what was sealed, not whatever the table holds today. We migrated 92 schemas. The account now reports proven.

Seals made under the old ordering cannot be repaired, because we never recorded the order behind their root. We mark them non-reproducible and leave them out of the continuity chain, with the reason stated. We do not quietly present them as something they are not.


Why publish this

Because staying quiet would hurt the thing we sell.

Examiners, auditors and boards who rely on a sealed record are trusting the verifier's PASS and its FAIL. If either one was ever wrong, they should hear it from us.

A verifier is only worth something if it fails when it should. To find out whether yours does, write down what it promises in enough detail to check, and then check it. All three of these defects turned up that way.

Two of them slipped past our test suite, and both for the same reason: our test data was too small. The broken tree and the correct one produce identical roots whenever the leaf count is a power of two, so a test with four or eight events passes either way and proves nothing. The ordering defect needed events written across several transactions with out-of-order timestamps. Our test data was small and single-transaction, so it had no ties and no backdated events to trigger it.

The challenge is still open. There is no prize pot. We say so in the first section of the rules: nothing is paid, to you or to a charity on your behalf. A confirmed break gets a write-up under your name, published on your timetable, with our response beside it whether we agree with you or not. The verifier and its specification are on GitHub. If you find a fourth defect, we will publish that too.

The rest of what we would rather you heard from us.

Every open gap we know about, dated, on one page. That includes two that have nothing to do with cryptography and block a self-serve customer more than anything described here.