Respuesta breve. Un manifiesto C2PA es una declaración firmada sobre un archivo multimedia: un claim que enumera aserciones (qué se hizo, como «creado por una cámara» o «editado con IA generativa», más un hash de los bytes del archivo), firmado con COSE por una clave cuyo certificado X.509 va dentro de la firma. Los validadores clasifican un manifiesto como bien formado (Well-Formed), válido (Valid: la firma y los hashes cuadran) o de confianza (Trusted: el certificado del firmante se encadena hasta un ancla de confianza, de forma predeterminada la C2PA Trust List). Un sello de tiempo RFC 3161 lo mantiene válido después de que caduque el certificado. Lo que no hace: demostrar que la imagen muestra la verdad, ni sobrevivir a las herramientas que vuelven a codificar el archivo. Trata una credencial válida como «este firmante responde de estos bytes», y su ausencia, como ninguna información en absoluto.
Qué contiene un manifiesto
La especificación de C2PA (versión 2.4, abril de 2026) construye un manifiesto a partir de unas pocas partes:
- Aserciones, cada una en una caja CBOR o JSON con su etiqueta.
c2pa.actions.v2enumera lo que le ocurrió al recurso (desde la 2.1, «ahora es obligatorio unc2pa.createdo unc2pa.opened»), con undigitalSourceTypedel vocabulario de IPTC que distingue una captura de cámara de la IA generativa.c2pa.hash.dataes la vinculación fuerte (hard binding): un hash sobre los bytes del recurso que excluye el rango donde se almacena el propio manifiesto. Pueden seguir una miniatura, los ingredientes (los recursos a partir de los cuales se creó este) y metadatos. - El claim (la declaración): «una estructura de datos firmada digitalmente, en la que cualquier manipulación es detectable, que hace referencia a un conjunto de aserciones», mediante URI con hash, de modo que modificar cualquier aserción rompe el claim.
- La firma del claim: un
COSE_Sign1sobre el claim, con la cadena de certificados del firmante en el encabezado protegido. «Debe aparecer exactamente una instancia de una credencial de identidad», y desde la versión 2.0 esa credencial es un certificado X.509. - Un sello de tiempo opcional de una autoridad RFC 3161, sobre la firma.
Todo ello se serializa como cajas JUMBF dentro del archivo (un segmento APP11 en JPEG, una caja en MP4 o HEIF) o se almacena de forma remota y se referencia por URL. Cuando se edita un recurso, el editor añade un manifiesto nuevo y registra el anterior como ingrediente, de modo que el almacén se convierte en una cadena de manifiestos.
Los algoritmos de firma permitidos son ES256, ES384, ES512, PS256, PS384, PS512 y Ed25519 (§13.2.1); las claves deben corresponder al algoritmo, y las claves RSA deben tener al menos 2048 bits. Para video en directo, la especificación recomienda algoritmos deterministas, para que los segmentos idénticos sigan pudiendo almacenarse en caché.
Firma de un JPEG
El laboratorio crea una CA de prueba con pyca/cryptography 49.0.0 y un certificado de firmante P-256 con los EKU que busca un validador de C2PA, c2pa-kp-claimSigning (1.3.6.1.4.1.62558.2.1) y, para validadores más antiguos, id-kp-emailProtection. Después firma un JPEG generado de 640×360 con un sello de tiempo de DigiCert:
manifest = {
"claim_generator_info": [{"name": "ecdsa.com C2PA lab", "version": "1.0"}],
"title": "sunrise.jpg",
"assertions": [
{"label": "c2pa.actions.v2", "data": {"actions": [
{"action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"}
]}}
],
}
signer = c2pa.Signer.from_info(c2pa.C2paSignerInfo(alg=b"es256", sign_cert=chain_pem, private_key=key_pem, ta_url=b"http://timestamp.digicert.com"))
builder = c2pa.Builder.from_json(manifest)
signed = io.BytesIO()
builder.sign(signer, "image/jpeg", io.BytesIO(original), signed)
El perfil del certificado importa: el certificado del firmante lleva digitalSignature en el uso de clave (Key Usage), CA:FALSE y el EKU de firma de claims; la raíz es una CA con keyCertSign. Un certificado sin un EKU adecuado, o un certificado hoja autofirmado, no supera la validación aunque la firma sea matemáticamente correcta. Campos de un certificado X.509 explica esas extensiones.
Validación: válido, de confianza, no válido
El mismo script vuelve a leer el archivo cuatro veces: sin ninguna configuración de confianza, con la raíz de prueba como ancla de confianza, con un byte de los datos de imagen cambiado y después de decodificar la imagen y volver a codificarla con Pillow 12.0.0, como haría un pipeline de subida de archivos:
trusted = c2pa.Context.from_dict({"trust": {"trust_anchors": pem(root).decode()}, "verify": {"verify_trust": True}})
read("signed, test root as trust anchor", signed, trusted)
# Change one byte of the compressed image data (after the JPEG Start of Scan marker).
sos = signed.rfind(b"\xff\xda")
tampered = bytearray(signed)
tampered[sos + 200] ^= 0x01
read("one byte of image data changed", bytes(tampered), trusted)
# What many upload pipelines do: decode and re-encode, dropping metadata.
Image.open(io.BytesIO(signed)).save(out := io.BytesIO(), "JPEG", quality=85)
read("re-encoded by an image pipeline", out.getvalue(), trusted)
c2pa-python 0.38.1, 0.91.3
JPEG: 13376 bytes unsigned, 52438 bytes with the manifest (+39062)
--- signed, no trust anchors configured: validation_state = Valid
status codes: timeStamp.validated, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, assertion.dataHash.match, signingCredential.ocsp.skipped, timeStamp.untrusted, signingCredential.untrusted
signer: Example Newsroom / Example Newsroom Photo Desk, alg Es256, time 2026-10-22T06:43:11+00:00
assertions: ['c2pa.actions.v2']
assertion store: ['c2pa.actions.v2', 'c2pa.hash.data', 'c2pa.thumbnail.claim']
certificate chain in the signature: 981 bytes DER
--- signed, test root as trust anchor: validation_state = Trusted
status codes: timeStamp.validated, signingCredential.trusted, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, assertion.dataHash.match, signingCredential.ocsp.skipped, timeStamp.untrusted
--- one byte of image data changed: validation_state = Invalid
status codes: timeStamp.validated, signingCredential.trusted, claimSignature.insideValidity, claimSignature.validated, assertion.hashedURI.match, assertion.hashedURI.match, assertion.hashedURI.match, signingCredential.ocsp.skipped, timeStamp.untrusted, assertion.dataHash.mismatch
--- re-encoded by an image pipeline: no C2PA manifest found
Cómo interpretarlo:
- Válido, pero sin confianza. Todas las comprobaciones criptográficas se superaron (
claimSignature.validated, tresassertion.hashedURI.matchpara las tres aserciones,assertion.dataHash.matchpara los bytes de la imagen), pero aparecesigningCredential.untrusted: cualquiera puede crear una CA y firmar. La especificación define este estado con precisión: un manifiesto es válido cuando está bien formado, no se ha modificado, su firma se valida «dentro de la validez» y la credencial no está revocada; solo es de confianza cuando, además, la credencial de firma «recibe el código de éxitosigningCredential.trusted» (§14.3). - De confianza en cuanto el validador conoce la raíz. En producción, esa raíz viene de la C2PA Trust List, «una lista gestionada por C2PA de anclas de confianza de certificados X.509 que emiten certificados para firmantes de hardware y software», que los validadores deben incluir para el EKU
c2pa-kp-claimSigning; los usuarios pueden añadir sus propias anclas. - No válido tras cambiar un byte: la firma sigue estando bien, pero la vinculación fuerte no (
assertion.dataHash.mismatch). Un recorte, un filtro o una recompresión tienen el mismo efecto, y por eso los editores deben escribir un manifiesto nuevo en lugar de conservar el antiguo. - Desaparecido tras volver a codificar. El manifiesto vive en los metadatos; un pipeline que decodifica los píxeles y escribe un archivo nuevo lo descarta, igual que los datos EXIF. Nada en la salida indica que alguna vez hubo una credencial.
El manifiesto añadió 39 KB a una imagen de 13 KB: además del claim y de nuestra cadena de certificados de 981 bytes, lleva una miniatura que generó c2pa-rs y el token de sello de tiempo con los certificados de DigiCert. timeStamp.untrusted aparece porque el laboratorio no configuró la C2PA TSA Trust List (la lista de confianza de autoridades de sellado de tiempo): el sello de tiempo es criptográficamente válido; simplemente, su autoridad no estaba en la lista del validador.
Sellos de tiempo y vigencia del certificado
La especificación es directa: «los manifiestos sin sellos de tiempo dejan de ser válidos cuando la credencial de firma caduca o se revoca», y cuando hay un sello de tiempo de confianza, los validadores «deben usar la hora atestiguada, y no la hora actual» para comprobar los certificados (§10.3.2.5.1, §15.8.2). Es la misma construcción que en Authenticode: el certificado solo tiene que ser válido en el momento de la firma, y la autoridad de sellado de tiempo demuestra cuándo fue. Los dispositivos que firman sin conexión necesitan otra solución; la cámara del Pixel de Google, por ejemplo, usa «sellos de tiempo de confianza en el dispositivo» para que se pueda confiar en las fotos «después de que caduque el certificado, incluso si se tomaron cuando el dispositivo estaba sin conexión» (Google Security Blog).
De dónde viene la confianza
La identidad del firmante es el sujeto del certificado, así que el valor de una credencial depende de quién emitió ese certificado y de qué comprobó. C2PA gestiona un programa de conformidad y las listas de confianza; los validadores las configuran por EKU y pueden añadir anclas con fechas notBefore y notAfter. El hardware también cuenta: Google informa de que la aplicación Cámara del Pixel 10 «alcanzó el nivel de garantía 2 (Assurance Level 2), la calificación de seguridad más alta que define actualmente el programa de conformidad de C2PA», con claves de firma de claims «generadas y almacenadas con Android StrongBox en el chip de seguridad Titan M2» y certificados que solo se emiten después de que la atestación de claves de Android (Android Key Attestation) demuestre que el dispositivo y la aplicación son genuinos. Las claves en hardware seguro se tratan en claves respaldadas por hardware.
Solo las aserciones firmadas son declaración del firmante. La especificación pone un ejemplo: «el firmante no es responsable de la exactitud de una ubicación GPS que aparezca en los metadatos EXIF, aunque esos metadatos estén cubiertos por la vinculación fuerte» (§9.2.6); solo los metadatos incluidos en una aserción c2pa.metadata se atribuyen al firmante.
Lo que no hacen las credenciales de contenido
- No demuestran que una imagen sea verdadera. Una credencial de cámara dice que este dispositivo produjo estos bytes; una escena preparada o una foto de una pantalla sigue siendo una captura de cámara. Una credencial de IA generativa es una declaración de la herramienta que decidió añadirla.
- Se eliminan con facilidad. Nuestra recodificación descartó el manifiesto sin ningún error, y la gente suele eliminar los metadatos a propósito, por privacidad o por tamaño. La respuesta de la especificación es la vinculación débil (soft binding): una huella digital o una marca de agua invisible que permite volver a encontrar el manifiesto en un repositorio. Una «credencial de contenido duradera» (Durable Content Credential) es aquella «para la que existen una o varias vinculaciones débiles que permiten encontrarla en un repositorio de manifiestos» (§2.4.1). Que una plataforma concreta almacene y busque manifiestos de esa forma depende de la plataforma.
- La ausencia no significa nada. La mayoría de las imágenes de la web no tienen credenciales; que falte un manifiesto no es prueba de manipulación.
- Pueden revelar más de lo previsto. Las cadenas de ingredientes, los nombres de dispositivo y las ubicaciones viajan con el archivo. Los creadores pueden suprimir aserciones cuando un recurso pasa a ser un ingrediente (§6.8), y las herramientas deberían permitir elegir qué se incluye.
Si desarrollas con C2PA
- Conserva o vuelve a firmar en tu pipeline. Si tu servicio redimensiona o comprime lo que se sube, conserva el original con su credencial, o crea un manifiesto nuevo con el original como ingrediente y una acción
c2pa.editedoc2pa.resizedhonesta, firmado con tu propio certificado. - Muestra los estados de validación con veracidad. Distingue «de confianza» de «solo válido», muestra el nombre del firmante que figura en el certificado y no muestres nada especial cuando no haya manifiesto.
- Valida con las listas de confianza de C2PA y con software actualizado. Códigos de estado como
signingCredential.untrustedotimeStamp.untrustedsuelen indicar un problema de configuración, no una falsificación. - Configura bien el certificado: EKU
c2pa-kp-claimSigning, la clave en un HSM o en hardware seguro, y sellos de tiempo en todas las firmas. - No tomes decisiones basadas en la ausencia. Una credencial que falta o que se ha eliminado es el caso normal.
Fuentes
- Especificación técnica de C2PA 2.4: §2.3.15 y §2.3.16 (listas de confianza), §2.4.1 (credenciales duraderas), §6.8 (supresión), §9.2.6 (metadatos ajenos a C2PA), §10.3.2.5.1 y §15.8.2 (sellos de tiempo), §13.2.1 (algoritmos), §14.3 (estados de validación), §14.4 (listas de confianza)
- Anuncio de C2PA 2.3
- Google Security Blog: credenciales de contenido C2PA en Pixel y Android (10 de septiembre de 2025)
- RFC 9052: firma y cifrado de objetos CBOR (COSE), RFC 3161
- Herramientas: c2pa-python 0.38.1 sobre c2pa-rs 0.91.3, pyca/cryptography 49.0.0, Pillow 12.0.0