Skip to content

nqseal(3)

nqseal, nqsealwhy — seal a folder of files into a signed capsule

libnoqtis, -lnoqtis

#include <noqtis.h>

int
nqseal(const char *dir, nqsalg alg, const uint8_t *sk, size_t sklen, const uint8_t *pk, size_t pklen, const char *meta, const char *tsa);

int
nqsealwhy(const char *dir, int *why, char *path, size_t cap);

() writes the manifest noqtis.json and the seal noqtis.sig into the folder dir (a UTF-8 path on every operating system), making it a capsule as noqtis-capsule(5) defines.

It lists every entry under dir without following links, leaving out noqtis.json and noqtis.sig at the top; records each regular file's path, size, SHA-256 and SHA3-256 in canonical JSON (RFC 8785), sorted by path; adds meta; signs the exact manifest bytes with alg and the secret key sk under the context string ‘noqtis-capsule-1’ (pure ML-DSA or SLH-DSA, hedged); verifies the new signature with pk; and only then writes both files, replacing earlier ones.

sklen and pklen must be exactly the algorithm's key sizes (nqsign(3)), and the keys one pair: a seal that would not verify is never written.

meta is NULL or a NUL-terminated JSON object of at most 1 MiB, stored in canonical form as the manifest's ‘meta’ member and covered by the signature; noqtis does not interpret it.

The creation time is the environment variable SOURCE_DATE_EPOCH (decimal seconds since the epoch) when it is set and not empty, otherwise the clock, as an RFC 3339 UTC time to the second.

tsa must be NULL. The library makes no network requests, so it cannot obtain an RFC 3161 timestamp itself; any other value returns NQENOSUP. To add one, build the request with nqtsreq(3), send it to the TSA, and store the reply with nqtsput(3), as noqtis-seal(1) --tsa does.

The capsule and parser layers allocate through the hook of nqsetal(3). () is not safe to call on the same folder from two threads or processes at once.

When () or nqseal2(3) returns NQEARG for a folder, () says why: it sets *why to the first thing in dir that a capsule cannot hold and copies its path, relative to dir, into path (cap bytes, always NUL-terminated when cap is not 0, cut when too long):

a symbolic link or reparse point;
not a regular file or folder (a device, a FIFO, a socket);
a name noqtis-capsule(5) refuses;
a path equal to another but for ASCII case;
a file that starts like a noqtis secret key (its first line ‘noqtis-key-1 <alg> sec’), which would ship with the capsule.

*why is 0 when nothing in the folder stops a seal; the NQEARG was then about the arguments, most often keys that are not one pair.

NQOK (0) when both files were written, or a negative error code; nothing is written on any error:

[NQEARG]
dir, sk or pk is NULL; alg is unknown; a key length is not the algorithm's; the keys are not a pair; SOURCE_DATE_EPOCH is not a plain decimal number (or after the year 9999); or the folder holds a symbolic link, a reparse point, a special file, a refused name (empty, a control character, ‘/’, ‘\’, ‘:’, invalid UTF-8), two paths equal but for ASCII case, or a file that starts like a noqtis secret key; nqsealwhy() says which.
[NQELIM]
More than 2,000,000 files, a path over 4096 bytes, folders more than 64 levels deep, a manifest over 512 MiB, a file over 2^53-1 bytes, or meta over 1 MiB or nested more than 64 levels deep.
[NQEDEC]
meta is not a JSON object (strict JSON: no duplicate keys, valid UTF-8).
[NQEIO]
The folder or a file could not be read, or a file could not be written.
[NQEMEM]
The allocation hook returned NULL.
[NQENOSUP]
tsa is not NULL.
[NQERNG]
The random source failed while signing.

nqsealwhy() returns NQOK, NQEARG when dir or why is NULL (or path is with cap not 0), NQEIO when the folder or a file cannot be read, or NQELIM or NQEMEM as nqseal() does.

The creation time, for reproducible capsules.

Seal a folder with a fresh ML-DSA-65 key, then check it with that key pinned (the program docs/examples/seal.c of the source tree):

static uint8_t pk[1952], sk[4032];
nqrep *rep = NULL;
int rc = nqskg(NQMLDSA65, pk, sizeof pk, sk, sizeof sk);
if (rc == NQOK)
  rc = nqseal(argv[1], NQMLDSA65, sk, sizeof sk, pk, sizeof pk,
              "{\"tool\":\"example\"}", NULL);
nqwipe(sk, sizeof sk);
if (rc == NQOK)
  rc = nqchk(argv[1], pk, sizeof pk, &rep);

noqtis-seal(1), noqtis(3), nqchk(3), nqsign(3), nqtsput(3), noqtis-capsule(5)