Getting started with noqtis

noqtis brings post-quantum signatures (ML-DSA, SLH-DSA), key encapsulation (ML-KEM, HQC) and hashes to R, through a bundled C library. Keys, signatures and ciphertexts are raw vectors in the encodings the standards define.

library(noqtis)
head(nqalgs())
#>                name type   pk   sk   sig ct ss id
#> 1         ML-DSA-44  sig 1312 2560  2420 NA NA  1
#> 2         ML-DSA-65  sig 1952 4032  3309 NA NA  2
#> 3         ML-DSA-87  sig 2592 4896  4627 NA NA  3
#> 4 SLH-DSA-SHA2-128s  sig   32   64  7856 NA NA  4
#> 5 SLH-DSA-SHA2-128f  sig   32   64 17088 NA NA  5
#> 6 SLH-DSA-SHA2-192s  sig   48   96 16224 NA NA  6

Signing

k <- nqkeygen("ML-DSA-65")
sig <- nqsign(k$sk, "the results in table 2", "ML-DSA-65")
length(sig)
#> [1] 3309
nqverify(k$pk, "the results in table 2", sig, "ML-DSA-65")
#> [1] TRUE
nqverify(k$pk, "the results in table 3", sig, "ML-DSA-65")
#> [1] FALSE

A context string binds a signature to one use; verification needs the same context.

sig <- nqsign(k$sk, "data", "ML-DSA-65", ctx = "analysis-v1")
nqverify(k$pk, "data", sig, "ML-DSA-65", ctx = "analysis-v1")
#> [1] TRUE
nqverify(k$pk, "data", sig, "ML-DSA-65")
#> [1] FALSE

Key encapsulation

k <- nqkeygen("ML-KEM-768")
e <- nqencap(k$pk, "ML-KEM-768")
identical(nqdecap(k$sk, e$ct, "ML-KEM-768"), e$ss)
#> [1] TRUE

Hashes

nqhash("abc", hex = TRUE)
#> [1] "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
nqhash("abc", "sha3-256", hex = TRUE)
#> [1] "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"

Capsules

A capsule is a signed record of the exact files in a folder. Seal it once, and anyone with the public key can check later that no file was changed, added or removed.

d <- file.path(tempdir(), "analysis")
dir.create(d)
writeLines(c("x,y", "1,2"), file.path(d, "data.csv"))
k <- nqkeygen("ML-DSA-44")
nqseal(d, k$sk, k$pk, "ML-DSA-44", meta = '{"project":"vignette"}')
nqcheck(d, k$pk)
#>                 check   ok                                        detail
#> 1           seal.read TRUE                                    noqtis.sig
#> 2          seal.parse TRUE                                 a JSON object
#> 3      seal.canonical TRUE                                canonical JSON
#> 4         seal.fields TRUE                   the fields of noqtis-seal-1
#> 5         seal.format TRUE                               "noqtis-seal-1"
#> 6            seal.alg TRUE                                     ML-DSA-44
#> 7        seal.context TRUE                            "noqtis-capsule-1"
#> 8            seal.key TRUE                a public key of the right size
#> 9      seal.signature TRUE                 a signature of the right size
#> 10      manifest.read TRUE                                   noqtis.json
#> 11    manifest.digest TRUE       SHA-256 of noqtis.json matches the seal
#> 12                key TRUE             the seal's key is the trusted key
#> 13          signature TRUE ML-DSA-44 signature over noqtis.json is valid
#> 14             folder TRUE                                        listed
#> 15               file TRUE                                      data.csv
#> 16     manifest.parse TRUE                                 a JSON object
#> 17 manifest.canonical TRUE                                canonical JSON
#> 18    manifest.fields TRUE                the fields of noqtis-capsule-1
#> 19    manifest.format TRUE                            "noqtis-capsule-1"
#> 20   manifest.created TRUE                          2026-10-31T07:13:17Z
#> 21     manifest.order TRUE              files sorted by path, no repeats

writeLines(c("x,y", "1,3"), file.path(d, "data.csv"))
r <- nqcheck(d, k$pk)
r[!r$ok, ]
#>    check    ok                   detail
#> 15  file FALSE data.csv: sha256 differs

The package ships a capsule made by the noqtis library’s own tests:

g <- system.file("extdata", "capsule", package = "noqtis")
attr(nqcheck(g), "ok")
#> [1] TRUE

A format 2 capsule can also prove single files to a reader who should not see the others, and records the capsules it was made from:

d2 <- file.path(tempdir(), "model")
dir.create(d2)
writeLines("coef,0.4", file.path(d2, "fit.csv"))
writeLines("private", file.path(d2, "notes.txt"))
nqseal(d2, k$sk, k$pk, "ML-DSA-44", inputs = c(raw = g))
p <- nqprove(d2, "fit.csv")
attr(nqcheck(NULL, k$pk, proof = p), "ok")
#> [1] TRUE
attr(nqcheck(d2, k$pk, lineage = g), "ok")
#> [1] TRUE

nqrotate() and nqrevoke() write key statements that nqcheck(keys = ) follows, and nqdsse() signs a DSSE envelope, the wrapper in-toto and SLSA use.

Randomness

Key generation, signing and encapsulation take their randomness from the operating system (getrandom(), arc4random_buf() or BCryptGenRandom()), never from R’s generator: set.seed() does not make them repeat. For test vectors, nqkeygen() and nqencap() take an explicit seed, and nqsign() has det = TRUE.