Some checks failed
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 30s
wai-audit collector and player integration / does this change touch it (push) Successful in 24s
wai-app and Create / wai-app · does this change touch it (push) Successful in 21s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 28s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 33s
wai-key-proxy / does this change touch it (push) Successful in 27s
wai-fleet service / does this change touch it (push) Successful in 16s
WAI operation catalogue / does this change touch it (push) Successful in 24s
wai-ledger service / does this change touch it (push) Successful in 26s
wai-quantum for JavaScript / does this change touch it (push) Successful in 26s
wai-sink-android native sink library / does this change touch it (push) Successful in 32s
WAI Studio / does this change touch it (push) Successful in 32s
wai-quantum studio and desktop / does this change touch it (push) Successful in 44s
wai-sink-swift native sink package / does this change touch it (push) Successful in 34s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m16s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 7m31s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 6m13s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 9m1s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 23s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 17s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 57s
wai-audit collector and player integration / collector · linux (push) Successful in 2m24s
wai-key-proxy / proxy · linux (push) Successful in 2m32s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m42s
wai-ledger service / service · linux (push) Successful in 3m21s
wai-fleet service / service · linux (push) Successful in 5m24s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m33s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m13s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 9m58s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m36s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 13m27s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 11m10s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m50s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 47s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m55s
wai-fleet service / service · macos (push) Successful in 1m5s
wai-ledger service / service · macos (push) Successful in 1m38s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m20s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 8m5s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m45s
wai-quantum studio and desktop / desktop app · macos (push) Failing after 12m13s
WAI Studio / Studio in Chromium and Firefox · macos (push) Failing after 12m25s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 17m39s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m42s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 7m51s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 8m29s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m9s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 18m57s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m7s
c2pa-binding §4.8 to §4.10 and §6.4: after its three assertions a store of wai.asset.c2pa may carry wai-mark:tent64 as a C2PA soft-binding assertion naming its robustness claim, the envelope's ISCC identifier, and the Creator Assertions Working Group's identity assertions (1.3, cawg.x509.cose); a verifier may hold its anchors as trust lists in the C2PA Trust List's form, pinned by the SHA-256 of their bytes. The algorithm-list entries for tent64 and band64 are written in extensions/c2pa-soft-binding-entries.json. extensions/iscc.md: ISO 24138's codec, ISCC-SEQ, Instance-, Image- and Audio-Codes and the ISCC-CODE from the IEP texts, replaying the public test vectors; WAI's integer pre-processing. extensions/detection.md: wai.media.detection, a detector's signed verdict at an operating point named by digest, its evidence a robustness record of binding type detector; refused as provenance, never a change to an origin claim's class. Corpora c2pa-identity/, iscc/ and detection/, each with a second implementation in Python. Co-Authored-By: Claude Opus 5.5 <[email protected]>
233 lines
15 KiB
Markdown
233 lines
15 KiB
Markdown
# WAI Extension: Detection Verdicts
|
||
|
||
> Status: Draft. Registers the record `wai.media.detection`: a synthetic-media detector's signed
|
||
> **verdict** over one object, a claim of the detector's operator and of nobody else. A verdict
|
||
> names the detector at its **operating point** — its term, the SHA-256 of its model or program,
|
||
> and its threshold — by the digest of that definition; states the detector's score for the
|
||
> object and its threshold as exact fractions, and the verdict the score gives at the threshold;
|
||
> and names the **robustness record** ([robustness-evidence](robustness-evidence.md) §5, its
|
||
> binding type `detector`) that measures the detector at that operating point. A verifier checks
|
||
> a verdict's arithmetic, its signature and its evidence. A verdict is never provenance: it is
|
||
> refused where it is presented as an origin claim ([synthesis](synthesis.md) §3.1), as the record
|
||
> that derived a subject, as an input's origin, or as an assertion of a C2PA manifest
|
||
> ([c2pa-binding](c2pa-binding.md) §6.2 step 4); and a verifier states it beside the origin claims
|
||
> of the case, whose classes it never raises or lowers. No new transport. The reference
|
||
> implementation is the `detection` module of `wai-rs` (feature `detection`); the corpus is
|
||
> [`live-conformance/detection/`](../live-conformance/detection/), and its second implementation
|
||
> is `live-conformance/detection_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision, each pinned to the version named: the C2PA Technical
|
||
> Specification, version 2.5 (October 2026), §2.3.9 and §2.3.10 (provenance and provenance data)
|
||
> and §16.3.7 (soft bindings, which a detector is not); and the Code of Practice on Transparency of
|
||
> AI-Generated Content as [synthesis](synthesis.md) pins it, Section 1 Measures 2.1 and 2.3, which
|
||
> distinguish a detection resting on a watermark from one that does not.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the record's encodings (§2); the verdict (§3); the evidence it names (§4); verification
|
||
(§5); why a verdict is never provenance (§6); the reasons (§7); and the corpus (§8).
|
||
|
||
Out of scope: any detector: this extension names a detector by digest and says nothing of how it
|
||
works or how good it is, which its robustness records state; a watermark's detection, which
|
||
synthesis §7.3 records as `wai-detection` and checks against the claim the mark's payload names;
|
||
what a party does with a verdict; and a verdict over a group, a track or a span.
|
||
|
||
## 2. Records
|
||
|
||
A verdict is a JSON object under the encodings and the reading of synthesis §2 — byte strings in
|
||
lowercase hexadecimal, unsigned integers of at most 2^53 − 1, no other number, no boolean, strict
|
||
reading of RFC 8259, exactly the members §3 lists, and a `kind` naming it; otherwise
|
||
`record_malformed` — with one exception: a **rate** member is read after every other member, and
|
||
one that holds anything but an exact fraction is `rate_not_exact`. A **rate** is `[n, d]`, two
|
||
integers with 1 ≤ *d* and *n* ≤ *d*, in lowest terms (robustness-evidence §7). It is signed:
|
||
|
||
```
|
||
sig = Ed25519(signer_key, "wai:detection\x01" || JCS(record without "sig"))
|
||
link_hash = BLAKE3("wai:detection-link\x01" || JCS(record))
|
||
```
|
||
|
||
JCS is RFC 8785. The signature is verified by WAI's strict rule ([SPEC](../SPEC.md) §7
|
||
*Signatures*): a key that fails step 1 or 2 is `bad_key`, a signature that fails a later step
|
||
`bad_signature`. A verdict is named by its link hash. Its `issued_at` is a date-time of
|
||
jwp-receipts §2.4.3, self-asserted.
|
||
|
||
## 3. The verdict
|
||
|
||
`kind` `"wai-detection-verdict"`, signed by the party that ran the detector:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `kind` | string | `"wai-detection-verdict"` |
|
||
| `subject` | 32 B hex | the content hash of the object examined (jwp-receipts §2.1) |
|
||
| `media` | string | `image`, `video`, `audio` or `text` |
|
||
| `detector` | term | the detector's name |
|
||
| `model` | 32 B hex | the SHA-256 of the detector's model or program |
|
||
| `threshold` | rate | the score at and above which the detector gives `synthetic` |
|
||
| `detector_digest` | 32 B hex | the digest of the detector's definition at this operating point, below |
|
||
| `score` | rate | the detector's score for the object |
|
||
| `verdict` | string | `synthetic` where `score` ≥ `threshold`, `not-detected` otherwise |
|
||
| `robustness` | 32 B hex or `null` | the link hash of the robustness record that measures the detector at this operating point (§4), or `null` where none is named |
|
||
| `issued_at` | date-time | when it signed |
|
||
| `signer_pubkey`, `sig` | | §2 |
|
||
|
||
The **definition** of a detector at an operating point is the JSON object `{"detector": D,
|
||
"media": M, "model": H, "threshold": T}` of the verdict's members of those names, and its
|
||
**digest** is the SHA-256 of its JCS. A verdict whose `detector_digest` is another is
|
||
`detector_digest_mismatch`: the digest names the detector and its threshold together, so a
|
||
verdict cannot move its threshold and keep its evidence. A score is a fraction the detector's
|
||
operator states; a verifier compares it with the threshold exactly, and a `verdict` that is not
|
||
the one they give is `verdict_inconsistent`.
|
||
|
||
## 4. The evidence
|
||
|
||
A detector's evidence is a robustness record (robustness-evidence §5) with `binding_type`
|
||
`detector`, whose `algorithm` is the detector's term, whose `algorithm_digest` is the verdict's
|
||
`detector_digest`, and whose `media` is the verdict's: it measures the detector at that operating
|
||
point. Robustness-evidence §5 reads it as it reads any record: its corpus's items are content the
|
||
detector is meant to flag, each value the one byte `01`; an item **survives** an attack where the
|
||
detector, run on it attacked, gives `synthetic`; its negatives are content the detector is meant
|
||
not to flag, and a **false positive** is one it flags, attacked. A verifier that does not run the
|
||
detector reads the record as stated (robustness-evidence §5 check 8), its table its signer's
|
||
statement, checked for its arithmetic and its signature. A verdict whose `robustness` names a
|
||
record the verifier does not hold, verified, is `robustness_not_held`; one whose record is of
|
||
another detector, digest, binding type or media is `robustness_mismatch`.
|
||
|
||
## 5. Verification
|
||
|
||
### 5.1 Inputs
|
||
|
||
A verifier is given a **case**: the origin claims presented as the provenance of the case's
|
||
objects (synthesis §3.1), the robustness records and claims it was given (robustness-evidence
|
||
§9.1), and the verdicts, each a list of texts in order; and its policy, `evidence`: `required`,
|
||
every verdict names a record, or `when-given`.
|
||
|
||
### 5.2 Steps
|
||
|
||
The verifier runs these steps and stops at the first refusal, which it reports with the reason,
|
||
the list and the index in it.
|
||
|
||
1. **The provenance.** Each text of the provenance list, in order, is an origin claim: one whose
|
||
`kind` is a verdict's is `verdict_as_provenance`; one that does not read as synthesis §2 and
|
||
§3.1 read an origin claim, or whose signature does not verify, is `provenance_invalid`.
|
||
2. **The evidence.** The robustness list verifies as robustness-evidence §9.2 has it, under the
|
||
policy `stated`: otherwise `robustness_invalid`, at the record that failed.
|
||
3. **The verdicts**, in order: each reads and its signature verifies (§2, §3: `record_malformed`,
|
||
`rate_not_exact`, `bad_key`, `bad_signature`); its `detector_digest` is its definition's
|
||
(`detector_digest_mismatch`); its `verdict` is its score's at its threshold
|
||
(`verdict_inconsistent`); and its evidence (§4): `null` under `evidence` `required`, or a record
|
||
not held, is `robustness_not_held`, and a record of another operating point
|
||
`robustness_mismatch`.
|
||
4. **No verdict as provenance.** No origin claim names a verdict's link hash as its `record`, the
|
||
record that derived its subject, or as an input's `origin`, the claim of an input: otherwise
|
||
`verdict_as_provenance`, at that claim.
|
||
|
||
A case every step accepts is accepted. This verifier reads the origin claims for their subjects
|
||
and classes; verifying them as provenance — their classes against their inputs and steps, their
|
||
evidence, their marks — is synthesis §11's.
|
||
|
||
### 5.3 What an accepted case states
|
||
|
||
- for each origin claim of an object, in order: its link hash, its object, and its class, as the
|
||
claim signs it;
|
||
- for each verdict, in order: its link hash, its object, its detector, its verdict, the robustness
|
||
record it names or `null`, and where it stands beside the claim of the same object: `agrees`
|
||
where the verdict is `synthetic` and the claim's class `synthesized`, or the verdict
|
||
`not-detected` and the class another; `disagrees` otherwise; `no-claim` where the case holds no
|
||
claim of the object.
|
||
|
||
The classes are the claims' own, whatever the verdicts: a case states the same classes with its
|
||
verdicts as without them.
|
||
|
||
## 6. Never provenance
|
||
|
||
Provenance is what the parties that made a piece of content state about how they made it, each
|
||
signing for its own step (synthesis §3.1; C2PA 2.5 §2.3.9). A detector made nothing: its verdict
|
||
is an inference by a third party from the content alone, and a classifier's inference is a
|
||
different kind of statement from a signed account of a step. So a verdict:
|
||
|
||
- is never an origin claim, nor the record that derived a subject, nor an input's claim (§5.2
|
||
steps 1 and 4);
|
||
- is never an assertion of a C2PA manifest: a store holding one, labelled
|
||
`science.transaction.wai.detection`, is refused (c2pa-binding §6.2 step 4,
|
||
`detection_as_provenance`), since the manifest's assertions are its claim signer's statements;
|
||
- never raises or lowers an origin claim's class (§5.3). A `synthetic` verdict over a claim of
|
||
captured content disagrees with it, and the claim stays captured; a `not-detected` verdict over a
|
||
claim of synthesized content disagrees with it, and the claim stays synthesized. Synthesis §3.2
|
||
raises a class only along a chain of signed steps, and a verdict is not a step;
|
||
- is not a soft binding: a soft binding is a value computed from the content to find its
|
||
provenance again (C2PA 2.5 §16.3.7), and a verdict is a judgement about the content.
|
||
|
||
A party that relies on a verdict relies on the detector's operator and on the detector's
|
||
measured robustness, by its policy; a verifier states the verdict beside the claims so that
|
||
neither is mistaken for the other.
|
||
|
||
## 7. Reasons
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `record_malformed` | §2, §3, §5.2 step 3 |
|
||
| `rate_not_exact` | §2, §5.2 step 3 |
|
||
| `bad_key` | §2, §5.2 step 3 |
|
||
| `bad_signature` | §2, §5.2 step 3 |
|
||
| `provenance_invalid` | §5.2 step 1 |
|
||
| `verdict_as_provenance` | §5.2 steps 1 and 4, §6 |
|
||
| `robustness_invalid` | §5.2 step 2 |
|
||
| `detector_digest_mismatch` | §3, §5.2 step 3 |
|
||
| `verdict_inconsistent` | §3, §5.2 step 3 |
|
||
| `robustness_not_held` | §4, §5.2 step 3 |
|
||
| `robustness_mismatch` | §4, §5.2 step 3 |
|
||
|
||
## 8. Conformance
|
||
|
||
[`live-conformance/detection/`](../live-conformance/detection/) holds the corpus, written from
|
||
fixed seeds by `wai_detection_vectors gen`: `vectors.json`, and each origin claim, robustness
|
||
record and verdict a file of its own. Every key is derived from the seeds, and none is a key
|
||
anyone holds. The detector is the corpus's stand-in, `wai-detector:conformance`: it scores a
|
||
picture by the share of its pixels whose grey, (77 R + 150 G + 29 B + 128) >> 8, is at least 128,
|
||
its `model` the SHA-256 of the text of that definition the corpus states, its threshold `[1, 2]`.
|
||
It detects nothing about how a picture was made; it stands for a detector a verifier does not
|
||
implement. Its robustness records are measured: the generator runs it, at the threshold `[1, 2]`
|
||
and at `[3, 4]`, over two bright pictures as items and two dark ones as negatives under four
|
||
attacks of robustness-evidence §4.
|
||
|
||
- **Accepted cases**: four objects whose claims say captured, synthesized, captured and
|
||
synthesized, and a verdict over each — two that agree with their claims and two that disagree,
|
||
the classes stated as signed; the same claims with no verdict, stating the same classes; and a
|
||
verdict over an object no claim names.
|
||
- **Refused cases**, each a control of one defect: a verdict whose signature is altered, one under
|
||
a key that does not decode, one with a member §3 does not list, one whose score is not in lowest
|
||
terms, one whose threshold is not its definition's, one whose verdict is not its score's, one
|
||
that names a record the verifier does not hold, one that names none under the policy that
|
||
requires one, one that names the record of another threshold, a robustness record that does not
|
||
verify, an origin claim that does not read and one whose signature is not its signer's, a
|
||
verdict in the place of an origin claim, an origin claim that names a verdict as the record that
|
||
derived its object, and one that names a verdict as its input's claim. Every reason of §7
|
||
refuses one.
|
||
|
||
A conforming verifier gives every case the outcome, the reason, the list and the index, and for
|
||
an accepted case what §5.3 states, as the corpus states them. `wai_detection_vectors verify` and
|
||
the test `tests/detection.rs` check that every file regenerates byte for byte, that no other file
|
||
is there, that `wai-rs` gives every case its outcome, that every reason of §7 refuses a case, that
|
||
the case with verdicts states the classes the case without them states, and that §7 lists exactly
|
||
the reference's codes. `live-conformance/detection_verify.py` is the second implementation,
|
||
written from this text: it shares no code with `wai-rs`; it verifies signatures with WAI's strict
|
||
rule (`claim-conformance/_ed25519.py`), hashes with BLAKE3 from `claim-conformance/_blake3.py`,
|
||
verifies the robustness records with robustness-evidence's second implementation, gives every case
|
||
the same outcome, mutates copies of the accepted cases and requires every copy to be refused, and
|
||
drops each of its own checks in turn and requires the corpus to catch it.
|
||
|
||
## 9. Security and privacy considerations
|
||
|
||
- **A verdict is an opinion.** It says what a detector, at a threshold its operator chose, made
|
||
of the content; detectors miss content and flag content, and their rates move with the content
|
||
and the attack. The robustness record says how this detector did on the corpus it names, under
|
||
the attacks it names, and no more (robustness-evidence §14).
|
||
- **Disagreement is information.** A verdict that disagrees with a signed claim is reported, not
|
||
resolved: the claim is its signer's statement, attributable to that signer, and the verdict the
|
||
detector operator's. A relying party that weighs them does so by its own policy.
|
||
- **Thresholds.** A verdict's threshold is in its signed definition, so an operator cannot report
|
||
a score against a threshold other than the one its evidence measured.
|
||
- **Privacy.** A verdict names an object by its content hash and its detector by digest; it names
|
||
no viewer. A verdict about a person's content is a statement about that content made by a third
|
||
party, and its publication is that party's act.
|