For assessors
You have been given a PRGuard evidence pack: one ZIP file holding an organisation’s records for a quarter. You can check, on your own machine and without asking PRGuard, that it is the pack PRGuard made and that nothing in it has changed since.
$ python3 verify.py example-ltd-q3-2026.zip \ --expect <the root hash> --key <PRGuard’s public key> VERIFIED pack <the pack’s id> for Example Ltd, Q3 2026 files 278, each matching its hash root hash <the root hash> strength E2 signed by key <the key id>, the key given journal 1639 lines, each linked to the one before The root hash is the one expected.
The pack
A ZIP file, named for the organisation and the quarter. It holds its own checker.
Its root hash, and the checker’s
Two 64-character codes. Ask the organisation for them: they have both in the email PRGuard sent when the pack was made, and on their Packs page. Do not take them from the pack itself, which could have been changed with them.
Python 3.9 or later
Nothing else to install and no PRGuard account. The check reads the pack, writes nothing and needs no network; only looking up PRGuard’s published keys (step 3) does.
An organisation has handed you its evidence pack for the quarter. You can check it yourself, on your own machine, without asking PRGuard.
With the pack come two codes, from PRGuard's email or the organisation's Packs page: the pack's root hash, and the checker's.
First, check the checker inside the pack. Its fingerprint must match the code you were given, so a forged pack cannot bring a checker that always says yes.
Then run it with the root hash and PRGuard's published key. Verified: every file is the one PRGuard made, unchanged, the signature is PRGuard's, and the journal has no gap.
Change a single line, and it says so. It names the first thing that does not match, and stops.
Every message it can print is explained on one page, with what to do next.
No account, nothing to install but Python, and no need to take our word for it.
Check the ZIP exactly as it was sent, or the folder it unzips to: the checker reads either, and ignores the hidden files a computer adds when it zips or unzips. The checker is the file verify.py inside the pack, one file of Python’s standard library only. Before you run it, check it is PRGuard’s: its SHA-256 (shasum -a 256 verify.py) must be the checker’s hash the organisation gave you. A forged pack could carry a checker that always says VERIFIED.
shasum -a 256 verify.py
Run it with the root hash the organisation gave you.
python3 verify.py <the pack’s ZIP or folder> --expect <the root hash>
If the pack is signed, add PRGuard’s public key. PRGuard publishes every key it has signed packs with at the address below, each with its id and the dates it was in use: find the one with the key id you were given, check that it is on that list and not revoked, and pass its public key. The key id alone also works, but it names only part of the key; the public key is compared whole, and the signature is checked with it.
python3 verify.py <the pack’s ZIP or folder> --expect <the root hash> --key <PRGuard’s public key>
https://prguard.dev/.well-known/prguard-pack-key.json
Read the first line it prints: VERIFIED or FAILED. It exits 0 when the pack is verified and 1 when it is not, so it can be scripted. An option it does not understand, or one given without its value, is refused with exit 2 and nothing checked, so a check you asked for is never skipped.
Teams that use PRGuard’s command-line tool can run prguard verify instead. It is the same check, written separately in Go, and both are held to one set of test packs. It does not use the verify.py inside the pack, so it is also a check that does not depend on the pack’s own copy.
Halcyon Payments is a fictional company PRGuard uses for demonstrations. This is the evidence pack for its demo quarter, made by the same code that makes a customer's, with verify.py inside. Download it and run the check on your own machine. Halcyon's activity is made up; the hashes, the journal chain and the signature are real.
The sample is signed with a sample key, never PRGuard's own, so it is not on the list of PRGuard's keys. Pass the sample key shown here instead.
unzip prguard-sample-halcyon-2026-q3.zip verify.py
python3 verify.py prguard-sample-halcyon-2026-q3.zip --expect 834dd88421f300c6a8d4167b5e9f974697f060ade012be235aa897d584edc751 --key 0jqV9sYRNhr/O6gpC4zVfC/WMlG7WeyxKyUuQKvpmP8=
Under VERIFIED it prints the pack’s id, the organisation and the quarter, how many files, the root hash, the strength (E1 or E2), whether and by which key it was signed, and how many journal lines it walked. Check that the organisation and the quarter are the ones you expected. The journal count includes lines the pack carries by their hashes only, entries outside the period, about a repository it leaves out or naming a framework not offered, so it can be higher than the count of entries in summary.html.
It names the first thing that does not match and stops. A pack that fails should not be relied on. A file that still reads but no longer matches its hash has been changed, which may have been done on purpose, not only by damage: ask the organisation to download the pack again from their Packs page, with the root hash from PRGuard’s email. If you have been given more than one copy, that root hash decides: only the organisation’s own pack verifies with it.
$ python3 verify.py halcyon-2026-q3.zip --expect 4acc8913… FAILED: coverage.csv does not match its hash (the manifest says 3f9a1c07…, the file is 8c21d4be…) $ echo $? 1
FAILED: missing file…FAILED: extra file…FAILED: does not match its hash…FAILED: bytes and the manifest says…FAILED: cannot be read…FAILED: the root hash is…FAILED: the root hash does not match…FAILED: does not match scope.json…FAILED: a path a pack cannot hold…FAILED: the signature…FAILED: the pack is signed with key…FAILED: the pack is not signed…FAILED: names key…FAILED: journal.jsonl line…FAILED: more than one folder at its top…FAILED: more than once…FAILED: is not a ZIP file…A verified pack is the one PRGuard made, unchanged since. Signed, it also shows PRGuard made it. Its journal shows no entry between the first line and the last was removed after it was written.
It does not show that the records are right, or that the organisation conforms to any framework. The records are evidence: whether a control is met, and whether the evidence is enough, is your judgement.
The records are append-only and hashed on export. That is not the same as tamper-proof.
Captured: every file is listed in a manifest with its SHA-256 and one root hash is taken over the list, so the pack can be checked offline against the root hash PRGuard gave when it made it.
Attested: E1, plus PRGuard's signature over the manifest, checked against its published key, and a journal whose entries are chained, so the pack shows who made it and that no entry between its first and last line was removed after it was written; neither shows the records were right.
Open summary.html first: it says what the pack covers, which repositories it leaves out and why, and repeats these steps. A pack never holds the organisation’s source code: a finding is given by its rule, file and line.
PRGuard publishes every key it has signed packs with, each by its id (prg- and sixteen characters), its public key, the day it was first used, the day it was retired, and whether it was revoked. A key is revoked only if it was exposed: do not rely on a pack it signed. The organisation’s email and Packs page name the one a pack was signed with.
The pack you are checking was gathered change by change, as the team shipped.