Verifica
Ogni busta completata è protetta da due prove indipendenti:
- 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.
- 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.
Sigillatura
Sezione intitolata “Sigillatura”Quando l’ultimo destinatario firma, la busta viene completata (envelope.completed) e subentra il processo di
sigillatura, che:
- Confronta lo SHA-256 dell’originale con quello registrato alla creazione della busta.
- 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.
- 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.
- 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). - 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".
Ancoraggio
Sezione intitolata “Ancoraggio”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:
{ "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:
{ "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" } ]}Verifica di un documento
Sezione intitolata “Verifica di un documento”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:
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.
Controllare personalmente la prova
Sezione intitolata “Controllare personalmente la prova”Non si limiti a fidarsi della risposta dell’API: tutto ciò che serve per controllarla si trova nella risposta e in
keys.json.
- Documento: lo SHA-256 del Suo file è uguale a
leaf.sealed_document_sha256(o aoriginal_document_sha256). - Foglia:
SHA-256(0x00 ‖ canonical JSON of leaf)è uguale aleaf_hash. - Inclusione: percorrendo
proof.hashesa partire dall’hash della foglia (RFC 9162 §2.1.3.2, conleaf_indexetree_size == anchor.leaf_count) si ottieneanchor.root. - Chiave:
anchor.key_idè presente inkeys.json, l’id della chiave corrisponde ai primi 16 caratteri esadecimali dello SHA-256 della chiave pubblica grezza eanchor.created_atrientra nel periodo di validità della chiave. - Firma: la firma Ed25519 risulta valida su
bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count.
# pip install requests cryptographyimport base64, hashlib, json, sys, requestsfrom 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"].
Il verificatore di riferimento, @bona-fide/verify, è isomorfo (browser e Node, nessun segreto di Bona Fide) ed è
quello eseguito dalla pagina /verify:
import { sha256Hex, verifyProof, type KeySet, type PublicVerifyResponse } from '@bona-fide/verify';import { readFile } from 'node:fs/promises';
const api = 'https://api.bonafidesign.com';const sha256 = sha256Hex(new Uint8Array(await readFile('signed.pdf')));const body = (await (await fetch(`${api}/v1/public/verify`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sha256 }),})).json()) as PublicVerifyResponse;const keys = (await (await fetch(`${api}/.well-known/bona-fide/keys.json`)).json()) as KeySet;
for (const m of body.results) { if (!m.leaf || !m.proof || !m.anchor) continue; // pending_anchor const r = await verifyProof(m.leaf, m.proof, m.anchor, keys, { documentSha256: sha256, expectedLeafHash: m.leaf_hash ?? undefined }); console.log(r.valid ? 'VERIFIED' : 'INVALID', m.envelope.id, r.checks);}Il pacchetto include anche uno strumento di controllo da riga di comando (bona-fide-verify <file.pdf | sha256> [--api URL] [--json];
da un checkout del repository Bona Fide: pnpm --filter @bona-fide/verify cli signed.pdf). Calcola l’hash del file in
locale, controlla ogni passaggio descritto sopra ed esce con 0 se la verifica riesce, 1 se il documento non viene
trovato o non è valido, e 2 se il documento non è ancora ancorato.
Controlli la firma di un’ancora con la chiave PEM presa da keys.json (OpenSSL 3):
curl -s https://api.bonafidesign.com/v1/public/anchors/412 > anchor.jsoncurl -s https://api.bonafidesign.com/.well-known/bona-fide/keys.json > keys.json
printf %s "$(jq -r .signed_message anchor.json)" > message.txtjq -r .signature anchor.json | python3 -c 'import base64,sys; s=sys.stdin.read().strip(); sys.stdout.buffer.write(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)))' > signature.binKEY_ID=$(jq -r '.key_id' anchor.json)jq -r --arg k "$KEY_ID" '.keys[] | select(.key_id == $k) | .public_key_pem' keys.json > key.pem
openssl pkeyutl -verify -pubin -inkey key.pem -rawin -in message.txt -sigfile signature.bin# Signature Verified SuccessfullyLe 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}.