Zum Inhalt springen

Transparenzprotokoll

GET https://api.kennmarq.com/v1/log/root

GET https://api.kennmarq.com/v1/log/proof/{entry}

GET https://api.kennmarq.com/v1/log/consistency

GET https://api.kennmarq.com/v1/log/entries

GET https://api.kennmarq.com/v1/log/roots

GET https://api.kennmarq.com/v1/log/key

Jede Signatur und jeder Zeitstempel, den wir ausgeben, wird zuerst an ein öffentliches, nur erweiterbares Protokoll angehängt. Eine Wurzel über das gesamte Protokoll wird jede Stunde signiert, solange es wächst, und mindestens einmal am Tag. Mit einer Wurzel, ihrer Signatur und unserem öffentlichen Schlüssel kann jeder prüfen, dass ein Eintrag im Protokoll steht und dass das Protokoll nie umgeschrieben wurde. Kein Schlüssel nötig. Die Seite des Transparenzprotokolls zeigt die neueste Wurzel.

Was ein Eintrag enthält

Jeder Eintrag ist ein JSON-Objekt mit v (1), kind und logged_at (UTC, auf die Sekunde genau) sowie den Feldern seiner Art. Es sind ausschließlich Hashes, Zeitpunkte und unsere eigenen Kennungen: nie der Name oder die ID einer Organisation, ein Dateiname, eine Wasserzeichen-ID, ein Modell- oder App-Name. Wer eine Datei oder ein Token hat, kann die Felder seines Eintrags nachrechnen; alle anderen erfahren nichts daraus.

Arten von Einträgen
kindErfasstFelder
c2pa.signatureEine über POST /v1/mark markierte Dateienvironment, manifest (sein Label), asset_sha256 (die Datei, wie wir sie zurückgegeben haben), claim_signature_sha256 (die COSE-Signatur: das CBOR in der Box c2pa.signature des Manifests), signer_sha256 (das Signaturzertifikat, DER), tsa_serial, tsa_time, tsa_token_sha256 (das Zeitstempel-Token in der Signatur, DER)
c2pa.local_signatureEin über POST /v1/sign signiertes Manifest (lokaler Modus)environment, manifest, binding_sha256 (der Hash in der Assertion c2pa.hash.data des Manifests: Die Datei sehen wir nie), claim_signature_sha256, signer_sha256, tsa_serial, tsa_time, tsa_token_sha256
rfc3161.time_stampEin Token von POST /v1/tsa/{key}environment, tsa_serial, tsa_time, tsa_token_sha256 (das TimeStampToken, DER), imprint_alg und imprint (der Hash, den Sie gesendet haben)
audit.headDer jüngste Stand des Audit-Logs einer Organisation, verankert, sobald er sich ändertorg (SHA-256 von "audit-org:", gefolgt von der ID der Organisation), seq und head (letzter Eintrag und Hash der Audit-Kette)
checker.resultEin signiertes Ergebnis von POST /v1/verifyjws_sha256 (der kompakte JWS, wie zurückgegeben), file_sha256 (die geprüfte Datei), key_id (der Schlüssel des Prüftools: SHA-256 seiner DER-codierten SubjectPublicKeyInfo)

Das Protokoll ist ein Merkle-Baum nach RFC 9162 (das Hashing von Certificate Transparency): Der Leaf-Hash eines Eintrags ist der SHA-256 eines 0x00-Bytes, gefolgt vom kanonischen JSON des Eintrags (Schlüssel sortiert, keine Leerzeichen; jeder Wert ist ASCII). API-Antworten enthalten genau diesen Text als entry_json. Die Antworten auf Ihre Aufrufe von POST /v1/mark und POST /v1/tsa nennen den erzeugten Eintrag in X-Log-Index.

Die neueste Wurzel

curl
curl https://api.kennmarq.com/v1/log/root
200 application/json
{
  "object": "log_root",
  "seq": 412,
  "tree_size": 1834467,
  "timestamp": 1794067200000,
  "signed_at": "2026-11-12T00:00:00.000Z",
  "root_hash": "9c8d5e0a4b7f…",
  "key_id": "5b2e7a19c0d4…",
  "signature": "MEUCIQDx…"
}

signature ist ECDSA P-256 mit SHA-256 (DER, Base64) über den Tree Head nach RFC 6962: die Bytes 00 01, dann timestamp (Millisekunden) und tree_size als 8-Byte-Zahlen in Big-Endian, dann die 32 Bytes von root_hash. GET /v1/log/roots listet jede Wurzel auf, die neueste zuerst (?before=<seq> blättert zurück, ?limit= bis 100).

Inklusionsbeweis

Geben Sie den Eintrag über seinen Index (X-Log-Index), seinen Leaf-Hash oder, bei einer markierten Datei, das Label ihres Manifests an (urn:c2pa:…, mit jedem C2PA-Tool lesbar). Fügen Sie ?tree_size= hinzu, um ihn gegen eine bestimmte signierte Wurzel zu beweisen; sonst wird die neueste verwendet.

curl
curl https://api.kennmarq.com/v1/log/proof/urn:c2pa:3fa85f64-5717-4562-b3fc-2c963f66afa6
200 application/json
{
  "object": "log_proof",
  "status": "included",
  "leaf_index": 1834001,
  "leaf_hash": "d1f0…",
  "entry": { "v": 1, "kind": "c2pa.signature", "manifest": "urn:c2pa:3fa85f64-…", "asset_sha256": "…", … },
  "entry_json": "{\"asset_sha256\":\"…\",…,\"v\":1}",
  "tree_size": 1834467,
  "audit_path": ["7a3c…", "0e91…", …],
  "root": { "object": "log_root", "seq": 412, … }
}

Für einen Eintrag, der neuer ist als die neueste Wurzel, lautet die Antwort "status": "pending", mit dem Eintrag und noch ohne Beweis: Die nächste Wurzel erfasst ihn innerhalb einer Stunde.

Konsistenzbeweis

Der Beweis, dass der Baum aus second Einträgen mit dem Baum aus first beginnt: Dazwischen wurde nichts entfernt, verändert oder umsortiert. Beide müssen Größen signierter Wurzeln sein; second ist standardmäßig die neueste. Bewahren Sie die Wurzeln auf, die Sie gesehen haben, und prüfen Sie jede neue gegen sie.

curl
curl "https://api.kennmarq.com/v1/log/consistency?first=1790012&second=1834467"
200 application/json
{
  "object": "log_consistency",
  "first": { "object": "log_root", "tree_size": 1790012, … },
  "second": { "object": "log_root", "tree_size": 1834467, … },
  "proof": ["44be…", "c07d…", …]
}

Alle Einträge

Für Prüfer, die den Baum selbst nachbauen: ?start= und optional ?end= (exklusiv), bis zu 1.000 Einträge je Aufruf, die ältesten zuerst, jeweils mit leaf_index, leaf_hash, entry und entry_json.

curl
curl "https://api.kennmarq.com/v1/log/entries?start=0&end=1000"

Der öffentliche Schlüssel

curl
curl "https://api.kennmarq.com/v1/log/key?format=pem" -o kennmarq.com-log-key.pem

Ohne format=pem antwortet der Endpunkt mit JSON: key_id (SHA-256 der DER-codierten SubjectPublicKeyInfo des Schlüssels, wie jede Wurzel ihn nennt), algorithm und public_key. Ein älterer Schlüssel bleibt über ?key_id= abrufbar.

Selbst prüfen

Die Signatur der Wurzel, mit OpenSSL (und jq):

Shell
curl -s "https://api.kennmarq.com/v1/log/key?format=pem" -o log-key.pem
curl -s "https://api.kennmarq.com/v1/log/root" -o root.json
printf '0001%016x%016x%s' "$(jq .timestamp root.json)" "$(jq .tree_size root.json)" \
  "$(jq -r .root_hash root.json)" | xxd -r -p > tree-head.bin
jq -r .signature root.json | base64 --decode > signature.der
openssl dgst -sha256 -verify log-key.pem -signature signature.der tree-head.bin   # Verified OK

Ein Inklusionsbeweis (RFC 9162 § 2.1.3.2), in Python:

Python
import hashlib, json, urllib.request

proof = json.load(urllib.request.urlopen("https://api.kennmarq.com/v1/log/proof/urn:c2pa:3fa85f64-5717-4562-b3fc-2c963f66afa6"))
sha256 = lambda data: hashlib.sha256(data).digest()

r = sha256(b"\x00" + proof["entry_json"].encode())
assert r.hex() == proof["leaf_hash"]
fn, sn = proof["leaf_index"], proof["tree_size"] - 1
for step in map(bytes.fromhex, proof["audit_path"]):
    assert sn > 0
    if fn % 2 == 1 or fn == sn:
        r = sha256(b"\x01" + step + r)
        while fn % 2 == 0 and fn != 0:
            fn, sn = fn >> 1, sn >> 1
    else:
        r = sha256(b"\x01" + r + step)
    fn, sn = fn >> 1, sn >> 1
assert sn == 0 and r.hex() == proof["root"]["root_hash"]
print("in the log, under root", proof["root"]["seq"])

Prüfen Sie dann die Signatur dieser Wurzel wie oben und ob die Felder des Eintrags zu Ihrer Datei passen: asset_sha256 ist der SHA-256 der Datei, die wir zurückgegeben haben, und manifest das Label ihres Manifests. Bei einem Zeitstempel ist tsa_token_sha256 der SHA-256 des Tokens in Ihrer Antwort.

Limits und Caching

120 Aufrufe pro Minute je Adresse, mit den üblichen Headern X-RateLimit-*. Antworten zur neuesten Wurzel dürfen eine Minute lang zwischengespeichert werden; Antworten zu einer von Ihnen genannten Baumgröße ändern sich nie und dürfen einen Tag lang zwischengespeichert werden.

Unsere Zeitstempel stammen von unserem eigenen Server nach RFC 3161, daher steht jeder von ihnen in diesem Protokoll, wo keiner rückdatiert oder entfernt werden kann, ohne dass die Wurzeln einander widersprechen.

Fehler

Fehler der Protokoll-Endpunkte
StatusCodeWann
400invalid_requestEin Parameter ist keine ganze Zahl, eine Baumgröße gehört zu keiner unserer signierten Wurzeln, oder ein Eintragsname ist fehlerhaft.
404not_foundKein solcher Eintrag oder Schlüssel, oder noch keine Wurzel signiert.
429rate_limitedMehr als 120 Aufrufe pro Minute von einer Adresse. Retry-After gibt an, wann Sie es erneut versuchen können.
500internal_errorAuf unserer Seite ist etwas fehlgeschlagen, auch ein Beweis, der sich vor dem Versand nicht verifizieren ließ. Versuchen Sie es erneut.