Gå til indhold

Verificering

To uafhængige beviser beskytter hver fuldført kuvert:

  1. Seglet, inde i PDF’en: et PAdES-dokumenttidsstempel fra en offentlig tidsstempelmyndighed. Adobe Acrobat og Reader tjekker det og viser, at dokumentet ikke er ændret, siden det blev forseglet.
  2. Ankeret, uden for PDF’en: dokumentets fingeraftryk er et blad i et Merkle-træ, hvor der kun kan tilføjes, og Bona Fide signerer hver trærod med Ed25519. Alle kan tjekke beviset med de offentliggjorte offentlige nøgler, uden konto og uden at skulle stole på en PDF-læser.

Når den sidste modtager underskriver, fuldføres kuverten (envelope.completed), og forseglingstjenesten tager over:

  1. Sammenligner originalens SHA-256 med den, der blev registreret, da kuverten blev oprettet.
  2. Flader dokumentet ud: feltværdier (tekst, afkrydsningsfelter, tegnede, skrevne eller uploadede signaturer) tegnes ind i sideindholdet, og alle formularfelter, annoteringer, JavaScript-handlinger og indlejrede filer fjernes.
  3. Tilføjer fuldførelsescertifikatet: kuvertens id, originalens SHA-256, hver underskriver med den verificeringsmetode, der faktisk blev brugt (f.eks. E-maillink (unikt link sendt til …) eller E-maillink + engangskode, se verificering af underskrivere), tidsstempler, IP-adresse og enhed samt hændelsesforløbet. Hver side får sidefoden Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. Tilføjer et PAdES-dokumenttidsstempel (/DocTimeStamp, ETSI.RFC3161, SHA-256) fra en offentlig tidsstempelmyndighed (flere offentligt betroede udbydere med automatisk failover) og indlejrer valideringsdata for tidsstempelmyndighedens certifikatkæde (OCSP/CRL i DSS), så det forbliver verificerbart på lang sigt (LTV).
  5. Validerer sit eget resultat, gemmer det og registrerer den forseglede fils SHA-256 (sealedDocumentHash) og et blad til ankerloggen.

Derefter udløses envelope.sealed, og GET /v1/envelopes/{id}/document?version=sealed returnerer filen. Hvis ingen tidsstempelmyndighed er tilgængelig, forsøges forseglingen igen; kuverten forbliver completed med seal: "pending" imens.

Hver forseglet kuvert bidrager med ét blad, den kanoniske JSON (sorterede nøgler, ingen mellemrum) af:

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

Hver time bliver de blade, der er registreret siden det forrige anker, til et Merkle-træ (RFC 6962 / RFC 9162: leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), og dets rod offentliggøres som et signeret anker:

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 dækker bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8). previous_root kæder hvert anker til det foregående (det første peger på 64 nuller), så et anker ikke kan fjernes eller flyttes uden at bryde kæden, og ankertabellerne tillader kun tilføjelser.

Signeringsnøglerne offentliggøres med deres gyldighedsperioder; udfasede nøgler forbliver på listen, så gamle ankre fortsat kan verificeres:

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

Den hurtigste måde er den offentlige side: bonafidesign.com/verify. Den beregner PDF’ens hash i din browser (filen uploades aldrig), slår hashen op og tjekker beviset og signaturen på din enhed. Ankerloggen viser alle ankre.

Det samme opslag findes som et offentligt API: ingen nøgle, vilkårligt origin, hastighedsbegrænset pr. IP-adresse. Send kun 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 er verified, pending_anchor (fuldført og registreret, venter på det næste anker i timen) eller not_found: ingen fuldført kuvert har præcis denne fil, så det er ikke det dokument, der blev underskrevet (eller det er ændret siden). Både den forseglede fil og originalen, som den blev sendt, giver et match.

Du behøver ikke at tage API’ets ord for det: alt, hvad der skal til for at tjekke svaret, står i svaret og i keys.json.

  1. Dokument: SHA-256 af din fil er lig med leaf.sealed_document_sha256 (eller original_document_sha256).
  2. Blad: SHA-256(0x00 ‖ canonical JSON of leaf) er lig med leaf_hash.
  3. Inklusion: hvis du gennemløber proof.hashes fra bladets hash (RFC 9162 §2.1.3.2, med leaf_index og tree_size == anchor.leaf_count), får du anchor.root.
  4. Nøgle: anchor.key_id findes i keys.json, nøgle-id’et er de første 16 hexadecimale tegn af SHA-256 af den rå offentlige nøgle, og anchor.created_at ligger inden for nøglens gyldighedsperiode.
  5. Signatur: Ed25519-signaturen verificeres over 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']}")

Bladet har kun strengværdier med ASCII-nøgler, så json.dumps(…, sort_keys=True, separators=(",", ":")) giver den samme kanoniske JSON. Tjek også nøglens valid_from / valid_until mod anchor["created_at"].

Ankre vises med det nyeste først med GET /v1/public/anchors?limit=20&before=<sequence> (send next_before for at bladre tilbage) og hentes ét ad gangen med GET /v1/public/anchors/{sequence}.