Salta ai contenuti

Verifica

Ogni busta completata è protetta da due prove indipendenti:

  1. Il sigillo, all’interno del PDF: una marca temporale PAdES rilasciata da un’autorità di marcatura temporale pubblica. Adobe Acrobat e Reader la controllano e mostrano il documento come non modificato dal momento della sigillatura.
  2. L’ancora, all’esterno del PDF: l’impronta del documento è una foglia in un albero di Merkle append-only e Bona Fide firma ogni radice dell’albero con Ed25519. Chiunque può controllare questa prova con le chiavi pubbliche pubblicate, senza bisogno di un account né di fidarsi di un visualizzatore PDF.

Quando l’ultimo destinatario firma, la busta viene completata (envelope.completed) e subentra il processo di sigillatura, che:

  1. Confronta lo SHA-256 dell’originale con quello registrato alla creazione della busta.
  2. Appiattisce il documento: i valori dei campi (testo, caselle di controllo, firme disegnate, digitate o caricate) vengono disegnati nel contenuto della pagina e ogni campo modulo, annotazione, azione JavaScript e file incorporato viene rimosso.
  3. Aggiunge in coda il certificato di completamento: id della busta, SHA-256 dell’originale, ogni firmatario con il metodo di verifica effettivamente usato (ad esempio Link via e-mail (link univoco inviato a …) o Link via e-mail + codice monouso, veda verifica del firmatario), orari, indirizzo IP e dispositivo, e la cronologia degli eventi. Ogni pagina riceve il piè di pagina Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. Aggiunge una marca temporale PAdES (/DocTimeStamp, ETSI.RFC3161, SHA-256) di un’autorità di marcatura temporale pubblica (diversi fornitori riconosciuti pubblicamente, con failover automatico) e incorpora i dati di validazione della catena di certificati della TSA (OCSP/CRL nel DSS), in modo che resti verificabile a lungo termine (LTV).
  5. Convalida il proprio output, lo memorizza e registra lo SHA-256 sigillato (sealedDocumentHash) e una foglia per il registro delle ancore.

Viene quindi emesso envelope.sealed e GET /v1/envelopes/{id}/document?version=sealed restituisce il file. Se nessuna TSA è disponibile, la sigillatura viene ritentata; nel frattempo la busta resta completed con seal: "pending".

Ogni busta sigillata contribuisce con una foglia, il JSON canonico (chiavi ordinate, senza spazi) di:

{
"completed_at": "2026-10-13T14:09:52.004Z",
"envelope_id": "env_8Gm2kQ4rT1",
"event_chain_head": "<head of the envelope's audit hash chain>",
"original_document_sha256": "<SHA-256 of the document as sent>",
"sealed_document_sha256": "<SHA-256 of the sealed PDF>"
}

Ogni ora, le foglie registrate dall’ancora precedente formano un albero di Merkle (RFC 6962 / RFC 9162: leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)) e la sua radice viene pubblicata come ancora firmata:

GET /v1/public/anchors/latest
{
"id": "…",
"version": "bona-fide-anchor-v1",
"sequence": 412,
"root": "<hex Merkle root>",
"previous_root": "<root of anchor 411>",
"leaf_count": 37,
"created_at": "2026-10-13T15:00:00.118Z",
"key_id": "<first 16 hex of SHA-256(public key)>",
"signature": "<base64url Ed25519 signature>",
"signed_message": "bona-fide-anchor-v1|412|<root>|<previous_root>|2026-10-13T15:00:00.118Z|37"
}

La firma Ed25519 copre bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8). previous_root concatena ogni ancora a quella precedente (la prima punta a 64 zeri), quindi un’ancora non può essere rimossa o riordinata senza spezzare la catena, e le tabelle delle ancore sono append-only.

Le chiavi di firma vengono pubblicate con i rispettivi periodi di validità; le chiavi ritirate restano elencate, così le ancore meno recenti restano verificabili:

GET /.well-known/bona-fide/keys.json
{
"issuer": "Bona Fide",
"anchor_format": "bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8, Ed25519; root = RFC 6962 SHA-256 Merkle tree)",
"keys": [
{
"key_id": "<16 hex>",
"algorithm": "Ed25519",
"public_key": "<base64url raw 32-byte key>",
"public_key_pem": "-----BEGIN PUBLIC KEY-----…",
"valid_from": "2026-10-14T00:00:00.000Z",
"valid_until": null,
"status": "active"
}
]
}

Il modo più rapido è la pagina pubblica: bonafidesign.com/verify. Calcola l’hash del PDF nel Suo browser (il file non viene mai caricato), cerca l’hash e controlla la prova e la firma sul Suo dispositivo. Il registro delle ancore elenca tutte le ancore.

La stessa ricerca è disponibile come API pubblica: nessuna chiave, qualsiasi origine, limiti di frequenza per IP. Invii solo lo SHA-256:

Finestra del terminale
curl https://api.bonafidesign.com/v1/public/verify \
-H "Content-Type: application/json" \
-d "{\"sha256\": \"$(shasum -a 256 signed.pdf | cut -d' ' -f1)\"}"
{
"sha256": "…",
"status": "verified",
"results": [
{
"status": "verified",
"matched": "sealed_document",
"envelope": {
"id": "env_8Gm2kQ4rT1",
"status": "completed",
"completed_at": "2026-10-13T14:09:52.004Z",
"sealed_at": "2026-10-13T14:10:31.442Z",
"organization_name": "Acme Robotics, Inc.",
"signer_count": 1,
"signers": [{ "name": "Jane Doe", "email": "jane@acme.dev", "role": "client", "verification_method": "Email OTP", "signed_at": "…" }],
"verification_methods": ["Email OTP"],
"test_mode": false
},
"leaf": { "envelope_id": "env_8Gm2kQ4rT1", "…": "…" },
"leaf_hash": "…",
"proof": { "leaf_index": 12, "tree_size": 37, "hashes": ["…", "…"] },
"anchor": { "sequence": 412, "…": "…" },
"links": { "anchor": "…", "keys": "…/.well-known/bona-fide/keys.json" }
}
]
}

status è verified, pending_anchor (completata e registrata, in attesa della prossima ancora oraria) o not_found: nessuna busta completata contiene esattamente questo file, quindi non è il documento che è stato firmato (oppure è stato modificato da allora). Trovano corrispondenza sia il file sigillato sia l’originale così come è stato inviato.

Non si limiti a fidarsi della risposta dell’API: tutto ciò che serve per controllarla si trova nella risposta e in keys.json.

  1. Documento: lo SHA-256 del Suo file è uguale a leaf.sealed_document_sha256 (o a original_document_sha256).
  2. Foglia: SHA-256(0x00 ‖ canonical JSON of leaf) è uguale a leaf_hash.
  3. Inclusione: percorrendo proof.hashes a partire dall’hash della foglia (RFC 9162 §2.1.3.2, con leaf_index e tree_size == anchor.leaf_count) si ottiene anchor.root.
  4. Chiave: anchor.key_id è presente in keys.json, l’id della chiave corrisponde ai primi 16 caratteri esadecimali dello SHA-256 della chiave pubblica grezza e anchor.created_at rientra nel periodo di validità della chiave.
  5. Firma: la firma Ed25519 risulta valida su bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count.
# pip install requests cryptography
import base64, hashlib, json, sys, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
API = "https://api.bonafidesign.com"
b64url = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
sha = lambda b: hashlib.sha256(b).digest()
def root_from_proof(leaf: bytes, index: int, size: int, path: list[bytes]) -> bytes:
fn, sn, r = index, size - 1, leaf
for p in path:
if sn == 0: raise ValueError("proof too long")
if fn % 2 == 1 or fn == sn:
r = sha(b"\x01" + p + r)
while fn % 2 == 0 and fn != 0:
fn, sn = fn // 2, sn // 2
else:
r = sha(b"\x01" + r + p)
fn, sn = fn // 2, sn // 2
if sn != 0: raise ValueError("proof too short")
return r
digest = hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest()
res = requests.post(f"{API}/v1/public/verify", json={"sha256": digest}).json()
keys = {k["key_id"]: k for k in requests.get(f"{API}/.well-known/bona-fide/keys.json").json()["keys"]}
for m in res["results"]:
leaf, proof, anchor = m["leaf"], m["proof"], m["anchor"]
if not anchor:
print("pending anchor"); continue
assert digest in (leaf["sealed_document_sha256"], leaf["original_document_sha256"])
leaf_json = json.dumps(leaf, sort_keys=True, separators=(",", ":")).encode()
leaf_hash = sha(b"\x00" + leaf_json)
assert proof["tree_size"] == anchor["leaf_count"]
root = root_from_proof(leaf_hash, proof["leaf_index"], proof["tree_size"], [bytes.fromhex(h) for h in proof["hashes"]])
assert root.hex() == anchor["root"], "not included under the anchor root"
key = keys[anchor["key_id"]]
raw = b64url(key["public_key"])
assert hashlib.sha256(raw).hexdigest()[:16] == anchor["key_id"]
message = "|".join([anchor["version"], str(anchor["sequence"]), anchor["root"], anchor["previous_root"],
anchor["created_at"], str(anchor["leaf_count"])]).encode()
Ed25519PublicKey.from_public_bytes(raw).verify(b64url(anchor["signature"]), message) # raises if invalid
print(f"VERIFIED {leaf['envelope_id']} in anchor #{anchor['sequence']}")

La foglia contiene solo valori stringa con chiavi ASCII, quindi json.dumps(…, sort_keys=True, separators=(",", ":")) produce lo stesso JSON canonico. Confronti anche valid_from / valid_until della chiave con anchor["created_at"].

Le ancore si elencano dalla più recente con GET /v1/public/anchors?limit=20&before=<sequence> (passi next_before per tornare indietro tra le pagine) e si recuperano una alla volta con GET /v1/public/anchors/{sequence}.