Aller au contenu

Vérification

Deux preuves indépendantes protègent chaque enveloppe finalisée :

  1. 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.
  2. 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.

Quand le dernier destinataire a signé, l’enveloppe est finalisée (envelope.completed) et le service de scellement prend le relais :

  1. Il compare le SHA-256 de l’original à celui enregistré à la création de l’enveloppe.
  2. 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.
  3. 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.
  4. 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).
  5. 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 :

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

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 :

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

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 :

Fenêtre de terminal
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.

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.

  1. Document : le SHA-256 de votre fichier est égal à leaf.sealed_document_sha256 (ou à original_document_sha256).
  2. Feuille : SHA-256(0x00 ‖ canonical JSON of leaf) est égal à leaf_hash.
  3. Inclusion : le parcours de proof.hashes à partir de l’empreinte de la feuille (RFC 9162 §2.1.3.2, avec leaf_index et tree_size == anchor.leaf_count) donne anchor.root.
  4. Clé : anchor.key_id figure dans keys.json, l’identifiant de clé correspond aux 16 premiers caractères hexadécimaux du SHA-256 de la clé publique brute, et anchor.created_at se situe dans la période de validité de la clé.
  5. Signature : la signature Ed25519 est valide pour 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']}")

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

Les 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}.