Weryfikacja
Każdą zakończoną kopertę chronią dwa niezależne dowody:
- Pieczęć wewnątrz pliku PDF: znacznik czasu dokumentu PAdES od publicznego urzędu znacznika czasu. Adobe Acrobat i Reader sprawdzają go i pokazują, że dokument nie został zmieniony od chwili zapieczętowania.
- Kotwica poza plikiem PDF: odcisk dokumentu jest liściem w drzewie Merkle, do którego można wyłącznie dopisywać, a Bona Fide podpisuje każdy korzeń drzewa kluczem Ed25519. Każdy może sprawdzić ten dowód za pomocą opublikowanych kluczy publicznych – bez konta i bez konieczności ufania przeglądarce plików PDF.
Pieczętowanie
Dział zatytułowany „Pieczętowanie”Gdy podpisze ostatni odbiorca, koperta zostaje zakończona (envelope.completed), a dalsze działania przejmuje mechanizm pieczętujący:
- Porównuje SHA-256 oryginału z wartością zapisaną przy tworzeniu koperty.
- Spłaszcza dokument: wartości pól (tekst, pola wyboru, podpisy narysowane, wpisane lub przesłane) są wrysowywane w treść strony, a każde pole formularza, adnotacja, akcja JavaScript i osadzony plik są usuwane.
- Dołącza certyfikat ukończenia: identyfikator koperty, SHA-256 oryginału, każdą osobę podpisującą wraz z faktycznie użytą metodą weryfikacji (na przykład Link e-mail (unikalny link wysłany na …) lub Link e-mail + jednorazowy kod, zobacz weryfikację osoby podpisującej), znaczniki czasu, adres IP i urządzenie oraz oś czasu zdarzeń. Każda strona otrzymuje stopkę Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Dodaje znacznik czasu dokumentu PAdES (
/DocTimeStamp,ETSI.RFC3161, SHA-256) od publicznego urzędu znacznika czasu (kilku publicznie zaufanych dostawców z automatycznym przełączaniem awaryjnym) i osadza dane walidacyjne łańcucha certyfikatów TSA (OCSP/CRL w DSS), dzięki czemu dokument pozostaje weryfikowalny długoterminowo (LTV). - Waliduje własny wynik, zapisuje go i rejestruje SHA-256 zapieczętowanego pliku (
sealedDocumentHash) oraz liść dla dziennika kotwic.
Następnie wywoływane jest envelope.sealed, a GET /v1/envelopes/{id}/document?version=sealed zwraca plik. Jeśli żaden urząd TSA
nie jest dostępny, pieczętowanie jest ponawiane; w tym czasie koperta pozostaje w stanie completed z seal: "pending".
Kotwiczenie
Dział zatytułowany „Kotwiczenie”Każda zapieczętowana koperta dodaje jeden liść – kanoniczny JSON (klucze posortowane, bez białych znaków) z:
{ "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>"}Co godzinę liście zarejestrowane od poprzedniej kotwicy tworzą drzewo Merkle (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), a jego korzeń jest publikowany jako podpisana
kotwica:
{ "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"}Podpis Ed25519 obejmuje bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root łączy każdą kotwicę z poprzednią (pierwsza wskazuje na 64 zera), więc kotwicy nie da się
usunąć ani przestawić bez przerwania łańcucha, a tabele kotwic pozwalają wyłącznie na dopisywanie.
Klucze podpisujące są publikowane wraz z okresami ważności; wycofane klucze pozostają na liście, aby stare kotwice dało się nadal zweryfikować:
{ "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" } ]}Weryfikacja dokumentu
Dział zatytułowany „Weryfikacja dokumentu”Najszybciej jest skorzystać z publicznej strony: bonafidesign.com/verify. Oblicza ona skrót pliku PDF w Twojej przeglądarce (plik nigdy nie jest przesyłany), wyszukuje go oraz sprawdza dowód i podpis na Twoim urządzeniu. Dziennik kotwic zawiera listę wszystkich kotwic.
To samo wyszukiwanie jest dostępne jako publiczne API: bez klucza, z dowolnego źródła, z limitem żądań na adres IP. Wyślij tylko 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 to verified, pending_anchor (zakończona i zarejestrowana, czeka na następną cogodzinną kotwicę) lub
not_found: żadna zakończona koperta nie zawiera dokładnie tego pliku, więc nie jest to podpisany dokument (albo został
od tego czasu zmodyfikowany). Dopasowanie działa zarówno dla zapieczętowanego pliku, jak i dla oryginału w postaci wysłanej.
Samodzielne sprawdzenie dowodu
Dział zatytułowany „Samodzielne sprawdzenie dowodu”Nie musisz wierzyć API na słowo: wszystko, co potrzebne do sprawdzenia odpowiedzi, znajduje się w odpowiedzi i w keys.json.
- Dokument: SHA-256 Twojego pliku jest równe
leaf.sealed_document_sha256(luboriginal_document_sha256). - Liść:
SHA-256(0x00 ‖ canonical JSON of leaf)jest równeleaf_hash. - Zawieranie: przejście przez
proof.hashesod skrótu liścia (RFC 9162 §2.1.3.2, zleaf_indexitree_size == anchor.leaf_count) dajeanchor.root. - Klucz:
anchor.key_idznajduje się wkeys.json, identyfikator klucza to pierwsze 16 znaków szesnastkowych SHA-256 surowego klucza publicznego, aanchor.created_atmieści się w okresie ważności klucza. - Podpis: podpis Ed25519 weryfikuje się względem
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']}")Liść zawiera wyłącznie wartości tekstowe z kluczami ASCII, więc json.dumps(…, sort_keys=True, separators=(",", ":")) daje
ten sam kanoniczny JSON. Sprawdź także valid_from / valid_until klucza względem anchor["created_at"].
Referencyjny weryfikator, @bona-fide/verify, jest izomorficzny (przeglądarka i Node, bez żadnych sekretów Bona Fide) i to on działa
na stronie /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);}Pakiet zawiera też narzędzie wiersza poleceń (bona-fide-verify <file.pdf | sha256> [--api URL] [--json]; z
kopii repozytorium Bona Fide: pnpm --filter @bona-fide/verify cli signed.pdf). Oblicza ono skrót pliku lokalnie,
sprawdza każdy z powyższych kroków i kończy działanie z kodem 0 po pomyślnej weryfikacji, 1, gdy plik nie został znaleziony lub dowód jest nieprawidłowy, oraz 2, gdy dokument
nie jest jeszcze zakotwiczony.
Sprawdź podpis kotwicy za pomocą klucza PEM z 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 SuccessfullyKotwice są wyświetlane od najnowszej za pomocą GET /v1/public/anchors?limit=20&before=<sequence> (przekaż next_before, aby
cofnąć się o stronę), a pojedynczo pobiera się je przez GET /v1/public/anchors/{sequence}.