Zum Inhalt springen
ecdsa.com
← Alle Artikel

C2PA-Content-Credentials: Manifeste, Signaturen, Grenzen

Wir haben ein JPEG mit c2pa-python 0.38.1 (c2pa-rs 0.91.3) signiert, es einmal mit nicht vertrauenswürdigem und einmal mit vertrauenswürdigem Signierer validiert, ein Byte geändert und es durch eine gewöhnliche Bild-Pipeline geschickt. Zitiert wird die Spezifikation C2PA 2.4, die aktuelle Version.

Kurz gesagt. Ein C2PA-Manifest ist eine signierte Aussage über eine Mediendatei: ein Claim, der Assertions auflistet (was getan wurde, etwa „mit einer Kamera erstellt“ oder „mit generativer KI bearbeitet“, dazu ein Hash über die Bytes der Datei), signiert mit COSE von einem Schlüssel, dessen X.509-Zertifikat in der Signatur steckt. Validatoren melden ein Manifest als Well-Formed (wohlgeformt), Valid (gültig: Signatur und Hashes stimmen) oder Trusted (vertrauenswürdig: Das Zertifikat des Signierers führt über eine Kette zu einem Vertrauensanker, standardmäßig aus der C2PA-Vertrauensliste). Ein Zeitstempel nach RFC 3161 hält es über den Ablauf des Zertifikats hinaus gültig. Was es nicht leistet: beweisen, dass das Bild die Wahrheit zeigt, oder Tools überstehen, die die Datei neu kodieren. Behandle ein gültiges Credential als „dieser Signierer steht für diese Bytes ein“ und sein Fehlen als überhaupt keine Information.

Was in einem Manifest steht

Die C2PA-Spezifikation (Version 2.4, April 2026) setzt ein Manifest aus wenigen Teilen zusammen:

  • Assertions, jede eine Box in CBOR oder JSON mit einem Label. c2pa.actions.v2 listet auf, was mit dem Asset geschehen ist (seit 2.1 gilt: „Entweder ein c2pa.created oder ein c2pa.opened ist jetzt Pflicht“), mit einem digitalSourceType aus dem IPTC-Vokabular, der eine Kameraaufnahme von generativer KI unterscheidet. c2pa.hash.data ist das Hard Binding: ein Hash über die Bytes des Assets ohne den Bereich, in dem das Manifest selbst gespeichert ist. Ein Vorschaubild, Ingredients (die Assets, aus denen dieses entstanden ist) und Metadaten können folgen.
  • Der Claim: „eine digital signierte und manipulationserkennbare Datenstruktur, die auf eine Reihe von Assertions verweist“, und zwar über URIs mit Hash, sodass jede Änderung an einer Assertion den Claim ungültig macht.
  • Die Claim-Signatur: eine Struktur COSE_Sign1 über den Claim, mit der Zertifikatskette des Signierers im geschützten Header. „Es muss genau eine Instanz eines Identitäts-Credentials vorhanden sein“, und seit Version 2.0 ist dieses Credential ein X.509-Zertifikat.
  • Ein optionaler Zeitstempel eines Zeitstempeldienstes nach RFC 3161 über die Signatur.

All das wird als JUMBF-Boxen in der Datei serialisiert (ein APP11-Segment in JPEG, eine Box in MP4 oder HEIF) oder extern gespeichert und per URL referenziert. Wird ein Asset bearbeitet, fügt das Bearbeitungsprogramm ein neues Manifest hinzu und vermerkt das vorherige als Ingredient; so wird der Manifest-Store zu einer Kette von Manifesten.

Zulässige Signaturalgorithmen sind ES256, ES384, ES512, PS256, PS384, PS512 und Ed25519 (§13.2.1); Schlüssel müssen zum Algorithmus passen, und RSA-Schlüssel müssen mindestens 2048 Bit haben. Für Live-Video empfiehlt die Spezifikation deterministische Algorithmen, damit identische Segmente cachebar bleiben.

Ein JPEG signieren

Im Versuch legen wir mit pyca/cryptography 49.0.0 eine Test-CA an, dazu ein P-256-Signaturzertifikat mit den EKUs, nach denen ein C2PA-Validator sucht: c2pa-kp-claimSigning (1.3.6.1.4.1.62558.2.1) und, für ältere Validatoren, id-kp-emailProtection. Dann signieren wir ein generiertes 640×360-JPEG mit einem Zeitstempel von DigiCert:

lab.py (Auszug): Manifest definieren und signieren (c2pa-python 0.38.1)
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)

Auf das Zertifikatsprofil kommt es an: Das Signaturzertifikat hat digitalSignature in Key Usage, CA:FALSE und die EKU für Claim-Signing; das Root-Zertifikat ist ein CA-Zertifikat mit keyCertSign. Ein Zertifikat ohne passende EKU oder ein selbstsigniertes Endzertifikat besteht die Validierung nicht, auch wenn die Signatur mathematisch korrekt ist. X.509-Zertifikatsfelder erklärt diese Erweiterungen.

Validierung: Valid, Trusted, Invalid

Dasselbe Skript liest die Datei viermal zurück: ohne jede Vertrauenskonfiguration, mit dem Test-Root-Zertifikat als Vertrauensanker, mit einem geänderten Byte der Bilddaten und nachdem das Bild mit Pillow 12.0.0 dekodiert und neu kodiert wurde, wie es eine Upload-Pipeline tun würde:

lab.py (Auszug): Lesen mit Vertrauensanker, Manipulation, Neukodierung
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)
Ausgabe (python:3.14.8-slim-trixie)
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

So liest du die Ausgabe:

  • Valid, aber nicht vertrauenswürdig. Jede kryptografische Prüfung war erfolgreich (claimSignature.validated, dreimal assertion.hashedURI.match für die drei Assertions, assertion.dataHash.match für die Bildbytes), aber signingCredential.untrusted: Jeder kann eine CA anlegen und signieren. Die Spezifikation definiert diesen Zustand genau: Ein Manifest ist Valid, wenn es wohlgeformt und unverändert ist, seine Signatur „innerhalb der Gültigkeit“ validiert wird und das Credential nicht widerrufen ist; Trusted ist es nur, wenn zusätzlich das Signaturzertifikat „den Erfolgscode signingCredential.trusted erhält“ (§14.3).
  • Trusted, sobald der Validator das Root-Zertifikat kennt. Im Produktivbetrieb stammt dieses Root-Zertifikat aus der C2PA-Vertrauensliste, „einer von C2PA verwalteten Liste von X.509-Zertifikaten als Vertrauensanker, die Zertifikate an Hardware- und Software-Signierer ausstellen“. Validatoren müssen diese Liste für die EKU c2pa-kp-claimSigning einbeziehen; Benutzer dürfen eigene Vertrauensanker hinzufügen.
  • Invalid nach einem geänderten Byte: Die Signatur ist weiterhin in Ordnung, das Hard Binding nicht (assertion.dataHash.mismatch). Ein Zuschnitt, ein Filter oder eine erneute Kompression hat dieselbe Wirkung; deshalb müssen Bearbeitungsprogramme ein neues Manifest schreiben, statt das alte zu behalten.
  • Verschwunden nach dem Neukodieren. Das Manifest steckt in den Metadaten; eine Pipeline, die Pixel dekodiert und eine neue Datei schreibt, verwirft es, genau wie EXIF-Daten. Nichts in der Ausgabe verrät, dass es je ein Credential gab.

Das Manifest hat ein 13-KB-Bild um 39 KB vergrößert: Neben dem Claim und unserer 981 Byte großen Zertifikatskette enthält es ein von c2pa-rs erzeugtes Vorschaubild und das Zeitstempel-Token mit den Zertifikaten von DigiCert. timeStamp.untrusted erscheint, weil im Versuch die TSA-Vertrauensliste von C2PA nicht konfiguriert war: Der Zeitstempel ist kryptografisch gültig, nur stand sein Zeitstempeldienst nicht in der Liste des Validators.

Zeitstempel und Zertifikatslaufzeit

Die Spezifikation ist eindeutig: „Manifeste ohne Zeitstempel verlieren ihre Gültigkeit, wenn das Signaturzertifikat abläuft oder widerrufen wird“, und ist ein Zeitstempel vorhanden und vertrauenswürdig, müssen Validatoren zur Prüfung der Zertifikate „die bestätigte Zeit und nicht die aktuelle Zeit verwenden“ (§10.3.2.5.1, §15.8.2). Das ist dieselbe Konstruktion wie bei Authenticode: Das Zertifikat muss nur zum Zeitpunkt des Signierens gültig sein, und der Zeitstempeldienst belegt, wann das war. Geräte, die offline signieren, brauchen eine andere Lösung; Googles Pixel-Kamera etwa verwendet „vertrauenswürdige Zeitstempel auf dem Gerät“, damit Fotos „auch nach Ablauf des Zertifikats vertrauenswürdig sind, selbst wenn sie aufgenommen wurden, während dein Gerät offline war“ (Google Security Blog).

Woher das Vertrauen kommt

Die Identität des Signierers ist das Subject des Zertifikats; der Wert eines Credentials hängt also davon ab, wer dieses Zertifikat ausgestellt hat und was dabei geprüft wurde. C2PA betreibt ein Konformitätsprogramm und die Vertrauenslisten; Validatoren konfigurieren sie pro EKU und können Vertrauensanker mit Daten für notBefore und notAfter hinzufügen. Auch die Hardware spielt eine Rolle: Laut Google hat die Kamera-App des Pixel 10 „Assurance Level 2 erreicht, die höchste Sicherheitsstufe, die das C2PA Conformance Program derzeit definiert“; die Signaturschlüssel für Claims werden „mit Android StrongBox im Sicherheitschip Titan M2 erzeugt und gespeichert“, und Zertifikate werden erst ausgestellt, nachdem Android Key Attestation ein echtes Gerät und eine echte App nachgewiesen hat. Schlüssel in sicherer Hardware behandelt der Artikel hardwaregestützte Schlüssel.

Nur die signierten Assertions sind die Aussage des Signierers. Die Spezifikation nennt ein Beispiel: „Der Signierer ist nicht für die Richtigkeit einer GPS-Position in EXIF-Metadaten verantwortlich, selbst wenn diese Metadaten vom Hard Binding abgedeckt sind“ (§9.2.6); nur Metadaten in einer Assertion c2pa.metadata werden dem Signierer zugerechnet.

Was Content Credentials nicht leisten

  • Content Credentials beweisen nicht, dass ein Bild wahr ist. Ein Kamera-Credential sagt, dass dieses Gerät diese Bytes erzeugt hat; eine gestellte Szene oder ein abfotografierter Bildschirm ist trotzdem eine Kameraaufnahme. Ein Credential für generative KI ist eine Aussage des Tools, das sich entschieden hat, es hinzuzufügen.
  • Credentials lassen sich leicht entfernen. Unser Neukodieren hat das Manifest ohne Fehlermeldung verworfen, und Metadaten werden oft absichtlich entfernt, aus Datenschutzgründen oder wegen der Dateigröße. Die Antwort der Spezifikation ist das Soft Binding: ein Fingerabdruck oder ein unsichtbares Wasserzeichen, über das sich das Manifest in einem Repository wiederfinden lässt. Ein „Durable Content Credential“ ist eines, „für das es ein oder mehrere Soft Bindings gibt, die es in einem Manifest-Repository auffindbar machen“ (§2.4.1). Ob eine bestimmte Plattform Manifeste so speichert und nachschlägt, entscheidet die Plattform.
  • Fehlende Credentials bedeuten nichts. Die meisten Bilder im Web haben keine Credentials; ein fehlendes Manifest ist kein Beleg für eine Manipulation.
  • Credentials können mehr verraten als beabsichtigt. Ketten von Ingredients, Gerätenamen und Standorte reisen mit der Datei. Wer ein Asset erstellt, kann Assertions entfernen (Redaction), wenn das Asset zum Ingredient wird (§6.8), und Tools sollten dir die Wahl lassen, was aufgenommen wird.

Wenn du mit C2PA entwickelst

  1. Erhalte oder signiere neu in deiner Pipeline. Wenn dein Dienst Uploads skaliert oder komprimiert, behalte entweder das Original mit seinem Credential, oder lege ein neues Manifest mit dem Original als Ingredient und einer ehrlichen Aktion c2pa.edited oder c2pa.resized an, signiert mit deinem eigenen Zertifikat.
  2. Zeige Validierungsstatus wahrheitsgemäß an. Unterscheide Trusted von bloß Valid, zeige den Namen des Signierers aus dem Zertifikat an und zeige nichts Besonderes, wenn es kein Manifest gibt.
  3. Validiere mit den C2PA-Vertrauenslisten und aktueller Software. Statuscodes wie signingCredential.untrusted oder timeStamp.untrusted deuten meist auf die Konfiguration hin, nicht auf eine Fälschung.
  4. Sorge für das richtige Zertifikat: EKU c2pa-kp-claimSigning, Schlüssel in einem HSM oder in sicherer Hardware und Zeitstempel auf jeder Signatur.
  5. Triff keine Entscheidungen anhand fehlender Credentials. Ein fehlendes oder entferntes Credential ist der Normalfall.

Quellen