Gå til innholdet

Verifisering

To uavhengige bevis beskytter hver fullførte konvolutt:

  1. Seglet, inne i PDF-en: et PAdES-dokumenttidsstempel fra en offentlig tidsstempeltjeneste. Adobe Acrobat og Reader kontrollerer det og viser dokumentet som uendret siden det ble forseglet.
  2. Ankeret, utenfor PDF-en: dokumentets fingeravtrykk er et løv i et Merkle-tre der oppføringer bare kan legges til, og Bona Fide signerer hver trerot med Ed25519. Alle kan kontrollere beviset med de publiserte offentlige nøklene, uten konto og uten å måtte stole på en PDF-leser.

Når den siste mottakeren signerer, fullføres konvolutten (envelope.completed), og forseglingstjenesten tar over:

  1. Kontrollerer originalens SHA-256 mot verdien som ble registrert da konvolutten ble opprettet.
  2. Flater ut dokumentet: feltverdiene (tekst, avmerkingsbokser, tegnede, skrevne eller opplastede signaturer) tegnes inn i sideinnholdet, og alle skjemafelt, merknader, JavaScript-handlinger og innebygde filer fjernes.
  3. Legger til fullføringssertifikatet: konvolutt-ID, originalens SHA-256, hver signatar med verifiseringsmetoden som faktisk ble brukt (for eksempel E-postlenke (unik lenke sendt til …) eller E-postlenke + engangskode, se verifisering av signatarer), tidsstempler, IP-adresse og enhet, og tidslinjen over hendelser. Hver side får bunnteksten Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. Legger til et PAdES-dokumenttidsstempel (/DocTimeStamp, ETSI.RFC3161, SHA-256) fra en offentlig tidsstempeltjeneste (flere offentlig klarerte leverandører med automatisk failover), og bygger inn valideringsdataene for tidsstempeltjenestens sertifikatkjede (OCSP/CRL i DSS) slik at det forblir verifiserbart på lang sikt (LTV).
  5. Validerer sitt eget resultat, lagrer det og registrerer den forseglede SHA-256-verdien (sealedDocumentHash) og et løv for ankerloggen.

Deretter utløses envelope.sealed, og GET /v1/envelopes/{id}/document?version=sealed returnerer filen. Hvis ingen tidsstempeltjeneste er tilgjengelig, prøves forseglingen på nytt; konvolutten forblir completed med seal: "pending" i mellomtiden.

Hver forseglede konvolutt bidrar med ett løv, den kanoniske JSON-en (nøkler sortert, uten mellomrom) 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>"
}

Hver time blir løvene som er registrert siden forrige anker, til et Merkle-tre (RFC 6962 / RFC 9162: leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), og roten publiseres som et signert 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 dekker bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8). previous_root kjeder hvert anker til det forrige (det første peker på 64 nuller), så et anker kan ikke fjernes eller flyttes i rekkefølgen uten å bryte kjeden, og ankertabellene tillater bare at oppføringer legges til.

Signeringsnøklene publiseres med gyldighetsperiodene sine; nøkler som er tatt ut av bruk, blir stående på listen, så gamle ankre fortsatt kan verifiseres:

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 raskeste måten er den offentlige siden: bonafidesign.com/verify. Den beregner hashen av PDF-en i nettleseren din (filen lastes aldri opp), slår opp hashen og kontrollerer beviset og signaturen på enheten din. Ankerloggen viser alle ankre.

Det samme oppslaget finnes som et offentlig API: ingen nøkkel, hvilken som helst opprinnelse, hastighetsbegrenset per IP-adresse. Send bare SHA-256-verdien:

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 (fullført og registrert, venter på neste ankring som skjer hver time) eller not_found: ingen fullført konvolutt har nøyaktig denne filen, så den er ikke dokumentet som ble signert (eller den er endret siden). Både den forseglede filen og originalen slik den ble sendt, gir treff.

Du trenger ikke ta API-et på ordet: alt som trengs for å kontrollere svaret, finnes i svaret og i keys.json.

  1. Dokument: SHA-256 av filen din er lik leaf.sealed_document_sha256 (eller original_document_sha256).
  2. Løv: SHA-256(0x00 ‖ canonical JSON of leaf) er lik leaf_hash.
  3. Inkludering: når du går gjennom proof.hashes fra løvhashen (RFC 9162 §2.1.3.2, med leaf_index og tree_size == anchor.leaf_count), får du anchor.root.
  4. Nøkkel: anchor.key_id finnes i keys.json, nøkkel-ID-en er de første 16 heksadesimale tegnene i SHA-256 av den rå offentlige nøkkelen, og anchor.created_at ligger innenfor nøkkelens gyldighetsperiode.
  5. Signatur: Ed25519-signaturen verifiseres 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']}")

Løvet har bare strengverdier med ASCII-nøkler, så json.dumps(…, sort_keys=True, separators=(",", ":")) gir den samme kanoniske JSON-en. Kontroller også nøkkelens valid_from / valid_until mot anchor["created_at"].

Ankre listes opp med de nyeste først med GET /v1/public/anchors?limit=20&before=<sequence> (send next_before for å bla bakover) og hentes ett og ett med GET /v1/public/anchors/{sequence}.