Ir al contenido

Verificación

Dos pruebas independientes protegen cada sobre completado:

  1. El sello, dentro del PDF: un sello de tiempo de documento PAdES de una autoridad de sellado de tiempo pública. Adobe Acrobat y Reader lo comprueban y muestran que el documento no se modificó desde que se selló.
  2. El anclaje, fuera del PDF: la huella digital del documento es una hoja de un árbol de Merkle al que solo se pueden agregar 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 tener que confiar en un visor de PDF.

Cuando firma el último destinatario, el sobre se completa (envelope.completed) y el proceso de sellado toma el control:

  1. Compara el SHA-256 del original con el que se registró al crear el sobre.
  2. Aplana 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, las anotaciones, las acciones de JavaScript y los archivos incorporados.
  3. Anexa el certificado de finalización: el ID del sobre, el SHA-256 del original, cada firmante con el método de verificación que realmente se usó (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 de página Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. Agrega 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 por error automática) e incorpora 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).
  5. Valida su propio resultado, lo almacena y registra el SHA-256 sellado (sealedDocumentHash) y una hoja para el registro de anclajes.

Luego se dispara envelope.sealed y GET /v1/envelopes/{id}/document?version=sealed devuelve el archivo. Si ninguna TSA está disponible, el sellado se reintenta; mientras tanto, el sobre permanece en completed con seal: "pending".

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:

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 firma Ed25519 abarca 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 las tablas de anclajes solo admiten agregar entradas.

Las claves de firma se publican con sus períodos de validez; las claves retiradas siguen en la lista para que los anclajes antiguos sigan siendo verificables:

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

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 el hash y comprueba la prueba y la firma en su dispositivo. El registro de anclajes enumera todos los anclajes.

La misma búsqueda 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:

Terminal window
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 próximo anclaje horario) o not_found: ningún sobre completado tiene este archivo exacto, así que no es el documento que se firmó (o se modificó desde entonces). Coinciden tanto el archivo sellado como el original tal como se envió.

No se fíe solo de lo que dice la API: todo lo necesario para comprobar la respuesta está en la respuesta y en keys.json.

  1. Documento: el SHA-256 de su archivo es igual a leaf.sealed_document_sha256 (o a original_document_sha256).
  2. Hoja: SHA-256(0x00 ‖ canonical JSON of leaf) es igual a leaf_hash.
  3. Inclusión: recorrer proof.hashes a partir del hash de la hoja (RFC 9162 §2.1.3.2, con leaf_index y tree_size == anchor.leaf_count) da como resultado anchor.root.
  4. Clave: anchor.key_id figura en keys.json, el ID de la clave son los primeros 16 caracteres hexadecimales del SHA-256 de la clave pública sin procesar, y anchor.created_at está dentro del período de validez de la clave.
  5. Firma: la firma Ed25519 se verifica sobre 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 hoja solo tiene valores de tipo string con claves ASCII, así que json.dumps(…, sort_keys=True, separators=(",", ":")) produce el mismo JSON canónico. Compare también los valores valid_from / valid_until de la clave con anchor["created_at"].

Los anclajes se enumeran 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 de a uno con GET /v1/public/anchors/{sequence}.