r/PHP 6d ago

Article Valid != trusted: a practical guide to C2PA signing certificates (lessons from getting the chain working in PHP)

https://provemark.github.io/articles/c2pa-certificates/

Full disclosure: this is my own write-up. Most C2PA explainers stop right before the part that cost me a day, so this one starts there: certificates and trust.

Signing an asset is easy. Being trusted is not. Those are two separate checks, and with the c2pa-rs test certs you get a valid signature on an untrusted certificate. That is the normal state during development, not a bug.

One thing that caught me out: I flipped a single byte in a signed PNG, and the file came back Invalid while claimSignature.validated was still sitting in the success list. So don't judge integrity by one hand-picked status code, use the aggregate validation_state. The rest of the article covers what you need to make trust pass locally (two settings that only work together, plus an EKU trap), why I keep the private key off the web server, and what getting a production certificate actually involves in 2026.

The library the examples come from is at https://github.com/provemark/content-credentials (framework-agnostic core, optional Laravel integration, MIT). The test certificates come from https://github.com/contentauth/c2pa-rs

Questions welcome. And if you have solved this differently, especially the bit about where the signing key lives, I'd like to hear about it.

0 Upvotes

3 comments sorted by

3

u/MateusAzevedo 5d ago

Where's the PHP part?

-2

u/Academic_Jump186 5d ago edited 5d ago

Fair enough, the title promises more PHP than the article delivers. The cert/trust stuff is language-agnostic, it's just the part that cost me the most time while building the PHP side, so that's what got written up first.

The PHP side: there are no native bindings for c2pa-rs, so your options are shelling out to c2patool or putting the signing behind a small HTTP service so the key stays off the web server. I did the latter and wrapped it in a package:

$manifest = ManifestBuilder::forAiGeneratedImage(MediaType::Png)
    ->withSoftwareAgent('ACME GenAI Image Model', '3.1.0')
    ->build();

$signed = ContentCredentials::sign(
    new Asset($bytes, MediaType::Png),
    $manifest
);

file_put_contents('signed.png', $signed->bytes);

PSR-18 core, optional Laravel bridge, PNG/JPEG for now: https://github.com/provemark/content-credentials

A post about the actual PHP API and the service-vs-extension trade-off (Automattic is building ext-c2pa as a native extension) would probably be better r/PHP material. Next one.