Skip to contents

Generates a key for HQC (Hamming Quasi-Cyclic), the code-based key encapsulation mechanism NIST selected for standardisation in March 2025 (NIST IR 8545) as a second KEM next to ML-KEM. It is implemented in this package, with no system dependency. A key encapsulation mechanism (KEM) lets two parties agree on a secret key: the recipient publishes the public half of this key pair, a sender uses it with hqc_encapsulate() to make a ciphertext and a shared secret, and only the holder of the secret half can recover that secret from the ciphertext with hqc_decapsulate(). The shared secret is then used as the key of a symmetric cipher. hqc_public_key() strips the secret half so the public key can be handed out.

Usage

hqc_keygen(level = 3L, seed = NULL, version = c("v5", "round4"))

hqc_public_key(key)

Arguments

level

Security level, as a NIST category: 1, 3 (the default) or 5 (HQC-1, HQC-3, HQC-5; 128, 192 and 256 are accepted for the same three). Level 3 matches the category of ML-KEM-768, kem_keygen()'s default.

seed

Optional raw vector: 32 bytes (seed_KEM) for v5; for round 4 the hqc_sizes()["seed"] bytes its key generation draws (sk_seed, sigma, pk_seed: 96, 104 or 112). Supplying it makes the key reproducible, which is what the known-answer tests need; the default draws from the operating system's CSPRNG.

version

"v5" (the default: the specification of 2025-08-22, a 32-byte shared secret) or "round4" (the submission of 2023-04-30, a 64-byte shared secret). Keys remember it; encapsulation and decapsulation follow the key.

key

A key from hqc_keygen() (hqc_public_key() only).

Value

hqc_keygen() returns a list of class bricklayer_hqc_key with public and secret (character, lowercase hex of the encapsulation and decapsulation keys; see hqc_sizes() for their byte lengths), level (integer 1, 3 or 5) and version ("v5" or "round4"). Its print method shows the level and the start of the public key and withholds the secret. hqc_public_key() returns a list of class bricklayer_hqc_public_key with public, level and version only. The v5 secret key is the specification's ek || seed_dk || sigma || seed_KEM; the round-4 one is sk_seed || sigma || ek.

Details

The implementation follows the HQC specification of 2025-08-22 and is byte for byte the scheme of the authors' reference implementation v5.0.0: all 300 official known-answer vectors are reproduced (the package's tests check them). It runs in constant time with respect to secrets – no secret reaches a branch, a memory index or a variable shift, checked under valgrind memcheck in CI (inst/ctcheck: the hqc1, hqc3, hqc5 and hqc1-round4 cases) – wipes its secret intermediates (the same harness scans the dead stack for them), and gives the same bytes on little- and big-endian machines. On x86-64 processors with PCLMULQDQ and on ARMv8 with the crypto extension the polynomial products use the carry-less multiply instruction; elsewhere a portable constant-time product.

NIST will publish HQC-KEM as FIPS 207 but has not yet released it, or a draft of it (checked 2026-10-05). The specification of 2025-08-22 followed here already makes the changes NIST listed for FIPS 207 (the salted FO transform, SHA3-512 seed expansion, the 32-byte shared secret, the new sampler, the seed in the decapsulation key, the seed-only compressed key that hqc_compress_key() writes); the published standard may still differ in detail. Keep keys and ciphertexts tagged with the scheme, version and level they belong to.

version = "round4" gives the earlier revision instead: the fourth-round submission of 2023-04-30 (HQC-128/192/256), the HQC of liboqs up to 0.12 and of PQClean, with a 64-byte shared secret. It uses the same codes and parameters under a different key encapsulation (40-byte seeds, SHAKE256 with domain bytes in place of SHA3, another sampler), so its keys and ciphertexts are not interchangeable with v5's. It reproduces that revision's 300 official known-answer vectors (the package's tests check them); use it to exchange keys with software built on it, and v5 otherwise.

Errors: a level other than 1, 3, 5, 128, 192 or 256 (a typo is never mapped to a default, because the level is not recorded in the ciphertext); a seed that is not a raw vector of exactly hqc_sizes(level, version)[["seed"]] bytes; a version other than "v5" or "round4". hqc_public_key() errors on anything that is not a bricklayer_hqc_key. Key generation takes a few tens of milliseconds even at level 5.

Security

A hand-written implementation. The 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) and power leakage is assessed in simulation under the value and the transition models (inst/tvla) for the masked ML-KEM and ML-DSA kernels; this scheme is not masked. These are checks of this code on those compilers; no third-party security audit has been commissioned, and the README's security section lists what each check does and does not cover.

References

Gaborit, P., Aguilar-Melchor, C., Aragon, N., Bettaieb, S., Bidoux, L., Blazy, O., Deneuville, J.-C., Persichetti, E., Zemor, G., et al. (2025). Hamming Quasi-Cyclic (HQC), specification of 2025-08-22; reference implementation v5.0.0. https://pqc-hqc.org/

Alagic, G. et al. (2025). Status Report on the Fourth Round of the NIST Post-Quantum Cryptography Standardization Process. NIST IR 8545. doi:10.6028/NIST.IR.8545

Examples

key <- hqc_keygen(1)
pub <- hqc_public_key(key)

# the sender holds only the public key
sent <- hqc_encapsulate(pub)
# the recipient recovers the same secret from the ciphertext
got <- hqc_decapsulate(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) <- if (substring(bad, 1L, 2L) == "00") "01" else "00"
identical(hqc_decapsulate(key, bad), got)
#> [1] FALSE

# a reproducible key from a fixed seed
identical(hqc_keygen(1, seed = as.raw(1:32))$public,
          hqc_keygen(1, seed = as.raw(1:32))$public)
#> [1] TRUE

# the round-4 revision: the same exchange, a 64-byte shared secret
old <- hqc_keygen(1, version = "round4")
cap <- hqc_encapsulate(hqc_public_key(old))
nchar(cap$shared) / 2
#> [1] 64
identical(hqc_decapsulate(old, cap$ciphertext), cap$shared)
#> [1] TRUE