Aller au contenu

Journal de transparence

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

Chaque signature et chaque horodatage que nous délivrons est d’abord ajouté à un journal public en ajout seul. Une racine couvrant tout le journal est signée toutes les heures tant qu’il grandit, et au moins une fois par jour. Avec une racine, sa signature et notre clé publique, chacun peut vérifier qu’une entrée figure dans le journal, et que le journal n’a jamais été réécrit. Aucune clé API n’est nécessaire. La page du journal de transparence affiche la dernière racine.

Contenu d’une entrée

Chaque entrée est un objet JSON avec v (1), kind et logged_at (UTC, à la seconde près), plus les champs propres à son type. Ce ne sont que des empreintes, des heures et nos propres identifiants : jamais le nom ou l’ID d’une organisation, un nom de fichier, un ID de filigrane, un modèle ou un nom d’application. Qui détient un fichier ou un jeton peut en recalculer les champs ; personne d’autre n’en apprend quoi que ce soit.

Types d’entrées
kindCe qu’elle enregistreChamps
c2pa.signatureUn fichier marqué via POST /v1/markenvironment, manifest (son libellé), asset_sha256 (le fichier tel que nous l’avons renvoyé), claim_signature_sha256 (la signature COSE : le CBOR de la boîte c2pa.signature du manifeste), signer_sha256 (le certificat de signature, DER), tsa_serial, tsa_time, tsa_token_sha256 (le jeton d’horodatage contenu dans la signature, DER)
c2pa.local_signatureUn manifeste signé via POST /v1/sign (mode local)environment, manifest, binding_sha256 (l’empreinte de l’assertion c2pa.hash.data du manifeste : nous ne voyons jamais le fichier), claim_signature_sha256, signer_sha256, tsa_serial, tsa_time, tsa_token_sha256
rfc3161.time_stampUn jeton émis par POST /v1/tsa/{key}environment, tsa_serial, tsa_time, tsa_token_sha256 (le TimeStampToken, DER), imprint_alg et imprint (l’empreinte que vous avez envoyée)
audit.headLa tête du journal d’audit d’une organisation, ancrée chaque fois qu’elle avanceorg (SHA-256 de « audit-org: » suivi de l’ID de l’organisation), seq et head (la dernière entrée de la chaîne d’audit et son empreinte)
checker.resultUn résultat signé émis par POST /v1/verifyjws_sha256 (le JWS compact, tel que renvoyé), file_sha256 (le fichier vérifié), key_id (la clé du vérificateur : SHA-256 de son SubjectPublicKeyInfo en DER)

Le journal est un arbre de Merkle RFC 9162 (le hachage de Certificate Transparency) : l’empreinte de feuille d’une entrée est le SHA-256 d’un octet 0x00 suivi du JSON canonique de l’entrée (clés triées, sans espaces ; toutes les valeurs en ASCII). Les réponses de l’API contiennent ce texte exact dans entry_json. Vos réponses de POST /v1/mark et de POST /v1/tsa indiquent dans X-Log-Index l’entrée qu’elles ont créée.

La dernière racine

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 est une signature ECDSA P-256 avec SHA-256 (DER, base64) sur la tête d’arbre de la RFC 6962 : les octets 00 01, puis timestamp (en millisecondes) et tree_size en nombres big-endian sur 8 octets, puis les 32 octets de root_hash. GET /v1/log/roots liste toutes les racines, de la plus récente à la plus ancienne (?before=<seq> remonte d’une page, ?limit= jusqu’à 100).

Preuve d’inclusion

Désignez l’entrée par son index (X-Log-Index), son empreinte de feuille ou, pour un fichier marqué, le libellé de son manifeste (urn:c2pa:…, lisible avec n’importe quel outil C2PA). Ajoutez ?tree_size= pour obtenir la preuve par rapport à une racine signée précise ; sinon, la dernière est utilisée.

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, … }
}

Pour une entrée plus récente que la dernière racine, la réponse est "status": "pending", avec l’entrée mais pas encore de preuve : la prochaine racine la couvrira dans l’heure.

Preuve de cohérence

La preuve que l’arbre de second entrées commence par l’arbre de first entrées : rien n’a été supprimé, modifié ni réordonné entre les deux. Les deux doivent être des tailles de racines signées ; second vaut par défaut la dernière. Conservez les racines que vous avez vues et vérifiez chaque nouvelle racine par rapport à elles.

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…", …]
}

Toutes les entrées

Pour les auditeurs qui reconstruisent l’arbre eux-mêmes : ?start= et, en option, ?end= (exclu), jusqu’à 1 000 entrées par appel, des plus anciennes aux plus récentes, chacune avec leaf_index, leaf_hash, entry et entry_json.

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

La clé publique

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

Sans format=pem, la réponse est en JSON : key_id (SHA-256 du SubjectPublicKeyInfo de la clé en DER, tel que chaque racine le mentionne), algorithm et public_key. Une clé plus ancienne reste disponible via ?key_id=.

Le vérifier vous-même

La signature de la racine, avec OpenSSL (et 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

Une preuve d’inclusion (RFC 9162, § 2.1.3.2), en 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"])

Vérifiez ensuite la signature de cette racine comme ci-dessus, et que les champs de l’entrée correspondent à votre fichier : asset_sha256 est le SHA-256 du fichier que nous avons renvoyé, et manifest le libellé de son manifeste. Pour un horodatage, tsa_token_sha256 est le SHA-256 du jeton contenu dans votre réponse.

Limites et mise en cache

120 appels par minute et par adresse, avec les en-têtes X-RateLimit-* habituels. Les réponses portant sur la dernière racine peuvent être mises en cache une minute ; celles qui portent sur une taille d’arbre que vous avez indiquée ne changent jamais et peuvent être mises en cache une journée.

Nos horodatages proviennent de notre propre serveur RFC 3161 : chacun d’eux figure donc dans ce journal, où aucun ne peut être antidaté ni supprimé sans que les racines se contredisent.

Erreurs

Erreurs des endpoints du journal
StatutCodeQuand
400invalid_requestUn paramètre n’est pas un nombre entier, une taille d’arbre ne correspond à aucune de nos racines signées, ou un nom d’entrée est mal formé.
404not_foundEntrée ou clé inexistante, ou aucune racine encore signée.
429rate_limitedPlus de 120 appels par minute depuis une même adresse. Retry-After indique quand réessayer.
500internal_errorUn problème est survenu de notre côté, y compris une preuve dont la vérification a échoué avant l’envoi. Réessayez.