Verificering
To uafhængige beviser beskytter hver fuldført kuvert:
- Seglet, inde i PDF’en: et PAdES-dokumenttidsstempel fra en offentlig tidsstempelmyndighed. Adobe Acrobat og Reader tjekker det og viser, at dokumentet ikke er ændret, siden det blev forseglet.
- Ankeret, uden for PDF’en: dokumentets fingeraftryk er et blad i et Merkle-træ, hvor der kun kan tilføjes, og Bona Fide signerer hver trærod med Ed25519. Alle kan tjekke beviset med de offentliggjorte offentlige nøgler, uden konto og uden at skulle stole på en PDF-læser.
Forsegling
Sektion kaldt “Forsegling”Når den sidste modtager underskriver, fuldføres kuverten (envelope.completed), og forseglingstjenesten tager over:
- Sammenligner originalens SHA-256 med den, der blev registreret, da kuverten blev oprettet.
- Flader dokumentet ud: feltværdier (tekst, afkrydsningsfelter, tegnede, skrevne eller uploadede signaturer) tegnes ind i sideindholdet, og alle formularfelter, annoteringer, JavaScript-handlinger og indlejrede filer fjernes.
- Tilføjer fuldførelsescertifikatet: kuvertens id, originalens SHA-256, hver underskriver med den verificeringsmetode, der faktisk blev brugt (f.eks. E-maillink (unikt link sendt til …) eller E-maillink + engangskode, se verificering af underskrivere), tidsstempler, IP-adresse og enhed samt hændelsesforløbet. Hver side får sidefoden Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Tilføjer et PAdES-dokumenttidsstempel (
/DocTimeStamp,ETSI.RFC3161, SHA-256) fra en offentlig tidsstempelmyndighed (flere offentligt betroede udbydere med automatisk failover) og indlejrer valideringsdata for tidsstempelmyndighedens certifikatkæde (OCSP/CRL i DSS), så det forbliver verificerbart på lang sigt (LTV). - Validerer sit eget resultat, gemmer det og registrerer den forseglede fils SHA-256 (
sealedDocumentHash) og et blad til ankerloggen.
Derefter udløses envelope.sealed, og GET /v1/envelopes/{id}/document?version=sealed returnerer filen. Hvis ingen
tidsstempelmyndighed er tilgængelig, forsøges forseglingen igen; kuverten forbliver completed med seal: "pending"
imens.
Forankring
Sektion kaldt “Forankring”Hver forseglet kuvert bidrager med ét blad, den kanoniske JSON (sorterede nøgler, ingen mellemrum) af:
{ "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>"}Hver time bliver de blade, der er registreret siden det forrige anker, til et Merkle-træ (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), og dets rod offentliggøres som et
signeret 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"}Ed25519-signaturen dækker bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root kæder hvert anker til det foregående (det første peger på 64 nuller), så et anker ikke kan fjernes
eller flyttes uden at bryde kæden, og ankertabellerne tillader kun tilføjelser.
Signeringsnøglerne offentliggøres med deres gyldighedsperioder; udfasede nøgler forbliver på listen, så gamle ankre fortsat kan verificeres:
{ "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" } ]}Verificering af et dokument
Sektion kaldt “Verificering af et dokument”Den hurtigste måde er den offentlige side: bonafidesign.com/verify. Den beregner PDF’ens hash i din browser (filen uploades aldrig), slår hashen op og tjekker beviset og signaturen på din enhed. Ankerloggen viser alle ankre.
Det samme opslag findes som et offentligt API: ingen nøgle, vilkårligt origin, hastighedsbegrænset pr. IP-adresse. Send kun 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 er verified, pending_anchor (fuldført og registreret, venter på det næste anker i timen) eller
not_found: ingen fuldført kuvert har præcis denne fil, så det er ikke det dokument, der blev underskrevet (eller
det er ændret siden). Både den forseglede fil og originalen, som den blev sendt, giver et match.
Tjek selv beviset
Sektion kaldt “Tjek selv beviset”Du behøver ikke at tage API’ets ord for det: alt, hvad der skal til for at tjekke svaret, står i svaret og i keys.json.
- Dokument: SHA-256 af din fil er lig med
leaf.sealed_document_sha256(elleroriginal_document_sha256). - Blad:
SHA-256(0x00 ‖ canonical JSON of leaf)er lig medleaf_hash. - Inklusion: hvis du gennemløber
proof.hashesfra bladets hash (RFC 9162 §2.1.3.2, medleaf_indexogtree_size == anchor.leaf_count), får duanchor.root. - Nøgle:
anchor.key_idfindes ikeys.json, nøgle-id’et er de første 16 hexadecimale tegn af SHA-256 af den rå offentlige nøgle, oganchor.created_atligger inden for nøglens gyldighedsperiode. - Signatur: Ed25519-signaturen verificeres 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']}")Bladet har kun strengværdier med ASCII-nøgler, så json.dumps(…, sort_keys=True, separators=(",", ":")) giver den
samme kanoniske JSON. Tjek også nøglens valid_from / valid_until mod anchor["created_at"].
Referenceverifikatoren, @bona-fide/verify, er isomorf (browser og Node, ingen hemmeligheder fra Bona Fide) og er
det, siden /verify kører:
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);}Pakken har også et kommandolinjeværktøj (bona-fide-verify <file.pdf | sha256> [--api URL] [--json]; fra en
checkout af Bona Fides repository: pnpm --filter @bona-fide/verify cli signed.pdf). Det beregner filens hash
lokalt, tjekker alle trinene ovenfor og afslutter med 0, når filen er verificeret, 1, når den ikke findes eller
er ugyldig, og 2, når dokumentet endnu ikke er forankret.
Tjek et ankers signatur med PEM-nøglen fra 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 SuccessfullyAnkre vises med det nyeste først med GET /v1/public/anchors?limit=20&before=<sequence> (send next_before for at
bladre tilbage) og hentes ét ad gangen med GET /v1/public/anchors/{sequence}.