Generates a key pair for ML-KEM (FIPS 203), the standardised post-quantum
key encapsulation mechanism, and kempubkey() strips a key pair
down to the half that can be shared. Use them when a capsule, a data file
or a credential has to reach one recipient without being readable on the
way: the recipient makes the key pair and publishes the public half, and
the sender uses it with kemencap(). It is
implemented in this
package, with no system dependency.
Arguments
- lvl
Security level, one whole number: 512 (ML-KEM-512, NIST category 1), 768 (the default, category 3) or 1024 (category 5). 768 is the level NIST and the IETF have settled on for general use.
- seed
Optional raw vector of 64 seed bytes (
d || z) . Supplying it makes the key reproducible, which is what the standard's test vectors need; the default draws from the operating system's CSPRNG.- key
For
kempubkey(): a key pair fromkemkg().
Value
kemkg(): a list of class
c("bricklayer_kem_key", "list") with public (character,
the encapsulation key as lower-case hex, 800, 1184 or 1568 bytes),
secret (character, the decapsulation key as hex, 1632, 2400 or
3168 bytes) and level (integer). Printing it never shows the
secret. kempubkey(): a list of class
c("bricklayer_kem_public_key", "list") with public and
level only.
Details
A KEM is not a signature scheme and not a cipher. Encapsulation produces two things: a ciphertext to send, and a 32-byte shared secret to keep. The holder of the decapsulation key recovers the same secret from the ciphertext. What either side then does with that secret – feed it to a KDF, key an AEAD – is outside the mechanism.
Key generation is ML-KEM.KeyGen of FIPS 203 (Algorithm 19) from the
64-byte seed \(d \| z\): \(d\) seeds the K-PKE key and \(z\) is the
implicit-rejection value kept inside the decapsulation key. With a seed
the result is deterministic (the same seed gives the same key, which is
how the NIST test vectors are reproduced); without one the seed is drawn
with rndbytes() from the operating system. The key
sizes per level
are given by kemsz(); the level is not recorded in a
ciphertext, so
both sides must agree on it.
Errors: lvl other than one whole number 512, 768 or 1024;
seed that is not a raw vector of 64 bytes; key passed to
kempubkey() that is not from kemkg().
Security
The implementation is hand-written. Its standardised schemes are checked
byte for byte against OpenSSL 3.5 and NIST known-answer vectors in the test
suite. Side channels are checked, not assumed: inst/ctcheck runs every
operation that touches a secret under valgrind memcheck with the secret
marked undefined (the ctgrind method), so a branch or a memory address
that depends on it is a reported error, and afterwards scans the dead
stack for copies of the secret. Both checks run in CI on every change,
with GCC and with Clang. Timing is also measured on x86-64 and arm64
hardware (inst/dudect), power leakage is assessed in simulation under
the value and the transition models (inst/tvla), and ML-KEM
decapsulation and ML-DSA signing are first-order masked by default. These
are checks of this code on those compilers and under those leakage
models; the README's security section lists what each check does and
does not cover.
References
National Institute of Standards and Technology (2024). Module-Lattice-Based Key-Encapsulation Mechanism Standard. FIPS 203. doi:10.6028/NIST.FIPS.203
See also
kemencap(),
kemdecap(),
kemsz(),
fipskg() for the signature schemes.
Examples
key <- kemkg(768)
pub <- kempubkey(key)
# the sender holds only the public key
sent <- kemencap(pub)
# the recipient recovers the same secret from the ciphertext
got <- kemdecap(key, sent$ciphertext)
identical(sent$shared, got)
#> [1] TRUE
# a corrupted ciphertext yields a DIFFERENT secret, not an error
bad <- sent$ciphertext
substring(bad, 1L, 2L) <- "00"
identical(kemdecap(key, bad), got)
#> [1] FALSE
# a fixed seed gives the same key every time (for test vectors only)
k1 <- kemkg(512, seed = as.raw(0:63))
k2 <- kemkg(512, seed = as.raw(0:63))
identical(k1$public, k2$public)
#> [1] TRUE
k1
#> ╭── Key encapsulation key (ML-KEM, FIPS 203) ─────────────────────────────────╮
#> │ Level ML-KEM-512 │
#> │ Public key 3995815e597d104355cf29aa5333c932... (800 bytes) │
#> │ Secret key <withheld> │
#> │ │
#> │ Keep this key private: it opens what others encapsulate to its public │
#> │ half (kempubkey()) with kemdecap(). │
#> ╰─────────────────────────────────────────────────────────────────────────────╯
# levels other than the three FIPS 203 defines are refused
try(kemkg(256))
#> Error : `lvl` must be one of 512, 768 or 1024
try(kemkg(768, seed = as.raw(1:10)))
#> Error : `seed` must be a raw vector of 64 bytes
