Skip to content

feat: Content Provenance experiment (C2PA 2.3 §A.7 text authentication)#294

Open
erik-sv wants to merge 6 commits intoWordPress:developfrom
erik-sv:feature/content-provenance-experiment
Open

feat: Content Provenance experiment (C2PA 2.3 §A.7 text authentication)#294
erik-sv wants to merge 6 commits intoWordPress:developfrom
erik-sv:feature/content-provenance-experiment

Conversation

@erik-sv
Copy link
Copy Markdown

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

Changes since last review

  • Rebased onto latest develop (0.6.0)
  • Ported from Abstract_Experiment to Abstract_Feature API — class signature and registration updated to match the 0.6.0 feature model
  • Added Well_Known_Handler test coverage — was 0% before this rebase; now covered
  • All hook/filter names updated to wpai_ prefix convention — e.g. wpai_title_generation_result (was wp_ai_experiment_*)
  • 102 tests, 223 assertions passing locally

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

  • Auto-signs posts on publish/update with c2pa.created / c2pa.edited actions and provenance-chain ingredient references
  • Three signing tiers — Local (zero setup, self-signed), Connected (delegated to an HTTP signing service), BYOK (publisher's own certificate)
  • c2pa/sign and c2pa/verify Abilities — any plugin can call wp_do_ability('c2pa/sign', ['text' => …])
  • Gutenberg sidebar panel — 5-state shield badge (verified / local-signed / modified / tampered / unsigned) with one-click sign/verify
  • /.well-known/c2pa discovery endpoint — C2PA §6.4 compliant JSON document
  • Verification badge — optional frontend badge on public posts

AI fragment provenance (latest commit)

  • Output filter hook on each Ability result: wpai_title_generation_result, wpai_excerpt_generation_result, wpai_summarization_result, wpai_review_notes_result, wpai_alt_text_result
  • New sign_ai_fragments setting — when enabled, Content Provenance intercepts these filters and embeds a C2PA manifest into each AI-generated fragment before it reaches the editor
  • Fails open — signing errors return the original result unchanged, never blocking output

Post 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]
Loading

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| E
Loading

Signing tiers

Tier Trust model Setup required On WordPress trust list
Local Self-signed RSA-2048, stored in site options None No — yellow badge
Connected Delegated to HTTP signing service Service URL + API key Yes — green badge
BYOK Publisher's own certificate PEM file path Yes — green badge

WordPress Abilities API

// Sign any text
$result = wp_do_ability( 'c2pa/sign', [
    'text'   => 'The content to sign',
    'action' => 'c2pa.created',  // or c2pa.edited
] );
// $result['signed_text'] — Unicode-embedded provenance
// $result['manifest']    — full C2PA JSON manifest
// $result['signer_tier'] — local | connected | byok

// Verify any text
$result = wp_do_ability( 'c2pa/verify', [
    'text' => $post->post_content,
] );
// $result['verified'] — bool
// $result['status']   — verified | unsigned | tampered | modified
// $result['manifest'] — parsed manifest array if present

Fragment hook usage for third-party plugins

add_filter( 'wpai_title_generation_result', function( $result, $context ) {
    // $result['titles'] — array of generated title strings, each may be signed
    // $context['post_id'] — the post being edited
    return $result;
}, 10, 2 );

Files changed

File Type Description
includes/Experiments/Content_Provenance/Content_Provenance.php New Main experiment class + fragment hooks
includes/Experiments/Content_Provenance/C2PA_Manifest_Builder.php New Manifest construction + verification
includes/Experiments/Content_Provenance/Unicode_Embedder.php New VS1–VS256 embed/extract/strip
includes/Experiments/Content_Provenance/Well_Known_Handler.php New /.well-known/c2pa endpoint
includes/Experiments/Content_Provenance/Verification_Badge.php New Frontend badge
includes/Experiments/Content_Provenance/Signing/Signing_Interface.php New Signer contract
includes/Experiments/Content_Provenance/Signing/Local_Signer.php New Self-signed tier
includes/Experiments/Content_Provenance/Signing/Connected_Signer.php New HTTP service tier
includes/Experiments/Content_Provenance/Signing/BYOK_Signer.php New Cert-based tier
includes/Abilities/Content_Provenance/C2PA_Sign.php New c2pa/sign Ability
includes/Abilities/Content_Provenance/C2PA_Verify.php New c2pa/verify Ability
includes/Abilities/Title_Generation/Title_Generation.php Modified Add wpai_title_generation_result filter
includes/Abilities/Excerpt_Generation/Excerpt_Generation.php Modified Add wpai_excerpt_generation_result filter
includes/Abilities/Summarization/Summarization.php Modified Add wpai_summarization_result filter
includes/Abilities/Review_Notes/Review_Notes.php Modified Add wpai_review_notes_result filter
includes/Abilities/Image/Alt_Text_Generation.php Modified Add wpai_alt_text_result filter
src/experiments/content-provenance/index.js New Gutenberg sidebar panel
includes/Experiment_Loader.php Modified Register experiment
webpack.config.js Modified Add JS entry point
tests/Integration/…/Content_ProvenanceTest.php New 64 integration tests
tests/Integration/…/C2PA_Sign_Test.php New Ability tests
docs/experiments/content-provenance.md New User guide
docs/experiments/content-provenance-developer.md New Developer reference

Test plan

  • Run composer test -- --filter Content_Provenance — all 64 tests pass
  • Activate experiment → publish a post → verify _c2pa_manifest meta is set
  • Edit a signed post → confirm c2pa.edited action + ingredient reference to previous manifest
  • Call wp_do_ability('c2pa/sign', ['text' => 'hello']) in wp shell — returns signed text
  • Call wp_do_ability('c2pa/verify', ['text' => $signed]) — returns verified: true
  • Visit /.well-known/c2pa — returns valid JSON discovery document
  • Tamper with post content in DB → verify badge shows tampered status
  • Enable "Sign AI fragments" → generate a title → inspect title text for invisible Unicode variation selectors
  • Hook wpai_title_generation_result in a test plugin → confirm callback receives correct args

Related

Open WordPress Playground Preview

@erik-sv erik-sv force-pushed the feature/content-provenance-experiment branch from 5aadfe6 to 6f950d1 Compare March 10, 2026 17:49
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 10, 2026

Codecov Report

❌ Patch coverage is 88.51351% with 153 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.22%. Comparing base (32c5e2d) to head (40706ab).

Files with missing lines Patch % Lines
...eriments/Content_Provenance/Content_Provenance.php 93.11% 33 Missing ⚠️
...ments/Content_Provenance/C2PA_Manifest_Builder.php 60.27% 29 Missing ⚠️
...riments/Content_Provenance/Signing/BYOK_Signer.php 71.83% 20 Missing ⚠️
...iments/Content_Provenance/Signing/Local_Signer.php 77.02% 17 Missing ⚠️
...ncludes/Abilities/Content_Provenance/C2PA_Sign.php 85.26% 14 Missing ⚠️
...eriments/Content_Provenance/C2PA/Claim_Builder.php 91.00% 9 Missing ⚠️
...nts/Content_Provenance/C2PA/COSE_Sign1_Builder.php 87.87% 8 Missing ⚠️
...ts/Content_Provenance/Signing/Connected_Signer.php 87.93% 7 Missing ⚠️
...eriments/Content_Provenance/Well_Known_Handler.php 81.81% 6 Missing ⚠️
...periments/Content_Provenance/C2PA/CBOR_Encoder.php 97.36% 2 Missing ⚠️
... and 5 more
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     
Flag Coverage Δ
unit 67.22% <88.51%> (+9.37%) ⬆️

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.

@erik-sv erik-sv force-pushed the feature/content-provenance-experiment branch from 235f898 to 83e9dea Compare March 10, 2026 19:34
@erik-sv
Copy link
Copy Markdown
Author

erik-sv commented Mar 10, 2026

Plugin Check failure appears to be pre-existing on trunk, happy to investigate if needed.

@jeffpaul
Copy link
Copy Markdown
Member

@erik-sv mind updating to branch from develop?

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)?

@erik-sv erik-sv changed the base branch from trunk to develop March 11, 2026 17:50
@erik-sv
Copy link
Copy Markdown
Author

erik-sv commented Mar 11, 2026

@erik-sv mind updating to branch from develop?

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:

  1. AI Content Provenance: hooks into the existing experiment output filters (Title Generation, Excerpt Generation, etc.) so that when WordPress AI generates content, provenance metadata records that fact. This implements a "digital nutrition label" for content, as touched on in 10up/classifai#652
  2. Image Provenance with CDN Continuity: directly addresses the problem where CDNs strip metadata during image transforms. We built a provenance sidecar indexed by perceptual hash that makes the original C2PA manifest retrievable even after aggressive CDN transforms, with edge worker implementations for Cloudflare, Fastly, and CloudFront. For text, we've already solved this CDN survival problem.

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.

@jeffpaul
Copy link
Copy Markdown
Member

Let me know if you have any feedback on this PR or would like me to submit the other two PRs.

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!

One caveat is that to display the CR logo overlay, you need to go through the C2PA compliance program.

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?

@Jameswlepage
Copy link
Copy Markdown
Contributor

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.

@erik-sv
Copy link
Copy Markdown
Author

erik-sv commented Mar 17, 2026

@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 Program

The 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 & Trust

This 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.

  • Pros: Zero setup for users, single conformance + trust list application, broadest coverage
  • Cons: The signature asserts "published via WordPress" not "published by example.com", WordPress.org takes on trust responsibility for everything signed through the service

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."

  • Pros: User-level attribution, each org controls their own identity
  • Cons: Higher friction, requires each publisher to independently obtain a trusted cert

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.

@jeffpaul
Copy link
Copy Markdown
Member

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.

@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/content-provenance-experiment branch from b96f9e2 to 9758181 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

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: @[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.

Unlinked contributors: [email protected], erik-sv.

Co-authored-by: jeffpaul <[email protected]>
Co-authored-by: Jameswlepage <[email protected]>

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

Both PRs are now rebased onto latest develop (0.6.0) and moved out of draft:

What changed in this rebase:

  • Ported from Abstract_ExperimentAbstract_Feature API (static get_id(), load_metadata(), get_stability())
  • Registration via Experiments::EXPERIMENT_CLASSES instead of the old Experiment_Loader
  • All option names now use the wpai_feature_* convention
  • Added Well_Known_Handler test coverage (was at 0%)
  • Fixed namespace mismatches in test files for PSR-4 compliance
  • Fixed duplicate PHPDoc block and stale phpcs:ignore comment
  • Updated developer docs for new hook names (wpai_register_features, wpai_feature_content-provenance_enabled)

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.

Erik Svilich added 4 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)
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
@erik-sv
Copy link
Copy Markdown
Author

erik-sv commented Mar 26, 2026

Update: Native C2PA signing + ingredient chains + sidebar fix

Two commits pushed since the last update:

feat: implement native C2PA signing with JUMBF/COSE/ES256

Replaced the custom JSON manifest format with spec-compliant C2PA 2.3 binary signing:

  • CBOR_Encoder — minimal deterministic CBOR (CTAP2 canonical) for COSE structures
  • JUMBF_Writer — ISO 19566-5 box serialization for C2PA manifest stores
  • COSE_Sign1_Builder — RFC 9052 COSE_Sign1 envelope with ES256 signatures
  • Claim_Builder — C2PA 2.3 claim + assertion structure generator
  • Switched from RSA-2048 to EC P-256 with self-signed X.509 certificates
  • Binary JUMBF manifests are base64-encoded for safe WordPress meta storage
  • Legacy JSON verification preserved for backwards compatibility

feat: add C2PA ingredient chains and fix sidebar JS naming

  • Ingredient chains — edited content now 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
  • Sidebar JS fixwindow.ContentProvenanceDatawindow.aiContentProvenanceData to match what Asset_Loader::localize_script actually produces (the sidebar panel was fully built but silently broken)

Test results

  • 159 tests, 353 assertions, 0 failures
  • PHPCS clean
  • PHPStan level 8 clean

Signing tiers status

All three tiers now produce spec-compliant JUMBF/COSE_Sign1 output:

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.
@jeffpaul jeffpaul requested a review from dkotter March 27, 2026 16:35
@jeffpaul jeffpaul moved this from In progress to Needs review in WordPress AI Planning & Roadmap Mar 27, 2026
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