05 · Security review
What the audit found
Twenty-three findings against the previous candidate, two of them Critical. The remediation then surfaced three more that the audit itself had missed — which is what a real review looks like, and is the argument for the next one.
- Critical
- 2both resolved
- Major
- 7all resolved
- Minor
- 11incl. 3 found in remediation
- Deferred
- 1editorial — e-5
Contents
01
C-1 — the transparency log could silently rewrite the past
The log's timestamp was not committed to the Merkle tree
Every authority decision in BI-004 §7.1 turns on when a statement was
logged: the timestamp is what decides whether the publisher's signing key was authorised at that
moment. In the previous candidate, that timestamp was not bound into the leaf at all.
A log could therefore restate it after the fact — breaking no leaf hash, breaking no consistency proof, and raising no error anywhere — and thereby slide a statement across a key-validity boundary.
What that buys an attacker
- Turn a valid correction into an "unauthorised" one, so consumers refuse it and the false record stands
- Resurrect a withdrawal that had been repudiated, so content is purged that should not be
No misbehaviour by the publisher was required. The log alone could do it. RFC 9162 does not have this hole — its Merkle leaf embeds a timestamped entry. BSP had dropped that structure while claiming to adopt the construction unmodified.
Fixed The leaf now binds the immutable first-merge timestamp. A one-millisecond restatement is a hard verification failure, proven by regression test and by a normative test vector that a conformant log must reject.
leaf_hash = SHA-256( 0x00 ‖ JCS({ "timestamp": FIRST_MERGE_TIME,
"statement": STATEMENT }) )
FIRST_MERGE_TIME — assigned once, on first merge, and never reassigned.
Source: BI-008 §5.1, Appendix B
A related finding was closed with no change
C-1a was raised alongside it and closed without a specification change — the reported behaviour turned out to be correct as written. It is recorded anyway. A finding that was investigated and dismissed is part of the audit record; deleting it would make the review look tidier than it was.
Source: audit/MANIFEST.json · findings_closed_no_change
02
C-2 — the reference canonicaliser was not RFC 8785
Two defects, neither of which produces an error
| Behaviour | Ours (RC1) | RFC 8785 |
|---|---|---|
| Object key ordering | Unicode code point | UTF-16 code unit |
| Small-number serialisation | 1e-07 |
1e-7 |
The orderings agree on ASCII and disagree the moment a key contains an emoji or a rare CJK character. The number formatting disagrees on any sufficiently small float.
Neither defect raises an exception. It produces a different hash for the same statement. Every proof such an implementation emits fails against an independent verifier — and nobody notices until an auditor tries to reconcile two trees and finds they disagree about history.
Fixed
And promoted to a gating suite: JCS-1 … JCS-8,
30 golden vectors. An implementation failing any of them must not claim conformance at
any role or level. Not waivable, not reportable as not-automatable.
That gate exists because the reference implementation itself failed it — and the specification's own test suite did not catch it, because the suite had been written against the same wrong assumption.
The generalisable lesson, and the ask
A test suite written by the same people who wrote the bug will agree with the bug. C-2 was found by comparing against the standard, not against ourselves.
This is why the most valuable thing an external reviewer can do is write an independent RFC 8785 canonicaliser and check that the leaf hashes match ours.
Verified: core/test_canonical.py → 30/30 passed
03
The seven Major findings
All seven resolved. Every one produced a new normative section or a tightened requirement — none was closed by argument.
| ID | Fixed in | Status |
|---|---|---|
M-1 |
BI-008 §7.0 (new), §5.4, and the Internet-Draft — Maximum Poll Interval. A consumer that polls weekly cannot meet a one-hour deadline; the MAD was meaningless without a bound on how late a statement could arrive. | Resolved |
M-2 |
BI-008 §11.4.1 (new), §11.2 — "independent" defined normatively and made non-self-assertable. The is_self_operated boolean was deleted: a commonly-owned second log would simply have set it to false. |
Resolved |
M-3 |
BI-008 §6.3.1, §6.3.2 (new), and the Internet-Draft — the honest cost of the completeness property, rather than an implied guarantee the construction does not provide. | Resolved |
M-4 |
BI-008 §11.6.1 (new) — disaster recovery. A naive restore re-issues different leaves at the same indices, which is log equivocation by accident. | Resolved |
M-5 |
BI-008 §4.3; BI-004 §6.1, §11; Internet-Draft — algorithm downgrade closed. none, HS* and all MAC/symmetric algorithms are forbidden in any position; the algorithm is fixed by the resolved JWK type, never by a supplied header. |
Resolved |
M-6 |
BI-008 §11 heading, §12.4 — operator requirements made operator-neutral, so nothing in the log role presumes it is us. | Resolved |
M-7 |
BI-002 §2.4 rule 6; BI-004 §11.2 — the namespace steward gate. Now a normative publication gate rather than a silently-carried hazard. It is still unmet. | Gate open |
M-7 is resolved as a specification change and unmet as a condition
The audit's finding was that the namespace hazard was undeclared. That is fixed: it is now a normative gate. The gate itself has not been passed — no entity is named as steward, and no entity owns the IANA registration.
Declaring a gate is not the same as passing it, and this page does not conflate the two. See Governance.
04
Threat model, before and after
What the audit changed, stated as threats rather than as document sections.
| Threat | RC1 | FC1 |
|---|---|---|
| T-6 Retroactive repudiation |
Bounded publisher-side only. Log-side restatement was not modelled — and was possible. | Log-side closed by C-1. Publisher-side (back-dating revoked_at) remains bounded, not eliminated — full closure needs key transparency (v1.1). |
| T8 Replay |
Deduplicated by leaf hash. | Deduplicated by a timestamp-excluded statement hash — so a replay cannot cause re-timestamping into a window where a revoked key appears valid. |
| T8b Cross-log replay new |
Not modelled. | Cannot be prevented — and need not be. Statements are public signed blobs; anyone can copy one into any log. Closed at the consumer: only the logs an object itself lists are authoritative for it. |
| Algorithm downgrade | alg unconstrained; no ban on none or MAC. |
none, HS*, all MAC/symmetric forbidden in any position. Algorithm determined by the resolved JWK type. |
| Unhonoured withdrawal via algorithm gap | Possible. An Ed25519-only consumer silently drops a ps256-signed withdrawal — fail-closed becomes not-honoured. |
Closed. Ed25519 mandatory to sign; all three mandatory to verify. |
| Log equivocation via disaster recovery | Undefined. A naive restore re-issues different leaves at the same indices. | Defined in §11.6.1. |
The T8b row is the one worth reading twice
Some threats are not preventable at the layer where you first notice them. Anyone can copy a signed statement into any log — that is a property of public signed blobs, not a flaw to be patched.
The defence is to move the decision to the consumer: only the logs an object lists are authoritative for that object. Trying to prevent it at the log would have required a registry of permitted logs — and a registry is a chokepoint.
05
What the audit missed
Fixing the twenty-three findings surfaced three more that the audit had not found. They are numbered separately and published, rather than folded quietly into the totals.
| ID | Finding |
|---|---|
m-9 |
signedAt is publisher-asserted and corroborated by nothing.
A consumer must not treat it as evidence of when anything actually happened — the log
timestamp is the only time value with a witness.
|
m-10 |
BI-004 §11's allowance to reuse the Authority Certificate for C2PA claim signing conflated two roles that should not share a key. |
m-11 |
versionId is computed over the compact JSON document as
serialized — which is what makes an identifier provably stable across a
namespace reallocation. Without this, moving the namespace would have silently
rewritten every identifier in the archive.
|
An audit that finds nothing has not been performed. An audit whose remediation finds nothing further has not been remediated carefully. Three findings emerged from fixing twenty-three. There is no reason to believe the next review will find zero.
06
What remains open
Published, not buried. Every item below is a known weakness of FC1.
| Issue | Position |
|---|---|
| Proof of purge K-4 | We can prove a purge from retrieval indexes, embeddings, and grounding data. We cannot prove absence from model weights, and we do not claim to. Where content has reached weights, a conformant consumer reports compliance incomplete, with the reason — it does not mark itself compliant. This is an open research problem, not an implementation gap. |
| Retroactive repudiation K-1 | Narrowed by C-1, not closed. The log-side half is fixed. A publisher back-dating its own key revocation remains bounded — by append-only revocation, Memento history, and mandatory Monitor archival — but not eliminated. Full closure requires key transparency, deferred to v1.1. |
| Domain hijack K-2 | Unclosable at this layer, by anyone. Authority is rooted in control of an origin domain; whoever controls the domain controls the authority. Mitigation is procedural — registry lock, long registration — not cryptographic. Stating this plainly is better than implying a defence we do not have. |
| Six non-automatable assertions K-6 |
Reported not-automatable, never pass. Each now
requires a named human attestor of record with a resolvable identity.
|
| No Monitor, no Auditor |
Sixteen assertions unexercised. Gossip is a MUST, and without an
auditor that gossips there is no defence against a log equivocating — showing one
history to one consumer and another to another, with both proofs verifying.
The protocol specifies the defence. Nothing implements it.
|
| No independent Consumer K-7 residual |
Reference Consumer #001 exists and passes 43/43. It is ours. Every
CON-* result is therefore a self-report.
|
| Deferred editorial e-5 | One editorial item formally deferred, with its reason, risk, target, and workaround stated. No conformance impact. Deferring it openly is better than fixing it quietly. |
07
The limits of this review
What follows is the part of a security review that is usually left out.
This audit was not independent
It was commissioned by 360WiSE, conducted against 360WiSE's specifications, and remediated by 360WiSE. It was adversarial in intent and it found real defects — including two Critical ones in our own code — but it was not independent, and adversarial intent is not the same thing as independence.
C-2 is the proof. The specification's own test suite did not catch the canonicaliser bug, because the suite had been written against the same wrong assumption. A reviewer who shares your assumptions cannot find the errors that live inside them.
What has not been reviewed at all
- No external cryptographic review of the Merkle construction or the signature profile
- No independent implementation of any role — so no interoperability evidence exists
- No operational review — FC1 has never been run at scale, by anyone
- No Monitor or Auditor — the equivocation defence is specified and untested
Zero external reviews have been performed. Everything on this page is what we found when we looked at our own work. It should be read as a starting point for scrutiny, not as evidence of soundness.
08
How to attack it
In descending order of usefulness to us — which is to say, in descending order of how much damage each would do if you succeed.
-
01
Write an independent RFC 8785 canonicaliser and check that your leaf hashes match ours.
This is how C-2 was found. A disagreement here means one of us is computing a different history — and it has been us before.
-
02
Make Reference Consumer #001 purge something it should not.
Forge a withdrawal it accepts. Find a scope it fails to check. If you succeed, you have found a censorship vector, and it must be known before anyone deploys this.
-
03
Break the timestamp binding.
C-1 is the load-bearing fix in FC1. If the binding is insufficient, everything above it is wrong. Tell us how.
-
04
Build a Monitor or an Auditor.
Sixteen assertions are unexercised, and the equivocation defence is specified but untested. Nobody has ever run one.
cd spec
python3 bsp/reference_merkle.py # RFC 9162 vectors + the attack suite a log must fail
python3 tests/negative_tests.py # 27 documents that MUST be rejected
cd ../implementations/massmediahub-consumer-001
python3 tests/test_consumer.py # incl. forged withdrawal, wrong scope, 1ms restatement
Verify the bytes first: sha256sum -c SHA256SUMS.txt
Read the audit, unedited Known issues
Submit an Independent Engineering Review
Email [email protected]
Security findings, interoperability reports, conformance issues, implementation feedback, and specification comments are welcome.
Substantive technical reviews may be published, with attribution unless anonymity is requested.
