Vérification
Deux preuves indépendantes protègent chaque enveloppe finalisée :
- Le sceau, à l’intérieur du PDF : un horodatage de document PAdES émis par une autorité d’horodatage publique. Adobe Acrobat et Reader le vérifient et indiquent que le document n’a pas été modifié depuis son scellement.
- L’ancre, à l’extérieur du PDF : l’empreinte du document est une feuille d’un arbre de Merkle en ajout seul, et Bona Fide signe chaque racine de l’arbre en Ed25519. N’importe qui peut vérifier cette preuve avec les clés publiques publiées, sans compte et sans devoir faire confiance à un lecteur de PDF.
Scellement
Section intitulée « Scellement »Quand le dernier destinataire a signé, l’enveloppe est finalisée (envelope.completed) et le service de scellement
prend le relais :
- Il compare le SHA-256 de l’original à celui enregistré à la création de l’enveloppe.
- Il aplatit le document : les valeurs des champs (texte, cases à cocher, signatures dessinées, tapées ou téléversées) sont intégrées au contenu de la page, et tous les champs de formulaire, annotations, actions JavaScript et fichiers joints sont supprimés.
- Il ajoute le certificat de finalisation : identifiant de l’enveloppe, SHA-256 de l’original, chaque signataire avec la méthode de vérification réellement utilisée (par exemple Lien par courriel (lien unique envoyé à …) ou Lien par courriel + code à usage unique, consultez la section sur la vérification du signataire), les horodatages, l’adresse IP et l’appareil, ainsi que la chronologie des événements. Chaque page reçoit le pied de page Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Il ajoute un horodatage de document PAdES (
/DocTimeStamp,ETSI.RFC3161, SHA-256) émis par une autorité d’horodatage publique (plusieurs fournisseurs reconnus publiquement, avec basculement automatique), et intègre les données de validation de la chaîne de certificats de l’autorité (OCSP/CRL dans le DSS) pour qu’il reste vérifiable à long terme (LTV). - Il valide son propre résultat, le stocke et enregistre le SHA-256 scellé (
sealedDocumentHash) ainsi qu’une feuille pour le journal des ancres.
Ensuite, envelope.sealed est déclenché et GET /v1/envelopes/{id}/document?version=sealed renvoie le fichier. Si
aucune autorité d’horodatage n’est disponible, le scellement est relancé; entre-temps, l’enveloppe reste à l’état
completed avec seal: "pending".
Chaque enveloppe scellée fournit une feuille, soit le JSON canonique (clés triées, sans espaces) de :
{ "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>"}Toutes les heures, les feuilles enregistrées depuis l’ancre précédente forment un arbre de Merkle (RFC 6962 / RFC 9162 :
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), dont la racine est publiée sous forme
d’ancre signée :
{ "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"}La signature Ed25519 porte sur bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root relie chaque ancre à la précédente (la première pointe vers 64 zéros) : une ancre ne peut donc pas
être supprimée ni déplacée sans briser la chaîne, et les tables d’ancres sont en ajout seul.
Les clés de signature sont publiées avec leur période de validité; les clés retirées restent listées pour que les anciennes ancres demeurent vérifiables :
{ "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" } ]}Vérifier un document
Section intitulée « Vérifier un document »Le moyen le plus rapide est la page publique : bonafidesign.com/verify. Elle calcule l’empreinte du PDF dans votre navigateur (le fichier n’est jamais téléversé), recherche cette empreinte, puis vérifie la preuve et la signature sur votre appareil. Le journal des ancres liste toutes les ancres.
La même recherche est offerte sous forme d’API publique : sans clé, depuis n’importe quelle origine, avec une limite de débit par IP. N’envoyez que le 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 vaut verified, pending_anchor (finalisée et enregistrée, en attente de la prochaine ancre horaire) ou
not_found : aucune enveloppe finalisée ne correspond à ce fichier exact; il ne s’agit donc pas du document qui a
été signé (ou il a été modifié depuis). Le fichier scellé et l’original tel qu’envoyé donnent tous deux une correspondance.
Vérifier la preuve vous-même
Section intitulée « Vérifier la preuve vous-même »Ne vous fiez pas à la parole de l’API : tout ce qu’il faut pour vérifier la réponse se trouve dans la réponse et dans keys.json.
- Document : le SHA-256 de votre fichier est égal à
leaf.sealed_document_sha256(ou àoriginal_document_sha256). - Feuille :
SHA-256(0x00 ‖ canonical JSON of leaf)est égal àleaf_hash. - Inclusion : le parcours de
proof.hashesà partir de l’empreinte de la feuille (RFC 9162 §2.1.3.2, avecleaf_indexettree_size == anchor.leaf_count) donneanchor.root. - Clé :
anchor.key_idfigure danskeys.json, l’identifiant de clé correspond aux 16 premiers caractères hexadécimaux du SHA-256 de la clé publique brute, etanchor.created_atse situe dans la période de validité de la clé. - Signature : la signature Ed25519 est valide pour
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']}")La feuille ne contient que des valeurs de type chaîne avec des clés ASCII : json.dumps(…, sort_keys=True, separators=(",", ":"))
produit donc le même JSON canonique. Vérifiez aussi les champs valid_from / valid_until de la clé par rapport à
anchor["created_at"].
Le vérificateur de référence, @bona-fide/verify, est isomorphe (navigateur et Node, sans aucun secret de Bona Fide)
et c’est lui qu’exécute la page /verify :
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);}Le paquet fournit aussi un outil de vérification en ligne de commande (bona-fide-verify <file.pdf | sha256> [--api URL] [--json];
depuis une copie de travail du dépôt Bona Fide : pnpm --filter @bona-fide/verify cli signed.pdf). Il calcule
l’empreinte du fichier localement, vérifie chacune des étapes ci-dessus et se termine avec le code 0 si la
vérification réussit, 1 si le document est introuvable ou invalide, et 2 si le document n’est pas encore ancré.
Vérifiez la signature d’une ancre avec la clé PEM de keys.json (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 SuccessfullyLes ancres sont listées de la plus récente à la plus ancienne avec GET /v1/public/anchors?limit=20&before=<sequence>
(transmettez next_before pour remonter dans la liste) et récupérées une à une avec GET /v1/public/anchors/{sequence}.