Verifying Wytness Signed Reports
Every PDF report Wytness generates — chain verification, agent register, forensic event — carries a detached Ed25519 signature in a trailer at the end of the file, and Evidence Packs download as a signed JSON pack. Auditors can verify the signature, and the customer's own per-event signatures, without a Wytness account.
POST /reports/verify and GET /reports/platform-public-key are live in the v2 backend (api/app/routes/reports.py).If you would rather not assemble the steps below yourself, there are two ready-made routes. The in-browser verifier checks a sealed pack in the page — the file never leaves your machine. The command-line verifier (pip install cryptography) runs exactly the same checks offline and exits non-zero on failure, so it drops into a pipeline. Both need the published fingerprint to complete; a specimen pack is there to practise on.
Signature model
Reports are signed with the Wytness platform's Ed25519 private key. The signature is computed over the SHA-256 hash of every byte of the PDF that precedes the signature trailer. Editing the PDF after generation — even one byte — invalidates the signature.
The trailer is appended after the PDF's %%EOF marker as a small base64 envelope. PDF readers stop reading at %%EOF, so the trailer doesn't affect rendering — but any verifier can parse it as plain text. The envelope contains:
signature— base64 Ed25519 signature over SHA-256(prefix bytes)public_key_pem— base64 of the platform public PEMfingerprint— last 16 hex chars of SHA-256(public PEM)meta— organisation, generated_by, generated_at, report_typesignature_algorithm: Ed25519,digest_algorithm: SHA-256
The /reports/verify endpoint
The simplest way to verify a Wytness PDF is to POST it back to Wytness:
curl -X POST \
--data-binary @report.pdf \
-H "Content-Type: application/pdf" \
https://api.wytness.ai/reports/verifyThe endpoint is open (no auth) so auditors can verify documents without a Wytness account. The response shape:
{
"verified": true,
"signed_by": "wytness-platform",
"fingerprint": "<16-hex-chars>",
"generated_at": "2026-05-14T01:23:45+00:00",
"organisation": "Customer Org Name",
"report_type": "Chain verification report",
"error": null,
"platform_fingerprint_live": "<16-hex-chars>"
}Confirm that verified is true, signed_by is wytness-platform, and the embedded fingerprint matches platform_fingerprint_live — both should also match the published platform fingerprint below.
Verifying off-platform
If you'd rather verify the signature in your own environment (offline, on a clean machine, with your own crypto library), you can. The math is straightforward:
- Read the PDF bytes.
- Find the byte index of the marker
%%WYTNESS-SIG-START\n. Everything before that marker is the hashed prefix. - Compute
SHA-256(prefix). - Read the base64 payload between
%%WYTNESS-SIG-START\nand%%WYTNESS-SIG-END\n, decode, parse as JSON. - Extract
signatureandpublic_key_pemfrom the envelope (both base64). - Verify the Ed25519 signature over the SHA-256 digest using the embedded public key. Confirm the public-key fingerprint matches the published platform fingerprint.
Any standard Ed25519 implementation works — Python's cryptography, Go's crypto/ed25519, Node's crypto.verify, OpenSSL CLI.
Platform public key fingerprint
The current platform fingerprint for this environment is:
39032cd1b7f1f139Pin against this value. It is set at deploy time from the platform's Ed25519 public key and is also exposed live at GET https://api.wytness.ai/reports/platform-public-key — the two must match. If they don't, treat any PDF that verifies against the older fingerprint as suspect and ask for a re-signed copy.
Evidence Packs
An Evidence Pack is two artefacts. The report on screen is meant to be printed and filed. The signed pack — the Download signed pack button — is the one you verify: a JSON file carrying the whole pack plus a detached Ed25519 signature made with the same platform key as the PDFs above.
The file has one top-level key, wytness_signed_pack, holding payload_b64 (the pack, base64-encoded), payload_sha256, signature, and public_key_pem. The signature covers the SHA-256 of the decoded payload bytes — so you verify over exactly the bytes in the file, and never have to reproduce a canonical JSON encoding yourself.
import base64, hashlib, json
from cryptography.hazmat.primitives.serialization import load_pem_public_key
env = json.load(open("pack.json"))["wytness_signed_pack"]
payload = base64.b64decode(env["payload_b64"])
assert hashlib.sha256(payload).hexdigest() == env["payload_sha256"]
load_pem_public_key(base64.b64decode(env["public_key_pem"])).verify(
base64.b64decode(env["signature"]), hashlib.sha256(payload).digest()
) # raises if the pack was altered
pack = json.loads(payload)Do not stop there. The pack carries its own public key, so the check above proves only that the file is internally consistent — anyone can rewrite a pack, sign it with their own key and embed that key. What makes it a Wytness pack is the last step: recompute the fingerprint from the key in the file, and compare it to the platform fingerprint published below.
import hashlib
pem = base64.b64decode(env["public_key_pem"])
assert hashlib.sha256(pem).hexdigest()[-16:] == env["fingerprint"] # key matches its own name
assert env["fingerprint"] == PUBLISHED_FINGERPRINT # and it is oursThe hosted verifier accepts sealed packs as well as PDFs: POST the file to /reports/verify and it returns the same verdict. That uploads the pack to Wytness, though, and the pack is your audit data — the steps above need no upload and are the stronger check.
Checking the customer's own signatures
Proving the pack is genuine is a claim about Wytness. Proving the events are genuine is a claim about the customer's agents, and it does not involve us at all. Each sampled event in pack["sample_events"] carries wytness_envelope_signed_payload — the exact bytes the agent's Ed25519 signature covers — alongside wytness_envelope_signature and the key fingerprint that made it. The customer's public keys travel in pack["crypto"]["signing_keys"].
for event in pack["sample_events"]:
if not event["offline_verifiable"]:
print(event["event_id"], "→", event["not_verifiable_detail"])
continue
key = next(k for k in pack["crypto"]["signing_keys"]
if k["key_fingerprint"] == event["wytness_envelope_key_fingerprint"])
load_pem_public_key(key["public_key"].encode()).verify(
base64.b64decode(event["wytness_envelope_signature"]),
event["wytness_envelope_signed_payload"].encode("utf-8"),
) # raises if the event was alterednot_verifiable_reason for which case applies.Why this matters
Auditors and regulators accept signed PDFs from third-party platforms when the signature is verifiable without trusting the platform's UI. Wytness publishes the platform public key, signs every generated report with the matching private key, and provides both a hosted verifier and the instructions above so you can verify entirely on your own infrastructure. The platform cannot fabricate a signature it doesn't possess; nor can it retroactively change a report it has already signed.