التحقق
يحمي كل مظروف مكتمل إثباتان مستقلان:
- الختم، داخل ملف PDF: طابع زمني للمستند وفق PAdES من جهة طوابع زمنية عامة. يتحقق منه Adobe Acrobat وReader ويعرضان المستند على أنه لم يُعدَّل منذ ختمه.
- المرساة، خارج ملف PDF: بصمة المستند ورقة في شجرة Merkle تقبل الإضافة فقط، وتوقّع Bona Fide كل جذر للشجرة بـ Ed25519. يمكن لأي شخص التحقق من هذا الإثبات بالمفاتيح العامة المنشورة، دون حساب ودون الحاجة إلى الثقة في عارض PDF.
عندما يوقّع آخر مستلم، يكتمل المظروف (envelope.completed) ويتولى نظام الختم المهمة:
- يقارن تجزئة SHA-256 للأصل بتلك المسجلة عند إنشاء المظروف.
- يدمج المستند في طبقة واحدة: تُرسم قيم الحقول (النصوص، ومربعات الاختيار، والتوقيعات المرسومة أو المكتوبة أو المرفوعة) في محتوى الصفحة، ويُزال كل حقل نموذج وكل تعليق توضيحي وكل إجراء JavaScript وكل ملف مضمَّن.
- يُلحق شهادة الإتمام: معرّف المظروف، وتجزئة SHA-256 للأصل، وكل موقِّع مع طريقة التحقق التي استُخدمت فعليًا (مثل رابط البريد الإلكتروني (رابط فريد أُرسل إلى …) أو رابط البريد الإلكتروني + رمز لمرة واحدة، راجع التحقق من الموقِّع)، والطوابع الزمنية، وعنوان IP والجهاز، والتسلسل الزمني للأحداث. وتحصل كل صفحة على التذييل Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- يضيف طابعًا زمنيًا للمستند وفق PAdES (
/DocTimeStamp،ETSI.RFC3161، SHA-256) من جهة طوابع زمنية عامة (عدة مزوّدين موثوقين علنًا مع تحويل تلقائي عند الفشل)، ويضمّن بيانات التحقق الخاصة بسلسلة شهادات جهة الطوابع الزمنية (TSA) (OCSP/CRL في DSS) كي يبقى قابلًا للتحقق على المدى الطويل (LTV). - يتحقق من صحة مخرجاته، ويخزّنها، ويسجّل تجزئة SHA-256 للملف المختوم (
sealedDocumentHash) وورقة لسجل المراسي.
بعد ذلك يُطلَق envelope.sealed ويعيد GET /v1/envelopes/{id}/document?version=sealed الملف. وإذا كانت كل جهات الطوابع الزمنية
غير متاحة، تُعاد محاولة الختم؛ ويبقى المظروف completed مع seal: "pending" في الأثناء.
الإرساء
Section titled “الإرساء”يُسهم كل مظروف مختوم بـ ورقة واحدة، هي 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 كل مرساة بالتي قبلها (وتشير الأولى إلى 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" } ]}التحقق من مستند
Section titled “التحقق من مستند”أسرع طريقة هي الصفحة العامة: 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: لا يوجد مظروف مكتمل لهذا الملف بعينه، لذا فهو ليس المستند الذي وُقِّع (أو أنه
عُدِّل منذ ذلك الحين). ويتطابق كل من الملف المختوم والأصل كما أُرسل.
التحقق من الإثبات بنفسك
Section titled “التحقق من الإثبات بنفسك”لا تكتفِ بكلمة الـ 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، ومعرّف المفتاح هو أول 16 حرفًا ست عشريًا من تجزئة SHA-256 للمفتاح العام الخام، و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 عندما لا يكون المستند
قد أُرسي بعد.
تحقّق من توقيع مرساة باستخدام مفتاح PEM من 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 Successfullyتُسرد المراسي من الأحدث إلى الأقدم باستخدام GET /v1/public/anchors?limit=20&before=<sequence> (مرّر next_before للرجوع
صفحةً إلى الخلف)، وتُجلب واحدة تلو الأخرى باستخدام GET /v1/public/anchors/{sequence}.