open-standards/wai/extensions/robustness-evidence.md
Transaction Science 1ff71887ff
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 stores carry identity assertions, soft bindings and the ISCC identifier, trust lists are pinned by digest, and a detection verdict is a claim of its own
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]>
2026-11-10 15:25:49 -05:00

615 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Robustness Evidence for Soft Bindings
> Status: Draft. Registers the record `wai.media.robustness`. A soft binding — a watermark read
> back out of the samples, or a fingerprint computed over them ([jwp-receipts](jwp-receipts.md)
> §2.6) — is worth what its algorithm survives, and a declaration states nothing about that. A
> **robustness record** measures it: the algorithm by term and definition digest, a **corpus**
> of pictures, clips or sounds each named by the SHA-256 of its samples, an **attack battery** of
> pinned, integer, seeded transforms with their exact parameters, the **harness** that runs
> them, and per attack the counts — items that survived out of the corpus, unmarked items read
> as marked out of the negatives — and rates derived from them as exact fractions, never
> floating point. Every step is an integer function of its inputs, so anyone who holds the
> corpus runs the battery, runs the detector and gets the same table, bit for bit; a verifier
> that does so **reproduces** the record, and one that cannot reads it as **stated**. A
> **robustness claim** — "survives `crop-5%`" — holds only where a record shows every item of
> its corpus survived that attack and no negative was read as marked; a claim with no record,
> or above what its records show, is refused. WAI's registered watermark,
> `wai-mark:tent64` ([synthesis](synthesis.md) §7.5), carries records over its measurement sets
> and a claim that is exactly the list its extension states; its audio watermark,
> `wai-mark:band64` (synthesis §7.10), carries records over its own, pinned transforms of sound
> and lossy codecs among them, the codecs' results **measured** and stated. The record maps field for field
> onto the `robustnessEvidence` object a proposal to the C2PA soft-binding list describes
> (§11). No new transport. The reference implementation is the `robustness` module of `wai-rs`
> (feature `robustness`); the corpus is
> [`live-conformance/robustness/`](../live-conformance/robustness/), and its second
> implementation is `live-conformance/robustness_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.4, §9.3 and §18.10; and *Robustness Evidence for C2PA Soft-Binding
> Algorithms*, a proposal to the C2PA soft-binding and conformance task forces whose status is
> "proposal / reference results. Draft for discussion" and which states no version number, so
> this revision pins its text by digest: SHA-256
> `4ea8ab27711ef812a3cf8ade364cd40082ab797d071c87a6313217070dfbc9a2` as fetched on 22 October
> 2026. A section number of the C2PA text in this extension is that version's.
## 1. Scope
In scope: the records and their encodings (§2); the corpus (§3); the registered transforms
(§4); the robustness record (§5) and the harness it names (§6); exact rates (§7); robustness
claims and when one holds (§8); verification (§9); the records of WAI's own watermark (§10);
the mapping onto the proposed `robustnessEvidence` object (§11).
Out of scope: any soft-binding algorithm itself, which its own definition specifies (WAI's is
synthesis §7.5); a transport; which attacks a deployment ought to care about, which is the
relying party's; transforms of text, which this revision does not register, so a corpus of
text has no battery here yet (§3); and an adversary who knows the algorithm and
searches for a transform outside every battery, which no measurement bounds (§14).
Relationship to the core spec: `wai.media.robustness` is a **record** ([SPEC](../SPEC.md) §5):
a signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
Relationship to [jwp-receipts](jwp-receipts.md): a soft-binding match is evidence at the
algorithm's own error rates (§3.2 there, *What a match is worth*). This extension is where
those rates are measured and how a claim about them is held to its measurement. A recovery
(jwp-receipts §3.2) is unchanged by it: a verifier **MAY** report beside a recovery the
accepted claims and records it holds for the declaration's algorithm, and they change no step
of the recovery.
Relationship to [c2pa-binding](c2pa-binding.md) and [detection](detection.md): a C2PA
soft-binding assertion of WAI's watermark names the robustness claim that states what the mark
survives (c2pa-binding §4.8), and a store whose claim does not hold for the binding's scheme is
refused; a detection verdict names the robustness record of its detector at its operating point
(detection §4). A record of `binding_type` `detector` reads as any record does: its items are
content the detector is meant to flag, each value the one byte `01`; an item survives an attack
where the detector gives `synthetic` on it attacked, and a negative read as marked is one it
flags. This revision implements no detector, so such a record is stated (§5 check 8).
Relationship to [synthesis](synthesis.md): synthesis §7.5.7 defines the attack battery of
`wai-mark:tent64` and §7.5.8 states what the scheme claims to survive. §4 here registers that
battery's attacks as transforms with parameters, so a record names them by term; §10 gives the
scheme's records and its claim, which a verifier checks under §8.
## 2. Records
Every record of this extension is a JSON object under the encodings and the reading of
[synthesis](synthesis.md) §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 its
section lists at every level, and a `kind` naming it; otherwise `record_malformed` — with one
exception: a **rate** member (§7) is read after every other member, and one that holds anything
but an exact fraction is `rate_not_exact`, never `record_malformed`.
A **robustness record** (§5) and a **robustness claim** (§8) are signed:
```
sig = Ed25519(signer_key, "wai:robustness\x01" || JCS(record without "sig"))
link_hash = BLAKE3("wai:robustness-link\x01" || JCS(record))
```
JCS is RFC 8785, which on these values reduces to what jwp-receipts §2.4.3 states. 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 record
is named by its link hash wherever another names it. `issued_at` is a date-time of
jwp-receipts §2.4.3, self-asserted.
A **corpus document** (§3) is not signed: it is named by its **digest**, the SHA-256 of its
JCS, which every record that measures it states.
The **samples** of a picture or a clip are its canonical buffer: `RGB8`, row-major, three bytes
a pixel, no row padding ([reconstruction-binding](reconstruction-binding.md) §2), frame after
frame in presentation order, as synthesis §2 has it. The samples of a sound are its canonical
buffer of synthesis §7.10.1: S16LE, two bytes a sample, the channels interleaved. An item is
named by the SHA-256 of its samples.
## 3. The corpus
A corpus document, `kind` `"wai-robustness-corpus"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-robustness-corpus"` |
| `name` | term | A label for people; nothing is checked against it. |
| `media` | string | `"image"`, `"video"` or `"audio"`: what every item is. |
| `items` | array | The items an algorithm binds: each marked with its value (a watermark) or bound to it (a fingerprint), then attacked. |
| `negatives` | array | The items it does not bind, read after the same attacks to count false positives. They may be the same samples as `items`, unmarked. |
Each entry of `items` and of `negatives` is an object of exactly these members:
| member | type | meaning |
|---|---|---|
| `name` | term | The item's name, unique within its list. |
| `frames` | integer | How many frames; 1 for every item of an `image` corpus. |
| `width`, `height` | integer | The size of each frame, in pixels. |
| `samples` | 32 B hex | SHA-256 of its samples (§2). |
An item of an `audio` corpus has `rate`, `channels` and `length` (integers: hertz, the channels,
the samples a channel holds) in place of `frames`, `width` and `height`, and its samples are
`length`·`channels`·2 bytes.
A corpus document with no item or no negative, a name twice in one list, a `frames`, `width`,
`height`, `rate`, `channels` or `length` of 0, an `image` item of more than one frame, an item
whose members are not its corpus's media's, or an item of more than 2^53 − 1 bytes is
`corpus_malformed`, as is one that does not read under §2.
An item is held by its samples: a verifier that holds a buffer finds it by its SHA-256, and a
buffer it holds for an item is the bytes its members give (`samples_mismatch`). A
corpus is a list of digests, so it can be published without its samples; a record over it is
then **stated** to everyone who does not hold them (§9).
A corpus of text needs transforms of its medium, which §4 does not register: its `media` is a
later revision's.
## 4. The transforms
A **transform** is named by a term and takes integer parameters. Applied to an item's frames at
a **seed**, each one gives other frames, an integer function of the frames, the parameters and
the seed alone, so its result is the same bytes on every machine. Below, rnd(*n* / *d*) is
*n* / *d* rounded to the nearest integer, halves away from zero, and every sample is clamped to
0 … 255, as in synthesis §7.5.7, whose definitions these are where the table names them.
| transform | parameters | media | the frames it gives |
|---|---|---|---|
| `wai-attack:none` | none | both | the frames unchanged |
| `wai-attack:int-motion` | *q*, 1 to 255 | both | each frame a keyframe of `wai.video.int_motion` at step *q* (`int-motion-q`*q*) |
| `wai-attack:staged-base` | *q*, 1 to 255 | both | each frame's staged base at step *q* (`staged-base-q8`'s definition at *q*) |
| `wai-attack:denoise3` | *n*, 1 to 16 | both | `wai-synthesis:denoise3` applied *n* times (synthesis §4.2) |
| `wai-attack:scale` | *n*, *d*, each 1 to 16 | both | area resampling to ⌊*W*·*n*/*d*⌋ × ⌊*H*·*n*/*d*⌋ (`scale-`*n*`/`*d*) |
| `wai-attack:aspect` | *n*, *d*, each 1 to 16 | both | area resampling of the width alone to ⌊*W*·*n*/*d*⌋ (`aspect-3/4`'s definition at *n*/*d*) |
| `wai-attack:upscale2` | none | both | `wai-synthesis:upscale2` (synthesis §4.1) |
| `wai-attack:half-then-int-motion` | *q*, 1 to 255 | both | `scale` 1/2, then `int-motion` at *q* |
| `wai-attack:crop` | *p*, 0 to 99 | both | ⌊*W*·*p*/200⌋ columns removed from each side and ⌊*H*·*p*/200⌋ rows from the top and from the bottom (`crop-`*p*`%`) |
| `wai-attack:mirror` | none | both | the columns in reverse order |
| `wai-attack:rotate-90` | none | both | a quarter turn clockwise |
| `wai-attack:rotate` | *c*, *s*, each 0 to 65536; *k*, 0 or 1 | both | the rotation of `rotate-0.5`'s definition with cosine *c* and sine *s* at 2^16, the sine negated where *k* is 1 |
| `wai-attack:noise` | *s*, 0 to 255 | both | noise uniform in −*s* … *s* from the stream of `noise-`*s* at the seed |
| `wai-attack:brightness` | *b*, 0 to 255; *k*, 0 or 1 | both | *v* + *b*, or *v* − *b* where *k* is 1 |
| `wai-attack:contrast` | *n*, *d*, each 1 to 16 | both | 128 + rnd((*v* − 128)·*n* / *d*) |
| `wai-attack:gamma` | *n*, *d*, each 1 to 6 | both | below |
| `wai-attack:local-blur` | none | both | `local-blur` |
| `wai-attack:clip-int-motion` | *q*, 1 to 255 | `video` | the clip encoded as `wai.video.int_motion` with motion at step *q* (`clip-int-motion-q`*q*) |
| `wai-attack:frame-drop` | none | `video` | frames 0, 2, 4, … |
| `wai-attack:one-frame` | none | `video` | the first frame alone |
`wai-attack:none` applies to every media; the other transforms of this table apply to `image`
and `video` corpora as the column says, and to no `audio` corpus.
**Gamma.** Each sample *v* becomes the greatest *y* from 0 to 255 such that *y* = 0 or
(2*y* − 1)^*d*·255^*n* ≤ 2^*d*·*v*^*n*·255^*d*: 255·(*v*/255)^(*n*/*d*) rounded to the nearest
integer, halves up, computed in integers. With *n* and *d* at most 6 every product is below
2^103. A gamma below 1 (*n* < *d*) lifts the dark samples, one above it darkens them; a
display's response and an encoder's transfer function differ from a picture's by such a curve.
A transform is applied to every frame of an item and to every channel of every sample. The
seed matters only to `noise`; every transform takes one. A transform's parameters, outside the
table's ranges or of the wrong number, are `transform_invalid`. A transform applied to a
corpus whose `media` the table does not list for it is `battery_not_applicable`. A term the
table does not register is admitted in a record: what it does is its definer's, a verifier
here cannot run it, and a record that names it is stated (§9).
The table is the battery of synthesis §7.5.7 with its parameters free, and `gamma`. More
transforms are registrations in a later revision, each an integer function with its
parameters' ranges.
### 4.1 Transforms of sound
These apply to `audio` corpora alone. Each is applied to every channel of every sample, at the
item's rate *R*; a sample is clamped to −32768 … 32767 after each step; rnd is as above.
| transform | parameters | the samples it gives |
|---|---|---|
| `wai-attack:audio-gain` | *n*, *d*, each 1 to 16 | rnd(*v*·*n* / *d*) |
| `wai-attack:audio-trim` | *a*, *b*, each 0 to 60000 | ⌊*R*·*a*/1000⌋ samples removed from the start and ⌊*R*·*b*/1000⌋ from the end (none left where they meet) |
| `wai-attack:audio-resample` | *r*, a rate synthesis §7.10.1 admits | **area resampling** to the rate *r*, below |
| `wai-attack:audio-resample-trip` | *r*, as above | area resampling to *r*, then back to *R* |
| `wai-attack:audio-noise` | *s*, 0 to 32767 | *v* + (*w* mod (2*s* + 1)) − *s*, *w* the next word of the noise stream of synthesis §7.5.7 at the seed, the samples in order and the channels of each in turn |
| `wai-attack:audio-mix-noise` | *n*, *d*, each 1 to 1000 | noise of *n*/*d* of the signal's power, below |
| `wai-attack:audio-mulaw` | none | each sample through G.711 μ-law's encoding and decoding, below |
| `wai-attack:audio-requantize` | *b*, 1 to 16 | ⌊*v* / 2^(16 − *b*)⌋·2^(16 − *b*): the top *b* bits kept |
| `wai-attack:audio-echo` | *t*, 1 to 1000; *n*, *d*, each 1 to 16, *n* ≤ *d* | *v*[*i*] + rnd(*v*[*i* − *D*]·*n* / *d*), *D* = ⌊*R*·*t*/1000⌋, where *i* ≥ *D* |
| `wai-attack:audio-mono` | none | one channel: the mono mix of synthesis §7.10.1 |
**Area resampling** of *L* samples from *R* to *r* gives ⌊*L*·*r*/*R*⌋: destination sample *X*
covers *X*·*R* to (*X* + 1)·*R* and source sample *s* covers *s*·*r* to (*s* + 1)·*r*, the length
of their overlap is *s*'s weight in *X*, the weights sum to *R*, and the value is
rnd(Σ weight·*v* / *R*), synthesis §7.5.7's area resampling along one axis.
**Mixed noise**: *u* = (*w* mod 65536) − 32768 for each sample, *w* the noise stream's next word
as above; *P*ₓ and *P*ᵤ the sums of the squares of every sample of the signal and of the noise;
where *P*ᵤ is 0 the samples are unchanged, and otherwise *k* = isqrt(⌊*P*ₓ·*n*·2^32 / (*P*ᵤ·*d*)⌋)
and each sample becomes *v* + rnd(*u*·*k* / 2^16).
**G.711 μ-law**: a sample's magnitude, at most 32635, plus 132, is coded as its sign, the
position *e* (0 to 7) of its highest set bit above bit 7, and the four bits below that bit, all
inverted; it decodes as ±((((the four bits) << 3) + 132) << *e*) ∓ 132: ITU-T G.711's μ-law at
14 bits, applied to every sample at the item's rate.
### 4.2 Measured transforms
These apply to `audio` corpora alone. Each is a lossy codec at its parameter's bitrate, or a
band-limited resampler: the samples encoded, decoded, and brought back to the item's rate and
channels; a codec of one channel codes a downmix to one channel, and its result is copied to
every channel. A standard pins its decoder, not its encoder, so the result is a measurement of
the encoder that ran it, not a function a verifier recomputes: a record whose battery names one
is **stated** to every verifier (§5 check 8), and its corpus's README names the encoder.
| transform | parameters | the samples it gives |
|---|---|---|
| `wai-attack:opus` | *b*, 6000 to 510000 | RFC 6716 at *b* bit/s, the encoder tuned for speech |
| `wai-attack:mp3` | *b*, 8000 to 320000 | MPEG-1/2 Audio Layer III at *b* bit/s |
| `wai-attack:aac` | *b*, 8000 to 320000 | MPEG-4 AAC-LC at *b* bit/s |
| `wai-attack:g722` | none | ITU-T G.722 at 64 kbit/s, at 16 kHz, one channel |
| `wai-attack:g726` | *b*: 16000, 24000, 32000 or 40000 | ITU-T G.726 at *b* bit/s, at 8 kHz, one channel |
| `wai-attack:g723-1` | none | ITU-T G.723.1 at 6.3 kbit/s, at 8 kHz, one channel |
| `wai-attack:resample-band` | *r*, a rate synthesis §7.10.1 admits | a band-limited resampler to *r* and back |
## 5. The robustness record
The party that measured an algorithm signs a robustness record, `kind` `"wai-robustness"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-robustness"` |
| `algorithm` | term | The algorithm measured: a registered soft-binding algorithm's term, as a soft-binding claim's `alg` names it (jwp-receipts §2.6.2), or a WAI scheme's (synthesis §7.4). |
| `algorithm_digest` | 32 B hex | SHA-256 of the algorithm's definition. |
| `binding_type` | string | `"watermark"` or `"fingerprint"`, as jwp-receipts §2.6.2 types it; or `"detector"`, a synthetic-media detector at an operating point ([detection](detection.md) §4). |
| `media` | string | The corpus's `media`. |
| `corpus` | 32 B hex | The corpus document's digest (§2). |
| `values` | array of hex | One per item of the corpus, in its order: the value the item is marked with (a watermark) or bound to (a fingerprint). Each is at least one byte. |
| `battery` | array | The attacks, in order, below. |
| `harness` | term | The procedure that ran them: `wai-robustness-harness:1` for §6. |
| `table` | array | One row per attack, in the battery's order, below. |
| `issued_at` | date-time | When it signed. |
| `signer_pubkey`, `sig` | | §2 |
An **attack** is an object of exactly `attack` (a term: its name, unique within the battery),
`transform` (a term of §4, or one this revision does not register), `params` (an array of
integers) and `seed` (an integer below 2^32: the four bytes a stream of synthesis §7.5.7 reads
it as). A **row** is an object of exactly:
| member | type | meaning |
|---|---|---|
| `attack` | term | The attack's name. |
| `trials` | integer | The corpus's items: how many marked items were attacked. |
| `survived` | integer | How many of them, attacked, gave their own value (§6). |
| `wrong` | integer | How many gave another value. |
| `negatives` | integer | The corpus's negatives: how many were attacked. |
| `false_positives` | integer | How many of them, attacked, gave a value (§6). |
| `survival` | rate | `survived` / `trials` (§7). |
| `false_positive_rate` | rate | `false_positives` / `negatives`. |
| `balanced_accuracy` | rate | The mean of `survival` and the true-negative rate (§7). |
| `tpr_at_1pct_fpr` | rate or `null` | `survival` where `false_positives`·100 ≤ `negatives`; `null` otherwise (§7). |
`missed`, the items that gave nothing, is `trials` − `survived` − `wrong`.
Its checks, in order, after reading and the signature (§2):
1. the battery has at least one attack, its names are unique and each seed is below 2^32
(`battery_malformed`); each registered transform's parameters fit §4, §4.1 or §4.2
(`transform_invalid`), and then each one's media the record's (`battery_not_applicable`);
2. the table has one row per attack, with the attack's name, in the battery's order
(`table_malformed`);
3. in each row: `trials` is the number of `values` and at least 1; `survived` + `wrong` ≤
`trials`; `negatives` is at least 1 and the same in every row; `false_positives` ≤
`negatives` (`table_inconsistent`);
4. each rate is the one its row's counts give (§7, `rate_mismatch`);
5. an algorithm this revision registers is named with its digest and its binding type —
`wai-mark:tent64` and `wai-mark:band64` are `watermark`s — (`algorithm_mismatch`), and the
values are values the algorithm takes: for each, 8-byte payloads whose first byte is a class
code, 0 to 3 (synthesis §7.2) (`values_invalid`).
Then against what the verifier was given (§9.1): the corpus document, and the samples it holds.
6. The corpus document reads under §3 (`corpus_malformed`); its digest is `corpus` and its
`media` the record's (`corpus_mismatch`); it has as many items as `values` and as many
negatives as each row's `negatives` (`table_inconsistent`).
7. Each item and negative whose samples it holds is the size §3 gives (`samples_mismatch`).
8. **Reproduction.** Where the verifier holds the corpus document and the samples of every item
and negative, implements the algorithm, the record names the harness of §6, and every
transform of the battery is registered in §4 or §4.1, it runs the harness (§6): a fingerprint's values are its own value of each item
(`values_invalid`), and the table it gets is the record's, row for row and count for count
(`table_irreproducible`). The record is then **reproduced**; otherwise **stated**.
A reproduced record says that the algorithm, run by this harness on this corpus under this
battery, gives this table: not that it survives an attack outside the battery, a parameter
between two measured, or content unlike the corpus. A stated record is its signer's statement,
checked for its arithmetic and nothing else.
## 6. The harness
`wai-robustness-harness:1` is this procedure. For each attack of the battery, in order, with
its transform *T*, parameters and seed *s*:
- **Watermark.** For each item *k*, in order: its frames are marked with `values`[*k*] by the
algorithm's embedding; *T* is applied to the marked frames at *s*; the algorithm's detector
runs on the result. It **survived** where the detector reads `values`[*k*], is **wrong** where
it reads another value, and is **missed** where it reads none. For each negative, in order:
*T* is applied to its frames, unmarked, at *s*, and the detector runs on the result; a
**false positive** is any value read.
- **Fingerprint.** For each item *k*: `values`[*k*] is the algorithm's value of the item's
frames; *T* is applied to the frames at *s*, and the value of the result is computed. It
survived where that value matches `values`[*k*] under the algorithm's own rule, is wrong where
it matches another item's value and not its own, and is missed otherwise. For each negative:
the value of *T* applied to its frames is computed, and it is a false positive where that
value matches any item's value.
Every attack sees the frames as marked or as given, never the result of another attack. Each
step is an integer function where the algorithm's embedding and detector are, as `tent64`'s
are (synthesis §7.5), so the table is a function of the record's inputs, and a second run
anywhere gives it bit for bit.
The **seed** is the record's: synthesis §7.5.7 seeds the attack at index *i* of its lists with
*i*, and a record of that battery states those seeds (§10).
## 7. Rates
A **rate** is an exact fraction, written as an array of two integers `[n, d]`: 1 ≤ *d*,
0 ≤ *n* ≤ *d*, and *n* and *d* with no common factor above 1, so 0 is `[0, 1]`, 1 is `[1, 1]`,
and every rate has one spelling. A number, a string, an array of another length or of other
values, a fraction not in lowest terms or one above 1 is `rate_not_exact`. A rate is never
floating point: a record's table is its counts, and its rates are the counts' quotients, which
any reader recomputes exactly and compares as integers.
For a row with counts `trials` *N*, `survived` *S*, `negatives` *U* and `false_positives` *F*:
- `survival` = *S* / *N*, the true-positive rate;
- `false_positive_rate` = *F* / *U*;
- `balanced_accuracy` = (*S*·*U* + (*U* − *F*)·*N*) / (2·*N*·*U*): the mean of the
true-positive rate *S*/*N* and the true-negative rate (*U* − *F*)/*U*;
- `tpr_at_1pct_fpr` = *S* / *N* where 100·*F* ≤ *U*, and `null` otherwise.
Each reduced to lowest terms. A detector that gives a value or none has one operating point, so
its true-positive rate at a false-positive rate of at most 1% is its rate where its measured
false-positive rate is at most 1%, and is not attained where it is above. The measured rate is
an observation over *U* negatives: 0 of 32 says no negative of the corpus was read as marked,
not that the algorithm's false-positive rate is below 1%, which only a bound of the algorithm's
own definition gives (synthesis §7.5.6 for `tent64`).
## 8. Robustness claims
An algorithm's **robustness claim** is the list of attacks it claims to survive. Its registrant
signs it, `kind` `"wai-robustness-claim"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-robustness-claim"` |
| `algorithm`, `algorithm_digest` | | As §5's: the algorithm the claim is about. |
| `survives` | array | The attacks claimed, at least one: each an object of exactly `media` (`"image"`, `"video"` or `"audio"`), `transform` (a term) and `params` (an array of integers), no two equal. |
| `evidence` | array of 32 B hex | The link hashes of the robustness records it rests on, no two equal. |
| `issued_at` | date-time | When it signed. |
| `signer_pubkey`, `sig` | | §2 |
A claimed attack is named by its media, its transform and its parameters, and not by a seed: a
claim is about the transform, and a record shows it at the seed it states. A record **measures**
a claimed attack where its `media` is the attack's and its battery has an attack with that
transform and those parameters; its row **shows survival** where `survived` = `trials`,
`wrong` = 0 and `false_positives` = 0: every item of the corpus, attacked, gave its own value,
and no negative, attacked, gave any. This is the rule synthesis §7.5.8 states for its scheme.
A claim **holds** where, after reading and the signature (§2), in this order:
1. every record it cites was verified earlier in the case (§9.2) (`evidence_not_held`), and is
about the claim's algorithm, by term and digest (`evidence_mismatch`);
2. under a policy that requires reproduced evidence (§9.1), every record it cites was
reproduced (`evidence_not_reproduced`);
3. for each attack it claims, in order: at least one cited record measures it
(`claim_unevidenced`), and every cited record that measures it shows survival
(`claim_exceeds_evidence`).
So a claim with no record, a claim of an attack no record measured, and a claim of an attack a
record shows lost — one item missed under it, or one negative read as marked — are refused,
each with its own reason. A claim is as strong as the records it cites and no stronger: it
covers the parameters measured and not those between them, the corpus's kind of content and
not every kind, and a stated record's table as its signer stated it. A claim that holds over
reproduced records is **reproduced**; over any stated record, **stated**.
## 9. Verification
### 9.1 Inputs
A verifier is given a **case**: records in an order, as texts; with any robustness record, the
corpus document it was handed as that record's corpus, or none; the samples it holds, each
found by its SHA-256; the algorithms it implements, by term (each with the digest §5 check 5
pins); and its policy, `evidence`: `reproduced`, a claim's records must be reproduced (§8 step
2), or `stated`. A verifier bounds what it runs: this revision's reproduces items whose frames
are at most 2^26 bytes each and at most 2^30 bytes in all, the caps of the integer video
encoder its transforms call ([integer-payloads](integer-payloads.md) §7), and states a record
over a larger item.
### 9.2 Steps
The records are verified in their order, each by its `kind`: a robustness record by §5, a
robustness claim by §8. A record whose `kind` is neither, or that does not read, is
`record_malformed`. The first check that fails is the case's refusal, with the record's
position. A robustness record a claim cites is held only once it has been verified, earlier
in the case.
### 9.3 What an accepted case states
For each record, in order, its `kind`, its link hash and its status: for a robustness record,
`reproduced` or `stated`, with its algorithm, its corpus and its number of attacks; for a
claim, `reproduced` or `stated` (§8), with its algorithm and its number of attacks claimed.
## 10. WAI's own schemes
`wai-mark:tent64` (synthesis §7.5) carries three robustness records, one per measurement set of
synthesis §7.5.8, and a robustness claim, in the corpus (§13):
- **The corpora.** `spectral` and `leaves`, of `media` `image`, sixteen pictures of 512 × 512
each; `clips`, of `media` `video`, four clips of eight frames of 256 × 256: the sets the
synthesis corpus generates from fixed seeds, each item named by the SHA-256 of its samples.
Their negatives are the same samples, unmarked, as synthesis §7.5.8 reads them. The
photographs of synthesis §7.5.8 are not in the repository, so they have no record here.
- **The values** are the payloads synthesis §7.5.8 marked each item with: class 3, then the
first seven bytes of SHA-256(`wai-mark:battery payload` ‖ 0x00 ‖ the item's name).
- **The battery** is synthesis §7.5.7's lists, the pictures' for `spectral` and `leaves` and
the clips' for `clips`, each attack under its name there, its transform and parameters by
this table, and the seed of its index in its list:
| synthesis §7.5.7 | transform and parameters |
|---|---|
| `none` | `none` |
| `int-motion-q`*q*, `clip-int-motion-q`*q*, `staged-base-q`*q*, `half-then-int-motion-q`*q* | `int-motion`, `clip-int-motion`, `staged-base`, `half-then-int-motion`, each [*q*] |
| `denoise3`, `denoise3-x4` | `denoise3` [1], [4] |
| `scale-`*n*`/`*d*, `aspect-`*n*`/`*d*, `contrast-`*n*`/`*d* | `scale`, `aspect`, `contrast`, each [*n*, *d*] |
| `upscale2`, `mirror`, `rotate-90`, `local-blur`, `frame-drop`, `one-frame` | the transform of that name |
| `crop-`*p*`%` | `crop` [*p*] |
| `rotate-0.5`, `rotate-1`, `rotate-2` | `rotate` [65534, 572, 0], [65526, 1144, 0], [65496, 2287, 0] |
| `noise-`*s* | `noise` [*s*] |
| `brightness+32`, `brightness-32` | `brightness` [32, 0], [32, 1] |
- **The tables** are the harness's on those inputs, and equal synthesis §7.5.8's columns
`spectral`, `leaves` and `clips`: `survived` its count of pictures that gave their payload,
`wrong` 0, `false_positives` the unmarked pictures that gave any, 0 in every row.
- **The claim** is exactly the list of synthesis §7.5.8, *claims to survive*: its picture
attacks with `media` `image`, its clip attacks with `media` `video`, citing the three records.
It holds (§8): each attack it claims is measured, and shows survival, in every record of its
media. `scale-1/4`, which both sets survive, is not claimed, because one photograph of
synthesis §7.5.8 did not; a claim may claim less than its records show.
The registrant of `wai-mark:tent64`'s records in the corpus is a key of the corpus, derived
from a fixed seed: the records show the measurement, and their signature shows who stated it.
Anyone who holds the generated sets reproduces them; the corpus holds the sets' digests and not
their samples, so a verifier without the reference's generators reads them as stated, and
checks their tables against the synthesis corpus's `battery.json`, a measurement written by
another program (§13).
`wai-mark:tent64s`, the tiled scheme proposed beside it, is measured by the same battery with
attacks of its own; its records are a registration of the revision that registers it.
`wai-mark:band64` (synthesis §7.10) carries, in the audio scheme's corpus
(`live-conformance/audio-mark/robustness/`, synthesis §13):
- **a conformance record** over the corpus's two stored sounds, a voice and a piece of music,
their negatives the same samples unmarked, under five pinned attacks of §4.1, which any
verifier holding the samples reproduces, the harness run end to end;
- **records over the generated sets** of synthesis §7.10.7, `band64-voice` and `band64-music`,
under the pinned battery of synthesis §7.10.7, each attack at the seed of its index, and the
claim of every attack both show survived; the corpus holds the sets' digests, not their
samples, so a verifier without the reference's generators reads them as stated;
- **measured records**, the whole battery of synthesis §7.10.7 with its measured transforms
(§4.2), over the generated voices and over recorded speech named by digest, and the claim of
every attack they all show survived; stated, with the encoder that ran them named in
the corpus's README.
The values are synthesis §7.10.7's payloads; the registrant of these records is a key of that
corpus, derived from a fixed seed.
## 11. The proposed `robustnessEvidence` object
*Informative.* *Robustness Evidence for C2PA Soft-Binding Algorithms* (pinned in the header)
proposes an optional `robustnessEvidence` object in an entry of the C2PA soft-binding algorithm
list (C2PA 2.4 §18.10.4), filled by the algorithm's owner: a `methodology`, a `corpus`, a
`harness`, a `date`, and `results`, by attack, each with a `balancedAccuracy`. It observes that
the list's entries carry prose robustness claims with no measured evidence and no shared way to
produce it, and names, beside balanced accuracy, the survival (true-positive) rate and, for
formal use, the true-positive rate at 1% false-positive rate. A robustness record gives each of
its members:
| `robustnessEvidence` | from a robustness record |
|---|---|
| `methodology` | the extension and section of `harness`: `robustness-evidence §6, wai-robustness-harness:1` |
| `corpus` | `sha256:` and the hex of `corpus`; the corpus document names each item by the SHA-256 of its samples |
| `harness` | `harness`, and the implementation that ran it |
| `date` | the date of `issued_at` |
| `results` | one member per row, under the row's `attack` |
| `results.<attack>.balancedAccuracy` | `balanced_accuracy` [*n*, *d*], written as the decimal *n*/*d* |
| survival rate | `survival` |
| true-positive rate at 1% false-positive rate | `tpr_at_1pct_fpr`, absent where `null` |
| false-match rate | `false_positive_rate` |
The mapping runs one way. The object carries rates as decimal numbers, which a record never
does, and not the counts, the battery's transforms with their parameters and seeds, the
algorithm's digest, the corpus's items or a signature; so a record gives the object, and the
object does not give a record. A verifier of the object reads the decimals as their signer
wrote them; a verifier of the record recomputes the counts (§9). The proposal asks for each
owner to measure its own algorithm and for no ranking across owners; a record carries no
comparison either, and is about one algorithm on one corpus. The proposal's reference results
are of text, whose transforms §4 does not register (§3).
## 12. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `record_malformed` | §2, §9.2 |
| `rate_not_exact` | §2, §7 |
| `bad_key` | §2 |
| `bad_signature` | §2 |
| `battery_malformed` | §5 check 1 |
| `transform_invalid` | §4, §5 check 1 |
| `battery_not_applicable` | §4, §5 check 1 |
| `table_malformed` | §5 check 2 |
| `table_inconsistent` | §5 checks 3 and 6 |
| `rate_mismatch` | §5 check 4, §7 |
| `algorithm_mismatch` | §5 check 5 |
| `values_invalid` | §5 checks 5 and 8 |
| `corpus_mismatch` | §5 check 6 |
| `corpus_malformed` | §3, §5 check 6 |
| `samples_mismatch` | §3, §5 check 7 |
| `table_irreproducible` | §5 check 8 |
| `evidence_not_held` | §8 step 1 |
| `evidence_mismatch` | §8 step 1 |
| `evidence_not_reproduced` | §8 step 2 |
| `claim_unevidenced` | §8 step 3 |
| `claim_exceeds_evidence` | §8 step 3 |
## 13. Conformance
[`live-conformance/robustness/`](../live-conformance/robustness/) holds the corpus, written from
fixed seeds by `wai_robustness_vectors gen`. It has:
- **Corpus documents**: `conformance`, three pictures of 256 × 256 whose samples the corpus
holds; and `tent64-spectral`, `tent64-leaves` and `tent64-clips`, the measurement sets of §10,
by their digests.
- **Robustness records**: one over `conformance` with a battery of thirteen attacks — among
them both gammas, a crop and a small rotation the scheme does not survive, and a quarter turn,
a mirror and a re-encode it does — which any verifier holding its samples reproduces, the
harness rerun end to end; and the three records of `wai-mark:tent64` of §10, stated without
their samples.
- **Robustness claims**: `wai-mark:tent64`'s claim of §10, and a claim over the conformance
record under a policy that requires reproduced evidence.
- **Cases** refused for each reason of §12, among them a record whose table does not
recompute (one count changed and its rates with it, signed again), a record over a corpus
digest that is not the corpus document's, a rate written as a decimal number, a rate not in
lowest terms, a claim of an attack its records show lost, a claim of an attack no record
measured, a claim citing no record, and a claim under a policy that requires reproduced
evidence, citing stated records.
A conforming verifier gives every case the outcome, the reason, the record and the statements
the corpus states. `wai_robustness_vectors verify` and the test `tests/robustness.rs` check that
the files regenerate byte for byte — the records of `wai-mark:tent64` among them, which needs
the harness run over the generated sets — that `wai-rs` gives every case its outcome, that
every reason of §12 appears, that §12 lists exactly the reference's codes, that each transform
gives the bytes synthesis §7.5.7's battery gives for every attack of it, that the tent64
records' tables equal the synthesis corpus's `battery.json`, and that the tent64 claim is
synthesis §7.5.8's list. `live-conformance/robustness_verify.py` is the second implementation,
written from this text: it shares no code with `wai-rs`; it runs `wai-mark:tent64` with the
synthesis corpus's second implementation (`_wai_mark.py`), implements the transforms of §4,
reruns the conformance record's battery end to end on the samples the corpus holds and
requires the same table, checks every signature with WAI's strict rule
(`claim-conformance/_ed25519.py`) and every link hash with BLAKE3 from
`claim-conformance/_blake3.py`, checks the tent64 records against `battery.json` and the
claim against synthesis §7.5.8's text; it mutates copies of the corpus and requires every copy
to fail, and drops each of its own checks in turn and requires the corpus to catch the
weakened verifier.
## 14. Security and privacy considerations
- **A battery is not an adversary.** A record measures the transforms its battery names. An
adversary who knows the algorithm searches for one outside them, and a key-free watermark
(synthesis §7.5.6) can be read, and so attacked, by anyone. A claim is a floor under the
transforms it names, never a statement that the binding cannot be removed.
- **Choosing the corpus.** The party that measures chooses the corpus, and a corpus of content
the algorithm favours shows more than one that it does not. A record names its corpus by
digest so a reader can see what was measured, and a claim covers that kind of content and no
other (§8). A relying party that needs a rate for its own content measures its own corpus.
- **Stated records.** A record whose corpus a verifier does not hold is checked for its
arithmetic and its signature only. Its table is its signer's statement; under a policy that
requires reproduced evidence a claim resting on it is refused (§8 step 2).
- **What a negative count shows.** False positives over *U* negatives bound nothing below 1/*U*
(§7). An algorithm's false-positive bound is its definition's, derived and checked there.
- **Privacy.** A corpus is content; a published record names its items by digest and carries
no samples. A party that publishes a corpus of pictures of people publishes those pictures;
the record does not require it.