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