Aller au contenu

Vérification

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

  1. Le scellement, à l’intérieur du PDF : un horodatage de document PAdES émis par une autorité d’horodatage publique. Adobe Acrobat et Reader le contrôlent 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. Chacun peut vérifier cette preuve avec les clés publiques publiées, sans compte et sans devoir faire confiance à un lecteur PDF.

Lorsque le dernier destinataire signe, 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é lors de la création de l’enveloppe.
  2. Il aplatit le document : les valeurs des champs (texte, cases à cocher, signatures dessinées, saisies ou importées) sont dessinées dans le contenu de la page, et tous les champs de formulaire, annotations, actions JavaScript et fichiers incorporés 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 e-mail (lien unique envoyé à …) ou Lien par e-mail + code à usage unique, voir 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 prestataires reconnus publiquement, avec basculement automatique), et incorpore les données de validation de la chaîne de certificats de la TSA (OCSP/CRL dans le DSS) afin qu’il reste vérifiable à long terme (LTV).
  5. Il valide son propre résultat, le stocke, puis enregistre le SHA-256 scellé (sealedDocumentHash) et 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 TSA n’est disponible, le scellement est relancé ; l’enveloppe reste entre-temps completed avec seal: "pending".

Chaque enveloppe scellée apporte une feuille : 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 la forme d’une 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 chaîne 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 rompre la chaîne, et les tables d’ancres sont en ajout seul.

Les clés de signature sont publiées avec leurs périodes de validité ; les clés retirées restent listées afin 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 plus rapide est d’utiliser la page publique : bonafidesign.com/verify. Elle calcule l’empreinte du PDF dans votre navigateur (le fichier n’est jamais envoyé), 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 disponible 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 contient exactement ce fichier ; ce n’est donc pas le document qui a été signé (ou il a été modifié depuis). Le fichier scellé comme l’original tel qu’envoyé sont reconnus.

Ne vous fiez pas à la seule parole de l’API : tout ce qu’il faut pour contrôler la réponse se trouve dans celle-ci 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 : en parcourant proof.hashes à partir du hachage de la feuille (RFC 9162 §2.1.3.2, avec leaf_index et tree_size == anchor.leaf_count), on obtient 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 sur 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 les pages) et récupérées une par une avec GET /v1/public/anchors/{sequence}.