feat: Content Provenance experiment (C2PA 2.3 §A.7 text authentication)#294
feat: Content Provenance experiment (C2PA 2.3 §A.7 text authentication)#294erik-sv wants to merge 6 commits intoWordPress:developfrom
Conversation
5aadfe6 to
6f950d1
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #294 +/- ##
=============================================
+ Coverage 57.85% 67.22% +9.37%
- Complexity 615 886 +271
=============================================
Files 46 61 +15
Lines 3165 4497 +1332
=============================================
+ Hits 1831 3023 +1192
- Misses 1334 1474 +140
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:
|
235f898 to
83e9dea
Compare
|
Plugin Check failure appears to be pre-existing on trunk, happy to investigate if needed. |
|
@erik-sv mind updating to branch from Separately, @dkotter and I have been exploring this sort of work for some time (see 10up/classifai#652) and am curious how your work here might overlap/relate to media content (and whether you'd consider also helping on that front in this plugin)? |
Hi @jeffpaul, just moved to the develop branch (thanks for the tips, James LePage just directed me here so I'm new). I'm the co-chair for C2PA's text task force and wrote their spec here so I am very familiar with content provenance technology. Also the CEO of Encypher . Happy to help integration efforts. I actually have two other PRs for this repo that I have in mind but I didn't want to overwhelm you all with code. Happy to put them up for your review:
Let me know if you have any feedback on this PR or would like me to submit the other two PRs. In regards to the 10up repo, we have developed ways to do exactly what you require for images and text content. One caveat is that to display the CR logo overlay, you need to go through the C2PA compliance program. |
I'll defer to @dkotter for code review on this PR, once you pull it out of Draft state. Otherwise, additional PRs would be amazing, thanks!
Is that required per site leveraging this WordPress AI plugin or could "we" (either the WordPress AI team, or the WordPress.org project itself) go through that on behalf of every WordPress site leveraging this plugin? |
Interested in this one as well. If the project were to get closer to the protocol, it feels like an audited plugin could work for the universe of sites that the CMS enables. |
|
@jeffpaul @Jameswlepage Great questions, let me break this down into the two separate pieces: conformance and signing identity/trust. To directly answer your question @jeffpaul yes, the WordPress AI team or WordPress.org project can absolutely go through conformance and serve as the signing identity on behalf of every WordPress site. That's the lowest-friction path and follows the same model as Adobe, Microsoft, and the camera manufacturers. The BYOK option remains available for users who need their own organizational identity on the manifest, and you can do both: Conformance ProgramThe C2PA conformance program operates at the implementation level, not per-site. So the WordPress AI plugin (or the WordPress.org project itself) would go through conformance once on behalf of every site using the plugin. Happy to help with that process once the implementation is substantially complete. Signing Identity & TrustThis is the more interesting question. For signatures to show as trusted in C2PA-aware applications (browsers, social platforms, search engines), the signing certificate needs to chain to the C2PA Trust List. There are a few options here, and they're not mutually exclusive: Option 1: WordPress as the signing identity (recommended starting point)WordPress operates a centralized signing service and holds a trusted certificate, similar to how Adobe signs content from Photoshop and camera manufacturers (Nikon, Sony, Leica) sign photos under their brand. Every site using the plugin would sign through this service via the Connected tier already in this PR.
Option 2: Publisher BYOK (organizational identity)Individual users obtain their own certificate from a CA on the trust list and configure it via the BYOK tier in this PR. The manifest would say "published by XYZ Press" or "published by example.com."
Option 3: Hybrid (Option 1 + 2 recommended)WordPress serves as the default signing identity out of the box, while users who want organizational attribution can override with BYOK. This is probably the right long-term answer, it gives every WordPress site provenance by default while letting orgs that care about brand-level attestation bring their own identity. Let me know which direction feels right and I can adjust the implementation accordingly. |
|
Option 2 is almost certainly a non-starter for the majority (or at least statistically significant) of WordPress installs. Thus going with Option 3 to allow flexibility for sites, especially enterprise installs or publishers, to be able to use BYOK seems most optimal. |
|
@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
b96f9e2 to
9758181
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: @[email protected], @erik-sv. 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. |
|
Both PRs are now rebased onto latest What changed in this rebase:
PR #302 (Image Provenance + CDN) builds on top of this PR with the same API port applied. Ready for @dkotter's code 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)
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.
Replace custom JSON manifest format with spec-compliant C2PA 2.3 signing: - Add CBOR_Encoder: minimal deterministic CBOR (CTAP2 canonical) for COSE - Add JUMBF_Writer: ISO 19566-5 box serialization for C2PA manifest stores - Add COSE_Sign1_Builder: RFC 9052 COSE_Sign1 with ES256 signing - Add Claim_Builder: C2PA 2.3 claim + assertion builder - Switch Local_Signer from RSA-2048 to EC P-256 with X.509 certificate - Update BYOK_Signer for separate key/cert paths and JUMBF pipeline - Update Connected_Signer for base64 JUMBF response format - Pre-populate Encypher API URL for connected signing tier - Base64-encode binary manifests for safe WordPress meta storage - Support legacy JSON format verification for backwards compatibility - Update all signer, ability, and integration tests for new pipeline 181 tests pass, PHPCS clean, PHPStan level 8 clean.
Wire previous_manifest through the build pipeline so edited content includes a c2pa.ingredient.v2 assertion referencing the prior manifest by SHA-256 hash, forming a verifiable provenance chain per C2PA 2.3 §8.3. - Claim_Builder: add ingredient assertion with parentOf relationship - C2PA_Manifest_Builder: forward previous_manifest via metadata - Local_Signer/BYOK_Signer: extract previous_manifest and pass to builder - Connected_Signer: include base64-encoded previous_manifest in API request - Fix sidebar JS: window.ContentProvenanceData → window.aiContentProvenanceData to match Asset_Loader::localize_script which prepends 'ai' to object names - Add 5 new Claim_Builder tests for ingredient chain coverage - Update Content_ProvenanceTest to verify ingredient hash in edited manifest
Update: Native C2PA signing + ingredient chains + sidebar fixTwo commits pushed since the last update:
|
| Tier | Crypto | Output | Status |
|---|---|---|---|
| Local | EC P-256 + self-signed X.509 | JUMBF manifest store | ✅ |
| Connected | Encypher API (api.encypher.com) |
JUMBF via base64 response | ✅ |
| BYOK | Publisher-supplied key + CA cert | JUMBF manifest store | ✅ |
This PR is ready for review. @jeffpaul @Jameswlepage — would appreciate your eyes on this when you have a chance. Happy to walk through any part of the C2PA pipeline.
…ions extract_signature_from_cose() used strrpos to scan for the \x58\x40 CBOR byte marker, which could match certificate DER or payload bytes on some PHP versions (failing on PHP 8.0 CI). The signature is always the final 64 bytes of the COSE_Sign1 structure — extract from the end instead. Also test der_to_raw_ecdsa() directly rather than round- tripping through build + extraction.
Changes since last review
develop(0.6.0)Abstract_ExperimenttoAbstract_FeatureAPI — class signature and registration updated to match the 0.6.0 feature modelWell_Known_Handlertest coverage — was 0% before this rebase; now coveredwpai_prefix convention — e.g.wpai_title_generation_result(waswp_ai_experiment_*)Summary
Adds a new Content Provenance experiment that embeds cryptographic C2PA 2.3 §A.7 manifests into post content as invisible Unicode variation selectors. Publishers can prove authorship, detect tampering, and participate in the emerging content authenticity ecosystem (same standard used by Google, BBC, Adobe, OpenAI, and Microsoft).
The latest commit extends this with AI fragment provenance — output filter hooks on all five AI Ability classes so that individually generated titles, excerpts, summaries, review notes, and alt text can each carry their own embedded manifest.
What this adds
Content Provenance experiment
c2pa.created/c2pa.editedactions and provenance-chain ingredient referencesc2pa/signandc2pa/verifyAbilities — any plugin can callwp_do_ability('c2pa/sign', ['text' => …])/.well-known/c2padiscovery endpoint — C2PA §6.4 compliant JSON documentAI fragment provenance (latest commit)
wpai_title_generation_result,wpai_excerpt_generation_result,wpai_summarization_result,wpai_review_notes_result,wpai_alt_text_resultsign_ai_fragmentssetting — when enabled, Content Provenance intercepts these filters and embeds a C2PA manifest into each AI-generated fragment before it reaches the editorPost signing flow
flowchart TD A[Post Published or Updated] --> B{Content Provenance enabled?} B -->|No| Z[Skip] B -->|Yes| C[Strip HTML to plain text] C --> D[Build C2PA Manifest] D --> D1[c2pa.actions.v1] D --> D2[c2pa.hash.data.v1 SHA-256] D --> D3[c2pa.soft_binding.v1] D --> D4[c2pa.ingredient.v2 edit chain] D1 & D2 & D3 & D4 --> E{Signing tier} E -->|Local| F[RSA-2048 self-signed via OpenSSL] E -->|Connected| G[POST to signing service HTTP API] E -->|BYOK| H[Publisher cert PEM file] F & G & H --> I[Unicode Embedder: VS1-VS256 invisible bytes] I --> J[wp_update_post with embedded content] J --> K[Store post meta: _c2pa_manifest, _c2pa_status, _c2pa_signed_at] K --> L[Gutenberg sidebar shield badge]AI fragment provenance flow
flowchart LR A[Editor triggers AI Ability] --> B[Ability executes and returns result] B --> C[apply_filters on wpai_*_result] C --> D{sign_ai_fragments enabled?} D -->|No| E[Original result returned to editor] D -->|Yes| F[C2PA_Manifest_Builder::build] F --> G[Unicode_Embedder::embed] G -->|Success| H[Signed fragment returned to editor] G -->|Error| ESigning tiers
WordPress Abilities API
Fragment hook usage for third-party plugins
Files changed
includes/Experiments/Content_Provenance/Content_Provenance.phpincludes/Experiments/Content_Provenance/C2PA_Manifest_Builder.phpincludes/Experiments/Content_Provenance/Unicode_Embedder.phpincludes/Experiments/Content_Provenance/Well_Known_Handler.php/.well-known/c2paendpointincludes/Experiments/Content_Provenance/Verification_Badge.phpincludes/Experiments/Content_Provenance/Signing/Signing_Interface.phpincludes/Experiments/Content_Provenance/Signing/Local_Signer.phpincludes/Experiments/Content_Provenance/Signing/Connected_Signer.phpincludes/Experiments/Content_Provenance/Signing/BYOK_Signer.phpincludes/Abilities/Content_Provenance/C2PA_Sign.phpc2pa/signAbilityincludes/Abilities/Content_Provenance/C2PA_Verify.phpc2pa/verifyAbilityincludes/Abilities/Title_Generation/Title_Generation.phpwpai_title_generation_resultfilterincludes/Abilities/Excerpt_Generation/Excerpt_Generation.phpwpai_excerpt_generation_resultfilterincludes/Abilities/Summarization/Summarization.phpwpai_summarization_resultfilterincludes/Abilities/Review_Notes/Review_Notes.phpwpai_review_notes_resultfilterincludes/Abilities/Image/Alt_Text_Generation.phpwpai_alt_text_resultfiltersrc/experiments/content-provenance/index.jsincludes/Experiment_Loader.phpwebpack.config.jstests/Integration/…/Content_ProvenanceTest.phptests/Integration/…/C2PA_Sign_Test.phpdocs/experiments/content-provenance.mddocs/experiments/content-provenance-developer.mdTest plan
composer test -- --filter Content_Provenance— all 64 tests pass_c2pa_manifestmeta is setc2pa.editedaction + ingredient reference to previous manifestwp_do_ability('c2pa/sign', ['text' => 'hello'])inwp shell— returns signed textwp_do_ability('c2pa/verify', ['text' => $signed])— returnsverified: true/.well-known/c2pa— returns valid JSON discovery documentwpai_title_generation_resultin a test plugin → confirm callback receives correct argsRelated