Hoppa till innehåll

Verifiering

Två oberoende bevis skyddar varje slutfört kuvert:

  1. Förseglingen, inuti PDF:en: en PAdES-dokumenttidsstämpel från en offentlig tidsstämpelutfärdare. Adobe Acrobat och Reader kontrollerar den och visar att dokumentet inte har ändrats sedan det förseglades.
  2. Ankaret, utanför PDF:en: dokumentets fingeravtryck är ett löv i ett Merkle-träd där poster bara kan läggas till, och Bona Fide signerar varje trädrot med Ed25519. Vem som helst kan kontrollera beviset med de publicerade publika nycklarna, utan konto och utan att behöva lita på en PDF-läsare.

När den sista mottagaren signerar slutförs kuvertet (envelope.completed) och förseglingstjänsten tar över:

  1. Jämför originalets SHA-256 med den som registrerades när kuvertet skapades.
  2. Plattar till dokumentet: fältvärden (text, kryssrutor, ritade, skrivna eller uppladdade signaturer) ritas in i sidinnehållet, och alla formulärfält, anteckningar, JavaScript-åtgärder och inbäddade filer tas bort.
  3. Bifogar slutförandecertifikatet: kuvertets id, originalets SHA-256, varje undertecknare med den verifieringsmetod som faktiskt användes (till exempel E-postlänk (unik länk skickad till …) eller E-postlänk + engångskod, se verifiering av undertecknare), tidsstämplar, IP-adress och enhet samt händelseförloppet. Varje sida får sidfoten Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. Lägger till en PAdES-dokumenttidsstämpel (/DocTimeStamp, ETSI.RFC3161, SHA-256) från en offentlig tidsstämpelutfärdare (flera allmänt betrodda leverantörer med automatisk reservväxling) och bäddar in valideringsdata för tidsstämpelutfärdarens certifikatkedja (OCSP/CRL i DSS) så att den förblir verifierbar på lång sikt (LTV).
  5. Validerar sitt eget resultat, lagrar det och registrerar den förseglade filens SHA-256 (sealedDocumentHash) och ett löv för ankarloggen.

Sedan utlöses envelope.sealed och GET /v1/envelopes/{id}/document?version=sealed returnerar filen. Om ingen tidsstämpelutfärdare är tillgänglig görs förseglingen om; kuvertet förblir completed med seal: "pending" under tiden.

Varje förseglat kuvert bidrar med ett löv, den kanoniska JSON-representationen (sorterade nycklar, inga blanksteg) av:

{
"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>"
}

Varje timme blir de löv som registrerats sedan föregående ankare ett Merkle-träd (RFC 6962 / RFC 9162: leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), och dess rot publiceras som ett signerat ankare:

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-signaturen omfattar bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8). previous_root kedjar varje ankare till det föregående (det första pekar på 64 nollor), så ett ankare kan inte tas bort eller flyttas utan att kedjan bryts, och ankartabellerna tillåter bara tillägg.

Signeringsnycklarna publiceras med sina giltighetsperioder; avvecklade nycklar finns kvar i listan så att gamla ankare förblir verifierbara:

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"
}
]
}

Det snabbaste sättet är den offentliga sidan: bonafidesign.com/verify. Den beräknar PDF:ens hash i din webbläsare (filen laddas aldrig upp), slår upp hashen och kontrollerar beviset och signaturen på din enhet. Ankarloggen listar alla ankare.

Samma uppslag finns som ett offentligt API: ingen nyckel, valfritt ursprung, hastighetsbegränsat per IP-adress. Skicka bara 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 är verified, pending_anchor (slutfört och registrerat, väntar på nästa ankare varje timme) eller not_found: inget slutfört kuvert har exakt den här filen, så det är inte det dokument som signerades (eller så har det ändrats sedan dess). Både den förseglade filen och originalet som det skickades ger träff.

Lita inte bara på API:ets ord: allt som behövs för att kontrollera svaret finns i svaret och i keys.json.

  1. Dokument: SHA-256 för din fil är lika med leaf.sealed_document_sha256 (eller original_document_sha256).
  2. Löv: SHA-256(0x00 ‖ canonical JSON of leaf) är lika med leaf_hash.
  3. Inkludering: om du går igenom proof.hashes från lövets hash (RFC 9162 §2.1.3.2, med leaf_index och tree_size == anchor.leaf_count) får du anchor.root.
  4. Nyckel: anchor.key_id finns i keys.json, nyckel-id:t är de första 16 hexadecimala tecknen i SHA-256 av den råa publika nyckeln, och anchor.created_at ligger inom nyckelns giltighetsperiod.
  5. Signatur: Ed25519-signaturen verifieras över 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']}")

Lövet har bara strängvärden med ASCII-nycklar, så json.dumps(…, sort_keys=True, separators=(",", ":")) ger samma kanoniska JSON. Kontrollera också nyckelns valid_from / valid_until mot anchor["created_at"].

Ankare listas med det senaste först med GET /v1/public/anchors?limit=20&before=<sequence> (skicka next_before för att bläddra bakåt) och hämtas ett i taget med GET /v1/public/anchors/{sequence}.