Firmé las tres imágenes de un artículo de este blog con Content Credentials, atadas a mi cuenta de LinkedIn. Subí los archivos, verifiqué, y todo daba bien. Siete minutos después las tres se habían quedado sin manifiesto, y no las había tocado ningún atacante: las tocó mi propio plugin de optimización.

Cr — Verificar las credenciales de contenido de esta imagen
Lo que sigue es el registro de ese incidente, con los pesos exactos, el diagnóstico que hice mal durante tres rondas seguidas y el orden de operaciones que terminó funcionando.
Qué es firmar en el origen
C2PA nació para distinguir el contenido generado por IA del que no lo es, cuando ya no queda pista visual que sirva, y lo resuelve invirtiendo la lógica con la que crecimos: en vez de perseguir lo falso, certifica lo verdadero. La imagen viaja con un manifiesto firmado que dice quién la creó, con qué herramienta y qué se le hizo después.
Ya escribí sobre el estándar en su momento, así que acá voy directo al cómo: el estándar funciona, lo que no funciona es el camino entre tu carpeta local y el navegador de tu lector.
Conviene saber antes que los modos de falla son tres, y cuál te toca depende de qué capa rompió la imagen. Puede llegar sin manifiesto, y el verificador no muestra nada; con el manifiesto intacto pero el contenido cambiado, y muestra tus credenciales con un aviso de cambios no registrados; o con credenciales inválidas, que se leen como manipulación.
Lo primero: las copias que genera WordPress
Al subir una imagen, WordPress genera copias derivadas en thumbnail, medium y large. Conservan el manifiesto C2PA pero con el hash roto, porque el archivo ya no es el mismo que se firmó, y después el navegador sirve vía srcset la copia que corresponda al viewport en lugar de tu original. El lector nunca ve el archivo que firmaste. No es un problema solo mío: hay un issue abierto en el repositorio de WordPress pidiendo que detecte los manifiestos C2PA al subir, y el módulo de firma que publicó IPTC tampoco resuelve lo que pasa después.
El disparador es la clase wp-image-{id} en la etiqueta <img>, así que la solución es insertar el bloque Gutenberg sin ella:
<!-- wp:image {"sizeSlug":"full","linkDestination":"none"} -->
<figure class="wp-block-image size-full"><img src="URL_ORIGINAL" alt="..."/>
<figcaption class="wp-element-caption">...</figcaption></figure>
<!-- /wp:image -->Para verificarlo alcanza con contar los atributos srcset en el HTML renderizado de la página publicada, que tiene que dar cero, contando el atributo dentro de una etiqueta y no la palabra suelta.
Lo segundo: el optimizador que pisa el archivo
Este es el que me costó las tres rondas. El plugin de LiteSpeed manda las imágenes a la nube de QUIC.cloud por cron, recibe las versiones comprimidas y pisa los archivos originales en disco, sin confirmación y sin log visible en el flujo normal de trabajo. Lo traicionero está en el peso: el archivo seguía marcando 135 KB, casi lo mismo que antes, pero la validación C2PA había pasado de Valid a sin manifiesto.
Tres ajustes tienen que estar apagados antes de subir cualquier imagen firmada:
| Ajuste | Valor obligatorio | Por qué |
|---|---|---|
| Optimize Original Images | OFF | Pisa el archivo firmado en disco |
| Next-Gen Image Format | OFF | Reescribe img.src a un .webp reencodeado |
| Auto Request Cron | OFF | Dispara la optimización sin intervención |
Conviene dejar apagado también Remove Original Backups, que es lo único que permite revertir si el daño ya ocurrió. Y Conservar los datos EXIF/XMP viene apagado por defecto, con el manifiesto C2PA viajando justamente en XMP: el día que enciendas la optimización con ese ajuste en off destruís la firma aunque todo lo demás esté bien.
Mi primer método de diagnóstico fue buscar archivos .webp en el servidor y, si no había ninguno, concluir que el plugin no tocaba nada. No prueba nada: un servidor limpio de .webp a los dos minutos de subir es un servidor que va a destruir las imágenes a los siete. Subí, verifiqué, di el visto bueno y volví más tarde a encontrar el manifiesto destruido, tres veces seguidas, con la causa a un clic en el panel del plugin. Estaba midiendo un síntoma con latencia en lugar de leer una configuración estática, y de ahí me quedó la regla: verificá la configuración, no el efecto.
Lo tercero: la recompresión en el CDN
Supongamos que apagaste el plugin. Todavía te queda el CDN, que con optimización de imágenes activada recomprime en el POP: el origen queda intacto, tu archivo local valida, el del servidor valida, y lo que llega al navegador está roto. Es el más confuso de los tres, porque todo lo que podés inspeccionar cómodamente está bien. Lo delató un header, x-qc-cache: hit sirviendo un archivo ya destruido.
El daño del CDN y el del cron tampoco son el mismo daño, y confundirlos lleva a apagar cosas que no hacía falta apagar. En el destructivo, que es el del cron, el archivo en disco se pisa por completo y el verificador no tiene nada que mostrar. En el intermedio, que es el del CDN, la recompresión conserva el segmento del manifiesto pero cambia el contenido, así que el verificador sí muestra tus credenciales, quién firmó y el opt-out de entrenamiento de IA, con un aviso de cambios no registrados encima. Lo comprobé sobre un artículo publicado un mes antes: seguía comunicando lo esencial, con ese asterisco.
La diferencia importa al decidir: el asterisco puede ser un precio aceptable a cambio de imágenes más livianas, y en el nivel destructivo no hay nada que negociar. Averiguar en cuál estás toma treinta segundos, abriendo el verificador sobre una imagen tuya ya publicada.
Lo primero que intenté fue no apagar nada, porque apagar la optimización entera para proteger tres imágenes castiga a las otras trescientas del blog. Cache-Control: no-transform le dice a cualquier intermediario que no toque el contenido, y va en el .htaccess de la carpeta de subidas y no en el de la raíz, porque los plugins reescriben el de la raíz cada vez que guardás sus ajustes y no tocan los de las subcarpetas.
# .htaccess de la carpeta de subidas
<FilesMatch "^c2pa-.*\.(jpe?g|png)$">
Header set Cache-Control "public, max-age=31536000, no-transform"
Header set Access-Control-Allow-Origin "*"
</FilesMatch>El FilesMatch limita la protección al prefijo que uso para las imágenes firmadas, y el resto de la biblioteca se sigue optimizando igual.
Veinte minutos después de subir con la directiva puesta, los cuatro archivos seguían pesando exactamente lo firmado. Di el resultado por bueno y estaba equivocado: al día siguiente habían bajado entre 15,2% y 20,6%, con cinco .webp generados por imagen, uno por cada tamaño que recorta WordPress. Contra las marcas de tiempo del servidor, la reescritura cae entre 17 y 20 minutos después de cada carga, así que mi ventana cerró justo en el borde. Es el mismo falso positivo que este artículo denuncia, cometido dentro del artículo.
no-transform nunca iba a servir para esto. Ese header le habla a los intermediarios que están entre tu servidor y el lector, y lo que reescribía mis archivos era un plugin corriendo en el propio servidor, que no lo lee ni tiene por qué. Contra la recompresión del CDN sigue valiendo la pena ponerlo, y por eso el bloque se queda.
Lo que sí funciona es apagar la optimización automática al subir y dejar encendida la copia de originales que hace el mismo plugin. Esa copia fue lo que me salvó: los cuatro archivos firmados estaban ahí intactos, con el mismo sha256 que los de mi disco, así que la recuperación se resolvió restaurando. Todo se mide comparando el hash del archivo servido contra el firmado local, al menos media hora después de subir.
Lo cuarto, que encontré escribiendo esto
Antes de publicar fui a comprobar que la configuración del servidor siguiera como la había dejado, y encontré un bloque que no recordaba haber puesto, con la firma de SpeedyCache, un plugin de caché:
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME} \.(jpe?g|png|gif)$
RewriteRule ^(.+)\.(jpe?g|png|gif)$ $1.webp [T=image/webp,L]Traducido: si el navegador acepta WebP y existe el .webp en disco, sirve ese en lugar del original y la URL no cambia. El lector pide portada.jpg y recibe un WebP reencodeado, sin manifiesto. Es el más difícil de detectar porque el archivo en el origen está intacto, la URL que devuelve la API es la correcta y el peso en disco también: podés auditar las tres cosas, darlas por buenas, y estar sirviendo otro archivo. Es integridad rota sin una sola alarma.
El hallazgo de fondo me gustó menos que el bloque en sí. Fui a buscar en qué panel se configuraba ese plugin y resulta que no estaba instalado: lo había sacado meses atrás y sus reglas seguían operando, porque desinstalar un plugin de WordPress no siempre limpia lo que escribió en el .htaccess. Ya que estaba, listé todo lo que tenía instalado, que es lo que debí hacer el primer día, y apareció Imagify, un optimizador de imágenes que no había tocado en ningún momento del diagnóstico. Ese venía pisando los archivos mientras yo apagaba toggles en otro panel.
La pregunta útil no es qué plugin creés que tenés, es cuántas cosas pueden reescribir una imagen sin cambiarle la URL.
El orden que funciona para firmar sin romper
Todo lo anterior se resume en una línea:
generar → optimizar → FIRMAR → subir → no tocar nunca más
Cr — Verificar las credenciales de contenido de esta imagen
La optimización va antes de la firma, y cualquier cosa que recomprima después la mata, sin excepción y sin aviso. Estos son los números de mi caso, sobre las tres imágenes de un mismo artículo:
| Estado | Portada | Diagrama 1 | Diagrama 2 | Total |
|---|---|---|---|---|
| Firmadas sin optimizar | 411 KB | 453 KB | 427 KB | 1.291 KB |
| Optimizadas y luego firmadas | 135 KB | 161 KB | 158 KB | 454 KB |
| Optimizadas sin firmar | 55 KB | 68 KB | 65 KB | 188 KB |
El orden correcto ahorra un 65% de peso: firmar el archivo crudo cuesta 837 KB de página por artículo sin ninguna ganancia visual. Adobe además infla alrededor de 2,4 veces al firmar, porque recomprime al aplicar el manifiesto. El piso real de una imagen firmada queda entre 135 y 160 KB, así que el objetivo genérico de menos de 200 KB se cumple con bastante menos margen del que uno supone.

Cr — Verificar las credenciales de contenido de esta imagen
Cómo verificar que la credencial quedó bien
Antes que nada hace falta el header CORS sobre los archivos subidos, sin el cual nada de esto se puede comprobar desde el navegador, porque el verificador de Adobe lee el archivo desde el navegador del lector. Va sin envolverlo en <IfModule mod_headers.c>, porque LiteSpeed no siempre evalúa ese wrapper como verdadero y el bloque queda silenciosamente inerte:
<FilesMatch "\.(jpe?g|png|webp|avif|gif|pdf)$">
Header set Access-Control-Allow-Origin "*"
</FilesMatch>Lo puse en julio y cuando fui a verificarlo para publicar no estaba: cada plugin reescribe su propio tramo del .htaccess al guardar sus ajustes, y mi bloque había quedado dentro de uno de esos tramos. Desapareció sin error y sin log. Por eso conviene ponerlo en un .htaccess de subcarpeta y comprobarlo con una petición real antes de cada publicación. De los cuatro problemas es el único que deja la firma intacta y rompe solo la capacidad de comprobarla, que en la práctica se lee parecido: la credencial está impecable y el botón de verificar falla igual.
Sobre qué verificador enlazar, la respuesta tiene una ironía incorporada. El visor público del estándar, verify.contentauthenticity.org, solo lee el manifiesto embebido, o sea que falla precisamente en el escenario que acabo de documentar, y además no renderiza los datos de Connected Accounts, así que el dato que me interesa mostrar, mi nombre junto a mis cuentas, está en el archivo pero no se pinta. La credencial está completa; lo que falla es el visor.
Por las dos razones, el botón que va debajo de cada imagen publicada apunta a:
https://contentauthenticity.adobe.com/inspect?source=<URL>
Cr — Verificar las credenciales de contenido de esta imagen
Adobe inspect recupera la credencial desde la nube usando la credencial duradera cuando el manifiesto embebido no está. Lleva etiqueta Beta y pide un clic extra, y lo acepto. Un aviso que ahorra tiempo: el patrón contentcredentials.org/verify?source= circula en tutoriales de 2025 y hoy devuelve 404.
Para validar sin visor web queda el SDK c2pa sobre la URL de producción con cache-buster, comprobando que validation_state sea Valid y que los bytes sean idénticos al archivo firmado local, media hora después de la subida.
Con qué identidad quedan firmadas
Las imágenes de este artículo están firmadas desde Adobe Content Authenticity con mi cuenta de LinkedIn conectada. Quien abre el verificador ve mi nombre y mi perfil como origen del archivo, la herramienta con la que se creó, y el opt-out de entrenamiento de modelos de IA. Ese es el dato que justifica todo el trabajo de los apartados anteriores, y hay otras dos vías para firmar que no lo entregan.
| Vía | Qué identidad firma | Qué pierde |
|---|---|---|
| Adobe Content Authenticity (web, gratis) | Mi cuenta de LinkedIn conectada | Nada |
| Content Authenticity API (Firefly Services) | Un certificado corporativo | La identidad personal: firma la organización, no vos |
c2patool / SDK open source | Un certificado propio o autofirmado | El verificador muestra emisor no reconocido, sin cuentas conectadas |
La segunda cambia mi identidad por la de una empresa y la tercera produce una credencial que dice emisor no reconocido, que comunica menos que no firmar.
Una precisión que un lector con C2PA en las manos va a detectar enseguida: lo que uso no es una identidad verificada por documento, es una cuenta conectada por OAuth, el modelo CAWG de agregador de declaraciones de identidad, que prueba control de la cuenta y no identidad documentada. Para marca personal alcanza y es honesto, porque mi audiencia verifica mi perfil público y no mi pasaporte.
Y ahí aparece el hallazgo que menos esperaba. El nivel superior de identidad, el sello respaldado por documento de gobierno, hoy está cerrado para mí por geografía: el proveedor de verificación exige pasaporte con chip NFC para Bolivia, y Bolivia no figura en la lista de países donde acepta cédula nacional. El escalón más alto del estándar de procedencia más relevante de la industria queda fuera del alcance de un profesional boliviano que hizo todo bien. La salida existe, un pasaporte biométrico que sí trae NFC, pero es un trámite y no un ajuste de configuración.
Casi toda la cobertura de C2PA se escribió desde Estados Unidos y Europa, donde el pasaporte con chip es lo normal y ese paso es invisible. La adopción de un estándar no se mide solo por su diseño técnico; se mide también por dónde queda parado el último que intenta cumplirlo.
Para cerrar
Nadie instala un CDN, ni un optimizador, ni un conversor a WebP para romper credenciales; los instala para ir más rápido. Ahí está el caso de manual de Secure by Design: la procedencia verificable y la compresión persiguen objetivos legítimos y opuestos, y cuando el orden no se diseña gana el rendimiento por default, sin que nadie haya decidido sacrificar nada.
Si estás por firmar el contenido de tu sitio, la pregunta que te ahorra tres rondas de subidas es cuál de tus procesos automáticos va a tocar ese archivo después de que dejes de mirarlo, y conviene contestarla antes de subir nada.
¿Ya intentaste implementar Content Credentials en tu sitio? Me interesa saber qué se te rompió a vos.
#ViveLibreViveSeguro

Deja un comentario