コンテンツにスキップ

検証

完了したすべてのエンベロープは、互いに独立した 2 つの証明によって保護されています。

  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 が 利用できない場合、封印はリトライされます。その間、エンベロープは seal: "pending" の状態で completed のままです。

封印された各エンベロープは 1 つのリーフを提供します。リーフは、次の内容の正規化 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>"
}

1 時間ごとに、前回のアンカー以降に記録されたリーフから 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 だけです。

ターミナルウィンドウ
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(完了して記録済みで、次の 1 時間ごとのアンカーを待っている状態)、または 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 桁の 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} で 1 件ずつ取得できます。