Skip to main content

WAI Extension: Detection Verdicts

Mirrored from the canonical text at commit c5e7133e ().

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 §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 §3.1), as the record that derived a subject, as an input’s origin, or as an assertion of a C2PA manifest (c2pa-binding §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/, 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 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 §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:

membertypemeaning
kindstring"wai-detection-verdict"
subject32 B hexthe content hash of the object examined (jwp-receipts §2.1)
mediastringimage, video, audio or text
detectortermthe detector’s name
model32 B hexthe SHA-256 of the detector’s model or program
thresholdratethe score at and above which the detector gives synthetic
detector_digest32 B hexthe digest of the detector’s definition at this operating point, below
scoreratethe detector’s score for the object
verdictstringsynthetic where score ≥ threshold, not-detected otherwise
robustness32 B hex or nullthe link hash of the robustness record that measures the detector at this operating point (§4), or null where none is named
issued_atdate-timewhen 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

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:

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

reasonraised 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/ 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.

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