# Verify a Walk-Check record yourself

*[Po slovensky](README.sk.md)*

You were given a link or a PDF report about an aircraft walkaround and you have to
decide whether to rely on it. This tool lets you check it **without an account,
without our servers and without trusting us**.

It does not import the application that produced the data and it does not call our
API. It reads a public key from a file and recomputes everything locally. That is
the whole point: if you do not trust us, you do not have to.

---

## What you need

| | |
|---|---|
| The proof | the PDF report you received, or the share link `walkcheck.eu/share/<code>`, which offers the PDF |
| Python | 3.12 or newer |
| Three packages | `cryptography`, `pydantic`, `structlog` |
| `pdfdetach` | part of Poppler (`poppler-utils` or `poppler`), to take the proof out of the PDF |
| An account | **no.** Nothing here signs in anywhere |
| Network | only to download this tool and the key once. The check itself is offline |

`psycopg` and `boto3` are listed in `pyproject.toml` but are imported only when you
read the chain straight from a database or ask for the key from AWS KMS. Verifying
from a file needs neither.

## Verify it, in one block

Everything below can be copied as it stands. Replace only the name of your PDF.

```bash
# 1. the tool, the key and the timestamp authority anchor (download once)
curl -O https://walkcheck.eu/verifier/verify_chain.py
curl -O https://walkcheck.eu/verifier/_asn1.py
curl -O https://walkcheck.eu/verifier/_tsa.py
curl -O https://walkcheck.eu/verifier/_anchor.py
curl -O https://walkcheck.eu/verifier/_worm.py
curl -O https://walkcheck.eu/verifier/walkcheck-audit-public-key.der
curl -O https://walkcheck.eu/verifier/tsa-trust-freetsa.pem

# 2. the three packages, in an environment of their own
#    (many systems refuse pip install outside one)
python3 -m venv venv
. venv/bin/activate
python3 -m pip install "cryptography>=43" "pydantic>=2.9" "structlog>=24.4"

# 3. the machine-readable evidence, attached inside the PDF
#    (only the proof: a file planted in the PDF next to the tool could take its place)
pdfdetach -savefile walkcheck-proof.json report.pdf

# 4. the check itself
python3 verify_chain.py \
  --export walkcheck-proof.json \
  --public-key walkcheck-audit-public-key.der \
  --tsa-trust tsa-trust-freetsa.pem
```

Use the key and the anchor from `walkcheck.eu/verifier/`, never a key that came
together with the PDF: whoever holds a private key can sign anything with it, so the
check is only as good as the key you give it.

## What the output means

A real run of step 4 over a production record prints exactly this on standard output
(log lines go to standard error):

```
STORAGE: pilot mode, the record is NOT under an irreversible lock; it is kept for at least four years, then in a deep archive, and only Walk-Check can delete it, as an exception (this does not weaken the chain)
VERIFIED (sealed=True, records=10), NOT CHECKED: global anchor (--require-anchor), WORM attestation (--require-attestation)
SEALED HEAD (record_hash): b321eb4bf2b55b0a57eefaf01f059463f728cb6c1746dd48b8e0b677f0f07e34, WALKAROUND ID: 4b3bcb62-2ea9-4c05-ab88-164e80e3de32
LOCATION (reported by device): none in chain (record predates device location)
```

- `STORAGE:` appears only for a record stored in pilot mode (see the end of this page).
- The verdict line. `records=` is the number of entries in this record's chain.
- `SEALED HEAD` and `WALKAROUND ID` **must match the PDF** (the fields of the same name).
  If they do not, the attachment is not the record the PDF describes: do not rely on it.
  The tool checks the attachment, not the printed pages: where the PDF and the attachment
  differ (a result, a registration, a video hash), the attachment is what was sealed.
- `LOCATION` is the position the phone reported, if any. It never changes the verdict.

| Verdict | What it says | Exit |
|---|---|---|
| `VERIFIED` | the chain is intact from genesis, every record hash and payload hash matches, every entry carries a valid ECDSA signature and the seal a valid timestamp | 0 |
| `TAMPERED` | the record does not match what was sealed. With the key and the anchor from this page that means a change after sealing; with a wrong key or a wrong anchor file a healthy record also fails here. The line names the entry and the reason, for example `TAMPERED at seq=284 reason=payload_sha256_mismatch`, and nothing else from the chain is printed | 1 |
| `PENDING` | sealed, but no storage attestation covers it yet. Appears only with `--require-attestation` and its data (below). Not a finalised proof | 0 |
| `UNSEALED` | the chain is internally consistent but was never sealed. A walkaround still in progress. **Not proof** | 0 |
| `UNVERIFIED_DEV` | you ran it with `--allow-unsigned`, so signatures were not required (without a key they are not checked at all). **Not proof** | 0 |

When more than one applies, the worse one wins: `TAMPERED` before `PENDING` before
`UNSEALED` before `UNVERIFIED_DEV` before `VERIFIED`.

**Exit code 2 means nothing was verified:** the command is wrong or a file cannot be
read. The tool says which in one line, for example without a key:

```
ERROR (config): FAIL-CLOSED: give --kms-key-id OR --public-key (a trusted key to verify signatures with), or --allow-unsigned explicitly for dev (prints UNVERIFIED_DEV, not VERIFIED)
```

or with a file it cannot read:

```
ERROR (input): cannot read the export: FileNotFoundError: [Errno 2] No such file or directory: 'walkcheck-proof.json'
```

An export that can be read but is malformed (a missing field, a broken time) stops
with a Python error and exit code 1, like `TAMPERED`: do not rely on it.

The tool is fail-closed: without `--public-key` or `--kms-key-id` it refuses to run
rather than silently skipping the signatures.

## The NOT CHECKED tail and the switches

Behind the verdict the tool names every layer it did **not** check in that run. That
tail is the scope of the verification, not a failure. Some layers need data that the
PDF does not carry; the tool refuses those switches when the data is missing or empty
(exit code 2) instead of giving a verdict it could not check.

| Switch | What it needs | Where you get it |
|---|---|---|
| `--tsa-trust FILE` | the certificate of the timestamp authority | this page: `tsa-trust-freetsa.pem` |
| `--require-anchor` | an export of the global anchor (`--anchor-export FILE`) or read access to the database (`--dsn`) | **not in the PDF.** From Walk-Check or the operator of the storage, on request |
| `--require-attestation` | the global anchor as above **and** the storage attestations (`--attestation-path DIR` or `--worm-bucket`) | **not in the PDF.** From Walk-Check or the operator of the storage, on request |
| `--no-tsa` | nothing. Skips the timestamp; the tail then says `timestamp (skipped by --no-tsa)` | |
| `--allow-unsigned` | nothing. Skips the signatures and prints `UNVERIFIED_DEV`, never `VERIFIED` | |

## One warning you may see

Depending on the version of the Python `cryptography` library, a run may print:

```
UserWarning: PKCS#7 certificates could not be parsed as DER, falling back to parsing as BER.
```

The rest of the message differs between versions. Some versions print it on every run,
others never. It does not affect the verdict.
RFC 3161 timestamp tokens from real authorities may carry their certificates BER
encoded, which CMS allows, so the library takes its BER path; the signature itself is
checked over the exact bytes by this tool's own decoder. We could silence the warning
in one line and deliberately did not: in a tool whose whole job is to check
certificates, a hidden warning about certificates is worth less than an explained one.
If the library ever turns it into an exception, the tool rejects the token rather than
passing it.

## Check that the tool really checks

Change one value inside `payload_json` in `walkcheck-proof.json`, for example one letter
of the registration, and run it again. It must print `TAMPERED`. If it still prints
`VERIFIED`, there is no point relying on the rest: the verification would be checking
nothing.

Not every character in the file is under the hash. The header of the file
(`generated_by`, `region`, `kms_key_id`) and the `kms_key_id` of each entry are labels,
and changing them does not change the verdict. Neither does a pretty-printer, a
reordered key or an added blank line: they do not alter the canonical form.

## What this proves, and what it does not

It proves that **the sealed record has not been changed since it was sealed**: the same
positions, the same times, the same video, the same person. Entries after the seal are
signed one by one and listed in the chain, and a correction of details after the seal
(for example the registration) can change what the PDF shows; the PDF then prints the
corrected value.

It does **not** prove any of the following, and nobody should let it be stretched
that far in a dispute:

- **not** that a part is in good condition. The walkaround is a record of what was
  in frame, not an airworthiness assessment. No verdict here says anything about the
  aircraft.
- **not** that the video was unedited before it was uploaded. The seal starts at the
  moment the server accepted the recording. What happened in front of the camera
  before that is outside the chain.
- **not** that a person stood next to that aircraft. The record holds the movement
  of the phone, the name of the site as it was entered, and the time. It is not a
  measurement of where anyone was.
- **not** that the walkaround was complete. Positions that were missed stay visible
  in the record as missing; that is data, not a fault.
- **not** that the file holds every entry written after the seal. Entries after the
  seal, such as archiving or a correction of details, are signed one by one, but if
  the last of them are left out, the file alone cannot show it: the chain up to the
  seal stays intact and the verdict stays `VERIFIED`, with a smaller `records=`.

Automatic part recognition, where it appears at all, is experimental and does not
enter the verdict.

## Two things we say out loud

The timestamp authority in the pilot is freetsa.org. It is a genuine RFC 3161
authority, but not an eIDAS qualified one.

Storage currently runs in pilot mode, so a record is not under an irreversible lock.
Before the seal the user can delete a record. After the seal it is only hidden and
archived, it is kept for at least four years, and only Walk-Check can delete it
permanently, as an exception. The sealed content never changes. The verifier prints
the storage mode itself rather than leaving it to us (the `STORAGE:` line above).

Neither weakens the chain. If a record exists, you can prove it was not changed.

---

Questions: [office@walkcheck.eu](mailto:office@walkcheck.eu)
