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.
| kind | Ce qu’elle enregistre | Champs |
|---|---|---|
c2pa.signature | Un fichier marqué via POST /v1/mark | environment, 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_signature | Un 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_stamp | Un 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.head | La tête du journal d’audit d’une organisation, ancrée chaque fois qu’elle avance | org (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.result | Un résultat signé émis par POST /v1/verify | jws_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 https://api.kennmarq.com/v1/log/root{
"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 https://api.kennmarq.com/v1/log/proof/urn:c2pa:3fa85f64-5717-4562-b3fc-2c963f66afa6{
"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 "https://api.kennmarq.com/v1/log/consistency?first=1790012&second=1834467"{
"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 "https://api.kennmarq.com/v1/log/entries?start=0&end=1000"La clé publique
curl "https://api.kennmarq.com/v1/log/key?format=pem" -o kennmarq.com-log-key.pemSans 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) :
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 OKUne preuve d’inclusion (RFC 9162, § 2.1.3.2), en 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.
Erreurs
| Statut | Code | Quand |
|---|---|---|
| 400 | invalid_request | Un 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é. |
| 404 | not_found | Entrée ou clé inexistante, ou aucune racine encore signée. |
| 429 | rate_limited | Plus de 120 appels par minute depuis une même adresse. Retry-After indique quand réessayer. |
| 500 | internal_error | Un problème est survenu de notre côté, y compris une preuve dont la vérification a échoué avant l’envoi. Réessayez. |