Verifikasi
Dua bukti independen melindungi setiap amplop yang selesai:
- Segel, di dalam PDF: stempel waktu dokumen PAdES dari otoritas stempel waktu publik. Adobe Acrobat dan Reader memeriksanya dan menampilkan dokumen sebagai tidak berubah sejak disegel.
- Anchor, di luar PDF: sidik jari dokumen menjadi daun dalam pohon Merkle yang hanya dapat ditambah, dan Bona Fide menandatangani setiap akar pohon dengan Ed25519. Siapa pun dapat memeriksa bukti tersebut dengan kunci publik yang dipublikasikan, tanpa akun dan tanpa perlu memercayai penampil PDF.
Penyegelan
Section titled “Penyegelan”Saat penerima terakhir menandatangani, amplop selesai (envelope.completed) dan penyegel mengambil alih:
- Memeriksa SHA-256 dokumen asli terhadap nilai yang dicatat saat amplop dibuat.
- Meratakan dokumen: nilai kolom (teks, kotak centang, tanda tangan yang digambar, diketik, atau diunggah) digambar ke dalam konten halaman, dan setiap kolom formulir, anotasi, aksi JavaScript, dan file tersemat dihapus.
- Menambahkan sertifikat penyelesaian: id amplop, SHA-256 dokumen asli, setiap penanda tangan beserta metode verifikasi yang benar-benar digunakan (misalnya Tautan email (tautan unik dikirim ke …) atau Tautan email + kode sekali pakai, lihat verifikasi penanda tangan), stempel waktu, alamat IP dan perangkat, serta linimasa event. Setiap halaman mendapat footer Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Menambahkan stempel waktu dokumen PAdES (
/DocTimeStamp,ETSI.RFC3161, SHA-256) dari otoritas stempel waktu publik (beberapa penyedia yang tepercaya secara publik dengan failover otomatis), dan menyematkan data validasi rantai sertifikat TSA (OCSP/CRL di DSS) agar tetap dapat diverifikasi dalam jangka panjang (LTV). - Memvalidasi hasilnya sendiri, menyimpannya, dan mencatat SHA-256 yang disegel (
sealedDocumentHash) serta sebuah daun untuk log anchor.
Kemudian envelope.sealed dipicu dan GET /v1/envelopes/{id}/document?version=sealed mengembalikan file tersebut.
Jika semua TSA tidak tersedia, penyegelan dicoba ulang; sementara itu amplop tetap completed dengan
seal: "pending".
Anchoring
Section titled “Anchoring”Setiap amplop yang disegel menyumbang satu daun, yaitu JSON kanonis (kunci diurutkan, tanpa spasi) dari:
{ "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>"}Setiap jam, daun-daun yang dicatat sejak anchor sebelumnya menjadi pohon Merkle (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), dan akarnya dipublikasikan sebagai
anchor yang ditandatangani:
{ "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"}Tanda tangan Ed25519 mencakup bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root merangkai setiap anchor ke anchor sebelumnya (yang pertama menunjuk ke 64 angka nol), sehingga
sebuah anchor tidak dapat dihapus atau diurutkan ulang tanpa memutus rantai, dan tabel anchor hanya dapat ditambah.
Kunci penandatanganan dipublikasikan beserta masa berlakunya; kunci yang sudah dipensiunkan tetap tercantum agar anchor lama tetap dapat diverifikasi:
{ "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" } ]}Memverifikasi dokumen
Section titled “Memverifikasi dokumen”Cara tercepat adalah halaman publik: bonafidesign.com/verify. Halaman ini menghitung hash PDF di browser Anda (file tidak pernah diunggah), mencari hash tersebut, serta memeriksa bukti dan tanda tangannya di perangkat Anda. Log anchor mencantumkan setiap anchor.
Pencarian yang sama tersedia sebagai API publik: tanpa kunci, dari origin mana pun, dibatasi lajunya per IP. Kirim SHA-256-nya saja:
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 bernilai verified, pending_anchor (sudah selesai dan dicatat, menunggu anchor per jam berikutnya), atau
not_found: tidak ada amplop selesai yang memiliki file persis ini, sehingga file tersebut bukan dokumen yang
ditandatangani (atau telah diubah sejak itu). File yang disegel maupun dokumen asli seperti saat dikirim sama-sama
cocok.
Memeriksa bukti sendiri
Section titled “Memeriksa bukti sendiri”Jangan hanya percaya pada jawaban API: semua yang diperlukan untuk memeriksa jawabannya ada di dalam respons dan di
keys.json.
- Dokumen: SHA-256 file Anda sama dengan
leaf.sealed_document_sha256(atauoriginal_document_sha256). - Daun:
SHA-256(0x00 ‖ canonical JSON of leaf)sama denganleaf_hash. - Inklusi: menelusuri
proof.hashesdari hash daun (RFC 9162 §2.1.3.2, denganleaf_indexdantree_size == anchor.leaf_count) menghasilkananchor.root. - Kunci:
anchor.key_idada dikeys.json, id kunci adalah 16 karakter heksadesimal pertama dari SHA-256 kunci publik mentah, dananchor.created_atberada dalam masa berlaku kunci tersebut. - Tanda tangan: tanda tangan Ed25519 terverifikasi atas
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']}")Daun hanya berisi nilai string dengan kunci ASCII, sehingga json.dumps(…, sort_keys=True, separators=(",", ":"))
menghasilkan JSON kanonis yang sama. Periksa juga valid_from / valid_until kunci terhadap anchor["created_at"].
Verifier referensi, @bona-fide/verify, bersifat isomorfik (browser dan Node, tanpa secret Bona Fide) dan inilah
yang dijalankan halaman /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);}Paket ini juga memiliki pemeriksa baris perintah (bona-fide-verify <file.pdf | sha256> [--api URL] [--json]; dari
checkout repositori Bona Fide: pnpm --filter @bona-fide/verify cli signed.pdf). Alat ini menghitung hash file
secara lokal, memeriksa setiap langkah di atas, dan keluar dengan 0 jika terverifikasi, 1 jika tidak ditemukan
atau tidak valid, dan 2 jika dokumen belum di-anchor.
Periksa tanda tangan anchor dengan kunci PEM dari 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 SuccessfullyAnchor ditampilkan dari yang terbaru dengan GET /v1/public/anchors?limit=20&before=<sequence> (teruskan
next_before untuk membuka halaman sebelumnya) dan diambil satu per satu dengan GET /v1/public/anchors/{sequence}.