تخطَّ إلى المحتوى

التحقق

يحمي كل مظروف مكتمل إثباتان مستقلان:

  1. الختم، داخل ملف PDF: طابع زمني للمستند وفق PAdES من جهة طوابع زمنية عامة. يتحقق منه Adobe Acrobat وReader ويعرضان المستند على أنه لم يُعدَّل منذ ختمه.
  2. المرساة، خارج ملف PDF: بصمة المستند ورقة في شجرة Merkle تقبل الإضافة فقط، وتوقّع Bona Fide كل جذر للشجرة بـ Ed25519. يمكن لأي شخص التحقق من هذا الإثبات بالمفاتيح العامة المنشورة، دون حساب ودون الحاجة إلى الثقة في عارض PDF.

عندما يوقّع آخر مستلم، يكتمل المظروف (envelope.completed) ويتولى نظام الختم المهمة:

  1. يقارن تجزئة SHA-256 للأصل بتلك المسجلة عند إنشاء المظروف.
  2. يدمج المستند في طبقة واحدة: تُرسم قيم الحقول (النصوص، ومربعات الاختيار، والتوقيعات المرسومة أو المكتوبة أو المرفوعة) في محتوى الصفحة، ويُزال كل حقل نموذج وكل تعليق توضيحي وكل إجراء JavaScript وكل ملف مضمَّن.
  3. يُلحق شهادة الإتمام: معرّف المظروف، وتجزئة SHA-256 للأصل، وكل موقِّع مع طريقة التحقق التي استُخدمت فعليًا (مثل رابط البريد الإلكتروني (رابط فريد أُرسل إلى …) أو رابط البريد الإلكتروني + رمز لمرة واحدة، راجع التحقق من الموقِّع)، والطوابع الزمنية، وعنوان IP والجهاز، والتسلسل الزمني للأحداث. وتحصل كل صفحة على التذييل Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. يضيف طابعًا زمنيًا للمستند وفق PAdES (/DocTimeStamp، ETSI.RFC3161، SHA-256) من جهة طوابع زمنية عامة (عدة مزوّدين موثوقين علنًا مع تحويل تلقائي عند الفشل)، ويضمّن بيانات التحقق الخاصة بسلسلة شهادات جهة الطوابع الزمنية (TSA) (OCSP/CRL في DSS) كي يبقى قابلًا للتحقق على المدى الطويل (LTV).
  5. يتحقق من صحة مخرجاته، ويخزّنها، ويسجّل تجزئة SHA-256 للملف المختوم (sealedDocumentHash) وورقة لسجل المراسي.

بعد ذلك يُطلَق envelope.sealed ويعيد GET /v1/envelopes/{id}/document?version=sealed الملف. وإذا كانت كل جهات الطوابع الزمنية غير متاحة، تُعاد محاولة الختم؛ ويبقى المظروف 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 فقط:

نافذة طرفيّة
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.

  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، ومعرّف المفتاح هو أول 16 حرفًا ست عشريًا من تجزئة SHA-256 للمفتاح العام الخام، و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}.