You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Integrate traits and types in the relevant gamut libraries (e.g. gamut-exif).
Scope (settled)
docs/non-image-media.md — normative for scope — already splits this: gamut locates, bounds,
carries and reserves the manifest store; validation belongs to c2pa-rs. This epic is
gamut's half only.
No crypto in the shipped graph. No COSE, X.509, RFC 3161 or trust lists in any gamut crate. c2pa-rs is a consumer-side dependency and a dev-only oracle. gamut never reports a validity
verdict and no API may imply one.
The store is opaque bytes plus byte ranges. No JUMBF interior, no CBOR, no claim or
assertion model in this epic.
Home is the metadata facade, not gamut-exif. The body above is wrong on this: a manifest
store is a JUMBF superbox (ISO/IEC 19566-5), never an IFD value. It becomes a fourth carrier
plus per-format raw payloads, exactly as EXIF/XMP/ICC work today. In TIFF/DNG an IFD tag points
at the store, but the payload is opaque to the IFD layer just as ICC and XMP already are.
Acceptance
MetadataBlock::C2pa / Metadata::c2pa / EncodedMetadata::c2pa land as a fourth carrier
(all three already #[non_exhaustive] — semver-minor), with the "why a carrier and not an
extension" rationale in gamut-metadata/README.md.
A manifest is never copied forward across a re-encode or a metadata-only rewrite. Both
invalidate the hard binding; a derivative needs a new manifest carrying the parent as an
ingredient. The facade drops or refuses by explicit policy, never silently passes through —
and this exception to the "extract → embed → extract is a true equality" keystone is
documented.
Every container that can carry a store can locate it and report the exclusion set an
external signer needs: byte ranges for c2pa.hash.data (§18.5), box paths for c2pa.hash.bmff.v3 (§18.6, §A.5.6). One primitive serves both reading and post-encode
patching; EncodeImage is not touched.
Encoders accept a reserved placeholder of caller-chosen size, and nothing after placement
moves a byte — gamut-jpeg's appmeta::patch_stream ordering pinned, is_crate_owned
classifying APP11 explicitly (a JPEG may carry non-C2PA APP11 segments — JPEG 360, JPEG
Privacy & Security — which must not be swept into the store, §15.12.1.2).
Multi-segment reassembly reuses the IccAssembler pattern and box walking reuses gamut_isobmff::BoxReader — no second implementation of either.
Hostile input: cumulative size budget (with_max_metadata_bytes precedent), nesting-depth
cap, bounded reassembly; malformed payloads skip rather than error.
Byte accounting stays complete: a located store is a claimed span, not an unclassified
run, in gamut-ifd's SegmentReport and HeifContainer::segments.
mise run check-ffi-features still passes and mise run check-cross wasm32-unknown-unknown is green — proof no crypto reached the shipped graph.
Dev-only differential oracle against c2pa-rs in a workspace-excluded tooling/ crate,
both directions: gamut reserves → external signer completes → c2pa-rs validates; and c2pa-rs embeds → gamut locates the identical byte range.
references/c2pa/README.md in house style, staking every clause used.
Semver recorded: minor for gamut-metadata / gamut-png / gamut-webp / gamut-jpeg, whose
metadata structs are #[non_exhaustive]; major for gamut-dng (DngMetadata) and gamut-riff (MetadataChunks), which are deliberately exhaustive.
mise run test, lint, fmt-check, coverage (≥80%), mutants pass.
Progress
#427, #428 and #429 landed 2026-08-31 (PRs #431, #432, #433). Verified on the three merged
together over master, not only per-branch: fmt-check, check-ffi-features, check-cross wasm32-unknown-unknown (2m53s — the acceptance criterion above, which extended CI only runs on
master), clippy -D warnings and cargo test over gamut-metadata + gamut-heic (213 tests).
Open where it matters: gamut-heic's locator bounds a store by its JUMBF LBox alone, which is
content-dependent — a wrong offset can read as a valid bound and silently trim the store rather
than fail. The TBox == jumb check that would close it is deliberately not asserted: both traces
for that constant are JPEG XL clauses attributing the box to ISO/IEC 18181-2 §9.3 rather than
defining it. Recorded as a deferred row in gamut-heic/STATUS.md, and #447 carries the fixture
that settles it empirically.
Child issues
Each stakes its own container clause of the C2PA spec.
Ordering: #427 + #428 first → (#429, #441, #440 in parallel) → #442 → #443 → #444 → #445 → #446.#447 tracks whichever container landed first and grows with each; #448 follows the first
container that can locate a store, and #449 only needs #428.
Three things easy to get wrong
DNG's data hash needs two disjoint exclusion ranges, not one. §18.5.5: besides the store
payload, "the count field of the C2PA Manifest Store's IFD Entry … should be included in the
exclusion ranges", so an update manifest can change the store's size. The entry sits at a
different file offset from the payload. Also note §18.7.3.3 removed general box hash for
TIFF entirely — c2pa.hash.data is the only binding for TIFF/DNG.
A JPEG C2PA segment's own header is inside the exclusion, not hashed. §18.5.3: "the
APP11 marker (FFEB) and the segment's length (Lp) of all APP11 segments containing the
JUMBF data shall be included in the exclusion range." Because §A.3.1 requires the segments be
contiguous, one range covers them all. Do not hash the marker bytes and exclude only the
payload.
Asset metadata should be included in the binding, not excluded. §9.2.6: a claim generator "should, where possible, include asset metadata (i.e., metadata outside a C2PA Manifest such as
EXIF or XMP) in the hard binding, in order to protect its integrity." Excluding it anyway is
permitted but forces the informational status assertion.dataHash.additionalExclusionsPresent.
The store's own range plus format extras is the only required exclusion.
A correction to docs/non-image-media.md
Lines 201 and 301 say the ISOBMFF carriage is "uuid box c2ma/c2um". That conflates two
nesting levels. c2ma/c2um are the leading four ASCII bytes of JUMBF type UUIDs
(c2ma = 63326D61…) naming boxes inside the manifest store; they never appear as ISOBMFF box
types. Accurate wording:
ISOBMFF top-level uuid box, user type D8FEC3D6-1B0E-483C-9297-5828877EC481, whose payload
carries the JUMBF manifest store (c2pa superbox), itself containing c2ma/c2um manifest
superboxes.
Also worth recording there: there is no item-based (infe/iloc/ipco) C2PA placement in
the spec — Appendix A defines only the top-level uuid box for every BMFF-based asset, HEIF and
AVIF named explicitly.
References to vendor
New directory references/c2pa/ with a README.md in the house style (see references/iptc/README.md). The C2PA documents are CC BY 4.0 — vendorable verbatim, keeping
the copyright line, the CC BY notice and the "AS IS" disclaimer. All clause numbers above are from C2PA Technical Specification 2.4 (April 2026).
(The 2.4 index page's download menu only lists PDFs up to 2.3, but both 2.4 _attachments/ URLs
above resolve.)
Not vendored — paywalled, transcribe the clauses used:ISO/IEC 19566-5:2023 (JUMBF; the
base container standard, CHF 135 — https://www.iso.org/standard/84635.html) · ISO/IEC 18477-3
(JPEG XT box file format; the source of the APP11 field framing) · ISO/IEC 18181-2:2024 (JPEG XL
container, jumb box).
Procurement decision to make before the gamut-jpeg slice. ISO/IEC 19566-5 Annex D.2 is
the only normative text for the APP11 field layout (CI/En/Z/LBox/TBox), and it is
paywalled. Every other constant in this issue is sourced from the CC BY C2PA spec. Writing JPEG
segmentation against the reference implementation instead of the ISO text would violate "specification as source of truth" — so either buy 19566-5 or descope JPEG. Note in particular
that c2pa-rs writes box instance number En = 0x0211 with the comment "can be any unique ID"
— that value is a c2pa-rs convention, not normative, and must not be transcribed as if it
were spec.
c2pa-rs (Apache-2.0/MIT) as a dev-only differential oracle in a workspace-excluded tooling/c2pa-oracle, invoked by manifest path — the tooling/gamut-dng-real-conformance shape.
Being a Rust crate it needs no third_party/ submodule. Build it --no-default-features --features rust_native_crypto: the default openssl feature is pulled vendored and would compile OpenSSL from C source.
Two facts that shaped the split above, both verified in c2pa-rs 0.90.16:
Builder's *_embeddable path plus composed_manifest(bytes, "application/c2pa") yields the
raw manifest-store bytes for a host to embed itself, and CallbackSigner carries a reserve_size — c2pa-rs already models signing as reserve-then-fill, which is exactly the seam
gamut needs to expose.
There is no cheap parse-only mode: ValidationState::Invalid is also what you get when
verification is disabled, so c2pa-rs cannot serve as a "parse but don't judge" front end. That
is why gamut owns the locate/bound step rather than shelling out for it.
Do not build on c2pa::jumbf_io — its public functions name crate-private traits
(CAIRead/CAIReadWrite), a private-in-public leak.
The JUMBF interior: claims, CBOR, assertion schemas, ingredient ancestry. A later issue, if a
consumer needs typed observability.
JPEG XL — §A.3.9 puts the store in a top-level jumb box, and a bare FF 0A codestream cannot carry one at all. Blocked here regardless: jxl-rs surfaces no box bytes on decode
(gamut-jxl/STATUS.md:120-122).
Note on the minimized comment below: it is from a non-maintainer, proposes an unvendored
third-party CI as an acceptance oracle, and asserts a downstream dependency nothing in this repo
corroborates. It is not a requirements source — oracles here are dev-only and live under tooling/ against a vendored reference.
There is an official Rust library: https://github.com/contentauth/c2pa-rs
Integrate traits and types in the relevant gamut libraries (e.g.
gamut-exif).Scope (settled)
docs/non-image-media.md— normative for scope — already splits this: gamut locates, bounds,carries and reserves the manifest store; validation belongs to
c2pa-rs. This epic isgamut's half only.
c2pa-rsis a consumer-side dependency and a dev-only oracle. gamut never reports a validityverdict and no API may imply one.
assertion model in this epic.
gamut-exif. The body above is wrong on this: a manifeststore is a JUMBF superbox (ISO/IEC 19566-5), never an IFD value. It becomes a fourth carrier
plus per-format raw payloads, exactly as EXIF/XMP/ICC work today. In TIFF/DNG an IFD tag points
at the store, but the payload is opaque to the IFD layer just as ICC and XMP already are.
Acceptance
MetadataBlock::C2pa/Metadata::c2pa/EncodedMetadata::c2paland as a fourth carrier(all three already
#[non_exhaustive]— semver-minor), with the "why a carrier and not anextension" rationale in
gamut-metadata/README.md.invalidate the hard binding; a derivative needs a new manifest carrying the parent as an
ingredient. The facade drops or refuses by explicit policy, never silently passes through —
and this exception to the "extract → embed → extract is a true equality" keystone is
documented.
external signer needs: byte ranges for
c2pa.hash.data(§18.5), box paths forc2pa.hash.bmff.v3(§18.6, §A.5.6). One primitive serves both reading and post-encodepatching;
EncodeImageis not touched.moves a byte —
gamut-jpeg'sappmeta::patch_streamordering pinned,is_crate_ownedclassifying APP11 explicitly (a JPEG may carry non-C2PA APP11 segments — JPEG 360, JPEG
Privacy & Security — which must not be swept into the store, §15.12.1.2).
IccAssemblerpattern and box walking reusesgamut_isobmff::BoxReader— no second implementation of either.with_max_metadata_bytesprecedent), nesting-depthcap, bounded reassembly; malformed payloads skip rather than error.
unclassifiedrun, in
gamut-ifd'sSegmentReportandHeifContainer::segments.dcterms:provenanceURL in its XMP reports remoteprovenance, not none (§11.5, §15.5.3.1 — that key is for external manifests only).
→ gamut-xmp/gamut-metadata: report remote provenance from dcterms:provenance #449
gamut inspectreports presence, size and offsets, and states that it does not validate.→ gamut-cli: report a C2PA manifest store in gamut inspect, and state that it is not validated #448
mise run check-ffi-featuresstill passes andmise run check-cross wasm32-unknown-unknownis green — proof no crypto reached the shipped graph.c2pa-rsin a workspace-excludedtooling/crate,both directions: gamut reserves → external signer completes →
c2pa-rsvalidates; andc2pa-rsembeds → gamut locates the identical byte range.references/c2pa/README.mdin house style, staking every clause used.metadata structs are
#[non_exhaustive]; major forgamut-dng(DngMetadata) andgamut-riff(MetadataChunks), which are deliberately exhaustive.mise run test,lint,fmt-check,coverage(≥80%),mutantspass.Progress
#427, #428 and #429 landed 2026-08-31 (PRs #431, #432, #433). Verified on the three merged
together over master, not only per-branch:
fmt-check,check-ffi-features,check-cross wasm32-unknown-unknown(2m53s — the acceptance criterion above, which extended CI only runs onmaster),
clippy -D warningsandcargo testovergamut-metadata+gamut-heic(213 tests).Open where it matters:
gamut-heic's locator bounds a store by its JUMBFLBoxalone, which iscontent-dependent — a wrong offset can read as a valid bound and silently trim the store rather
than fail. The
TBox == jumbcheck that would close it is deliberately not asserted: both tracesfor that constant are JPEG XL clauses attributing the box to ISO/IEC 18181-2 §9.3 rather than
defining it. Recorded as a deferred row in
gamut-heic/STATUS.md, and #447 carries the fixturethat settles it empirically.
Child issues
Each stakes its own container clause of the C2PA spec.
gamut-metadata: the fourth carrier + the no-copy-forward policygamut-heicread — §A.5: top-leveluuid, user typeD8FEC3D6-1B0E-483C-9297-5828877EC481.HeifContainer::segments()already yields thatbox's byte-exact range; nearly free
gamut-jpeg— §A.3.1 APP11 (FFEB) segmentation, reassembly,patch_streamordering. A procurement decision first: the field layout is ISO/IEC 19566-5 D.2, which is
paywalled and not vendored — buy it or descope JPEG
gamut-png— §A.3.2caBXchunk (ancillary, private, unsafe-to-copy), should precedeIDATgamut-dng— §A.3.6 tag 52545 /0xCD41, type 7 (UNDEFINED), in the last IFD ofthe main chain, store at end of file. Today an untyped
RawTag(STATUS.md:305)gamut-isobmfftop-level box model + writer — largest slice:IsoBmffImagehas notop-level box list and neither
model.rsnorwriter.rsmentionsuuid, so the box hasnowhere to live. Position is after
ftyp, before the firstmdatand anymoov.Unblocks gamut-avif: reserve and write a C2PA manifest store, and locate one on read #444
gamut-avifwrite — reserve a caller-sized placeholder and report where it landed;EncodeImageuntouched. Blocked by gamut-isobmff: model and write top-level boxes, so a C2PA uuid box has somewhere to live #443gamut-riff/gamut-webp— §A.3.7C2PAchunk, last sub-chunk of the first RIFFchunk
gamut-tiff— §A.3.6 as gamut-dng: type the C2PA manifest store tag and report both exclusion ranges #442; needs a metadata seam built first, it has none todaytooling/c2pa-oracle+ the differential testsgamut-cli—gamut inspectreports presence, size and offsets, and says it does notvalidate (the acceptance criterion above)
gamut-xmp/gamut-metadata—dcterms:provenanceremote provenance; three states,not two (the acceptance criterion above)
references/c2pa/staking (table below)Ordering: #427 + #428 first → (#429, #441, #440 in parallel) → #442 → #443 → #444 → #445 →
#446. #447 tracks whichever container landed first and grows with each; #448 follows the first
container that can locate a store, and #449 only needs #428.
Three things easy to get wrong
payload, "the
countfield of the C2PA Manifest Store's IFD Entry … should be included in theexclusion ranges", so an update manifest can change the store's size. The entry sits at a
different file offset from the payload. Also note §18.7.3.3 removed general box hash for
TIFF entirely —
c2pa.hash.datais the only binding for TIFF/DNG.APP11 marker (
FFEB) and the segment's length (Lp) of all APP11 segments containing theJUMBF data shall be included in the exclusion range." Because §A.3.1 requires the segments be
contiguous, one range covers them all. Do not hash the marker bytes and exclude only the
payload.
"should, where possible, include asset metadata (i.e., metadata outside a C2PA Manifest such as
EXIF or XMP) in the hard binding, in order to protect its integrity." Excluding it anyway is
permitted but forces the informational status
assertion.dataHash.additionalExclusionsPresent.The store's own range plus format extras is the only required exclusion.
A correction to
docs/non-image-media.mdLines 201 and 301 say the ISOBMFF carriage is "
uuidboxc2ma/c2um". That conflates twonesting levels.
c2ma/c2umare the leading four ASCII bytes of JUMBF type UUIDs(
c2ma=63326D61…) naming boxes inside the manifest store; they never appear as ISOBMFF boxtypes. Accurate wording:
Also worth recording there: there is no item-based (
infe/iloc/ipco) C2PA placement inthe spec — Appendix A defines only the top-level
uuidbox for every BMFF-based asset, HEIF andAVIF named explicitly.
References to vendor
New directory
references/c2pa/with aREADME.mdin the house style (seereferences/iptc/README.md). The C2PA documents are CC BY 4.0 — vendorable verbatim, keepingthe copyright line, the CC BY notice and the "AS IS" disclaimer. All clause numbers above are from
C2PA Technical Specification 2.4 (April 2026).
C2PA_Specification_2.4.pdfC2PA_Specification_2.4.htmlC2PA_Schemas_2.4.zip(The 2.4 index page's download menu only lists PDFs up to 2.3, but both 2.4
_attachments/URLsabove resolve.)
Not vendored — paywalled, transcribe the clauses used: ISO/IEC 19566-5:2023 (JUMBF; the
base container standard, CHF 135 — https://www.iso.org/standard/84635.html) · ISO/IEC 18477-3
(JPEG XT box file format; the source of the APP11 field framing) · ISO/IEC 18181-2:2024 (JPEG XL
container,
jumbbox).Already vendored, no action:
references/jpeg/itu-t81.pdf(marker syntax §B.2.4.6) ·references/png/(chunk layout, 4.7.2) ·references/isobmff/(14496-12 box syntax) ·references/tiff/(IFD entry layout) ·references/webp/(RIFF chunk order).Oracle
c2pa-rs(Apache-2.0/MIT) as a dev-only differential oracle in a workspace-excludedtooling/c2pa-oracle, invoked by manifest path — thetooling/gamut-dng-real-conformanceshape.Being a Rust crate it needs no
third_party/submodule. Build it--no-default-features --features rust_native_crypto: the defaultopensslfeature is pulledvendored and would compile OpenSSL from C source.
Two facts that shaped the split above, both verified in c2pa-rs 0.90.16:
Builder's*_embeddablepath pluscomposed_manifest(bytes, "application/c2pa")yields theraw manifest-store bytes for a host to embed itself, and
CallbackSignercarries areserve_size— c2pa-rs already models signing as reserve-then-fill, which is exactly the seamgamut needs to expose.
ValidationState::Invalidis also what you get whenverification is disabled, so c2pa-rs cannot serve as a "parse but don't judge" front end. That
is why gamut owns the locate/bound step rather than shelling out for it.
c2pa::jumbf_io— its public functions name crate-private traits(
CAIRead/CAIReadWrite), a private-in-public leak.Out of scope
c2pa-rs.consumer needs typed observability.
jumbbox, and a bareFF 0Acodestreamcannot carry one at all. Blocked here regardless: jxl-rs surfaces no box bytes on decode
(
gamut-jxl/STATUS.md:120-122).docs/non-image-media.md, Extendgamutto non-image formats #217 / Extend gamut-metadata to audio and video files #216.Note on the minimized comment below: it is from a non-maintainer, proposes an unvendored
third-party CI as an acceptance oracle, and asserts a downstream dependency nothing in this repo
corroborates. It is not a requirements source — oracles here are dev-only and live under
tooling/against a vendored reference.