Skip to content

noqtis-check(1)

noqtis-check — check a sealed capsule folder against its seal

noqtis check [-p key.pub] [-j | --json] [--anchor der-file ...] [--only path ...] [--lineage input-dir ...] [--keys file ...] [--card file] dir | url

noqtis check [-p key.pub] [-j | --json] [--anchor der-file ...] --proof file | url [dir | url]

noqtis check checks the capsule dir (noqtis-capsule(5)): it reads and checks the seal noqtis.sig, checks that the SHA-256 of noqtis.json is the one in the seal, verifies the signature over the exact bytes of noqtis.json, then lists the folder and checks every listed file (present, a regular file, the size, the SHA-256 and the SHA3-256) and that no file is present that the manifest does not list.

It prints one row per check: a status (‘ok or ‘FAIL’’), the check's name and a detail, then a summary line

<dir>: OK (<n> checks passed)
or
<dir>: FAILED (<k> of <n> checks failed)

Without -p, the public key inside the seal is accepted: the check then shows that the files are those the key holder sealed, not who the key holder is. The ‘key’ row says so, and a note on standard error says to pass -p. With -p, the seal's key must be exactly the given key, and the seal's algorithm the one the key file names (a ‘key.alg’ row fails otherwise). Each option that takes a value may be given once.

Files are opened folder by folder from dir, never through a link, so a folder swapped for a link while the check runs fails it instead of being followed. A listed file whose size differs is reported without being read, and no file is read past one byte more than its listed size. File names and other text from the folder or a TSA are printed with control characters escaped (‘\x1b’), so they cannot rewrite the terminal.

With --anchor, the seal must carry an RFC 3161 timestamp over its signature whose token verifies under one of the anchors, with the TSA certificate's path valid at the token's genTime; the ‘timestamp’ row then shows the genTime. Without --anchor, a seal that carries a timestamp fails its ‘timestamp’ row: a timestamp that cannot be checked is never passed silently.

Given a url (‘http://, or ‘https:// when noqtis was built with its TLS client’’) in place of a folder, the capsule served there is checked: the URL of the folder that holds noqtis.sig and noqtis.json on the server, without a query or fragment. Both are fetched into a new temporary folder and checked first, against -p and the anchors when given; only when the seal, its signature and the manifest check are the listed files fetched (with --only, only those), each by its path under the URL, percent-encoded, and at most its listed size, a file over 1 GiB or past the 10000th not at all. The folder is then checked as any other, so a file the server lacks or changed fails its row, and everything written to it is removed after. The report names the URL. Redirects are followed, at most five, and only on the same server (the same host and port, or ‘http://’ on port 80 to ‘https://’ on 443); never from ‘https://’ to ‘http://’. A redirect to another server is refused, so a server cannot point the fetch at a host only this machine reaches, such as localhost or the local network; give the new URL itself if you trust it. A --proof may also be a URL.

After the table, for each failed check, a line on standard error says what to do:

noqtis check: check: what to do
key.pub, --pub=key.pub
The public signature key the capsule must be sealed with.
, --json
Print one line of JSON instead of the table:
{"capsule":"<dir>","ok":<bool>,"result":"<nqerr text>",
 "checks":[{"check":"<name>","ok":<bool>,"detail":"<text>"},...]}

A failed check also has ‘fix’, the text of the line on standard error.

der-file
A TSA trust anchor: the root or issuing certificate of a time-stamping authority, DER-encoded (convert PEM with ‘openssl x509 -outform DER’). Up to 32; one that verifies the token is enough.
path
Check only this listed file, up to 32 times: a partial check. The seal, the signature and the whole manifest are still checked; files not named are neither read nor needed, and files present but not listed are not looked for. ‘partial’ rows say so, so a partial pass is never mistaken for a full one. A named path the manifest does not list fails its ‘file’ row.
input-dir
The folder of a capsule that may be one of the inputs of dir (a format 2 capsule sealed with noqtis-seal(1) --input), up to 32 times. Each input is found among them by its id, the SHA-256 of the bytes its seal signs, and checked in full without a pinned key; its own inputs are looked for in the same folders, to 64 levels. One ‘lineage’ row per input says whether it was found and checks.
file
Check a proof made by noqtis-prove(1) instead of a folder: the seal in the proof, its signature over the manifest's head, and each proven entry's Merkle audit path to the root the head signs (‘proof.entry’). With dir, the proven files there are also checked against their entries, and a ‘partial’ row says the capsule's other files are not in the proof. --only, --lineage, --keys and --card do not apply.
file
A key statement from noqtis-rotate(1) or noqtis-revoke(1), up to 32 times; it needs -p. The statements are followed from the -p key: a seal made with a key it was rotated to passes the ‘key’ row, and one made with a key since rotated away or revoked passes only when a verified timestamp (--anchor) shows it was sealed before then (‘key.time’). One ‘keys’ row per statement; two statements about one key, or a rotation back to an earlier key, trust no key.
file
An identity card from noqtis-idcard(1). When it verifies and is for the key that made the seal, the ‘card’ row names who sealed the folder; when it does not verify, or is for another key, the row fails and says why. The card is signed by its own key, so without -p it names the sealer only if the seal's key is the one they published. A card that cannot be read is a usage error.

The row names are stable, for scripts:

noqtis.sig exists, is a regular file (not a link) and is at most 1 MiB.
, seal.canonical
It is a JSON object in canonical form (RFC 8785).
It has exactly the fields of ‘noqtis-seal-1’, with the right types; ‘manifest_sha256’ is 64 lower-case hex digits.
‘format’ is ‘noqtis-seal-1’.
‘alg’ names a signature algorithm noqtis knows.
‘context’ is at most 255 bytes.
, seal.signature
‘public_key’ and ‘signature’ are base64 of the algorithm's sizes.
noqtis.json exists, is a regular file and is at most 512 MiB.
Its SHA-256 is the seal's ‘manifest_sha256’.
Whether the key was pinned with -p.
The signature verifies over the exact bytes of noqtis.json.
Present only when the seal has a timestamp or --anchor was given.
The folder could be listed within the limits.
One row per listed file.
, manifest.canonical, manifest.fields
The manifest is canonical JSON with exactly the fields of ‘noqtis-capsule-1’.
, manifest.created, manifest.order
‘format’ is ‘noqtis-capsule-1’ (or ‘noqtis-capsule-2’ under a ‘noqtis-seal-2’ seal), ‘created’ is an RFC 3339 UTC time, and the files are sorted by path with no repeats.
Format 2: the Merkle tree over the file entries has the root and leaf count the signed head gives.
With --lineage: one row per input of the capsule.
With --keys: one row per statement, and its signatures.
With --card: the card verifies and is for the seal's key, and who it names.
With --keys, when the seal's key was rotated away or revoked: the verified timestamp's genTime is before then.
With --only or --proof: what the check left out; never a failure.
, proof.canonical, proof.fields, proof.entry
With --proof: the proof is canonical JSON with exactly the fields of ‘noqtis-proof-1’, and each entry's audit path leads to the signed root.
One row per refused, non-normal, duplicate or too long path, and per two paths that differ only in case.
One row per regular file present but not listed.
One row per link, reparse point, special file or refused name in the folder.

For an ‘https://’ url: a PEM bundle of the roots to trust, in place of the system's.
For an ‘https://’ url: a Certificate Transparency log list in the form of Chrome's log_list.json (version 3), such as https://www.gstatic.com/ct/log_list/v3/log_list.json. When it is set, the server's certificate must carry SCTs (embedded, or sent in TLS) that verify and come from logs of at least two operators, usable when they signed; otherwise the fetch fails and says why. Unset or empty, nothing is checked: a list goes stale, so keep it current.

0
Every check passed.
1
A check failed (the files changed, the seal or manifest is damaged or not signed by key.pub).
2
A usage error, dir does not exist, a key that is not a public signature key, an anchor that cannot be read or is not DER, or a library error such as running out of memory.

$ noqtis check -p me.pub results
      check               detail
ok    seal.read           noqtis.sig
...
ok    signature           ML-DSA-65 signature over noqtis.json is valid
ok    folder              listed
ok    file                data/in.csv
ok    file                report.txt
...
ok    manifest.order      files sorted by path, no repeats
results: OK (22 checks passed)

After a file was changed and one added:

$ noqtis check -p me.pub results; echo $?
...
FAIL  file                report.txt: size differs
...
FAIL  extra               new.txt: not in the manifest
results: FAILED (2 of 23 checks failed)
noqtis check: file: a sealed file changed after sealing: restore it from the original, or seal the folder again if the change was intended
noqtis check: extra: a file was added after sealing: remove it, or seal the folder again to include it
1

A timestamped capsule, against the TSA's root certificate:

$ noqtis check -p me.pub --anchor digicert.der results
...
ok    timestamp           RFC 3161 token over the signature verifies under a TSA anchor; genTime 2026-10-29T09:34:03Z
...
results: OK (23 checks passed)

A capsule published on a web server, without downloading it by hand:

$ noqtis check -p me.pub https://example.org/data/results/

Only one file of a format 2 capsule, and its inputs:

$ noqtis check -p me.pub --only data/in.csv results
$ noqtis check -p me.pub --lineage raw results

In CI, keeping only the failed rows:

$ noqtis check --json -p me.pub results |
    jq -r '.checks[] | select(.ok | not) | .detail'
report.txt: size differs
new.txt: not in the manifest

noqtis(1), noqtis-idcard(1), noqtis-prove(1), noqtis-revoke(1), noqtis-rotate(1), noqtis-seal(1), nqchk(3), nqchkt(3), nqtsput(3), noqtis-capsule(5)