Ga naar inhoud

Verificatie

Twee onafhankelijke bewijzen beschermen elke voltooide envelop:

  1. 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.
  2. 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.

Wanneer de laatste ontvanger ondertekent, wordt de envelop voltooid (envelope.completed) en neemt het verzegelingsproces het over:

  1. Het vergelijkt de SHA-256 van het origineel met de waarde die bij het aanmaken van de envelop is vastgelegd.
  2. 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.
  3. 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.
  4. 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).
  5. 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".

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:

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"
}

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:

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

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:

Terminal window
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.

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.

  1. Document: de SHA-256 van uw bestand is gelijk aan leaf.sealed_document_sha256 (of original_document_sha256).
  2. Leaf: SHA-256(0x00 ‖ canonical JSON of leaf) is gelijk aan leaf_hash.
  3. Inclusie: proof.hashes doorlopen vanaf de leaf-hash (RFC 9162 §2.1.3.2, met leaf_index en tree_size == anchor.leaf_count) levert anchor.root op.
  4. Sleutel: anchor.key_id staat in keys.json, het sleutel-id bestaat uit de eerste 16 hexadecimale tekens van de SHA-256 van de ruwe openbare sleutel, en anchor.created_at valt binnen de geldigheidsperiode van de sleutel.
  5. Handtekening: de Ed25519-handtekening is geldig over 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']}")

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"].

Ankers 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}.