검증
완료된 모든 봉투는 서로 독립적인 두 가지 증명으로 보호됩니다.
- PDF 안에 있는 봉인: 공개 타임스탬프 기관의 PAdES 문서 타임스탬프입니다. Adobe Acrobat과 Reader가 이를 확인하고, 봉인된 이후 문서가 수정되지 않았음을 표시합니다.
- PDF 밖에 있는 앵커: 문서의 지문은 추가 전용 Merkle 트리의 리프이며, Bona Fide는 각 트리 루트에 Ed25519로 서명합니다. 누구나 공개된 공개 키로 이 증명을 확인할 수 있으며, 계정도, PDF 뷰어에 대한 신뢰도 필요 없습니다.
마지막 수신자가 서명하면 봉투가 완료되고(envelope.completed) 봉인 처리기가 작업을 이어받습니다.
- 원본의 SHA-256을 봉투를 만들 때 기록한 값과 대조합니다.
- 문서를 평탄화합니다. 필드 값(텍스트, 체크박스, 그리거나 입력하거나 업로드한 서명)을 페이지 콘텐츠에 그려 넣고, 모든 양식 필드, 주석, JavaScript 동작, 포함된 파일을 제거합니다.
- 완료 인증서를 덧붙입니다. 봉투 ID, 원본의 SHA-256, 실제로 사용된 인증 방식과 함께 모든 서명자 (예: 이메일 링크(…로 보낸 고유 링크) 또는 이메일 링크 + 일회용 코드. 서명자 인증 참조), 타임스탬프, IP 주소와 기기, 이벤트 타임라인이 기재됩니다. 모든 페이지에는 바닥글 Bona Fide Envelope ID … · Verify at bonafidesign.com/verify가 들어갑니다.
- 공개 타임스탬프 기관(자동 장애 조치를 지원하는, 공개적으로 신뢰받는 여러 제공업체)의 PAdES 문서 타임스탬프
(
/DocTimeStamp,ETSI.RFC3161, SHA-256)를 추가하고, TSA 인증서 체인의 검증 데이터(DSS의 OCSP/CRL)를 포함해 장기간 검증할 수 있도록(LTV) 합니다. - 자체 출력을 검증하고 저장한 뒤, 봉인된 SHA-256(
sealedDocumentHash)과 앵커 로그용 리프를 기록합니다.
그런 다음 envelope.sealed가 발생하고 GET /v1/envelopes/{id}/document?version=sealed가 파일을 반환합니다. 모든 TSA를
사용할 수 없으면 봉인을 재시도하며, 그동안 봉투는 seal: "pending"인 completed 상태로 유지됩니다.
앵커링
섹션 제목: “앵커링”봉인된 각 봉투는 하나의 리프를 제공합니다. 리프는 다음 내용의 정규화된 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))를 만들고, 그 루트를 서명된
앵커로 게시합니다.
{ "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는 모든 앵커를 직전 앵커와 체인으로 연결하므로(첫 번째 앵커는 0이 64개인 값을 가리킴), 체인을 깨뜨리지 않고는
앵커를 제거하거나 순서를 바꿀 수 없으며, 앵커 테이블은 추가 전용입니다.
서명 키는 유효 기간과 함께 공개됩니다. 사용이 끝난 키도 목록에 남아 있으므로 오래된 앵커도 계속 검증할 수 있습니다.
{ "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(완료되어 기록되었고 다음 정시 앵커를 기다리는 중), 또는
not_found(이 파일과 정확히 같은 파일을 가진 완료된 봉투가 없음. 즉, 서명된 문서가 아니거나 이후에 수정되었음) 중 하나입니다.
봉인된 파일과 발송 당시의 원본 모두 일치 대상입니다.
증명 직접 확인하기
섹션 제목: “증명 직접 확인하기”API의 답을 그대로 믿을 필요는 없습니다. 답을 확인하는 데 필요한 모든 것이 응답과 keys.json에 들어 있습니다.
- 문서: 파일의 SHA-256이
leaf.sealed_document_sha256(또는original_document_sha256)과 같습니다. - 리프:
SHA-256(0x00 ‖ canonical JSON of leaf)가leaf_hash와 같습니다. - 포함: 리프 해시에서 시작해
proof.hashes를 따라가면(RFC 9162 §2.1.3.2,leaf_index와tree_size == anchor.leaf_count사용)anchor.root가 나옵니다. - 키:
anchor.key_id가keys.json에 있고, 키 ID가 원시 공개 키의 SHA-256 앞 16자리 16진수 문자이며,anchor.created_at이 키의 유효 기간 안에 있습니다. - 서명: Ed25519 서명이
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']}")리프는 ASCII 키와 문자열 값만 가지므로 json.dumps(…, sort_keys=True, separators=(",", ":"))로
같은 정규화된 JSON을 얻을 수 있습니다. 키의 valid_from / valid_until도 anchor["created_at"]과 비교해 확인하세요.
참조 검증 도구인 @bona-fide/verify는 아이소모픽(브라우저와 Node 모두 지원, Bona Fide 시크릿 불필요)이며,
/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);}이 패키지에는 명령줄 검사 도구도 있습니다(bona-fide-verify <file.pdf | sha256> [--api URL] [--json].
Bona Fide 저장소를 체크아웃했다면 pnpm --filter @bona-fide/verify cli signed.pdf). 파일의 해시를 로컬에서 계산하고,
위의 모든 단계를 확인한 뒤, 검증되면 0, 찾을 수 없거나 유효하지 않으면 1, 문서가 아직
앵커링되지 않았으면 2로 종료합니다.
keys.json의 PEM 키로 앵커의 서명을 확인합니다(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 Successfully앵커는 GET /v1/public/anchors?limit=20&before=<sequence>로 최신순 목록을 조회하고(이전 페이지로 가려면 next_before를
전달), GET /v1/public/anchors/{sequence}로 하나씩 가져올 수 있습니다.