Verifizierung
Zwei unabhängige Nachweise schützen jeden abgeschlossenen Umschlag:
- Das Siegel, im PDF: ein PAdES-Dokumentzeitstempel einer öffentlichen Zeitstempelstelle. Adobe Acrobat und Reader prüfen ihn und zeigen das Dokument als seit der Versiegelung unverändert an.
- Der Anker, außerhalb des PDFs: Der Fingerabdruck des Dokuments ist ein Blatt in einem nur erweiterbaren Merkle-Baum, und Bona Fide signiert jede Baumwurzel mit Ed25519. Jeder kann diesen Nachweis mit den veröffentlichten öffentlichen Schlüsseln prüfen, ohne Konto und ohne einem PDF-Viewer vertrauen zu müssen.
Versiegelung
Abschnitt betitelt „Versiegelung“Wenn der letzte Empfänger unterschreibt, wird der Umschlag abgeschlossen (envelope.completed), und der
Versiegelungsdienst übernimmt:
- Er vergleicht den SHA-256 des Originals mit dem Wert, der beim Erstellen des Umschlags erfasst wurde.
- Er reduziert das Dokument auf eine Ebene: Feldwerte (Text, Kontrollkästchen, gezeichnete, getippte oder hochgeladene Unterschriften) werden in den Seiteninhalt gezeichnet, und alle Formularfelder, Anmerkungen, JavaScript-Aktionen und eingebetteten Dateien werden entfernt.
- Er hängt das Abschlusszertifikat an: Umschlag-ID, SHA-256 des Originals, jeden Unterzeichner mit der tatsächlich verwendeten Verifizierungsmethode (zum Beispiel E-Mail-Link (eindeutiger Link, gesendet an …) oder E-Mail-Link + Einmalcode, siehe Verifizierung der Unterzeichner), Zeitstempel, IP-Adresse und Gerät sowie den Ablauf der Events. Jede Seite erhält die Fußzeile Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Er fügt einen PAdES-Dokumentzeitstempel (
/DocTimeStamp,ETSI.RFC3161, SHA-256) einer öffentlichen Zeitstempelstelle hinzu (mehrere öffentlich vertrauenswürdige Anbieter mit automatischem Failover) und bettet die Validierungsdaten der Zertifikatskette der TSA ein (OCSP/CRL im DSS), damit der Zeitstempel langfristig prüfbar bleibt (LTV). - Er validiert seine eigene Ausgabe, speichert sie und erfasst den SHA-256 der versiegelten Datei
(
sealedDocumentHash) sowie ein Blatt für das Anker-Protokoll.
Danach wird envelope.sealed ausgelöst, und GET /v1/envelopes/{id}/document?version=sealed liefert die Datei. Ist
keine TSA erreichbar, wird die Versiegelung wiederholt; der Umschlag bleibt in der Zwischenzeit completed mit
seal: "pending".
Verankerung
Abschnitt betitelt „Verankerung“Jeder versiegelte Umschlag trägt ein Blatt bei, das kanonische JSON (Schlüssel sortiert, ohne Leerzeichen) von:
{ "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>"}Jede Stunde werden die seit dem vorherigen Anker erfassten Blätter zu einem Merkle-Baum (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), und seine Wurzel wird als signierter
Anker veröffentlicht:
{ "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"}Die Ed25519-Signatur erstreckt sich über bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root verkettet jeden Anker mit seinem Vorgänger (der erste verweist auf 64 Nullen), sodass sich ein
Anker nicht entfernen oder umsortieren lässt, ohne die Kette zu brechen; die Anker-Tabellen sind nur erweiterbar.
Die Signaturschlüssel werden mit ihren Gültigkeitszeiträumen veröffentlicht; außer Betrieb genommene Schlüssel bleiben aufgeführt, damit alte Anker prüfbar bleiben:
{ "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" } ]}Ein Dokument prüfen
Abschnitt betitelt „Ein Dokument prüfen“Am schnellsten geht es über die öffentliche Seite: bonafidesign.com/verify. Sie berechnet den Hash des PDFs in Ihrem Browser (die Datei wird nie hochgeladen), schlägt den Hash nach und prüft Nachweis und Signatur auf Ihrem Gerät. Das Anker-Protokoll listet alle Anker auf.
Dieselbe Abfrage gibt es als öffentliche API: kein Schlüssel, beliebiger Origin, Rate Limit pro IP. Senden Sie nur den 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 ist verified, pending_anchor (abgeschlossen und erfasst, wartet auf den nächsten stündlichen Anker)
oder not_found: Kein abgeschlossener Umschlag enthält genau diese Datei, es ist also nicht das unterschriebene
Dokument (oder es wurde seitdem verändert). Sowohl die versiegelte Datei als auch das Original im versendeten
Zustand werden gefunden.
Den Nachweis selbst prüfen
Abschnitt betitelt „Den Nachweis selbst prüfen“Verlassen Sie sich nicht auf das Wort der API: Alles, was Sie zum Prüfen der Antwort brauchen, steht in der Antwort
und in keys.json.
- Dokument: Der SHA-256 Ihrer Datei entspricht
leaf.sealed_document_sha256(oderoriginal_document_sha256). - Blatt:
SHA-256(0x00 ‖ canonical JSON of leaf)entsprichtleaf_hash. - Inklusion: Wenn Sie
proof.hashesausgehend vom Blatt-Hash durchlaufen (RFC 9162 §2.1.3.2, mitleaf_indexundtree_size == anchor.leaf_count), ergibt sichanchor.root. - Schlüssel:
anchor.key_idsteht inkeys.json, die Schlüssel-ID besteht aus den ersten 16 Hex-Zeichen des SHA-256 des rohen öffentlichen Schlüssels, undanchor.created_atliegt im Gültigkeitszeitraum des Schlüssels. - Signatur: Die Ed25519-Signatur ist über
bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_countgültig.
# 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']}")Das Blatt enthält nur String-Werte mit ASCII-Schlüsseln, daher ergibt json.dumps(…, sort_keys=True, separators=(",", ":"))
dasselbe kanonische JSON. Prüfen Sie außerdem valid_from / valid_until des Schlüssels gegen anchor["created_at"].
Der Referenzprüfer @bona-fide/verify ist isomorph (Browser und Node, keine Secrets von Bona Fide) und genau das,
was die Seite /verify ausführt:
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);}Das Paket enthält außerdem ein Kommandozeilenwerkzeug (bona-fide-verify <file.pdf | sha256> [--api URL] [--json];
aus einem Checkout des Bona Fide-Repositorys: pnpm --filter @bona-fide/verify cli signed.pdf). Es berechnet den
Hash der Datei lokal, prüft jeden der obigen Schritte und endet mit 0, wenn das Dokument verifiziert ist, mit 1,
wenn es nicht gefunden wurde oder ungültig ist, und mit 2, wenn es noch nicht verankert ist.
Prüfen Sie die Signatur eines Ankers mit dem PEM-Schlüssel aus 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 SuccessfullyAnker werden mit GET /v1/public/anchors?limit=20&before=<sequence> aufgelistet, die neuesten zuerst (übergeben
Sie next_before, um zurückzublättern), und einzeln mit GET /v1/public/anchors/{sequence} abgerufen.