Article

What WordPress image plugins do to Content Credentials

I put signed images into WordPress, with and without the most-used image plugins, and checked what a visitor gets. Mostly, it's an image without Content Credentials. Some plugins also replace the signed original on the server, and in two cases a correct image came out looking tampered with.

Content Credentials (C2PA) are a signed record inside the file: who made it, with what, and whether AI was involved. It's one of the ways the marking that Article 50 of the AI Act asks for can travel with an AI-generated image. The signature covers the bytes of the file, so anything that rewrites those bytes either removes the record or breaks it.

WordPress rewrites images all the time. It makes smaller sizes, and image plugins compress and convert on top of that. I wanted to know what's left at the end.

How I measured

A fresh WordPress 7.1.2 on PHP 8.3, with Imagick. Three signed images: a small JPEG, a large JPEG from Lightroom (3280×2451, so WordPress also makes a -scaled copy), and a PNG generated and signed by OpenAI. Each was uploaded the way the browser does it, the background jobs were allowed to finish, and then I checked two things:

Each plugin ran on its own, with its defaults. The one exception is WP-Optimize, which doesn't compress until you turn that on, so I turned that one setting on. The files were checked with c2pa-verifier, which gives the same verdicts as c2patool.

Most of it ran on an arm64 Mac. EWWW Image Optimizer only ships its compression tools for x86, so I ran EWWW again on GitHub's x86 runners, with plain WordPress next to it. Plain WordPress gave the same results on both.

What a visitor gets

SetupSigned original on the serverVisitor gets valid credentials
WordPress onlykeptonly when the page uses the original file itself (the small JPEG, and the PNG, whose original is in the srcset)
Modern Image Formatskeptno: AVIF is served
Converter for Mediakeptno: WebP is served, also at the original's URL
WebP Expresskeptas with WordPress only
EWWW Image Optimizerlarge JPEG replaced by a 2560 px copy, no backup; PNG recompressed with the C2PA chunk left inonly the small JPEG; the PNG comes out broken, see below
SmushJPEGs replaced, signed copy kept as .baknot for the JPEGs
WP-Optimizesmall JPEG and PNG replaced, signed copy kept as a backup that's deleted after 50 days by defaultno
reSmush.itsmall JPEG and PNG replaced, signed copy kept as -unsmushedno
Jetpack image CDNnot touched, it serves its own copyno for JPEG; one PNG came back broken, see below

None of the generated sizes kept valid credentials, with or without a plugin. That isn't a bug. A smaller image is a different file, and a manifest signed for the original doesn't cover it. Each size would need its own manifest that points back to the original, which is what I proposed in WordPress/ai#1058.

Plugins that replace the original

What I didn't expect was the original itself. Smush, WP-Optimize and reSmush.it compress the uploaded file and strip its metadata, and the signed version is only kept as a backup under another name. WP-Optimize deletes those backups after 50 days unless you change that. EWWW shrinks large uploads to 2560 pixels in place and doesn't keep the signed file at all.

On a site like that the signed file can be gone. Then there's nothing to show a visitor, and nothing to sign the smaller sizes from later.

A correct image that looks tampered with

The OpenAI PNG went wrong in a different way, twice. EWWW on the server and Jetpack's CDN on the way to the visitor both returned the same pixels, all 1.5 million of them, packed differently, and left the C2PA chunk in. The signature covers the bytes, not the picture, so the check now says the image was changed after signing. For Jetpack's copy that's what c2patool 0.27 and 0.28 say as well as c2pa-verifier. EWWW's copy has the same size as what optipng -o2 makes of the file, and c2patool says the same about that.

I think that's worse than losing the credentials. Anyone who checks is told the image was edited, while nothing in it changed.

The cause looks the same in both: a lossless PNG optimiser that keeps chunks it doesn't know. With optipng -strip all the chunk is removed instead. In EWWW's case it was one line: it only passed -strip all when optipng's version matched 0.7, and the optipng it ships calls itself 7.9.1. On a server that has optipng 0.7 installed itself (Debian ships 0.7.8), EWWW does pass -strip all, and the PNG simply loses its credentials. I reported it (nosilver4u/ewww-image-optimizer#389), it was fixed in the development version the same afternoon, and I confirmed the fix on x86.

Jetpack's CDN did this with one of five signed PNGs I tried. The other four lost the chunk, and the two of those I compared pixel by pixel had been recompressed with some loss. That one is reported too (Automattic/jetpack#53217).

Limits, up front

If you run a site

The scripts and the results, file by file, are on GitHub, so you can run them against your own plugins. I did the measuring with Claude Code; every result here came from running those scripts. If you maintain one of these plugins and I got something wrong or missed a setting, I'd like to hear it.

provemark/c2pa-verifier is an open-source (MIT) C2PA verifier in pure PHP, with c2patool's verdicts. It's what checked every file in this article.