Skip to content

nqsign(3)

nqsign, nqsignd, nqvrfy, nqskg, nqskgd, nqssize, nqsname, nqsfind — ML-DSA and SLH-DSA signatures

libnoqtis, -lnoqtis

#include <noqtis.h>

int
nqskg(nqsalg alg, uint8_t *pk, size_t pklen, uint8_t *sk, size_t sklen);

int
nqskgd(nqsalg alg, const uint8_t *seed, size_t seedlen, uint8_t *pk, size_t pklen, uint8_t *sk, size_t sklen);

int
nqsign(nqsalg alg, const uint8_t *sk, size_t sklen, const uint8_t *msg, size_t msglen, const uint8_t *ctx, size_t ctxlen, int ph, uint8_t *sig, size_t siglen, size_t *outlen);

int
nqsignd(nqsalg alg, const uint8_t *sk, size_t sklen, const uint8_t *msg, size_t msglen, const uint8_t *ctx, size_t ctxlen, int ph, const uint8_t *rnd, size_t rndlen, uint8_t *sig, size_t siglen, size_t *outlen);

int
nqvrfy(nqsalg alg, const uint8_t *pk, size_t pklen, const uint8_t *msg, size_t msglen, const uint8_t *ctx, size_t ctxlen, int ph, const uint8_t *sig, size_t siglen);

int
nqssize(nqsalg alg, nqssz *sz);

const char *
nqsname(nqsalg alg);

int
nqsfind(const char *name, nqsalg *alg);

These functions make keys, sign and verify with the module-lattice signature ML-DSA (FIPS 204), the stateless hash-based signature SLH-DSA (FIPS 205), Ed25519 (RFC 8032), and hybrid ML-DSA + Ed25519. Keys and signatures are byte strings in the encodings the standards define.

nqsalg names the parameter set:

ML-DSA-44 1312 2560 2420
ML-DSA-65 1952 4032 3309
ML-DSA-87 2592 4896 4627
SLH-DSA-SHA2-128s 32 64 7856
SLH-DSA-SHA2-128f 32 64 17088
SLH-DSA-SHA2-192s 48 96 16224
SLH-DSA-SHA2-192f 48 96 35664
SLH-DSA-SHA2-256s 64 128 29792
SLH-DSA-SHA2-256f 64 128 49856
SLH-DSA-SHAKE-128s 32 64 7856
SLH-DSA-SHAKE-128f 32 64 17088
SLH-DSA-SHAKE-192s 48 96 16224
SLH-DSA-SHAKE-192f 48 96 35664
SLH-DSA-SHAKE-256s 64 128 29792
SLH-DSA-SHAKE-256f 64 128 49856
Ed25519 32 32 64
ML-DSA-44+Ed25519 1344 2592 2484
ML-DSA-65+Ed25519 1984 4064 3373
ML-DSA-87+Ed25519 2624 4928 4691

Sizes are in bytes. The ‘s’ sets give small signatures and slow signing, the ‘f’ sets fast signing and larger signatures.

NQED25519 is Ed25519 (RFC 8032): classical, not post-quantum, for interoperation (DSSE envelopes, say); a capsule is never sealed with it alone. No context and ph 0 is Ed25519; a context is Ed25519ctx; ph NQS512 is Ed25519ph (no other hash). It is deterministic: () takes no randomness (rnd may be NULL with rndlen 0). Verification refuses a public key of small or mixed order, a scalar S not below the group order, and any non-canonical encoding.

The hybrid sets NQMDED44, NQMDED65 and NQMDED87 sign with ML-DSA and Ed25519 at once: the public key is the ML-DSA public key followed by the Ed25519 one, the secret key likewise, the signature the ML-DSA signature followed by the Ed25519 one, and a signature verifies only when both parts do. Each part signs with the context ‘noqtis-hybrid:’ followed by the caller's, so the caller's context is at most 241 bytes, and neither part passes as a plain signature under the caller's context. The pre-hash variant is HashML-DSA with SHA-512 and Ed25519ph. The key-generation seed of a hybrid is the ML-DSA xi followed by the Ed25519 private key, 64 bytes.

() stores the sizes of alg in *sz, a nqssz with the members pk, sk and sig. () returns the name of alg as in the table, or NULL for a value that is not an nqsalg. () is its inverse: it stores in *alg the algorithm whose name is exactly name (case matters).

() makes a key pair from the operating system's random source (nqrand(3)), writing the public key to pk and the secret key to sk. () makes it from the standard's seed instead: the 32-byte xi for ML-DSA, or SK.seed || SK.prf || PK.seed (3n bytes: 48, 72 or 96) for SLH-DSA. It exists for test vectors and for keys that must be derived again; the seed is as secret as the key. The seed may overlap pk or sk, and on any error both are cleared.

nqsign() signs the msglen bytes at msg with the secret key sk of sklen bytes and writes the signature to sig, a buffer of siglen bytes; the signature's length is stored in *outlen when outlen is not NULL. Signing is hedged: fresh randomness is mixed in, so signatures of one message differ.

ctx is the FIPS 204/205 context string, ctxlen bytes, at most 255; NULL and 0 give the empty context. A signature verifies only under the context it was made with, which keeps signatures made for one purpose from being accepted for another.

ph selects the interface: 0 for pure ML-DSA or SLH-DSA, or an nqhalg (nqhash(3)) for HashML-DSA or HashSLH-DSA, where msg is still the message and is hashed by the function. Any of the twelve hashes may be used; for SHAKE128 and SHAKE256 the digest is 32 and 64 bytes.

() is () with the standard's per-signature randomness rnd given by the caller: exactly 32 bytes for ML-DSA, n bytes (16, 24 or 32) for SLH-DSA. All zero bytes give the deterministic variant of the standard.

() checks that sig of siglen bytes is a signature of msg under the public key pk with the context ctx and the interface ph used to sign.

Output buffers may be larger than the algorithm's size; only the defined number of bytes is written. Input keys and signatures must have exactly the algorithm's size. The signature buffer may overlap the message (signing in place).

Every function except nqsname() returns NQOK (0) on success or a negative error code:

[NQEARG]
alg is not an nqsalg; a pointer is NULL with a nonzero length, or an output pointer is NULL; ctxlen exceeds 255; ph is neither 0 nor an nqhalg; or, for nqskgd() and nqsignd(), seed or rnd is NULL or not the standard's length. nqsfind() returns it for a NULL argument.
[NQEBUF]
pklen, sklen or siglen is smaller than the algorithm's size.
[NQEDEC]
A key or signature given as input does not have the algorithm's length or encoding.
[NQEVRFY]
nqvrfy(): the signature does not verify. A verification never returns NQOK unless the signature is valid.
[NQERNG]
nqskg() or nqsign(): the operating system's random source failed.
[NQENOSUP]
nqsfind(): no algorithm has that name.

On any failure, nqsign() and nqsignd() set *outlen to 0, and when signing itself fails they wipe the signature buffer. nqsname() returns NULL for an unknown algorithm.

Make a key pair, sign and verify (the program docs/examples/sign.c of the source tree):

#include <noqtis.h>
#include <stdio.h>

int main(void) {
  static uint8_t pk[1952], sk[4032], sig[3309];
  const uint8_t msg[] = "results v1";
  size_t n = 0;
  int rc = nqskg(NQMLDSA65, pk, sizeof pk, sk, sizeof sk);
  if (rc == NQOK)
    rc = nqsign(NQMLDSA65, sk, sizeof sk, msg, sizeof msg - 1, NULL, 0, 0,
                sig, sizeof sig, &n);
  nqwipe(sk, sizeof sk);
  if (rc == NQOK)
    rc = nqvrfy(NQMLDSA65, pk, sizeof pk, msg, sizeof msg - 1, NULL, 0, 0,
                sig, n);
  printf("%s\n", nqerr(rc));
  return rc == NQOK ? 0 : 1;
}

Sizes at run time, for code that takes the algorithm as a parameter:

nqsalg alg;
nqssz sz;
if (nqsfind("SLH-DSA-SHAKE-128s", &alg) == NQOK &&
    nqssize(alg, &sz) == NQOK)
  printf("%s: %zu-byte signatures\n", nqsname(alg), sz.sig);

Build with ‘cc prog.c $(pkg-config --cflags --libs noqtis)’.

noqtis-sign(1), noqtis-verify(1), noqtis(3), nqerr(3), nqhash(3), nqwipe(3)

FIPS 204: Module-Lattice-Based Digital Signature Standard, NIST, August 2024. FIPS 205: Stateless Hash-Based Digital Signature Standard, NIST, August 2024.

Key generation and signing run in constant time with respect to the secret key, seed and randomness; ML-DSA signing is also first-order masked (two shares). Wipe secret keys with nqwipe(3) when done. Outside test vectors, prefer nqsign() to nqsignd(): the hedged variant keeps fault and side-channel attacks that rely on repeated signatures harder.