Tarkistus
Kaksi toisistaan riippumatonta todistetta suojaa jokaista valmista kirjekuorta:
- Sinetti PDF:n sisällä: PAdES-asiakirja-aikaleima julkiselta aikaleimapalvelulta. Adobe Acrobat ja Reader tarkistavat sen ja näyttävät asiakirjan muuttumattomana sinetöinnistä lähtien.
- Ankkuri PDF:n ulkopuolella: asiakirjan sormenjälki on lehti Merkle-puussa, johon voi vain lisätä merkintöjä, ja Bona Fide allekirjoittaa jokaisen puun juuren Ed25519:llä. Kuka tahansa voi tarkistaa todisteen julkaistuilla julkisilla avaimilla ilman tiliä ja tarvitsematta luottaa PDF-katseluohjelmaan.
Sinetöinti
Osio nimeltä “Sinetöinti”Kun viimeinen vastaanottaja allekirjoittaa, kirjekuori valmistuu (envelope.completed) ja sinetöintipalvelu ottaa
ohjat:
- Tarkistaa alkuperäisen asiakirjan SHA-256:n kirjekuoren luontihetkellä tallennettua arvoa vasten.
- Litistää asiakirjan: kenttien arvot (teksti, valintaruudut, piirretyt, kirjoitetut tai ladatut allekirjoitukset) piirretään sivun sisältöön, ja kaikki lomakekentät, merkinnät, JavaScript-toiminnot ja upotetut tiedostot poistetaan.
- Liittää valmistumistodistuksen: kirjekuoren tunnus, alkuperäisen SHA-256, jokainen allekirjoittaja ja hänen todellisuudessa käyttämänsä vahvistusmenetelmä (esimerkiksi Sähköpostilinkki (yksilöllinen linkki lähetetty osoitteeseen …) tai Sähköpostilinkki + kertakäyttökoodi, katso allekirjoittajan vahvistus), aikaleimat, IP-osoite ja laite sekä tapahtumien aikajana. Jokaiselle sivulle tulee alatunniste Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Lisää PAdES-asiakirja-aikaleiman (
/DocTimeStamp,ETSI.RFC3161, SHA-256) julkiselta aikaleimapalvelulta (useita julkisesti luotettuja palveluntarjoajia automaattisella varalle siirtymisellä) ja upottaa aikaleimapalvelun varmenneketjun validointitiedot (OCSP/CRL DSS:ssä), jotta aikaleima pysyy tarkistettavana pitkällä aikavälillä (LTV). - Validoi oman tuotoksensa, tallentaa sen ja kirjaa sinetöidyn SHA-256:n (
sealedDocumentHash) sekä lehden ankkurilokia varten.
Sen jälkeen laukeaa envelope.sealed, ja GET /v1/envelopes/{id}/document?version=sealed palauttaa tiedoston. Jos
yksikään aikaleimapalvelu ei ole käytettävissä, sinetöintiä yritetään uudelleen; kirjekuori pysyy sillä välin tilassa
completed arvolla seal: "pending".
Ankkurointi
Osio nimeltä “Ankkurointi”Jokainen sinetöity kirjekuori tuottaa yhden lehden, joka on seuraavan kanoninen JSON (avaimet lajiteltuina, ei välilyöntejä):
{ "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>"}Joka tunti edellisen ankkurin jälkeen tallennetuista lehdistä muodostetaan Merkle-puu (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), ja sen juuri julkaistaan allekirjoitettuna
ankkurina:
{ "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-allekirjoitus kattaa merkkijonon bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root ketjuttaa jokaisen ankkurin edelliseen (ensimmäinen osoittaa 64 nollaan), joten ankkuria ei voi
poistaa tai siirtää järjestyksessä rikkomatta ketjua, ja ankkuritauluihin voi vain lisätä merkintöjä.
Allekirjoitusavaimet julkaistaan voimassaoloaikoineen; käytöstä poistetut avaimet pysyvät luettelossa, jotta vanhat ankkurit voidaan edelleen tarkistaa:
{ "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" } ]}Asiakirjan tarkistaminen
Osio nimeltä “Asiakirjan tarkistaminen”Nopein tapa on julkinen sivu: bonafidesign.com/verify. Se laskee PDF:n tiivisteen selaimessasi (tiedostoa ei koskaan ladata palvelimelle), hakee tiivisteen ja tarkistaa todisteen ja allekirjoituksen laitteellasi. Ankkuriloki luettelee kaikki ankkurit.
Sama haku on julkinen API: ei avainta, mikä tahansa alkuperä, nopeusrajoitus IP-osoitetta kohden. Lähetä vain 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 on verified, pending_anchor (valmis ja tallennettu, odottaa seuraavaa tunnittaista ankkuria) tai
not_found: millään valmiilla kirjekuorella ei ole juuri tätä tiedostoa, joten se ei ole allekirjoitettu asiakirja
(tai sitä on muutettu sen jälkeen). Sekä sinetöity tiedosto että alkuperäinen lähetetty versio tuottavat osuman.
Todisteen tarkistaminen itse
Osio nimeltä “Todisteen tarkistaminen itse”Älä luota pelkkään API:n sanaan: kaikki vastauksen tarkistamiseen tarvittava on vastauksessa ja tiedostossa
keys.json.
- Asiakirja: tiedostosi SHA-256 on sama kuin
leaf.sealed_document_sha256(taioriginal_document_sha256). - Lehti:
SHA-256(0x00 ‖ canonical JSON of leaf)on sama kuinleaf_hash. - Sisältyminen: kun käyt läpi
proof.hashes-arvot lehden tiivisteestä alkaen (RFC 9162 §2.1.3.2, arvoillaleaf_indexjatree_size == anchor.leaf_count), tulokseksi saadaananchor.root. - Avain:
anchor.key_idlöytyy tiedostostakeys.json, avaimen tunnus on raa’an julkisen avaimen SHA-256:n ensimmäiset 16 heksamerkkiä, jaanchor.created_aton avaimen voimassaoloajan sisällä. - Allekirjoitus: Ed25519-allekirjoitus todentuu merkkijonolle
bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count.
# pip install requests cryptographyimport base64, hashlib, json, sys, requestsfrom 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']}")Lehdessä on vain merkkijonoarvoja ASCII-avaimilla, joten json.dumps(…, sort_keys=True, separators=(",", ":")) tuottaa
saman kanonisen JSONin. Tarkista myös avaimen valid_from / valid_until arvoa anchor["created_at"] vasten.
Viitetoteutus, @bona-fide/verify, on isomorfinen (selain ja Node, ei Bona Fiden salaisuuksia), ja sitä
/verify-sivu suorittaa:
import { sha256Hex, verifyProof, type KeySet, type PublicVerifyResponse } from '@bona-fide/verify';import { readFile } from 'node:fs/promises';
const api = 'https://api.bonafidesign.com';const sha256 = sha256Hex(new Uint8Array(await readFile('signed.pdf')));const body = (await (await fetch(`${api}/v1/public/verify`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sha256 }),})).json()) as PublicVerifyResponse;const keys = (await (await fetch(`${api}/.well-known/bona-fide/keys.json`)).json()) as KeySet;
for (const m of body.results) { if (!m.leaf || !m.proof || !m.anchor) continue; // pending_anchor const r = await verifyProof(m.leaf, m.proof, m.anchor, keys, { documentSha256: sha256, expectedLeafHash: m.leaf_hash ?? undefined }); console.log(r.valid ? 'VERIFIED' : 'INVALID', m.envelope.id, r.checks);}Paketissa on myös komentorivityökalu (bona-fide-verify <file.pdf | sha256> [--api URL] [--json];
Bona Fide -repositorion työkopiosta: pnpm --filter @bona-fide/verify cli signed.pdf). Se laskee tiedoston tiivisteen
paikallisesti, tarkistaa kaikki yllä olevat vaiheet ja päättyy koodiin 0, kun tarkistus onnistuu, 1, kun
tiedostoa ei löydy tai se on virheellinen, ja 2, kun asiakirjaa ei ole vielä ankkuroitu.
Tarkista ankkurin allekirjoitus tiedoston keys.json PEM-avaimella (OpenSSL 3):
curl -s https://api.bonafidesign.com/v1/public/anchors/412 > anchor.jsoncurl -s https://api.bonafidesign.com/.well-known/bona-fide/keys.json > keys.json
printf %s "$(jq -r .signed_message anchor.json)" > message.txtjq -r .signature anchor.json | python3 -c 'import base64,sys; s=sys.stdin.read().strip(); sys.stdout.buffer.write(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)))' > signature.binKEY_ID=$(jq -r '.key_id' anchor.json)jq -r --arg k "$KEY_ID" '.keys[] | select(.key_id == $k) | .public_key_pem' keys.json > key.pem
openssl pkeyutl -verify -pubin -inkey key.pem -rawin -in message.txt -sigfile signature.bin# Signature Verified SuccessfullyAnkkurit luetellaan uusimmasta alkaen kutsulla GET /v1/public/anchors?limit=20&before=<sequence> (välitä
next_before selataksesi taaksepäin) ja haetaan yksitellen kutsulla GET /v1/public/anchors/{sequence}.