Перейти к содержимому

Проверка

Каждый завершённый конверт защищают два независимых доказательства:

  1. Печать внутри PDF: метка времени документа PAdES от публичной службы меток времени. Adobe Acrobat и Reader проверяют её и показывают, что документ не изменялся с момента запечатывания.
  2. Якорь вне PDF: отпечаток документа — это лист в дереве Merkle, в которое можно только добавлять записи, а Bona Fide подписывает каждый корень дерева ключом Ed25519. Проверить это доказательство может любой с помощью опубликованных открытых ключей — без учётной записи и без необходимости доверять программе просмотра PDF.

Когда подписывает последний получатель, конверт завершается (envelope.completed), и за дело берётся механизм запечатывания:

  1. Сверяет SHA-256 оригинала со значением, записанным при создании конверта.
  2. Сводит документ в один слой: значения полей (текст, флажки, нарисованные, напечатанные или загруженные подписи) переносятся в содержимое страницы, а все поля форм, аннотации, действия JavaScript и вложенные файлы удаляются.
  3. Добавляет сертификат о завершении: идентификатор конверта, SHA-256 оригинала, каждого подписанта с фактически использованным способом проверки (например, Ссылка по почте (уникальная ссылка, отправленная на …) или Ссылка по почте + одноразовый код, см. проверку подписанта), метки времени, IP-адрес и устройство, а также хронологию событий. На каждую страницу добавляется нижний колонтитул Bona Fide Envelope ID … · Verify at bonafidesign.com/verify.
  4. Добавляет метку времени документа PAdES (/DocTimeStamp, ETSI.RFC3161, SHA-256) от публичной службы меток времени (несколько общепризнанных поставщиков с автоматическим переключением при сбое) и встраивает данные для проверки цепочки сертификатов TSA (OCSP/CRL в DSS), чтобы документ оставался проверяемым в долгосрочной перспективе (LTV).
  5. Проверяет собственный результат, сохраняет его и записывает SHA-256 запечатанного файла (sealedDocumentHash) и лист для журнала якорей.

Затем срабатывает envelope.sealed, а GET /v1/envelopes/{id}/document?version=sealed возвращает файл. Если все службы TSA недоступны, запечатывание повторяется; тем временем конверт остаётся в статусе completed с seal: "pending".

Каждый запечатанный конверт добавляет один лист — канонический JSON (ключи отсортированы, без пробельных символов) из:

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

Каждый час листья, записанные после предыдущего якоря, собираются в дерево Merkle (RFC 6962 / RFC 9162: leaf = SHA-256(0x00 ‖ leaf JSON), node = SHA-256(0x01 ‖ left ‖ right)), а его корень публикуется как подписанный якорь:

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

Подпись Ed25519 охватывает bona-fide-anchor-v1|sequence|root|previous_root|created_at|leaf_count (UTF-8). previous_root связывает каждый якорь с предыдущим (первый указывает на 64 нуля), поэтому якорь нельзя удалить или переставить, не нарушив цепочку, а в таблицы якорей можно только добавлять записи.

Ключи подписи публикуются вместе со сроками действия; выведенные из использования ключи остаются в списке, чтобы старые якоря можно было проверить:

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

Быстрее всего воспользоваться публичной страницей: bonafidesign.com/verify. Она вычисляет хеш PDF в вашем браузере (файл никогда не загружается на сервер), ищет этот хеш и проверяет доказательство и подпись на вашем устройстве. В журнале якорей перечислены все якоря.

Тот же поиск доступен как публичный API: без ключа, с любого источника, с ограничением частоты по IP. Отправляйте только 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 — это verified, pending_anchor (завершён и записан, ожидает следующего ежечасного якоря) или not_found: ни один завершённый конверт не содержит именно этот файл, значит, это не подписанный документ (или он был изменён с тех пор). Совпадение находится как для запечатанного файла, так и для оригинала в отправленном виде.

Самостоятельная проверка доказательства

Заголовок раздела «Самостоятельная проверка доказательства»

Не верьте API на слово: всё, что нужно для проверки ответа, есть в самом ответе и в keys.json.

  1. Документ: SHA-256 вашего файла равен leaf.sealed_document_sha256 (или original_document_sha256).
  2. Лист: SHA-256(0x00 ‖ canonical JSON of leaf) равен leaf_hash.
  3. Включение: проход по proof.hashes начиная с хеша листа (RFC 9162 §2.1.3.2, с leaf_index и tree_size == anchor.leaf_count) даёт anchor.root.
  4. Ключ: anchor.key_id есть в keys.json, идентификатор ключа — первые 16 шестнадцатеричных символов SHA-256 необработанного открытого ключа, а anchor.created_at попадает в срок действия ключа.
  5. Подпись: подпись Ed25519 проходит проверку для 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']}")

Лист содержит только строковые значения с ключами в ASCII, поэтому json.dumps(…, sort_keys=True, separators=(",", ":")) даёт тот же канонический JSON. Также сверьте valid_from / valid_until ключа с anchor["created_at"].

Якоря перечисляются начиная с самых новых через GET /v1/public/anchors?limit=20&before=<sequence> (передайте next_before, чтобы перейти к более ранним), а по одному запрашиваются через GET /v1/public/anchors/{sequence}.