Docs menu

Verify Without Us

Goal: Check a Provenance export and the signatures in it yourself, on your own machine, without trusting OpenX to tell you it is intact.

One command reads files you already hold: a signatures export, the workspace's public seal keys and, if you have them, the exports of the records the signatures attest. It makes no network call and needs no Fathym account. It prints a verdict for each file's chain and each signature, then one verdict for the whole export.

The command is published in @o-industrial/common as /verify. It uses the same functions OpenX uses to hash records and check seals, so it is not a second, different reading of the record.

Get the files

Ask someone with access to the workspace for these files, or read them yourself through the API. Keep each file exactly as it was downloaded: the check reads the bytes.

FileWhere it comes from
The signatures (sigs.json)GET /ox-api/provenance/events/export?target=DomainSignatures&format=json&scopeKey=<scope>
The seal keys (seal-keys.json)GET /ox-api/provenance/signatures/seal-keys, saved as it answers: { "Keys": [...] }
The records signed (optional, one per container)GET /ox-api/provenance/events/export?target=<Container>&format=json&scopeKey=<scope>, for example target=DomainEvents
  • scopeKey is the scope the records belong to (a study, in Sample Management). A signature is kept in the same scope as the record it signs, so export both with the same scopeKey.
  • An export holds at most 10,000 rows, with no paging. If the answer's x-provenance-has-more header is true, the file is only part of the record: narrow the date range (startDate, endDate) and export again.
  • A JSON export does not name its own container inside the file, so you name it when you run the command.
  • Reading the seal keys needs the Workspace.Provenance.Read right, the same right an auditor reads the record with.

Run it

You need Deno. Then:

deno run --allow-read jsr:@o-industrial/common@<version>/verify \
  --signatures sigs.json \
  --keys seal-keys.json \
  --records DomainEvents=events.json
  • --signatures <file>: the signatures export. Required.
  • --keys <file>: the seal keys. Required.
  • --records <Container>=<file>: an export of a container the signatures attest, named by its container. Repeat it once per container. Optional. The container name must be the one the signatures name as their TargetType, or their records are not found.
  • <version> is a release of @o-industrial/common that carries /verify (it first shipped in 0.1.2632-round-10). Name an exact version, and write it down with your results, so the same check can be run again later.

--allow-read is the only permission it is given. The command reads the files you name and nothing else: it makes no network call, reads no environment and writes no file, and if anything in it tried, Deno would stop and ask you rather than allow it. Deno downloads the command from JSR the first time you run it; after that it runs from Deno's cache, offline.

What it checks

Each file's chain. Every record carries a Hash over its own content and the PreviousHash of the record before it. For each file the command:

  • works out each record's hash again and compares it with the Hash the record carries. A record that does not match is changed.
  • where a record's predecessor is in the same file, checks that its PreviousHash is that record's Hash. One that is not is a broken link.
  • where a record's predecessor is not in the file (an export filtered by date, or cut at 10,000 rows), counts a gap. A gap is not a failure: the file simply does not hold that record.

Each signature.

  • Its own record: the signature is a record in the signatures chain; did its hash work out the same?
  • Its link: when the record it signs is in one of the --records files, the command works out that record's digest again and compares it with the signature's BindingDigest, the digest of what the person was shown when they signed.
  • Its seal: the signature's seal is checked against the key version it names (LinkSealKeyID), from the seal keys file. The seal is ES256 over the signature's BindingDigest, CeremonyID, Meaning, SignedAt, SignedBy, TargetRecordID and TargetType.

What it prints

One line per file's chain:

Chain DomainSignatures: 3 records, 0 changed, 0 broken links, 0 gaps
Chain DomainEvents: 42 records, 1 changed (000000000017), 0 broken links, 2 gaps

One line per signature, with its record, link and seal:

Signature 000000000003 Approval on DomainEvents 000000000017 by ana@acme.example at 2026-10-31T14:02:11.000Z: Changed (record Intact, link Changed, seal Valid)

Then the verdict for the whole export, for example Verdict: NotValid.

A signature's verdict

VerdictWhat it means
ValidEvery check ran and passed: its own record is intact, the record it signs still matches, and its seal verifies
ChangedThe signature's own record, or the record it signs, is not what was signed
InvalidThe seal does not verify against the key version the signature names
NotCheckedSomething could not be checked: the record it signs is not in the files given, the seal key is not in the keys file, or the signature was written before sealing (seal Unsealed). This is never a pass

The seal reads Valid, Invalid, Unsealed (written before sealing) or NotChecked (no key for it). The link reads Intact, Changed or NotInExport.

The export's verdict

VerdictWhat it means
ValidNothing failed and every signature was fully checked
NotValidSomething failed: a changed record or a broken link in any file, or a signature that reads Changed or Invalid
IncompleteNothing failed, but at least one signature reads NotChecked
NoSignaturesThe signatures file holds no signatures, so no signature was verified. It is never reported as Valid

Exit codes

CodeWhen
0The export reads Valid
1Any other verdict: NotValid, Incomplete or NoSignatures
2An argument or a file is wrong: a flag it does not know, a missing --signatures or --keys, a file it cannot read, a file that is not JSON, or a file in the wrong shape. It prints what was wrong and how to call it

So a script can stop on anything but 0.

What it cannot prove

Read these before you rely on a result. A Valid export is evidence about these files, nothing wider.

  • It checks the files, not the whole record. Records missing between two records in a file show as gaps. Records missing from the start or the end of a file leave no trace in it. To know you hold everything, export the whole date range and check that x-provenance-has-more was false.
  • A chain only agrees with itself. Someone who rewrote a file from start to finish, with fresh hashes, would make a chain that agrees with itself. What ties a signed record to what the person saw is the seal on its signature, which covers the BindingDigest. Records that no signature attests have no seal to hold them. The storage the export was read from (your workspace's Provenance, in your own Azure subscription, at the level you set) is outside what this command can see.
  • It trusts the keys you give it. A seal that verifies proves it was made by the private half of the key in your keys file. Get that file from the workspace itself, and keep it with the export.
  • It does not judge the signer. It does not check who the person was, how they signed in, whether they held the right to sign, or whether the meaning was the right one. OpenX checks those when a signature is made, and the validation pack says who is responsible for each. In particular, OpenX sees that a login happened, not that a password was typed again.
  • It is not a finding of compliance. The command checks data. Whether your system meets 21 CFR Part 11, EU Annex 11 or any other rule is for your quality unit and your auditor to decide.

Next steps

On this page