דלגו לתוכן

אימות

שתי הוכחות בלתי תלויות מגנות על כל מעטפה שהושלמה:

  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:

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, מזהה המפתח הוא 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}.