`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]>
8.4 KiB
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-sustainabilityassertion — supplying the acquisition class, the coverage, and the outcome binding the standard assertion has no field for. Reference impl: theenergy_bindingmodule ofwai-rs(featureprovenance), emitted into a real signed manifest byc2pa_emit(featurec2pa_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:
- It is self-declared.
measurementMethodis optional, and an unattributed number cannot be compared with any other number. In practice the optional field is the one that stays empty. - 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.
- 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
workUnitsfrom 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). methodMUST be present and name the published protocol the figure was obtained under. This is deliberately stricter than the standard assertion's optionalmeasurementMethod.- 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.
microjoulesis an integer and is authoritative.energy_kwhin 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_kgco2eorwater_litresfrom 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
Partialfigure 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.