跳转到内容

验证

每个已完成的信封都受到两项相互独立的证明保护:

  1. 封印,位于 PDF 内部:由公共时间戳机构签发的 PAdES 文档时间戳。Adobe Acrobat 和 Reader 会检查它,并显示该文档自封存以来未被修改。
  2. 锚点,位于 PDF 外部:文档的指纹是一棵仅追加的 Merkle 树中的叶,Bona Fide 用 Ed25519 对每个树根签名。任何人都可以用公布的公钥检查该证明,既不需要账户,也不需要信任某个 PDF 阅读器。

当最后一位收件人签署后,信封即告完成(envelope.completed),由封存程序接手:

  1. 将原始文档的 SHA-256 与创建信封时记录的值进行比对。
  2. 扁平化文档:字段值(文本、复选框、手写、键入或上传的签名)被绘制到页面内容中,所有表单字段、注释、JavaScript 动作和嵌入文件都会被移除。
  3. 附加完成证书:信封 ID、原始文档的 SHA-256、每位签署人及其实际使用的验证方式(例如 电子邮件链接(唯一链接已发送至 …) 或 电子邮件链接 + 一次性验证码,请参阅签署人验证)、时间戳、IP 地址和设备,以及事件时间线。每一页都会加上页脚 Bona Fide Envelope ID … · Verify at bonafidesign.com/verify。
  4. 添加由公共时间戳机构签发的 PAdES 文档时间戳(/DocTimeStamp、ETSI.RFC3161、SHA-256)(由多家公开受信任的服务商提供,可自动故障转移),并嵌入 TSA 证书链的验证数据(DSS 中的 OCSP/CRL),使其可长期验证(LTV)。
  5. 验证自身的输出并加以存储,记录封存后的 SHA-256(sealedDocumentHash)以及一个用于锚点日志的叶。

随后触发 envelope.sealed,GET /v1/envelopes/{id}/document?version=sealed 即可返回该文件。如果所有 TSA 都不可用,封存会重试;在此期间,信封保持 completed 状态,且 seal: "pending"。

每个已封存的信封贡献一个叶,即以下内容的规范化 JSON(键已排序,无空白):

{
"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>"
}

每小时,自上一个锚点以来记录的叶会构成一棵 Merkle 树(RFC 6962 / RFC 9162:leaf = SHA-256(0x00 ‖ leaf JSON),node = SHA-256(0x01 ‖ left ‖ right)),其树根作为经过签名的锚点发布:

GET /v1/public/anchors/latest
{
"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 签名覆盖的内容为 bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count(UTF-8)。previous_root 将每个锚点与前一个锚点链接起来(第一个锚点指向 64 个零),因此任何锚点都无法在不破坏链的情况下被删除或重新排序,而且锚点表是仅追加的。

签名密钥连同其有效期一并公布;已停用的密钥仍会保留在列表中,以确保旧锚点依然可以验证:

GET /.well-known/bona-fide/keys.json
{
"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"
}
]
}

最快的方式是使用公开页面:bonafidesign.com/verify。它会在您的浏览器中计算 PDF 的哈希(文件绝不会被上传),查询该哈希,并在您的设备上检查证明和签名。锚点日志列出了所有锚点。

同样的查询也以公开 API 的形式提供:无需密钥,允许任意源,按 IP 限制速率。只需发送 SHA-256:

Terminal window
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 为 verified、pending_anchor(已完成并已记录,正在等待下一个整点锚点)或 not_found:没有任何已完成的信封包含这个完全相同的文件,因此它不是被签署的那份文档(或者此后已被修改)。已封存的文件和发送时的原始文件都能匹配。

不必轻信 API 的结论:检查结果所需的一切都在响应和 keys.json 中。

  1. 文档:您文件的 SHA-256 等于 leaf.sealed_document_sha256(或 original_document_sha256)。
  2. 叶:SHA-256(0x00 ‖ canonical JSON of leaf) 等于 leaf_hash。
  3. 包含:从叶哈希出发遍历 proof.hashes(RFC 9162 §2.1.3.2,使用 leaf_index 和 tree_size == anchor.leaf_count),得到 anchor.root。
  4. 密钥:anchor.key_id 位于 keys.json 中,密钥 ID 是原始公钥 SHA-256 值的前 16 个十六进制字符,并且 anchor.created_at 位于该密钥的有效期内。
  5. 签名:Ed25519 签名对 bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count 验证通过。
# pip install requests cryptography
import base64, hashlib, json, sys, requests
from 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']}")

叶中只有字符串值和 ASCII 键,因此 json.dumps(…, sort_keys=True, separators=(",", ":")) 能得到相同的规范化 JSON。同时还要将密钥的 valid_from / valid_until 与 anchor["created_at"] 进行比对。

锚点按从新到旧的顺序通过 GET /v1/public/anchors?limit=20&before=<sequence> 列出(传入 next_before 可向前翻页),也可以通过 GET /v1/public/anchors/{sequence} 逐个获取。