PRGuard

For assessors

Check the Pack Yourself.

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.
Illustrative example

What You Need.

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.

Four Steps.

The video’s words

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.

  1. Check the checker

    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
  2. Run it with the root hash

    Run it with the root hash the organisation gave you.

    python3 verify.py <the pack’s ZIP or folder> --expect <the root hash>
  3. Add PRGuard’s public key

    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
  4. Read the first line

    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.

    • VERIFIEDexit 0
    • FAILEDexit 1
    • Refusedexit 2

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.

Try It on a Sample Pack.

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.

Covers
Halcyon Payments, 2026-Q3: Cyber Resilience Act, EU AI Act
Root hash
834dd88421f300c6a8d4167b5e9f974697f060ade012be235aa897d584edc751
Sample key
0jqV9sYRNhr/O6gpC4zVfC/WMlG7WeyxKyUuQKvpmP8= (prg-f2db5306e84901dc)
File SHA-256
e368900a216cc0cec4b3411fae761bee76d55b687c89e40a5f877c45815e6a26

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=

What VERIFIED Means.

  • Every file the manifest lists is in the pack and matches its SHA-256, and there is no file it does not list.
  • The root hash over that list is the one you were given.
  • Where the pack is signed, the signature over the manifest checks out, by the key you named.
  • Every line of the journal links to the line written before it, from the period’s first entry to its last.

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.

If It Says FAILED.

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
Illustrative example

The files5

FAILED: missing file…
A file the manifest lists has been taken out of the pack.
FAILED: extra file…
A file has been added that the manifest does not list. A computer’s own hidden files are ignored, so this is a file someone put there: check the ZIP as it was sent.
FAILED: does not match its hash…
A file has been changed since the pack was made.
FAILED: bytes and the manifest says…
A file has been changed since the pack was made.
FAILED: cannot be read…
A file in the pack is damaged and cannot be read.

The manifest and its root hash4

FAILED: the root hash is…
This is not the pack the organisation was sent, or the hash was copied wrongly. Check the hash with them.
FAILED: the root hash does not match…
The manifest itself has been changed.
FAILED: does not match scope.json…
The manifest and the scope statement disagree: one has been changed.
FAILED: a path a pack cannot hold…
The manifest names a file outside the pack. PRGuard never writes one.

The signature and the key4

FAILED: the signature…
The signature is not over this manifest, or not by the key it names.
FAILED: the pack is signed with key…
The pack was signed, but not by the key you named.
FAILED: the pack is not signed…
You named a key, and the pack carries no signature to check it against.
FAILED: names key…
The manifest, the signature and the key disagree about which key signed the pack.

The journal1

FAILED: journal.jsonl line…
A journal entry has been changed, removed or put out of order since it was written.

The ZIP itself3

FAILED: more than one folder at its top…
The ZIP holds more than one folder, so it is not one pack as PRGuard made it. Check the ZIP as it was sent, or ask the organisation for it again.
FAILED: more than once…
The ZIP has been rebuilt with a file in it twice.
FAILED: is not a ZIP file…
The file is not the ZIP PRGuard made, or is damaged: often a download that did not finish.

What a Verified Pack Shows, and What It Does Not.

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.

E1

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.

E2

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.

What Is in the Pack.

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.

summary.html
A readable summary of the pack, and how to check it.
manifest.json
Every file with its SHA-256, and the root hash over them.
signature.json
On a signed pack: PRGuard’s signature over manifest.json, and the key it was made with.
scope.json
What the pack covers, and each repository left out with its reason.
coverage.csv
Every change that reached a default branch or a release branch, and how it was governed.
releases.csv
Each tag pushed in the period, and the branch it was cut from.
controls.csv, controls.md
Each control’s state and counts, and the rules behind it.
rules/
The organisation’s own rule files, at the versions the audits ran with.
samples/
Sampled audits for each control: names, sizes, fingerprints and links.
exceptions.csv
The exceptions register for the period.
ai-register.csv
Changes an AI tool or a bot had a hand in, with who approved and merged each.
policies.json
The organisation’s policies and their changes.
journal.jsonl
The journal for the period, each entry linked to the one before. An entry outside the period, about a repository left out or naming a framework not offered, appears by its hashes only, so the chain has no gap.
attestations.json
The owner’s sign-offs of the period.
verify.py
The checker.

PRGuard’s Signing Keys.

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.

https://prguard.dev/.well-known/prguard-pack-key.json

How the evidence is gathered, part by part

Keep the Record as You Ship.

The pack you are checking was gathered change by change, as the team shipped.