Skip to content

noqtis-capsule(5)

noqtis-capsule — the noqtis capsule format, version 1

A capsule is a folder that holds the files of a result together with noqtis.json, the manifest, and noqtis.sig, the seal, both at the top of the folder. The manifest lists every file with its size and two digests; the seal signs the manifest's exact bytes with a post-quantum signature.

The key words MUST, MUST NOT, SHOULD and MAY are to be read as in RFC 2119 and RFC 8174.

Both files MUST be JSON (RFC 8259) in the canonical form of RFC 8785 (JCS): UTF-8, no whitespace outside strings, object members sorted by the UTF-16 code units of their names, strings with the minimal escapes, and numbers in the shortest form that round-trips. A checker MUST reject a file that is not byte-identical to its own canonical form, has a duplicate member name, or is not valid UTF-8.

An object with exactly these members:

A string: the creation time, RFC 3339 in UTC, ‘YYYY-MM-DDTHH:MM:SSZ’ with optional fractional seconds before the ‘Z’.
An array with one object per file, sorted by the bytes of path, with no path twice. Each object has exactly:
the file's path relative to the capsule folder, a string;
its SHA-256, 64 lower-case hex digits;
its SHA3-256, 64 lower-case hex digits;
its size in bytes, an integer from 0 to 2^53-1.
The string ‘noqtis-capsule-1’.
Optional: any JSON object, at most 1 MiB, opaque to noqtis.

Because of the canonical order, the manifest always reads

{"created":...,"files":[{"path":...,"sha256":...,
"sha3_256":...,"size":...},...],"format":"noqtis-capsule-1"
[,"meta":{...}]}

A path MUST use ‘/’ between names, MUST NOT start with ‘/’, and MUST be in normal form: no empty segment and no ‘.’. Every name (segment) MUST be valid UTF-8 without overlong forms or surrogates, MUST NOT be empty, ‘.’ or ‘..’, and MUST NOT contain a control character (U+0000 to U+001F, U+007F), ‘/’, ‘\’ or ‘:’. Two paths of one capsule MUST NOT differ only in ASCII case. A path MUST be at most 4096 bytes.

noqtis.json and noqtis.sig at the top of the folder are not listed. Only regular files are listed; folders are implied by the paths, so an empty folder is not recorded.

An object with exactly these members:

The signature algorithm, as () (nqsign(3)) names it, for example ‘ML-DSA-65’ or ‘SLH-DSA-SHAKE-128s’.
The FIPS 204/205 context string the signature was made with: exactly ‘noqtis-capsule-1’. A checker refuses any other context, so that a signature made for another purpose (a plain noqtis-sign(1) of the same bytes, say) is never taken for a seal.
The string ‘noqtis-seal-1’.
The SHA-256 of the exact bytes of noqtis.json, 64 lower-case hex digits.
The signer's public key, base64 (RFC 4648 section 4, padded, no line breaks) of the standard's encoding.
The pure ML-DSA or SLH-DSA signature over the exact bytes of noqtis.json with that context, base64 likewise.
Optional: base64 of an RFC 3161 time-stamp token whose message imprint is the hash (SHA-256, SHA-384 or SHA-512) of the decoded signature bytes. noqtis-seal(1) --tsa adds it (nqtsput(3)).

A checker MUST refuse, with a failing check and never by truncating: noqtis.json over 512 MiB (its size is checked before it is read, and again if it grows while being read); noqtis.sig over 1 MiB; meta over 1 MiB; more than 2,000,000 files; a path over 4096 bytes; folders more than 64 levels deep. JSON nesting is limited to 64 levels.

A sealer MUST list every entry of the folder without following links and MUST refuse to seal a folder that holds a symbolic link, a Windows reparse point, a special file (device, pipe, socket), a name the rules above refuse, two paths that differ only in ASCII case, or anything over a limit. It MUST verify the new signature before writing the seal.

A checker MUST:

  1. check that both files are canonical with exactly the fields above and the right formats, and that alg is known and public_key and signature have the algorithm's sizes;
  2. check that the SHA-256 of noqtis.json equals manifest_sha256;
  3. verify signature over the exact bytes of noqtis.json with context, using the trusted public key when the caller gave one (and then require that public_key equal it), otherwise public_key, and say which;
  4. for every listed file: require it to exist, to be a regular file reached without following a link, and to have the listed size, SHA-256 and SHA3-256;
  5. fail on every regular file present but not listed, every link, special file or refused name in the folder, every path out of order, listed twice, not in normal form, or differing from another only in case;
  6. when trust anchors for time-stamping authorities are given, require a timestamp whose imprint matches and whose token verifies, with the TSA certificate path valid at the token's genTime, under one of them; when none are given, fail a timestamp that is present, since it cannot be checked.

A capsule passes only when every check passes.

A format 2 capsule has the manifest format ‘noqtis-capsule-2’ and the seal format ‘noqtis-seal-2’. Its manifest is the format 1 manifest with these members after format, in canonical order:

Optional: 1 to 1024 objects {capsule, name}: capsule is the id of a capsule this one was made from, 64 lower-case hex digits; name is 1 to 255 bytes of UTF-8 with no control characters.
{alg, leaves, root}: alg is ‘sha256’, leaves the number of file entries and root the Merkle tree head, 64 lower-case hex digits.
Optional, as in format 1.

The tree is the Merkle Tree Hash of RFC 9162, section 2.1.1, over the file entries in manifest order: each leaf is SHA-256 of a 0x00 byte and the entry's canonical bytes as they appear in the manifest, each node SHA-256 of a 0x01 byte and its two children; the tree of no entries is SHA-256 of the empty string.

A capsule's is the SHA-256 of the bytes its seal signs: the manifest in format 1, the head in format 2.

A proof (noqtis-prove(1)) shows that some entries are in a format 2 capsule. It is canonical JSON:

{"entries":[{"entry":E,"index":I,"path":[H,...]},...],
 "format":"noqtis-proof-1","head":HEAD,"seal":SEAL}

seal is the seal object, head the head as a JSON object, and each entry entry a file entry exactly as in the manifest, at position index (from 0), with path its audit path (RFC 9162, section 2.1.3.1), each node 64 lower-case hex digits, from the leaf up. A checker MUST check the seal and its signature over the canonical bytes of head as for a capsule, then each entry's path to the head's root with the tree size leaves by the algorithm of RFC 9162, section 2.1.3.2. The tree size is authenticated by the signed head, not by the path. A proof holds no name, size or digest of the entries it does not prove.

A key statement is canonical JSON with the members alg, at, format, key, kind, reason and signature, and for a rotation new_alg, new_key and new_signature: format is ‘noqtis-keys-1’; kind is ‘rotate’ or ‘revoke’; alg and key are the key's algorithm and base64 public key, new_alg and new_key those of the key that replaces it; at is a time as ‘YYYY-MM-DDTHH:MM:SSZ’; reason is up to 255 bytes of text, possibly empty. signature is by key, and new_signature by new_key, each with context ‘noqtis-keys-1’ over the statement without its two signature members, in canonical form. Both algorithms are post-quantum.

A checker given a trusted key and statements follows them: the trusted key, the key a valid rotation of it names, and so on. It trusts no key when two statements are about one key or a rotation leads back to an earlier one. A seal's key passes when it is in that chain. When that key was rotated away or revoked at at, the seal passes only with a verified timestamp whose genTime is earlier.

An identity card is canonical JSON with the members alg, created, format, key, name and signature, and any of orcid, email, url and affil: format is ‘noqtis-idcard-1’; alg and key are a post-quantum signature algorithm and the base64 public key; created is a time as ‘YYYY-MM-DDTHH:MM:SSZ’; name and affil are 1 to 255 bytes of text with no control character and no space at either end; orcid is a bare ORCID iD (‘0000-0002-1825-0097’) whose last character is its ISO 7064 MOD 11-2 check digit, X for 10; email is ‘local@domain.tld’ in at most 254 bytes, and url an ‘https://’ address with a host in at most 1024 bytes, neither with a space. A member not given is left out, never empty. signature is by key with context ‘noqtis-idcard-1’ over the card without its signature member, in canonical form. A card is at most 256 KiB.

A card is self-signed: it shows that the holder of key wrote it, not that the name is true. A checker that also knows the key (pinned, or by the SHA-256 its holder published) and finds it is the seal's key may say who sealed the capsule.

noqtis.json
the manifest
noqtis.sig
the seal

A manifest (one line in the file; broken here for width):

{"created":"2026-10-28T23:35:26Z","files":[{"path":"data/in.csv",
"sha256":"5be08c96...","sha3_256":"cec130cc...","size":4},
{"path":"report.txt","sha256":"8e54b0ca...","sha3_256":"cfd6402f...",
"size":2}],"format":"noqtis-capsule-1","meta":{"project":"demo"}}

Its seal:

{"alg":"ML-DSA-65","context":"noqtis-capsule-1",
"format":"noqtis-seal-1","manifest_sha256":"9f322e95...",
"public_key":"1qWycP39...","signature":"..."}

noqtis-check(1), noqtis-idcard(1), noqtis-seal(1), nqchk(3), nqchkt(3), nqseal(3)

JSON Canonicalization Scheme (JCS), RFC 8785, June 2020. Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP), RFC 3161, August 2001. Certificate Transparency Version 2.0, RFC 9162, December 2021. Date and Time on the Internet: Timestamps, RFC 3339, July 2002. The Base16, Base32, and Base64 Data Encodings, RFC 4648, October 2006.

The signature's context is always ‘noqtis-capsule-1’, and a checker refuses any other, so a signature its key holder made for another purpose over the same bytes is never a seal. Key statements and identity cards have contexts of their own (‘noqtis-keys-1, ‘noqtis-idcard-1’’), so none of the three passes for another.

A timestamp's TSA certificate path is checked at the token's genTime, and revocation is not checked: a timestamp is only as good as the TSA's key was at that time. If the key is stolen later, tokens with an earlier genTime can be made that still check; pin the TSAs you trust, and stop trusting every token of a TSA that announces a key compromise.