Verificatie
Twee onafhankelijke bewijzen beschermen elke voltooide envelop:
- Het zegel, in de pdf: een PAdES-documenttijdstempel van een openbare tijdstempelautoriteit. Adobe Acrobat en Reader controleren het en tonen het document als ongewijzigd sinds het werd verzegeld.
- Het anker, buiten de pdf: de vingerafdruk van het document is een leaf in een append-only Merkle-boom, en Bona Fide ondertekent elke root van de boom met Ed25519. Iedereen kan dat bewijs controleren met de gepubliceerde openbare sleutels, zonder account en zonder een pdf-viewer te hoeven vertrouwen.
Verzegelen
Section titled “Verzegelen”Wanneer de laatste ontvanger ondertekent, wordt de envelop voltooid (envelope.completed) en neemt het
verzegelingsproces het over:
- Het vergelijkt de SHA-256 van het origineel met de waarde die bij het aanmaken van de envelop is vastgelegd.
- Het vlakt het document af: veldwaarden (tekst, selectievakjes, getekende, getypte of geüploade handtekeningen) worden in de pagina-inhoud getekend, en elk formulierveld, elke annotatie, elke JavaScript-actie en elk ingesloten bestand wordt verwijderd.
- Het voegt het voltooiingscertificaat toe: envelop-id, de SHA-256 van het origineel, elke ondertekenaar met de verificatiemethode die daadwerkelijk is gebruikt (bijvoorbeeld E-maillink (unieke link verstuurd naar …) of E-maillink + eenmalige code, zie verificatie van de ondertekenaar), tijdstempels, IP-adres en apparaat, en de tijdlijn van events. Elke pagina krijgt de voettekst Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Het voegt een PAdES-documenttijdstempel toe (
/DocTimeStamp,ETSI.RFC3161, SHA-256) van een openbare tijdstempelautoriteit (meerdere publiek vertrouwde aanbieders met automatische failover), en sluit de validatiegegevens van de certificaatketen van de TSA in (OCSP/CRL in de DSS), zodat het document op lange termijn verifieerbaar blijft (LTV). - Het valideert de eigen uitvoer, slaat die op en legt de verzegelde SHA-256 (
sealedDocumentHash) en een leaf voor het ankerlogboek vast.
Daarna wordt envelope.sealed afgevuurd en geeft GET /v1/envelopes/{id}/document?version=sealed het bestand terug.
Als geen enkele TSA beschikbaar is, wordt het verzegelen opnieuw geprobeerd; de envelop blijft intussen completed met
seal: "pending".
Verankeren
Section titled “Verankeren”Elke verzegelde envelop levert één leaf, de canonieke JSON (sleutels gesorteerd, zonder witruimte) van:
{ "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>"}Elk uur worden de leaves die sinds het vorige anker zijn vastgelegd een Merkle-boom (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), en de root daarvan wordt gepubliceerd als
een ondertekend anker:
{ "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"}De Ed25519-handtekening dekt bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root ketent elk anker aan het vorige (het eerste wijst naar 64 nullen), zodat een anker niet kan worden
verwijderd of verplaatst zonder de keten te breken, en de ankertabellen zijn append-only.
De ondertekeningssleutels worden gepubliceerd met hun geldigheidsperiodes; buiten gebruik gestelde sleutels blijven vermeld, zodat oude ankers verifieerbaar blijven:
{ "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" } ]}Een document verifiëren
Section titled “Een document verifiëren”De snelste manier is de openbare pagina: bonafidesign.com/verify. Die berekent de hash van de pdf in uw browser (het bestand wordt nooit geüpload), zoekt de hash op en controleert het bewijs en de handtekening op uw apparaat. Het ankerlogboek toont elk anker.
Dezelfde opzoeking is beschikbaar als openbare API: geen key, elke origin, met een rate limit per IP-adres. Stuur alleen de 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 is verified, pending_anchor (voltooid en vastgelegd, wacht op het volgende uurlijkse anker) of
not_found: geen enkele voltooide envelop bevat precies dit bestand, dus het is niet het document dat is ondertekend
(of het is sindsdien gewijzigd). Zowel het verzegelde bestand als het origineel zoals het is verstuurd, leveren een
overeenkomst op.
Het bewijs zelf controleren
Section titled “Het bewijs zelf controleren”Ga niet blind af op het antwoord van de API: alles wat nodig is om het te controleren, staat in het antwoord en in
keys.json.
- Document: de SHA-256 van uw bestand is gelijk aan
leaf.sealed_document_sha256(oforiginal_document_sha256). - Leaf:
SHA-256(0x00 ‖ canonical JSON of leaf)is gelijk aanleaf_hash. - Inclusie:
proof.hashesdoorlopen vanaf de leaf-hash (RFC 9162 §2.1.3.2, metleaf_indexentree_size == anchor.leaf_count) levertanchor.rootop. - Sleutel:
anchor.key_idstaat inkeys.json, het sleutel-id bestaat uit de eerste 16 hexadecimale tekens van de SHA-256 van de ruwe openbare sleutel, enanchor.created_atvalt binnen de geldigheidsperiode van de sleutel. - Handtekening: de Ed25519-handtekening is geldig over
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']}")De leaf bevat alleen stringwaarden met ASCII-sleutels, dus json.dumps(…, sort_keys=True, separators=(",", ":")) levert
dezelfde canonieke JSON op. Controleer ook valid_from / valid_until van de sleutel tegen anchor["created_at"].
De referentieverifier, @bona-fide/verify, is isomorf (browser en Node, geen geheimen van Bona Fide) en is wat de
pagina /verify uitvoert:
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);}Het package bevat ook een controletool voor de opdrachtregel (bona-fide-verify <file.pdf | sha256> [--api URL] [--json];
vanuit een checkout van de Bona Fide-repository: pnpm --filter @bona-fide/verify cli signed.pdf). De tool berekent
de hash van het bestand lokaal, controleert elke stap hierboven en eindigt met 0 als de verificatie slaagt, 1 als
het document niet is gevonden of ongeldig is, en 2 als het document nog niet is verankerd.
Controleer de handtekening van een anker met de PEM-sleutel uit 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 SuccessfullyAnkers worden van nieuw naar oud getoond met GET /v1/public/anchors?limit=20&before=<sequence> (geef next_before mee
om terug te bladeren) en één voor één opgehaald met GET /v1/public/anchors/{sequence}.