Xác minh
Hai bằng chứng độc lập bảo vệ mọi phong bì đã hoàn tất:
- Niêm phong, bên trong PDF: một dấu thời gian tài liệu PAdES từ một tổ chức cấp dấu thời gian công khai. Adobe Acrobat và Reader kiểm tra dấu này và cho thấy tài liệu chưa bị sửa đổi kể từ khi được niêm phong.
- Neo, bên ngoài PDF: dấu vân tay của tài liệu là một lá trong cây Merkle chỉ ghi thêm, và Bona Fide ký mỗi gốc của cây bằng Ed25519. Bất kỳ ai cũng có thể kiểm tra bằng chứng đó với các khóa công khai đã công bố, không cần tài khoản và không cần tin tưởng vào trình xem PDF nào.
Niêm phong
Phần tiêu đề “Niêm phong”Khi người nhận cuối cùng ký, phong bì hoàn tất (envelope.completed) và bộ niêm phong tiếp quản:
- Đối chiếu SHA-256 của bản gốc với giá trị đã ghi lại khi phong bì được tạo.
- Làm phẳng tài liệu: giá trị của các trường (văn bản, hộp kiểm, chữ ký được vẽ, gõ hoặc tải lên) được vẽ vào nội dung trang, và mọi trường biểu mẫu, chú thích, hành động JavaScript và tệp nhúng đều bị xóa.
- Nối thêm chứng nhận hoàn tất: id phong bì, SHA-256 của bản gốc, mọi người ký cùng phương thức xác minh thực sự đã dùng (ví dụ Liên kết email (liên kết duy nhất gửi đến …) hoặc Liên kết email + mã dùng một lần, xem xác minh người ký), dấu thời gian, địa chỉ IP và thiết bị, cùng dòng thời gian sự kiện. Mọi trang đều có chân trang Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Thêm dấu thời gian tài liệu PAdES (
/DocTimeStamp,ETSI.RFC3161, SHA-256) từ một tổ chức cấp dấu thời gian công khai (nhiều nhà cung cấp được tin cậy công khai, tự động chuyển đổi dự phòng), và nhúng dữ liệu xác thực của chuỗi chứng chỉ của TSA (OCSP/CRL trong DSS) để có thể xác minh lâu dài (LTV). - Tự kiểm tra kết quả đầu ra, lưu trữ, và ghi lại SHA-256 đã niêm phong (
sealedDocumentHash) cùng một lá cho nhật ký neo.
Sau đó envelope.sealed được phát và GET /v1/envelopes/{id}/document?version=sealed trả về tệp. Nếu mọi TSA
đều không khả dụng, việc niêm phong sẽ được thử lại; trong lúc đó phong bì vẫn ở trạng thái completed với seal: "pending".
Mỗi phong bì đã niêm phong đóng góp một lá, là JSON chuẩn tắc (khóa được sắp xếp, không có khoảng trắng) của:
{ "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>"}Mỗi giờ, các lá được ghi lại kể từ neo trước trở thành một cây Merkle (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), và gốc của cây được công bố dưới dạng một
neo có chữ ký:
{ "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"}Chữ ký Ed25519 bao trùm bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root liên kết mỗi neo với neo trước nó (neo đầu tiên trỏ đến 64 số không), nên không thể xóa hay
sắp xếp lại một neo mà không phá vỡ chuỗi, và các bảng neo chỉ cho phép ghi thêm.
Các khóa ký được công bố cùng thời hạn hiệu lực; các khóa đã ngừng dùng vẫn được liệt kê để các neo cũ vẫn xác minh được:
{ "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" } ]}Xác minh tài liệu
Phần tiêu đề “Xác minh tài liệu”Cách nhanh nhất là trang công khai: bonafidesign.com/verify. Trang này tính giá trị băm của tệp PDF trong trình duyệt của bạn (tệp không bao giờ được tải lên), tra cứu giá trị băm, và kiểm tra bằng chứng cùng chữ ký ngay trên thiết bị của bạn. Nhật ký neo liệt kê mọi neo.
Phép tra cứu tương tự cũng là một API công khai: không cần khóa, mọi origin, giới hạn tần suất theo IP. Chỉ cần gửi 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 là verified, pending_anchor (đã hoàn tất và được ghi lại, đang chờ neo hằng giờ tiếp theo) hoặc
not_found: không có phong bì đã hoàn tất nào chứa đúng tệp này, nên đây không phải tài liệu đã được ký (hoặc tệp đã bị
sửa đổi kể từ đó). Cả tệp đã niêm phong lẫn bản gốc như khi gửi đều khớp được.
Tự kiểm tra bằng chứng
Phần tiêu đề “Tự kiểm tra bằng chứng”Đừng chỉ tin vào lời của API: mọi thứ cần thiết để kiểm tra câu trả lời đều có trong phản hồi và trong keys.json.
- Tài liệu: SHA-256 của tệp của bạn bằng
leaf.sealed_document_sha256(hoặcoriginal_document_sha256). - Lá:
SHA-256(0x00 ‖ canonical JSON of leaf)bằngleaf_hash. - Bao hàm: đi qua
proof.hashesbắt đầu từ giá trị băm của lá (RFC 9162 §2.1.3.2, vớileaf_indexvàtree_size == anchor.leaf_count) cho raanchor.root. - Khóa:
anchor.key_idcó trongkeys.json, id khóa là 16 ký tự thập lục phân đầu tiên của SHA-256 của khóa công khai thô, vàanchor.created_atnằm trong thời hạn hiệu lực của khóa. - Chữ ký: chữ ký Ed25519 hợp lệ trên
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']}")Lá chỉ chứa các giá trị chuỗi với khóa ASCII, nên json.dumps(…, sort_keys=True, separators=(",", ":")) cho ra
cùng một JSON chuẩn tắc. Hãy kiểm tra thêm valid_from / valid_until của khóa so với anchor["created_at"].
Trình xác minh tham chiếu, @bona-fide/verify, là isomorphic (chạy trên trình duyệt và Node, không cần secret nào của Bona Fide) và chính là thứ mà
trang /verify chạy:
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);}Gói này cũng có công cụ kiểm tra dòng lệnh (bona-fide-verify <file.pdf | sha256> [--api URL] [--json]; từ một
bản checkout của kho mã Bona Fide: pnpm --filter @bona-fide/verify cli signed.pdf). Công cụ tính giá trị băm của tệp ngay trên máy,
kiểm tra mọi bước ở trên và thoát với mã 0 khi đã xác minh, 1 khi không tìm thấy hoặc không hợp lệ, và 2 khi tài liệu
chưa được neo.
Kiểm tra chữ ký của một neo bằng khóa PEM từ 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 SuccessfullyCác neo được liệt kê từ mới đến cũ bằng GET /v1/public/anchors?limit=20&before=<sequence> (truyền next_before để
xem trang trước) và được lấy từng cái bằng GET /v1/public/anchors/{sequence}.