Skip to content

feat: Image Provenance experiment and CDN worker templates (C2PA image signing)#302

Open
erik-sv wants to merge 6 commits intoWordPress:developfrom
erik-sv:feature/image-provenance-cdn
Open

feat: Image Provenance experiment and CDN worker templates (C2PA image signing)#302
erik-sv wants to merge 6 commits intoWordPress:developfrom
erik-sv:feature/image-provenance-cdn

Conversation

@erik-sv
Copy link
Copy Markdown

@erik-sv erik-sv commented Mar 11, 2026

Changes since last review


Summary

Adds an Image Provenance experiment that signs image attachments with C2PA manifests on upload and injects C2PA-Manifest-URL response headers for singular posts with featured images. Also adds CDN edge worker templates for Cloudflare, Lambda@Edge, and Fastly so CDN-served images can propagate the provenance header downstream.

Depends on #294 — reuses C2PA_Manifest_Builder, Local_Signer, and Signing_Interface from the Content Provenance namespace. Merge #294 first; the diff against develop will then show only the Image Provenance additions.

What this adds

  • Image_Provenance experiment — hooks add_attachment to sign JPEG/PNG/WebP/GIF uploads; stores C2PA manifest JSON and canonical URL in post meta
  • C2PA-Manifest-URL header injection — on singular pages with a signed featured image, injects the header pointing to the manifest REST endpoint
  • REST endpoints — lookup by URL and retrieve manifest by attachment ID
  • CDN worker templates — Cloudflare Worker (with KV caching), AWS Lambda@Edge, and Fastly Compute@Edge; all call the WordPress REST lookup endpoint
  • 10 integration tests covering upload signing, MIME filtering, REST lookup, manifest retrieval, header injection, and non-singular guard

Upload and signing flow

flowchart TD
    A["Media upload (JPEG, PNG, WebP, GIF)"] --> B["add_attachment hook"]
    B --> C{"Supported MIME type?"}
    C -->|No| D["Skip - no meta written"]
    C -->|Yes| E["wp_get_attachment_url()"]
    E --> F["Strip query params to get canonical URL"]
    F --> G["C2PA_Manifest_Builder build c2pa.created"]
    G --> H["Local_Signer RSA-2048 via OpenSSL"]
    H -->|Error| I["Store _c2pa_image_status = error"]
    H -->|Success| J["Write post meta"]
    J --> J1["_c2pa_image_manifest"]
    J --> J2["_c2pa_image_manifest_url"]
    J --> J3["_c2pa_image_canonical_url"]
    J --> J4["_c2pa_image_status = signed"]
Loading

Page header injection flow

flowchart LR
    A[Page request hits send_headers hook] --> B{is_singular?}
    B -->|No| C[Return — no header]
    B -->|Yes| D[get_post_thumbnail_id]
    D --> E{Has featured image?}
    E -->|No| C
    E -->|Yes| F[get_post_meta: _c2pa_image_manifest_url]
    F --> G{Manifest URL set?}
    G -->|No| C
    G -->|Yes| H[header: C2PA-Manifest-URL: REST URL]
Loading

REST lookup and CDN worker flow

flowchart TD
    A[CDN image request] --> B[Edge worker intercepts origin response]
    B --> C[Canonicalize image URL: strip query params]
    C --> D[GET /c2pa-provenance/v1/images/lookup?url=canonical]
    D --> E{Match found?}
    E -->|No| F[Pass through — no header added]
    E -->|Yes| G[Inject C2PA-Manifest-URL into response headers]

    H[GET /images/lookup?url=X] --> I[meta_query on _c2pa_image_canonical_url]
    I -->|Found| J[200: record_id + manifest_url]
    I -->|Not found| K[404: not_found]

    L[GET /images/manifest/id] --> M[get_post_meta: _c2pa_image_manifest]
    M -->|Found| N[200: manifest JSON]
    M -->|Not found| O[404: not_found]
Loading

Exact-URL limitation and upgrade path

These workers use exact URL matching (scheme + host + path, query params stripped). CDN transforms that rewrite the URL path (e.g. /cdn-cgi/image/width=800/photo.jpg) will not match the original upload URL.

For CDN-transform survival using perceptual hash (pHash) matching, use the Encypher free API which handles cross-CDN, multi-resolution image lookup at scale.

REST API reference

GET /wp-json/c2pa-provenance/v1/images/lookup?url=<canonical_url>
→ 200 { "record_id": "<id>", "manifest_url": "<rest_url>" }
→ 404 { "error": "not_found" }

GET /wp-json/c2pa-provenance/v1/images/manifest/<attachment_id>
→ 200 { "magic": "…", "version": 1, "claims": { … }, "signature": "…" }
→ 404 { "error": "not_found" }

CDN worker setup (Cloudflare example)

cp cdn-workers/cloudflare/wrangler.toml.template cdn-workers/cloudflare/wrangler.toml
# Edit: set WORDPRESS_REST_URL to your site's REST base
wrangler kv:namespace create "CDN_PROVENANCE_CACHE"
# Edit: paste the namespace ID into wrangler.toml
wrangler deploy

See cdn-workers/README.md for Lambda@Edge and Fastly instructions.

Files changed

File Type Description
includes/Experiments/Image_Provenance/Image_Provenance.php New Main experiment class
includes/Experiment_Loader.php Modified Register Image_Provenance
cdn-workers/cloudflare/cdn-provenance-worker.js New Cloudflare Worker with KV caching
cdn-workers/cloudflare/wrangler.toml.template New Wrangler config template
cdn-workers/lambda-edge/cdn-provenance-handler.mjs New Lambda@Edge Origin Response handler
cdn-workers/fastly/main.rs New Fastly Compute@Edge handler
cdn-workers/README.md New Setup instructions for all three platforms
.eslintignore New Exclude cdn-workers/ from WordPress ESLint rules
tsconfig.json Modified Exclude cdn-workers/** from TypeScript compilation
tests/Integration/…/Image_ProvenanceTest.php New 10 integration tests, 24 assertions

Test plan

  • Run composer test -- --filter Image_Provenance — all 10 tests pass
  • Upload a JPEG via Media Library → check _c2pa_image_status attachment meta is signed
  • GET /wp-json/c2pa-provenance/v1/images/lookup?url=<attachment-url> → returns record_id
  • GET /wp-json/c2pa-provenance/v1/images/lookup?url=<url>?w=800&format=webp → same record_id (CDN params stripped)
  • GET /wp-json/c2pa-provenance/v1/images/manifest/<id> → returns manifest JSON with claims and signature
  • View a singular post with signed featured image → response headers include C2PA-Manifest-URL
  • View a non-singular page (archive, home) → no C2PA-Manifest-URL header
  • Upload a PDF → _c2pa_image_status meta should NOT be set (MIME guard)
Open WordPress Playground Preview

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 87.02749% with 151 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.12%. Comparing base (7d20bba) to head (ddbf073).

Files with missing lines Patch % Lines
...eriments/Content_Provenance/Content_Provenance.php 89.21% 51 Missing ⚠️
.../Experiments/Image_Provenance/Image_Provenance.php 76.21% 44 Missing ⚠️
...ncludes/Abilities/Content_Provenance/C2PA_Sign.php 85.26% 14 Missing ⚠️
...riments/Content_Provenance/Signing/BYOK_Signer.php 72.34% 13 Missing ⚠️
...iments/Content_Provenance/Signing/Local_Signer.php 72.97% 10 Missing ⚠️
...eriments/Content_Provenance/Well_Known_Handler.php 81.81% 6 Missing ⚠️
...ments/Content_Provenance/C2PA_Manifest_Builder.php 94.73% 4 Missing ⚠️
...ts/Content_Provenance/Signing/Connected_Signer.php 92.45% 4 Missing ⚠️
...eriments/Content_Provenance/Verification_Badge.php 95.34% 2 Missing ⚠️
...ludes/Abilities/Content_Provenance/C2PA_Verify.php 98.14% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #302      +/-   ##
=============================================
+ Coverage      58.09%   66.12%   +8.02%     
- Complexity       630      833     +203     
=============================================
  Files             46       58      +12     
  Lines           3193     4357    +1164     
=============================================
+ Hits            1855     2881    +1026     
- Misses          1338     1476     +138     
Flag Coverage Δ
unit 66.12% <87.02%> (+8.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jeffpaul jeffpaul added this to the 0.6.0 milestone Mar 11, 2026
@jeffpaul jeffpaul added the [Type] Enhancement New feature or request label Mar 11, 2026
@erik-sv erik-sv force-pushed the feature/image-provenance-cdn branch from 335715c to 01a0f2b Compare March 12, 2026 13:16
@jeffpaul jeffpaul modified the milestones: 0.6.0, 0.7.0 Mar 20, 2026
@jeffpaul
Copy link
Copy Markdown
Member

@erik-sv any ETA on getting a PR out of draft and ready for review here? I'd love to see us get this stable and into the plugin before the WordPress 7.0 launch on April 9th, which would likely mean getting the PR ready for review/testing by sometime next week.

Port Content Provenance experiment to the 0.6.0 Abstract_Feature API.
Embeds cryptographic proof of origin into published content using C2PA
2.3 text authentication with three signing tiers: Local, Connected, and
BYOK. Includes c2pa/sign and c2pa/verify abilities, REST endpoints,
well-known discovery, block editor sidebar panel, and verification badge.

- Extend Abstract_Feature with static get_id() and load_metadata()
- Register via Experiments::EXPERIMENT_CLASSES
- Use wpai_feature_* option naming convention
- Add Well_Known_Handler test coverage (was 0%)
- Fix test namespace mismatches for PSR-4 compliance
- Fix duplicate PHPDoc block on get_public_signer()
- Remove stale phpcs:ignore comment
@erik-sv erik-sv force-pushed the feature/image-provenance-cdn branch from 01a0f2b to ca4f542 Compare March 26, 2026 09:58
@erik-sv erik-sv marked this pull request as ready for review March 26, 2026 09:58
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @erik@encypherai.com.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: erik@encypherai.com.

Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>
Co-authored-by: erik-sv <encypher@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@erik-sv
Copy link
Copy Markdown
Author

erik-sv commented Mar 26, 2026

Rebased onto latest develop (0.6.0) and moved out of draft alongside PR #294.

What changed in this rebase:

  • Ported Image_Provenance from Abstract_ExperimentAbstract_Feature API
  • Registration via Experiments::EXPERIMENT_CLASSES
  • All option names now use the wpai_feature_* convention
  • Updated test file for new Registry/Loader API
  • Updated developer docs for new hook names

This PR builds on #294 (Content Provenance) — that PR should be reviewed first.

Ready for review — aiming to be stable well before the April 9th WP 7.0 window.

Erik Svilich added 3 commits March 26, 2026 09:59
- Rename ai_content_provenance_experiment_instance filter to
  wpai_content_provenance_experiment_instance (prefix compliance)
- Update developer docs for 0.6.0 filter/action names
- Apply PHPCBF auto-fixes (use statement ordering, FQN annotations)
Add Image Provenance experiment on top of Content Provenance, ported to
the 0.6.0 Abstract_Feature API. Signs image attachments on upload with
C2PA manifests, injects C2PA-Manifest-URL headers, and provides REST
endpoints for manifest lookup and retrieval.

Includes CDN edge worker templates for Cloudflare, Fastly, and AWS
Lambda@Edge that inject provenance headers at the CDN layer.

- Extend Abstract_Feature with static get_id() and load_metadata()
- Register via Experiments::EXPERIMENT_CLASSES
- Use wpai_feature_* option naming convention
- Exclude cdn-workers from ESLint and TypeScript
- Include linter fixes for use statement ordering and PHPDoc FQCN
@erik-sv erik-sv force-pushed the feature/image-provenance-cdn branch from ca4f542 to 581f8ef Compare March 26, 2026 10:01
Content_ProvenanceTest::test_rest_verify_route_registered initializes
the global $wp_rest_server singleton, and Experiments::init() registers
a persistent wpai_default_feature_classes filter. Neither was cleaned up
in tearDown, causing cross-test contamination where Example_ExperimentTest
could not register its rest_api_init callbacks (the event had already
fired on the stale server instance).

Reset $GLOBALS['wp_rest_server'] and remove the Experiments filter in
both Content_ProvenanceTest and Image_ProvenanceTest tearDown methods.
@jeffpaul jeffpaul requested a review from dkotter March 27, 2026 16:34
@jeffpaul jeffpaul moved this from In progress to Needs review in WordPress AI Planning & Roadmap Mar 27, 2026
* CDN_PROVENANCE_CACHE = KV namespace binding
*
* For CDN-transform survival (pHash matching across resized images),
* use the Encypher free API: https://encypherai.com
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact-URL approach seems reasonable for an initial experiment. Would it make sense to document the CDN-transform limitation without recommending a specific commercial solution, and let that be addressed in a future iteration?

public function test_build_document_context_uri(): void {
$document = Well_Known_Handler::build_document();

$this->assertSame( 'https://c2pa.org/schemas/c2pa-well-known/v1', $document['@context'] );
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image Is this meant to lead to a real schema?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement New feature or request

Projects

Status: Needs review

Development

Successfully merging this pull request may close these issues.

3 participants