feat: Image Provenance experiment and CDN worker templates (C2PA image signing)#302
feat: Image Provenance experiment and CDN worker templates (C2PA image signing)#302erik-sv wants to merge 6 commits intoWordPress:developfrom
Conversation
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
335715c to
01a0f2b
Compare
|
@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
01a0f2b to
ca4f542
Compare
|
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 Unlinked AccountsThe 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. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Rebased onto latest What changed in this rebase:
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. |
- 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
ca4f542 to
581f8ef
Compare
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.
| * CDN_PROVENANCE_CACHE = KV namespace binding | ||
| * | ||
| * For CDN-transform survival (pHash matching across resized images), | ||
| * use the Encypher free API: https://encypherai.com |
There was a problem hiding this comment.
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'] ); |

Changes since last review
develop0.6.0)Image_Provenanceported toAbstract_FeatureAPI — matches the 0.6.0 feature model introduced in feat: Content Provenance experiment (C2PA 2.3 §A.7 text authentication) #294Summary
Adds an Image Provenance experiment that signs image attachments with C2PA manifests on upload and injects
C2PA-Manifest-URLresponse 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.What this adds
Image_Provenanceexperiment — hooksadd_attachmentto sign JPEG/PNG/WebP/GIF uploads; stores C2PA manifest JSON and canonical URL in post metaC2PA-Manifest-URLheader injection — on singular pages with a signed featured image, injects the header pointing to the manifest REST endpointUpload 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"]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]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]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
CDN worker setup (Cloudflare example)
See
cdn-workers/README.mdfor Lambda@Edge and Fastly instructions.Files changed
includes/Experiments/Image_Provenance/Image_Provenance.phpincludes/Experiment_Loader.phpImage_Provenancecdn-workers/cloudflare/cdn-provenance-worker.jscdn-workers/cloudflare/wrangler.toml.templatecdn-workers/lambda-edge/cdn-provenance-handler.mjscdn-workers/fastly/main.rscdn-workers/README.md.eslintignorecdn-workers/from WordPress ESLint rulestsconfig.jsoncdn-workers/**from TypeScript compilationtests/Integration/…/Image_ProvenanceTest.phpTest plan
composer test -- --filter Image_Provenance— all 10 tests pass_c2pa_image_statusattachment meta issignedGET /wp-json/c2pa-provenance/v1/images/lookup?url=<attachment-url>→ returnsrecord_idGET /wp-json/c2pa-provenance/v1/images/lookup?url=<url>?w=800&format=webp→ samerecord_id(CDN params stripped)GET /wp-json/c2pa-provenance/v1/images/manifest/<id>→ returns manifest JSON withclaimsandsignatureC2PA-Manifest-URLC2PA-Manifest-URLheader_c2pa_image_statusmeta should NOT be set (MIME guard)