En bref. Un manifeste C2PA est une déclaration signée concernant un fichier multimédia : un claim qui liste des assertions (ce qui a été fait, par exemple « créé par un appareil photo » ou « modifié avec une IA générative », ainsi qu’un condensat des octets du fichier), signé avec COSE par une clé dont le certificat X.509 figure dans la signature. Les validateurs attribuent à un manifeste l’état Well-Formed (bien formé), Valid (valide : la signature et les condensats sont corrects) ou Trusted (approuvé : la chaîne du certificat du signataire aboutit à une ancre de confiance, par défaut la liste de confiance C2PA). Un horodatage RFC 3161 le maintient valide après l’expiration du certificat. Ce qu’il ne fait pas : prouver que l’image montre la vérité, ni survivre aux outils qui réencodent le fichier. Considérez des Content Credentials valides comme « ce signataire se porte garant de ces octets », et leur absence comme une absence totale d’information.
Ce que contient un manifeste
La spécification C2PA (version 2.4, avril 2026) construit un manifeste à partir de quelques éléments :
- Les assertions, chacune dans une boîte CBOR ou JSON étiquetée.
c2pa.actions.v2liste ce qui est arrivé au contenu (depuis la version 2.1, « unc2pa.createdou unc2pa.openedest désormais obligatoire »), avec undigitalSourceTypeissu du vocabulaire IPTC qui distingue une prise de vue par un appareil photo d’une génération par IA.c2pa.hash.dataest la liaison forte (hard binding) : un condensat des octets du contenu, qui exclut la plage où est stocké le manifeste lui-même. Une miniature, des ingrédients (les contenus à partir desquels celui-ci a été créé) et des métadonnées peuvent suivre. - Le claim : « une structure de données signée numériquement, dont toute altération est détectable, qui référence un ensemble d’assertions », au moyen d’URI hachés, si bien que modifier n’importe quelle assertion invalide le claim.
- La signature du claim : un
COSE_Sign1sur le claim, avec la chaîne de certificats du signataire dans l’en-tête protégé. « Exactement une instance d’identifiant d’identité doit figurer », et depuis la version 2.0, cet identifiant est un certificat X.509. - Un horodatage facultatif d’une autorité RFC 3161, portant sur la signature.
L’ensemble est sérialisé sous forme de boîtes JUMBF à l’intérieur du fichier (un segment APP11 en JPEG, une boîte en MP4 ou HEIF), ou stocké à distance et référencé par une URL. Quand un contenu est modifié, le logiciel d’édition ajoute un nouveau manifeste et enregistre le précédent comme ingrédient : le magasin de manifestes devient ainsi une chaîne de manifestes.
Les algorithmes de signature autorisés sont ES256, ES384, ES512, PS256, PS384, PS512 et Ed25519 (§13.2.1) ; les clés doivent correspondre à l’algorithme, et les clés RSA doivent faire au moins 2048 bits. Pour la vidéo en direct, la spécification recommande des algorithmes déterministes, afin que des segments identiques puissent toujours être mis en cache.
Signer un JPEG
Le banc d’essai crée une AC de test avec pyca/cryptography 49.0.0 et un certificat de signataire P-256 portant les EKU que recherche un validateur C2PA : c2pa-kp-claimSigning (1.3.6.1.4.1.62558.2.1) et, pour les validateurs plus anciens, id-kp-emailProtection. Il signe ensuite un JPEG 640×360 généré pour l’occasion, avec un horodatage 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)
Le profil du certificat compte : le certificat du signataire porte digitalSignature dans l’extension Key Usage, CA:FALSE et l’EKU de signature de claim ; la racine est une AC avec keyCertSign. Un certificat sans EKU adaptée, ou un certificat feuille auto-signé, échoue à la validation même si la signature est mathématiquement correcte. L’article sur les champs des certificats X.509 explique ces extensions.
Validation : Valid, Trusted, Invalid
Le même script relit le fichier quatre fois : sans aucune configuration de confiance, avec la racine de test comme ancre de confiance, avec un octet des données d’image modifié, et après décodage puis réencodage de l’image avec Pillow 12.0.0, comme le ferait un pipeline de mise en ligne :
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
Comment lire ce résultat :
- Valid, mais non approuvé. Toutes les vérifications cryptographiques ont réussi (
claimSignature.validated, troisassertion.hashedURI.matchpour les trois assertions,assertion.dataHash.matchpour les octets de l’image), mais le résultat comportesigningCredential.untrusted: n’importe qui peut créer une AC et signer. La spécification définit précisément cet état : un manifeste est à l’état Valid lorsqu’il est bien formé et non modifié, que sa signature est validée « dans la période de validité » et que le certificat n’est pas révoqué ; il n’atteint l’état Trusted que si, en outre, le certificat de signature « reçoit le code de succèssigningCredential.trusted» (§14.3). - Trusted dès que le validateur connaît la racine. En production, cette racine provient de la liste de confiance C2PA (C2PA Trust List), « une liste, gérée par la C2PA, d’ancres de confiance de certificats X.509 qui délivrent des certificats aux signataires matériels et logiciels », que les validateurs doivent inclure pour l’EKU
c2pa-kp-claimSigning; les utilisateurs peuvent ajouter leurs propres ancres. - Invalid après un seul octet modifié : la signature est toujours correcte, mais pas la liaison forte (
assertion.dataHash.mismatch). Un recadrage, un filtre ou une recompression ont le même effet ; c’est pourquoi les logiciels d’édition doivent écrire un nouveau manifeste plutôt que conserver l’ancien. - Disparu après le réencodage. Le manifeste réside dans les métadonnées ; un pipeline qui décode les pixels et écrit un nouveau fichier le supprime, comme les données EXIF. Rien dans le fichier produit n’indique que des Content Credentials s’y soient jamais trouvés.
Le manifeste a ajouté 39 Ko à une image de 13 Ko : outre le claim et notre chaîne de certificats de 981 octets, il contient une miniature générée par c2pa-rs et le jeton d’horodatage avec les certificats de DigiCert. timeStamp.untrusted apparaît parce que le banc d’essai n’a pas configuré la liste de confiance des TSA C2PA (C2PA TSA Trust List) : l’horodatage est valide sur le plan cryptographique, son autorité ne figurait simplement pas dans la liste du validateur.
Horodatages et durée de vie des certificats
La spécification est explicite : « les manifestes sans horodatage cessent d’être valides lorsque le certificat de signature expire ou est révoqué », et lorsqu’un horodatage est présent et approuvé, les validateurs « doivent utiliser l’heure attestée, et non l’heure actuelle » pour contrôler les certificats (§10.3.2.5.1, §15.8.2). C’est la même construction que dans Authenticode : le certificat doit seulement être valide au moment de la signature, et l’autorité d’horodatage prouve à quel moment celle-ci a eu lieu. Les appareils qui signent hors ligne ont besoin d’une autre solution ; l’appareil photo des Pixel de Google, par exemple, utilise des « horodatages de confiance sur l’appareil » pour que les photos « restent fiables après l’expiration du certificat, même si elles ont été prises alors que votre appareil était hors ligne » (Google Security Blog).
D’où vient la confiance
L’identité du signataire est le sujet du certificat : la valeur des Content Credentials dépend donc de l’entité qui a émis ce certificat et de ce qu’elle a vérifié. La C2PA gère un programme de conformité et les listes de confiance ; les validateurs les configurent par EKU et peuvent ajouter des ancres avec des dates notBefore et notAfter. Le matériel compte aussi : Google indique que l’application Appareil photo du Pixel 10 « a obtenu le niveau d’assurance 2 (Assurance Level 2), la plus haute note de sécurité actuellement définie par le programme de conformité C2PA », avec des clés de signature de claim « générées et stockées à l’aide d’Android StrongBox dans la puce de sécurité Titan M2 », et des certificats émis uniquement après qu’Android Key Attestation a prouvé l’authenticité de l’appareil et de l’application. Les clés conservées dans du matériel sécurisé sont traitées dans l’article sur les clés protégées par le matériel.
Seules les assertions signées constituent la déclaration du signataire. La spécification donne un exemple : « le signataire n’est pas responsable de l’exactitude d’une position GPS figurant dans les métadonnées EXIF, même si ces métadonnées sont couvertes par la liaison forte » (§9.2.6) ; seules les métadonnées placées dans une assertion c2pa.metadata sont attribuées au signataire.
Ce que les Content Credentials ne font pas
- Ils ne prouvent pas qu’une image est vraie. Les Content Credentials d’un appareil photo indiquent que cet appareil a produit ces octets ; une scène arrangée ou la photo d’un écran reste une prise de vue d’appareil photo. Les Content Credentials d’une IA générative sont une déclaration de l’outil qui a choisi de les ajouter.
- Ils sont faciles à supprimer. Notre réencodage a supprimé le manifeste sans la moindre erreur, et les gens retirent souvent les métadonnées volontairement, pour protéger leur vie privée ou réduire la taille des fichiers. La réponse de la spécification est la liaison souple (soft binding) : une empreinte ou un filigrane invisible qui permet de retrouver le manifeste dans un dépôt. Un « Durable Content Credential » est un Content Credential « pour lequel il existe une ou plusieurs liaisons souples qui permettent de le retrouver dans un dépôt de manifestes » (§2.4.1). Il appartient à chaque plateforme de stocker et de rechercher ou non les manifestes de cette façon.
- L’absence ne signifie rien. La plupart des images du Web n’ont pas de Content Credentials ; un manifeste manquant n’est pas une preuve de manipulation.
- Ils peuvent divulguer plus que prévu. Les chaînes d’ingrédients, les noms d’appareils et les positions géographiques voyagent avec le fichier. Les créateurs peuvent caviarder des assertions lorsqu’un contenu devient un ingrédient (§6.8), et les outils devraient laisser chacun choisir ce qui est inclus.
Si vous développez avec C2PA
- Conservez ou signez à nouveau dans votre pipeline. Si votre service redimensionne ou compresse les fichiers mis en ligne, conservez l’original avec ses Content Credentials, ou créez un nouveau manifeste avec l’original comme ingrédient et une action
c2pa.editedouc2pa.resizedfidèle à la réalité, signé avec votre propre certificat. - Affichez fidèlement les états de validation. Distinguez l’état Trusted du simple état Valid, affichez le nom du signataire tiré du certificat, et n’affichez rien de particulier en l’absence de manifeste.
- Validez avec les listes de confiance C2PA et des logiciels à jour. Des codes d’état comme
signingCredential.untrustedoutimeStamp.untrustedsignalent généralement un problème de configuration, pas une falsification. - Configurez correctement le certificat : EKU
c2pa-kp-claimSigning, clé dans un HSM ou du matériel sécurisé, et horodatage sur chaque signature. - Ne prenez aucune décision sur la base d’une absence. Des Content Credentials manquants ou supprimés sont le cas normal.
Sources
- C2PA Technical Specification 2.4 : §2.3.15 et §2.3.16 (listes de confiance), §2.4.1 (Content Credentials durables), §6.8 (caviardage), §9.2.6 (métadonnées non C2PA), §10.3.2.5.1 et §15.8.2 (horodatages), §13.2.1 (algorithmes), §14.3 (états de validation), §14.4 (listes de confiance)
- Annonce de C2PA 2.3
- Google Security Blog : les Content Credentials C2PA sur Pixel et Android (10 septembre 2025)
- RFC 9052 : signature et chiffrement d’objets CBOR (COSE), RFC 3161
- Outils : c2pa-python 0.38.1 sur c2pa-rs 0.91.3, pyca/cryptography 49.0.0, Pillow 12.0.0