Vai al contenuto

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.

Tipi di voce
kindRegistraCampi
c2pa.signatureUn file marcato tramite POST /v1/markenvironment, 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_signatureUn 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_stampUn 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.headLa testa del registro di audit di un’organizzazione, ancorata ogni volta che avanzaorg (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.resultUn risultato firmato da POST /v1/verifyjws_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
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 è 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
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, … }
}

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

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
curl "https://api.kennmarq.com/v1/log/entries?start=0&end=1000"

La chiave pubblica

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

Senza 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):

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

Una prova di inclusione (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"])

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.

Le nostre marche temporali provengono dal nostro server RFC 3161, quindi ognuna è in questo registro, dove nessuna può essere retrodatata o rimossa senza che le radici smettano di concordare.

Errori

Errori degli endpoint del registro
StatoCodiceQuando
400invalid_requestUn 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.
404not_foundLa voce o la chiave non esiste, oppure non è ancora stata firmata alcuna radice.
429rate_limitedPiù di 120 chiamate al minuto da un solo indirizzo. Retry-After indica quando riprovare.
500internal_errorQualcosa non ha funzionato da parte nostra, compreso il caso di una prova che non ha superato la verifica prima dell’invio. Riprova.