Verificación
Dos pruebas independientes protegen cada sobre completado:
- El sello, dentro del PDF: un sello de tiempo de documento PAdES emitido por una autoridad de sellado de tiempo pública. Adobe Acrobat y Reader lo comprueban y muestran que el documento no se ha modificado desde que se selló.
- El anclaje, fuera del PDF: la huella digital del documento es una hoja de un árbol de Merkle al que solo se pueden añadir entradas, y Bona Fide firma cada raíz del árbol con Ed25519. Cualquiera puede comprobar esa prueba con las claves públicas publicadas, sin cuenta y sin necesidad de confiar en un visor de PDF.
Sellado
Sección titulada «Sellado»Cuando firma el último destinatario, el sobre se completa (envelope.completed) y el proceso de sellado toma el
relevo:
- Compara el SHA-256 del original con el que se registró al crear el sobre.
- Acopla el documento: los valores de los campos (texto, casillas de verificación, firmas dibujadas, escritas o subidas) se dibujan en el contenido de la página, y se eliminan todos los campos de formulario, anotaciones, acciones de JavaScript y archivos incrustados.
- Anexa el certificado de finalización: ID del sobre, SHA-256 del original, cada firmante con el método de verificación que se ha utilizado realmente (por ejemplo, Enlace por correo (enlace único enviado a …) o Enlace por correo + código de un solo uso; consulte verificación del firmante), las marcas de tiempo, la dirección IP y el dispositivo, y la cronología de eventos. Cada página lleva el pie Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
- Añade un sello de tiempo de documento PAdES (
/DocTimeStamp,ETSI.RFC3161, SHA-256) de una autoridad de sellado de tiempo pública (varios proveedores de confianza pública con conmutación automática por error) e incrusta los datos de validación de la cadena de certificados de la TSA (OCSP/CRL en el DSS) para que siga siendo verificable a largo plazo (LTV). - Valida su propio resultado, lo guarda y registra el SHA-256 sellado (
sealedDocumentHash) y una hoja para el registro de anclajes.
A continuación se dispara envelope.sealed y GET /v1/envelopes/{id}/document?version=sealed devuelve el archivo.
Si ninguna TSA está disponible, el sellado se reintenta; entretanto, el sobre permanece en completed con
seal: "pending".
Anclaje
Sección titulada «Anclaje»Cada sobre sellado aporta una hoja, el JSON canónico (claves ordenadas, sin espacios en blanco) 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>"}Cada hora, las hojas registradas desde el anclaje anterior forman un árbol de Merkle (RFC 6962 / RFC 9162:
leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), y su raíz se publica como un
anclaje firmado:
{ "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 firma Ed25519 cubre bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8).
previous_root encadena cada anclaje con el anterior (el primero apunta a 64 ceros), de modo que no se puede
eliminar ni reordenar un anclaje sin romper la cadena, y en las tablas de anclajes solo se pueden añadir entradas.
Las claves de firma se publican con sus periodos de validez; las claves retiradas siguen figurando en la lista para que los anclajes antiguos sigan siendo verificables:
{ "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" } ]}Verificación de un documento
Sección titulada «Verificación de un documento»La forma más rápida es la página pública: bonafidesign.com/verify. Calcula el hash del PDF en su navegador (el archivo nunca se sube), busca ese hash y comprueba la prueba y la firma en su dispositivo. El registro de anclajes muestra todos los anclajes.
La misma consulta está disponible como API pública: sin clave, desde cualquier origen y con límite de frecuencia por IP. Envíe solo el 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 es verified, pending_anchor (completado y registrado, a la espera del siguiente anclaje horario) o
not_found: ningún sobre completado tiene este archivo exacto, por lo que no es el documento que se firmó (o se ha
modificado desde entonces). Coinciden tanto el archivo sellado como el original tal como se envió.
Comprobación de la prueba por su cuenta
Sección titulada «Comprobación de la prueba por su cuenta»No tiene por qué fiarse de la palabra de la API: todo lo necesario para comprobar la respuesta está en la propia
respuesta y en keys.json.
- Documento: el SHA-256 de su archivo es igual a
leaf.sealed_document_sha256(o aoriginal_document_sha256). - Hoja:
SHA-256(0x00 ‖ canonical JSON of leaf)es igual aleaf_hash. - Inclusión: al recorrer
proof.hashesa partir del hash de la hoja (RFC 9162 §2.1.3.2, conleaf_indexytree_size == anchor.leaf_count) se obtieneanchor.root. - Clave:
anchor.key_idfigura enkeys.json, el ID de la clave son los 16 primeros caracteres hexadecimales del SHA-256 de la clave pública en bruto, yanchor.created_atestá dentro del periodo de validez de la clave. - Firma: la firma Ed25519 se verifica sobre
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 hoja solo contiene valores de tipo cadena con claves ASCII, así que json.dumps(…, sort_keys=True, separators=(",", ":"))
produce el mismo JSON canónico. Compare también valid_from / valid_until de la clave con
anchor["created_at"].
El verificador de referencia, @bona-fide/verify, es isomórfico (navegador y Node, sin secretos de Bona Fide) y es
el que ejecuta la página /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);}El paquete incluye también un verificador de línea de comandos (bona-fide-verify <file.pdf | sha256> [--api URL] [--json];
desde una copia local del repositorio de Bona Fide: pnpm --filter @bona-fide/verify cli signed.pdf). Calcula el
hash del archivo en local, comprueba todos los pasos anteriores y termina con 0 si la verificación es correcta,
con 1 si no se encuentra o no es válido, y con 2 si el documento aún no está anclado.
Compruebe la firma de un anclaje con la clave 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 SuccessfullyLos anclajes se listan del más reciente al más antiguo con GET /v1/public/anchors?limit=20&before=<sequence>
(pase next_before para retroceder de página) y se obtienen uno a uno con GET /v1/public/anchors/{sequence}.