Skip to content

Add C2PA Content Credentials support for JPEG files - #3

Merged
profradha merged 2 commits into
mainfrom
claude/c2pa-browser-credentials-0o8o5i
Aug 26, 2026
Merged

Add C2PA Content Credentials support for JPEG files#3
profradha merged 2 commits into
mainfrom
claude/c2pa-browser-credentials-0o8o5i

Conversation

@profradha

Copy link
Copy Markdown
Member

This PR adds comprehensive Content Credentials (C2PA 2.2) support to the image editor, enabling users to verify and sign JPEG files with cryptographic manifests that record the complete edit history.

Summary

The implementation provides a complete C2PA claim generator that runs entirely in the browser, with no external dependencies or network calls. Users can now:

  • Read and validate existing C2PA manifests in opened JPEG files
  • Sign exported JPEG files with a manifest recording all edits performed
  • View detailed credential information including signer details, edit history, and validation status

Key Changes

Core C2PA Implementation (crates/imagecore/src/c2pa/)

  • manifest.rs: Complete manifest builder implementing the two-render pattern to solve the circular dependency between manifest size and file hash (§10.4 of spec)
  • jumbf.rs: ISO/IEC 19566-5 JUMBF box container format with proper superbox handling and hashing
  • jpegxt.rs: JPEG XT APP11 segment embedding with exclusion range calculation for hard binding (§A.3.1, §18.5.3)
  • cbor.rs: Deterministic CBOR encoding (RFC 8949 §4.2.1) with fixed-width placeholders for offsets
  • cose.rs: COSE_Sign1 signatures (RFC 8152/9360) with detached payloads and x5chain certificate handling
  • x509.rs: Minimal DER parser to extract certificate details without full validation
  • signer.rs: Integration with build-time signing credentials

Build & Signing Infrastructure

  • build.rs: Embeds signing certificate and key from environment or demo files
  • signing/generate.sh: Script to generate demo P-256 ECDSA certificates following C2PA profile (§14.5.1)
  • Demo certificates included for development/testing

UI & Integration

  • src/credentials.ts: New credentials panel with manifest validation display, action history, and signer information
  • src/editor.css: Styling for credential states (valid/invalid/unsigned) with clear visual hierarchy
  • index.html: Credentials panel UI with manifest details and thumbnail display
  • src/types.ts: TypeScript interfaces for credential reports and signer information
  • src/main.ts, src/engine.ts, src/worker.ts: Integration points for credential reading/writing

Testing

  • crates/imagecore/tests/c2pa.rs: End-to-end tests verifying manifest generation, validation, and edit chaining

Notable Implementation Details

  • Circular Dependency Resolution: The manifest builder uses a two-render pattern where the first render with placeholder values determines exact size, then real values are substituted in the second render, ensuring byte-for-byte identical length
  • Fixed-Width Placeholders: SHA-256 hashes (32 bytes), ES256 signatures (64 bytes), and CBOR integers are all written in fixed width to maintain size invariance across renders
  • Hard Binding: The c2pa.hash.data assertion excludes the manifest's own bytes from the file hash, with precise exclusion ranges covering JPEG APP11 markers and length fields
  • Deterministic Encoding: All CBOR output uses shortest-form integers and lexicographically sorted map keys per RFC 8949 §4.2.1
  • No External Validation: The implementation reads and reports what certificates claim without validating chains or revocation, with UI text making this clear to users
  • Browser-Only Signing: All cryptographic operations happen in WebAssembly; the signing key is public by design since it ships in the browser

Limitations

  • JPEG output only (other formats remain unsigned but functional)
  • No RFC 3161 timestamps or OCSP responses (would require network calls)
  • Certificate validation not performed (reported honestly in UI)
  • Signing key is public (documented in signing/README.md)

https://claude.ai/code/session_01QzDZWjq5uHHgQdp84gXYnw

claude added 2 commits August 25, 2026 16:17
Adds a working C2PA 2.2 claim generator and validator to the editor,
running entirely in the tab. Opening a JPEG checks any credential it
carries; exporting a JPEG can attach a new signed manifest recording
every operation the pipeline performed, with the previous credential
carried forward as a parentOf ingredient.

Written against the specification rather than linking c2pa-rs, which
carries a trust-list and OCSP stack this has no use for and whose wasm
path does not go through wasm-bindgen. The layers are small enough to
read: deterministic CBOR (RFC 8949 4.2.1), JUMBF boxes (ISO 19566-5),
APP11 embedding, a minimal DER reader, and COSE_Sign1 over the claim.

The hard binding's circular dependency - the manifest hashes a file it
lives inside - is broken the way section 10.4 prescribes, with
fixed-width placeholders and two renders. Every placeholder is exactly
as wide as the value replacing it, and the two lengths are asserted
equal rather than assumed.

wasm32-unknown-unknown has neither a clock nor an RNG, so the host
supplies timestamps and UUIDs and p256's std feature is off to keep
getrandom 0.2 out of the tree. Signing is deterministic per RFC 6979,
which also makes the tests reproducible.

Scope is JPEG in, JPEG out: the hard binding commits to a byte range,
so embedding and exclusions are per-format. Other formats edit and
export exactly as before, and the UI says so rather than implying a
file was checked.

On trust: the signing key is compiled into a module served to browsers,
so it is public and cannot be otherwise. That still proves the pixels
are unaltered; it cannot prove who signed. The UI reports the two
claims separately, never shows a tick beside a signer, and names the
specification status code for every check. GitHub secrets are wired up
for the one thing they buy - keeping a key out of git history - and
signing/README.md is explicit that they cannot make a browser key
secret, with two designs that would.

Verified against c2patool, which reports validation_state Valid with
signingCredential.untrusted as the only failure, and reports
assertion.dataHash.mismatch on a tampered file exactly as this
validator does. 76 new tests cover the layers and the round trip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzDZWjq5uHHgQdp84gXYnw
Clippy 1.98 adds chunks_exact_to_as_chunks, which CI runs as an error
under -D warnings. The path also collected into a Vec only to iterate it
again, so this drops that too; as_chunks yields whole pairs directly and
still discards a trailing odd byte, which is the behaviour a malformed
BMPString wants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzDZWjq5uHHgQdp84gXYnw
@profradha
profradha merged commit 3cd5ba4 into main Aug 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants