-
Notifications
You must be signed in to change notification settings - Fork 91
feat: Image Provenance experiment and CDN worker templates (C2PA image signing) #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
erik-sv
wants to merge
6
commits into
WordPress:develop
Choose a base branch
from
erik-sv:feature/image-provenance-cdn
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9758181
feat: Content Provenance experiment (C2PA 2.3 text authentication)
227e5cd
fix: align hook names and coding standards with 0.6.0 API
d1828e7
feat: Image Provenance experiment and CDN worker templates
581f8ef
fix: sort use statements in Image_Provenance (PHPCS)
a4192c4
fix: clean up REST server and Experiments filter in test tearDown
ddbf073
Merge branch 'develop' into feature/image-provenance-cdn
jeffpaul File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| build | ||
| node_modules | ||
| vendor | ||
| cdn-workers/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # CDN Provenance Workers | ||
|
|
||
| These workers inject `C2PA-Manifest-URL` response headers for image requests, | ||
| enabling CDN-level content provenance verification. | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. An image is uploaded to WordPress -- the Image Provenance experiment signs it with a C2PA manifest. | ||
| 2. The manifest URL is stored in attachment meta (`_c2pa_image_manifest_url`). | ||
| 3. The CDN worker intercepts image responses, queries the WordPress REST API for the manifest URL, and injects `C2PA-Manifest-URL` into the response header. | ||
| 4. Consumers (browsers, C2PA validators) can follow the header to verify image origin. | ||
|
|
||
| ## Limitation: Exact URL Matching Only | ||
|
|
||
| These workers use **exact URL matching**. If your CDN transforms image URLs | ||
| (e.g. `/cdn-cgi/image/width=800/photo.jpg`), the lookup will not match the | ||
| original upload URL and no header will be injected. | ||
|
|
||
| For CDN-transform survival using perceptual hash (pHash) matching, use the | ||
| **[Encypher free API](https://encypherai.com)** -- it handles cross-CDN, multi- | ||
| resolution image lookup at scale. | ||
|
|
||
| ## Cloudflare Worker | ||
|
|
||
| ### Setup | ||
|
|
||
| 1. Copy `cloudflare/wrangler.toml.template` to `cloudflare/wrangler.toml` | ||
| 2. Set `WORDPRESS_REST_URL` to your WordPress site's REST API base URL | ||
| 3. Create a KV namespace: `wrangler kv:namespace create "CDN_PROVENANCE_CACHE"` | ||
| 4. Update the `id` in `wrangler.toml` with the namespace ID | ||
| 5. Deploy: `wrangler deploy` | ||
|
|
||
| ### Local Testing | ||
|
|
||
| ```bash | ||
| wrangler dev | ||
| ``` | ||
|
|
||
| ## AWS Lambda@Edge | ||
|
|
||
| ### Setup | ||
|
|
||
| 1. Set the `WORDPRESS_REST_URL` environment variable in your Lambda function config | ||
| 2. Deploy as a CloudFront Lambda@Edge function (Origin Response trigger) | ||
| 3. Ensure the Lambda has outbound internet access to reach your WordPress REST API | ||
|
|
||
| ### Local Testing | ||
|
|
||
| ```bash | ||
| # Using the SAM CLI | ||
| sam local invoke --event test-event.json | ||
| ``` | ||
|
|
||
| ## Fastly Compute | ||
|
|
||
| ### Setup | ||
|
|
||
| 1. Create an Edge Dictionary named `wordpress_rest_url` | ||
| 2. Add key `wordpress_rest_url` with your WordPress REST API base URL as value | ||
| 3. Add a backend named `wordpress_api` pointing to your WordPress host | ||
| 4. Build and deploy: `fastly compute build && fastly compute deploy` | ||
|
|
||
| ### Local Testing | ||
|
|
||
| ```bash | ||
| fastly compute serve | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /** | ||
| * Cloudflare Worker: C2PA Image Provenance | ||
| * | ||
| * Looks up the C2PA manifest URL for any image request via the WordPress | ||
| * REST API and injects a C2PA-Manifest-URL response header. | ||
| * | ||
| * Configuration (wrangler.toml): | ||
| * WORDPRESS_REST_URL = "https://your-site.com/wp-json" | ||
| * CDN_PROVENANCE_CACHE = KV namespace binding | ||
| * | ||
| * For CDN-transform survival (pHash matching across resized images), | ||
| * use the Encypher free API: https://encypherai.com | ||
| */ | ||
|
|
||
| export default { | ||
| async fetch(request, env) { | ||
| const response = await fetch(request); | ||
|
|
||
| // Only process image responses. | ||
| const contentType = response.headers.get('content-type') || ''; | ||
| if (!contentType.startsWith('image/')) { | ||
| return response; | ||
| } | ||
|
|
||
| const url = new URL(request.url); | ||
| // Canonical URL: scheme + host + path (strip CDN transform params). | ||
| const canonicalUrl = `${url.protocol}//${url.hostname}${url.pathname}`; | ||
| const cacheKey = `manifest:${canonicalUrl}`; | ||
|
|
||
| // Try KV cache first. | ||
| let manifestUrl = null; | ||
| if (env.CDN_PROVENANCE_CACHE) { | ||
| manifestUrl = await env.CDN_PROVENANCE_CACHE.get(cacheKey); | ||
| } | ||
|
|
||
| if (!manifestUrl) { | ||
| // Look up via WordPress REST API. | ||
| const lookupUrl = `${env.WORDPRESS_REST_URL}/c2pa-provenance/v1/images/lookup?url=${encodeURIComponent(canonicalUrl)}`; | ||
|
|
||
| try { | ||
| const lookupResponse = await fetch(lookupUrl, { | ||
| headers: { 'Accept': 'application/json' }, | ||
| }); | ||
|
|
||
| if (lookupResponse.ok) { | ||
| const data = await lookupResponse.json(); | ||
| manifestUrl = data.manifest_url || null; | ||
|
|
||
| // Cache the result. | ||
| if (manifestUrl && env.CDN_PROVENANCE_CACHE) { | ||
| await env.CDN_PROVENANCE_CACHE.put(cacheKey, manifestUrl, { expirationTtl: 3600 }); | ||
| } | ||
| } | ||
| } catch (e) { | ||
| // Lookup failed — serve original response without header. | ||
| return response; | ||
| } | ||
| } | ||
|
|
||
| if (!manifestUrl) { | ||
| return response; | ||
| } | ||
|
|
||
| // Inject the header into a new response. | ||
| const newHeaders = new Headers(response.headers); | ||
| newHeaders.set('C2PA-Manifest-URL', manifestUrl); | ||
|
|
||
| return new Response(response.body, { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: newHeaders, | ||
| }); | ||
| }, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| name = "cdn-provenance-worker" | ||
| main = "cdn-provenance-worker.js" | ||
| compatibility_date = "2024-01-01" | ||
|
|
||
| [vars] | ||
| WORDPRESS_REST_URL = "https://YOUR_WORDPRESS_SITE/wp-json" | ||
|
|
||
| # KV namespace for caching manifest URL lookups. | ||
| # Create with: wrangler kv:namespace create "CDN_PROVENANCE_CACHE" | ||
| # Then replace the id below with the output. | ||
| [[kv_namespaces]] | ||
| binding = "CDN_PROVENANCE_CACHE" | ||
| id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| //! Fastly Compute: C2PA Image Provenance | ||
| //! | ||
| //! Injects a C2PA-Manifest-URL header into image responses by looking up | ||
| //! the manifest via the WordPress REST API. | ||
| //! | ||
| //! Edge Dictionary key: `wordpress_rest_url` | ||
| //! Value: https://your-site.com/wp-json | ||
| //! | ||
| //! For CDN-transform survival (pHash matching), use Encypher free API: | ||
| //! https://encypherai.com | ||
|
|
||
| use fastly::http::{Method, StatusCode}; | ||
| use fastly::{Error, Request, Response}; | ||
|
|
||
| #[fastly::main] | ||
| fn main(req: Request) -> Result<Response, Error> { | ||
| let backend = "origin"; | ||
| let mut beresp = req.send(backend)?; | ||
|
|
||
| // Only process image responses. | ||
| let content_type = beresp | ||
| .get_header_str("content-type") | ||
| .unwrap_or("") | ||
| .to_string(); | ||
|
|
||
| if !content_type.starts_with("image/") { | ||
| return Ok(beresp); | ||
| } | ||
|
|
||
| // Get WordPress REST URL from Edge Dictionary. | ||
| let dict = fastly::Dictionary::open("wordpress_rest_url"); | ||
| let wp_rest_url = match dict.get("wordpress_rest_url") { | ||
| Some(url) => url, | ||
| None => return Ok(beresp), | ||
| }; | ||
|
|
||
| // Canonical URL: scheme + host + path (no query string). | ||
| let req_url = req.get_url(); | ||
| let canonical_url = format!( | ||
| "{}://{}{}", | ||
| req_url.scheme(), | ||
| req_url.host_str().unwrap_or(""), | ||
| req_url.path() | ||
| ); | ||
|
|
||
| let encoded_url = urlencoding::encode(&canonical_url); | ||
| let lookup_url = format!( | ||
| "{}/c2pa-provenance/v1/images/lookup?url={}", | ||
| wp_rest_url.trim_end_matches('/'), | ||
| encoded_url | ||
| ); | ||
|
|
||
| // Look up the manifest URL. | ||
| let lookup_req = Request::get(lookup_url); | ||
| let lookup_resp = lookup_req.send("wordpress_api"); | ||
|
|
||
| if let Ok(mut resp) = lookup_resp { | ||
| if resp.get_status() == StatusCode::OK { | ||
| if let Ok(body) = resp.take_body_str() { | ||
| if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) { | ||
| if let Some(manifest_url) = json["manifest_url"].as_str() { | ||
| beresp.set_header("C2PA-Manifest-URL", manifest_url); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(beresp) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /** | ||
| * AWS Lambda@Edge: C2PA Image Provenance | ||
| * | ||
| * Injects a C2PA-Manifest-URL header into image responses by looking up | ||
| * the manifest via the WordPress REST API. | ||
| * | ||
| * Environment variable: WORDPRESS_REST_URL | ||
| * e.g. https://your-site.com/wp-json | ||
| * | ||
| * For CDN-transform survival (pHash matching), use Encypher free API: | ||
| * https://encypherai.com | ||
| */ | ||
|
|
||
| import https from 'https'; | ||
|
|
||
| const WORDPRESS_REST_URL = process.env.WORDPRESS_REST_URL || ''; | ||
|
|
||
| function httpsGet(url) { | ||
| return new Promise((resolve, reject) => { | ||
| https.get(url, (res) => { | ||
| let data = ''; | ||
| res.on('data', (chunk) => { data += chunk; }); | ||
| res.on('end', () => { | ||
| try { | ||
| resolve({ status: res.statusCode, body: JSON.parse(data) }); | ||
| } catch (e) { | ||
| resolve({ status: res.statusCode, body: null }); | ||
| } | ||
| }); | ||
| }).on('error', reject); | ||
| }); | ||
| } | ||
|
|
||
| export const handler = async (event) => { | ||
| const response = event.Records[0].cf.response; | ||
| const request = event.Records[0].cf.request; | ||
|
|
||
| const contentType = (response.headers['content-type'] || [{ value: '' }])[0].value; | ||
| if (!contentType.startsWith('image/')) { | ||
| return response; | ||
| } | ||
|
|
||
| // Canonical URL: strip query params. | ||
| const uri = request.uri; | ||
| const host = request.headers['host'][0].value; | ||
| const canonicalUrl = `https://${host}${uri}`; | ||
|
|
||
| if (!WORDPRESS_REST_URL) { | ||
| return response; | ||
| } | ||
|
|
||
| try { | ||
| const lookupUrl = `${WORDPRESS_REST_URL}/c2pa-provenance/v1/images/lookup?url=${encodeURIComponent(canonicalUrl)}`; | ||
| const result = await httpsGet(lookupUrl); | ||
|
|
||
| if (result.status === 200 && result.body && result.body.manifest_url) { | ||
| response.headers['c2pa-manifest-url'] = [{ key: 'C2PA-Manifest-URL', value: result.body.manifest_url }]; | ||
| } | ||
| } catch (e) { | ||
| // Lookup failed — return original response. | ||
| } | ||
|
|
||
| return response; | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?