Veracly
Multi-jurisdiction

How to verify a Veracly report is authentic

A compliance report is only useful if its reader trusts it. Every Veracly PDF is cryptographically signed and independently verifiable, for a defined window. Here is the mechanic, and the expiry, in plain English.

By Veracly Compliance Team6 min read

A compliance audit is a claim about a moment in time. The reader of the report, your developer, your lawyer, a regulator, a procurement team, has to trust that the claim is what Veracly actually issued, not something edited along the way. That trust is too important to rest on “we are a reputable vendor.” Every Veracly report carries a cryptographic proof anyone can verify in under a minute. Here is the mechanic.

What gets signed

On every scan, we compute the SHA-256 digest of the exact PDF bytes about to be uploaded. We then sign a canonical record with Veracly’s private Ed25519 key and persist the signature in our audit ledger. The signature does not live inside the PDF (embedding it would be circular: the embedded bytes would change the digest and invalidate the signature). Instead, the PDF surfaces the scan UUID as a lookup key.

The record has seven fields, and all seven are inside the signature. If you are rebuilding it yourself, this is the part to get exactly right, a record missing a field produces signature_mismatch, which reads to a regulator as a forged report:

  • v — schema version, currently the integer 2.
  • issuer — pinned to the string veracly.app.
  • scan_id — the scan UUID printed on the report.
  • language — the locale of this report variant. Each language is a separately rendered PDF and is signed independently.
  • pdf_sha256 — lowercase hex, no separators.
  • issued_at — ISO 8601, UTC.
  • key_id — the first 16 hex characters of sha256(public_key_raw_bytes), so a verifier can pick the right key after a rotation.

Canonicalization is a deliberately small subset of JCS: top-level object only, keys sorted in ASCII order, no whitespace, values JSON-escaped. Sorted, that puts issued_at first and v last, which is not the order above, so sort rather than transcribing. One historical wrinkle: anchors signed before 2026-07-31 may carry an eighth optional field, requested_by. We stopped writing it because the verify endpoint is unauthenticated and the field carried a free-scan requester’s email address. Where it is present it was signed, so it must be included when you reconstruct the bytes.

The verification flow

Three steps. Anyone with the PDF can perform them:

  1. Read the scan UUID from the “Document integrity” block on the disclaimer page of the PDF.
  2. Compute the SHA-256 of the PDF on your local machine. On macOS / Linux: shasum -a 256 report.pdf. On Windows: Get-FileHash report.pdf -Algorithm SHA256.
  3. Visit veracly.app/verify/<uuid>. The page shows the canonical record we signed and the public key it was signed with. You compare the SHA-256 you computed to the one shown. If they match, the signature is valid and the bytes in front of you are the bytes we issued.

The public key is also published at veracly.app/.well-known/veracly-signing-key.json. Anyone who wants to do the full verification themselves, without trusting the rendering of the verify page, can pull the public key, fetch the signed record from veracly.app/api/verify/<uuid>, and run any standard Ed25519 verifier locally against the canonicalization rules above.

To be straight about one thing the security reviewers among you will check: our source repository is not public, and we should not imply otherwise. What is open is the part that matters for this claim, the verify page runs the same verification in your browser, so the code you are asked to trust is the code shipped to your machine, and everything it depends on, the public key, the signed record, and the retention policy, is served from documented, unauthenticated URLs. If you want the canonical-record spec in writing for a vendor review, mail support@veracly.app and we will send it.

How long the verify URL stays live

This is the part most vendors leave out, so: verification is not permanent. The integrity block on every Veracly PDF states that the Verify URL is valid for 30 days from issue, and that is the number to plan against when you hand a report to somebody.

  • Paid reports: no automatic expiry. The anchor is kept independently of the parent scan record, so it outlives the scan data when that is dropped at 12 months and the verify URL keeps resolving; the printed 30 days is a deliberately conservative floor, not the real ceiling.
  • Free scans: the anchor is retired by a daily sweep 60 days after signing, and the rendered PDF is deleted from our storage on the same window.

A retired anchor is not a 404, and that distinction is the whole point. The sweep nulls the verification material and stamps the row; the endpoint then returns a record confirming that the report existed, when it was signed, which key signed it, and which retention rule retired it, with a link to the machine-readable policy at veracly.app/.well-known/veracly-retention.json. A regulator who saved the URL months ago gets a policy reference rather than something indistinguishable from a quiet purge.

Be clear about what survives retirement, though, because it is less than it sounds. The digest exists in exactly two places, the ledger anchor and our report record. The sweep clears the first and the same window deletes the second, so once a free-scan anchor retires there is no fingerprint left for anyone to compare against, us included. Proof-of-existence survives; proof-of-integrity does not. Support can reconcile a SHA-256 against the stored fingerprint using the Verification ID for as long as the fingerprint exists, which in practice means paid reports.

Two practical consequences. Do not paste a verify URL into anything that will be read later, a cover letter, a tender response, a trust page, without checking that it still resolves. And if a report is going to matter beyond its window, get it verified while the anchor is live and keep the counterparty’s confirmation, rather than assuming the check will still be there when they get round to it.

Why this matters for an SMB

Most SMBs will never use the verification flow themselves. The customers that ask for proof of authenticity are downstream: an enterprise procurement team running a vendor security review, a regulator following up on a complaint, an insurer underwriting cyber liability cover, a B2B prospect doing due diligence before signing a contract.

When one of those parties asks, the answer is no longer “here is a PDF, please trust it.” The answer is “here is the PDF and the verify URL, confirm it independently.” That is a categorically different conversation.

What we deliberately did not do

A few design choices worth naming, because the audit-ledger space has a lot of theater dressed as cryptography:

  • We do not anchor to a public blockchain. A regulator does not want to learn about block heights to verify a compliance report. The well-known-URL plus Ed25519 keypair is the simplest construction that meets the actual requirement: independent verifiability without trusting Veracly.
  • We do not embed the signature in the PDF. Tempting but circular. Any embedded value would either be a self-reference (the PDF says “my hash is X”, change the hash, change the PDF, mathematically broken) or a detached signature tucked in metadata that PDF viewers routinely strip on re-save.
  • We do not require a Veracly account to verify. An authentication wall on a verification endpoint defeats the entire point of public-key cryptography. Authentication-gated audit trails are a controlled-narrative product; ours is not.
  • We do not roll our own crypto. Ed25519 is the digital-signature algorithm specified in RFC 8032, standardised for SSH in RFC 8709, for TLS in RFC 8422, and for DNSSEC in RFC 8080. The underlying libraries have been reviewed by people more qualified than we are.

What is coming next (phase 2)

The current ledger is per-scan: one signature per (scan, language), and it is queryable but not enumerable, you need the scan ID to look a record up. Two things are designed and not yet shipped. Treat them as roadmap, not as product:

  • A documented key-rotation playbook published at the well-known URL. Every signing key would carry a validity window, and an old key’s public counterpart would stay published so historical reports remain verifiable. The key_id field already in the signed record exists to make this work.
  • A transparency log of issued reports, so the population of signatures is itself auditable rather than only individual records. We would log the (scan_id, sha256, issued_at) triples and not the report contents. Until it ships, “is this the complete set of reports Veracly issued?” is a question our ledger cannot answer, only “is this specific report authentic?”

Not on the list, and consistent with the section above: a blockchain anchor. An earlier draft of this post floated a daily Merkle root posted to OpenTimestamps. It contradicted the design principle two paragraphs up and we dropped it rather than ship the contradiction.

See also: Reading your first Veracly report · Sharing reports with developers, lawyers, and regulators

Common questions

Why does Veracly sign its reports?+

Because the value of an audit report depends on the reader trusting it. An unsigned PDF can be edited; a signed one cannot, any byte change invalidates the signature. The threat model includes both bad-actor tampering and accidental drift (re-saving a PDF in a viewer that re-encodes it).

Do I need a Veracly account to verify a report?+

No. The verify endpoint uses no authentication, the public key is published at a well-known URL, and the verification runs in your browser against bytes you can inspect. A regulator who has only the PDF can confirm authenticity without contacting Veracly.

How long does the verify URL keep working?+

Not forever, and the report says so itself. The integrity block printed on every PDF states that the Verify URL is valid for 30 days from issue. Behind that: paid-report anchors carry no automatic expiry and are kept independently of the parent scan record, so they outlive it when it is dropped at 12 months and the verify URL and the support fallback both keep working afterwards. Free-scan anchors are retired by a daily sweep 60 days after signing, and the free PDF is deleted from our storage on the same window. A retired anchor does not 404, it returns a record confirming the report existed, when it was signed, which key signed it, and which retention rule retired it, with a link to the published policy at veracly.app/.well-known/veracly-retention.json. But the fingerprint itself is erased at that point, from the ledger and from our report store, so nobody can check byte-integrity afterwards, including us. That is storage limitation working as intended, not a gap. If a free-scan report matters to you beyond 60 days, verify it while the anchor is live.

What changes if a signature does not verify?+

The PDF in front of you is not the one Veracly issued. Either it has been edited (intentionally or by a re-encoding viewer), or it was never issued by Veracly. The verify page tells you which scan UUID was originally signed; if that scan exists in our ledger but the bytes disagree, the file has been altered.

See where your site stands.

Run a free Veracly scan and get a multi-jurisdiction report, EAA, GDPR, ADA, UK Equality Act, AODA, with copy-paste developer fixes.

Run a free scan

Keep reading