open-standards/wai/extensions/energy-binding.md
David Charlot 299688bdec wai: actually write the C2PA manifest the energy binding describes
`energy_binding` and `reconstruction_binding` define assertion *shapes*. Nothing
emitted them. A shape no implementation writes is a proposal, and the extension
document was describing a manifest that did not exist anywhere — so this adds
`c2pa_emit`, which signs a real asset with the C2PA SDK and puts all three
assertions in the manifest, behind an off-by-default `c2pa_emit` feature.

Writing it turned up four things that only surface when you try, each of which
is now normative in §6 of the extension:

**A reconstruction is not `c2pa.created`.** C2PA 2.x requires every created
action to carry a `digitalSourceType`, and that vocabulary describes how the
*content* came to exist — captured, human-drawn, model-generated. A decoder does
not know that; it reconstructed pixels from a codestream and whatever was encoded
could have been any of them. Reaching for `trainedAlgorithmicMedia` because a
codec has a neural stage would assert that a compressed photograph is
AI-generated content, which is false, and nothing in the vocabulary means
"decoded; origin not visible from here". A decode is a **conversion**: the
codestream is a `parentOf` ingredient, the manifest opens it, `c2pa.converted`
records what was done, and no action claims a source type at all.

**The codestream has to travel with it.** The whole construction turns on a
verifier re-decoding. A manifest carrying the reconstruction digest but not what
produces it hands over a digest and no way to reach it. The digest says which
decode the joules describe; the parent ingredient says what to decode to get
there.

**The assertions are `created`, not `gathered`** — C2PA's own distinction between
a claim the signer stands behind and material merely carried along, which is the
same distinction the acquisition ladder draws inside the figure. Load-bearing,
not editorial: a validator looks for the inception action among created
assertions, so leaving the actions assertion gathered (what `add_action` does on
its own) yields a manifest that signs happily and every validator then rejects as
`assertion.action.malformed` — on the far side of a signature.

**Signed is not trusted.** An ephemeral certificate reaches `Valid`, never
`Trusted`. `untrusted_local_signer` is named for its limit.

The emitter enforces one rule the SDK cannot. Third-party assertion bodies are
opaque to C2PA, so it will sign a manifest whose energy figure is measured over
one decode while the reconstruction assertion commits to another — the
self-refutation §2 describes — and the result validates.
`ReconstructionManifest::new` refuses that pair at construction, before a
signature can cover it, and `recover` applies the same rule on the way back in.
`recover` also reports the unaccompanied standard assertion — perfectly valid
C2PA, refused here — as its own error rather than a generic absence, because that
is the exact situation the companion exists to be distinguished from.

Assertion bodies are parsed from the same `to_json` functions the extension
documents pin, so the emitter cannot drift from the specified shape.

Verified: 7 tests, signing a real PNG and reading the manifest back with an
independent `Reader`. The trust test pins the single failure code as well as the
verdict, so a real defect cannot hide behind the expected one — which is not
hypothetical: the missing inception action was found exactly that way.
`cargo tree -i c2pa` under `--features provenance` finds nothing, so the default
build is untouched.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 06:24:56 -04:00

8.4 KiB
Raw Permalink Blame History

WAI Extension: Energy Binding

Status: Draft. Binds the energy a reconstruction cost to that reconstruction, as a companion to C2PA 2.4's c2pa.environmental-sustainability assertion — supplying the acquisition class, the coverage, and the outcome binding the standard assertion has no field for. Reference impl: the energy_binding module of wai-rs (feature provenance), emitted into a real signed manifest by c2pa_emit (feature c2pa_emit). Companion to Reproducible-Reconstruction Binding and the acquisition protocol in Energy Measurement. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

1. Why a companion assertion

C2PA 2.4 defines c2pa.environmental-sustainability, carrying energy_kwh (per ISO/IEC TR 20226:2025) alongside carbon and water figures. The dominant signed-provenance container in media now has a place to put joules.

Three properties it does not have, and this extension supplies:

  1. It is self-declared. measurementMethod is optional, and an unattributed number cannot be compared with any other number. In practice the optional field is the one that stays empty.
  2. It carries no acquisition class. A hardware counter reading and a modelled constant occupy the same field and are indistinguishable to a reader, though they are different claims about the world.
  3. It is not bound to any reproducible output. The assertion is about an asset; a validator can check every signature in a manifest and still not know what the figure was measured over.

The third is the load-bearing one. A signature proves who said a number, never that the number is about anything checkable.

2. What the binding is

An energy binding is the tuple

{ reconstructionHash, alg, microjoules, acquisition, coverage, workUnits, method }

carried as a third-party C2PA assertion under the reverse-domain label science.transaction.wai.energy.reconstruction, alongside — never instead of — the standard c2pa.environmental-sustainability assertion.

reconstructionHash is the SHA-256 of the canonical decoded pixels, the same digest reconstruction-binding commits to. Both assertions in one manifest therefore name the same decode by construction, and a manifest whose two digests disagree is self-refuting.

3. What this makes checkable, and what it does not

Because a WAI reconstruction is byte-exact and reproducible on every architecture, a verifier holding the codestream can:

  • re-decode under the pinned descriptor,
  • hash the pixels and confirm the energy figure is about those bytes,
  • recompute workUnits from the descriptor rather than trusting it,
  • and re-derive the rate.

What remains is exactly one unverifiable term: the joules. That is the point of the construction, and the honest limit of it. An energy claim about an output nobody can reproduce is not checkable at all. An energy claim about an output anyone can reproduce is checkable except for one number — and knowing precisely which number is the difference between a measurement and an assertion.

An implementation MUST NOT describe an energy binding as proving the energy figure. It binds the figure to a reproducible outcome; it does not attest the measurement.

4. Normative rules

  • The binding MUST carry an acquisition class (HwShunt / ModelBased / Estimator, per energy-measurement §2) and a coverage statement (Whole / Partial, §5 there).
  • method MUST be present and name the published protocol the figure was obtained under. This is deliberately stricter than the standard assertion's optional measurementMethod.
  • A figure of zero MUST NOT be recorded. A reconstruction that measured 0 µJ did not consume nothing; it failed to measure, and the honest artifact is the absence of an energy assertion rather than a zero in one.
  • microjoules is an integer and is authoritative. energy_kwh in the standard assertion MUST be treated as a derived interop value: one microjoule is 2.8 × 10⁻¹³ kWh, so a whole image decode lands near 10⁻⁹ kWh, where the trailing digits of the float are artifacts of the conversion rather than of the measurement.
  • An implementation MUST NOT populate carbon_kgco2e or water_litres from an energy figure and a grid-average factor while presenting the result as measured. Those are modelled quantities; emitting them beside a measured joule figure, in the same assertion, with no per-field provenance, is how a model comes to be read as a measurement.
  • A Partial figure MUST NOT be presented as the reconstruction's total.

5. Relationship to the standard assertion

Interoperability is the reason both are emitted. A validator that understands only C2PA 2.4 reads energy_kwh and a populated measurementMethod, which is already better than the common case. A validator that understands this extension additionally learns how the figure was acquired, how much of the work it covers, and which reconstruction it describes — and can check the last of those itself.

6. Emitting a manifest

The assertion shapes above are emitted into an actual signed C2PA manifest by the c2pa_emit module of wai-rs. Four decisions in that emitter are normative for anyone writing their own, because each is a place where the obvious implementation produces a manifest that validators reject, or that overstates what a decoder knows.

6.1 The inception action is c2pa.opened, not c2pa.created

A manifest MUST NOT describe a reconstruction with c2pa.created.

C2PA 2.x requires every c2pa.created action to carry a digitalSourceType, and that vocabulary describes how the content came to exist — captured, drawn by a human, generated by a model. A decoder does not know that. It reconstructed pixels from a codestream, and whatever was encoded could have been any of them. Selecting trainedAlgorithmicMedia because a codec has a neural stage would assert that a compressed photograph is AI-generated content, which is false, and the vocabulary has no member meaning "decoded; origin not visible from here".

A decode is in any case not a creation. It is a conversion of an existing asset into another representation, which C2PA already models: the codestream is a parentOf ingredient, the manifest opens it, and c2pa.converted records what was done. No action asserts a digitalSourceType, so no claim about the original's origin is made anywhere — which is precisely the position a decoder is in.

6.2 The codestream is carried as the parent ingredient

An emitted manifest MUST carry the codestream as a parentOf ingredient.

§3 turns on a verifier being able to re-decode. A manifest that carries the reconstruction digest but does not name what produces it hands the verifier a digest and no way to reach it. The digest says which decode the joules were measured over; the parent ingredient says what to decode to get there. Both halves, or the check §3 describes is not actually available.

6.3 The bindings are created assertions, not gathered

The bindings MUST be marked as attributed to the signer.

C2PA distinguishes assertions the signer stands behind from material gathered elsewhere and carried along — the same distinction between a claim and hearsay that the acquisition ladder draws inside the figure itself. These are first-party statements: the signer measured the joules and hashed the pixels.

This is load-bearing rather than editorial. A validator looks for the inception action among the created assertions, so an actions assertion left gathered yields a manifest that signs without complaint and every validator then rejects as assertion.action.malformed — a failure that surfaces only on the far side of a signature.

6.4 What a signature does not settle

An emitted manifest signed by an untrusted certificate reaches ValidationState::Valid — well-formed, signature intact, asset unmodified — and not ValidationState::Trusted, which additionally requires a certificate chaining to a known authority. An implementation MUST NOT present the first as though it were the second.

The gap between them is the whole of who says so, and it is orthogonal to everything §3 makes checkable: a perfectly reproducible reconstruction signed by nobody in particular is still a claim from nobody in particular.