Registro di trasparenza
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
Ogni firma e ogni marca temporale che rilasciamo viene prima aggiunta a un registro pubblico a sola aggiunta. Una radice dell’intero registro viene firmata ogni ora mentre il registro cresce, e comunque almeno una volta al giorno. Con una radice, la sua firma e la nostra chiave pubblica, chiunque può verificare che una voce sia nel registro e che il registro non sia mai stato riscritto. Non serve alcuna chiave API. La pagina del registro di trasparenza mostra l’ultima radice.
Cosa contiene una voce
Ogni voce è un oggetto JSON con v (1), kind e logged_at (UTC, al secondo), più i campi del suo tipo. Sono tutti hash, orari e identificativi nostri: mai il nome o l’ID di un’organizzazione, il nome di un file, un ID della filigrana, il nome di un modello o di un’app. Chi possiede un file o un token può ricalcolarne i campi; nessun altro ne ricava alcuna informazione.
| kind | Registra | Campi |
|---|---|---|
c2pa.signature | Un file marcato tramite POST /v1/mark | environment, manifest (la sua etichetta), asset_sha256 (il file come te l’abbiamo restituito), claim_signature_sha256 (la firma COSE: il CBOR nel box c2pa.signature del manifest), signer_sha256 (il certificato di firma, DER), tsa_serial, tsa_time, tsa_token_sha256 (il token di marca temporale nella firma, DER) |
c2pa.local_signature | Un manifest firmato tramite POST /v1/sign (modalità locale) | environment, manifest, binding_sha256 (l’hash nell’asserzione c2pa.hash.data del manifest: il file non lo vediamo mai), claim_signature_sha256, signer_sha256, tsa_serial, tsa_time, tsa_token_sha256 |
rfc3161.time_stamp | Un token da POST /v1/tsa/{key} | environment, tsa_serial, tsa_time, tsa_token_sha256 (il TimeStampToken, DER), imprint_alg e imprint (l’hash che hai inviato) |
audit.head | La testa del registro di audit di un’organizzazione, ancorata ogni volta che avanza | org (SHA-256 di "audit-org:" seguito dall’ID dell’organizzazione), seq e head (l’ultima voce della catena di audit e il suo hash) |
checker.result | Un risultato firmato da POST /v1/verify | jws_sha256 (il JWS compatto, come restituito), file_sha256 (il file verificato), key_id (la chiave del verificatore: SHA-256 della sua SubjectPublicKeyInfo DER) |
Il registro è un albero di Merkle RFC 9162 (lo stesso hashing di Certificate Transparency): l’hash foglia di una voce è lo SHA-256 di un byte 0x00 seguito dal JSON canonico della voce (chiavi ordinate, nessuno spazio; ogni valore è ASCII). Le risposte dell’API riportano esattamente quel testo in entry_json. Le risposte di POST /v1/mark e POST /v1/tsa indicano in X-Log-Index la voce che hanno creato.
L’ultima radice
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 è una firma ECDSA P-256 con SHA-256 (DER, base64) sulla tree head dell’RFC 6962: i byte 00 01, poi timestamp (in millisecondi) e tree_size come numeri big-endian di 8 byte, poi i 32 byte di root_hash. GET /v1/log/roots elenca tutte le radici, dalla più recente (?before=<seq> va alle pagine precedenti, ?limit= fino a 100).
Prova di inclusione
Indica la voce con il suo indice (X-Log-Index), il suo hash foglia oppure, per un file marcato, l’etichetta del suo manifest (urn:c2pa:…, leggibile con qualsiasi strumento C2PA). Aggiungi ?tree_size= per ottenere la prova rispetto a una specifica radice firmata; altrimenti si usa l’ultima.
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, … }
}Per una voce più recente dell’ultima radice la risposta è "status": "pending", con la voce e ancora nessuna prova: la radice successiva la include entro un’ora.
Prova di coerenza
La prova che l’albero di second voci inizia con l’albero di first voci: nel frattempo nulla è stato rimosso, modificato o riordinato. Entrambi i valori devono essere dimensioni di radici firmate; second è per impostazione predefinita quella dell’ultima. Conserva le radici che hai visto e verifica ogni nuova radice rispetto a quelle.
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…", …]
}Tutte le voci
Per i revisori che ricostruiscono l’albero da sé: ?start= e, facoltativo, ?end= (escluso), fino a 1.000 voci per chiamata, dalla più vecchia, ciascuna con leaf_index, leaf_hash, entry e entry_json.
curl "https://api.kennmarq.com/v1/log/entries?start=0&end=1000"La chiave pubblica
curl "https://api.kennmarq.com/v1/log/key?format=pem" -o kennmarq.com-log-key.pemSenza format=pem risponde in JSON: key_id (SHA-256 della SubjectPublicKeyInfo DER della chiave, come la indica ogni radice), algorithm e public_key. Una chiave precedente resta disponibile con ?key_id=.
Verificare in autonomia
La firma della radice, con OpenSSL (e 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 OKUna prova di inclusione (RFC 9162 §2.1.3.2), in 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"])Poi verifica la firma di quella radice come sopra, e che i campi della voce corrispondano al tuo file: asset_sha256 è lo SHA-256 del file che ti abbiamo restituito, e manifest l’etichetta del suo manifest. Per una marca temporale, tsa_token_sha256 è lo SHA-256 del token nella tua risposta.
Limiti e caching
120 chiamate al minuto per indirizzo, con i consueti header X-RateLimit-*. Le risposte sull’ultima radice possono restare in cache per un minuto; quelle su una dimensione dell’albero che hai indicato non cambiano mai e possono restare in cache per un giorno.
Errori
| Stato | Codice | Quando |
|---|---|---|
| 400 | invalid_request | Un parametro non è un numero intero, una dimensione dell’albero non corrisponde a nessuna delle nostre radici firmate, oppure il nome di una voce non è valido. |
| 404 | not_found | La voce o la chiave non esiste, oppure non è ancora stata firmata alcuna radice. |
| 429 | rate_limited | Più di 120 chiamate al minuto da un solo indirizzo. Retry-After indica quando riprovare. |
| 500 | internal_error | Qualcosa non ha funzionato da parte nostra, compreso il caso di una prova che non ha superato la verifica prima dell’invio. Riprova. |