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

Read the audit, unedited What remains open

Contents
  1. 01  C-1 — the log could rewrite the past
  2. 02  C-2 — the canonicaliser was wrong
  3. 03  The seven Major findings
  4. 04  Threat model, before and after
  5. 05  What the audit missed
  6. 06  What remains open
  7. 07  The limits of this review
  8. 08  How to attack it

01

C-1 — the transparency log could silently rewrite the past

Critical C-1 Resolved

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.

The corrected leaf construction
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

Critical C-2 Resolved

Two defects, neither of which produces an error

What the canonicaliser did, and what RFC 8785 requires
BehaviourOurs (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-1JCS-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.py30/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.

Major findings and where each was fixed
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 model — RC1 to FC1
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.

Findings discovered during remediation
IDFinding
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.

Unsolved and partially-solved problems
IssuePosition
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.

  1. 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.

  2. 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.

  3. 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.

  4. 04

    Build a Monitor or an Auditor.

    Sixteen assertions are unexercised, and the equivocation defence is specified but untested. Nobody has ever run one.

The attack suite ships with the package
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

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.

Frozen Broadcast Infrastructure™ v1.0-FC1 · Final Candidate 1 · Not Final

One internal adversarial audit. Zero external reviews. This specification family is published for independent engineering review and is not to be operationally relied upon.