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.

Note
Verified against backend HEAD: 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 PEM
  • fingerprint — last 16 hex chars of SHA-256(public PEM)
  • meta — organisation, generated_by, generated_at, report_type
  • signature_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/verify

The 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:

  1. Read the PDF bytes.
  2. Find the byte index of the marker %%WYTNESS-SIG-START\n. Everything before that marker is the hashed prefix.
  3. Compute SHA-256(prefix).
  4. Read the base64 payload between %%WYTNESS-SIG-START\n and %%WYTNESS-SIG-END\n, decode, parse as JSON.
  5. Extract signature and public_key_pem from the envelope (both base64).
  6. 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:

39032cd1b7f1f139

Pin 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.

Note
Tampering and chain breaks are different things. A PDF that fails signature verification has been edited or corrupted. A PDF that verifies but contains a "TAMPERING DETECTED" verdict on its cover page is a faithful Wytness report about a chain break in the customer's data — the PDF itself is intact, but the data it describes is suspect.

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 ours

The 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 altered
Note
Not every event can be checked this way, and the pack says so per event rather than leaving you to infer it. Events captured by a collector or sent straight from AGT carry no customer signature — their integrity rests on the AGT hash chain. Events erased under GDPR Article 17 keep their signature but lose the content it covered. And events captured before Wytness retained the signed bytes carry the verdict Wytness recorded at capture instead. Read not_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.

We set no cookies. Sign-in and preferences use essential first-party browser storage only — no tracking, advertising, or third-party analytics. Privacy Policy

Verifying Wytness Signed Reports
TABLE OF CONTENTS