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]>
615 lines
40 KiB
Markdown
615 lines
40 KiB
Markdown
# 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.
|