Standard Library
Post-quantum crypto (`mpl_pqc` / `empl_pqc.h`)
Post-quantum crypto (mpl_pqc / empl_pqc.h)
The empl_pqc.h header (395 lines, MPL_Standard/System/empl_pqc.h) exposes post-quantum cryptographic primitives via OpenSSL 3.5+. It supports the NIST-standardized families:
- ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism, FIPS 203) — formerly Kyber
- ML-DSA (Module-Lattice-Based Digital Signature Algorithm, FIPS 204) — formerly Dilithium
- SLH-DSA (Stateless Hash-Based Digital Signature Algorithm, FIPS 205) — formerly SPHINCS+
All implementations delegate to OpenSSL EVP APIs (EVP_PKEY_*, EVP_SIGNATURE_fetch, EVP_PKEY_CTX_*). On older OpenSSL (< 3.5) the header compiles, but NID lookups return 0 and callers must check __mpl_pqc_primitive_available() first.
Header guard and includes
// empl_pqc.h:24
#ifndef MPL_PQC_H
#define MPL_PQC_H
#include <string>
#include <stdexcept>
#include <cstddef>
#include <openssl/evp.h>
#include <openssl/opensslv.h>
#include <openssl/core_names.h>
Plus compatibility shims for OpenSSL < 3.2 (empl_pqc.h:39-82) that emulate EVP_PKEY_sign_message_init and EVP_PKEY_verify_message_init (introduced in OpenSSL 3.2) using EVP_PKEY_sign_init + EVP_PKEY_CTX_set_params. On OpenSSL ≥ 3.2 these come from <openssl/evp.h> directly.
Namespace
All public functions and types are in namespace mpl_pqc (lines 84-377). C-style __mpl_pqc_* aliases (lines 379-393) are provided for the JS interop surface in empl.h.
namespace mpl_pqc {
// types
struct kem_keypair { std::string publicKey; std::string privateKey; };
struct sig_keypair { std::string publicKey; std::string privateKey; };
struct kem_encap_result { std::string ciphertext; std::string sharedSecret; };
// NID lookups
inline int kem_nid(const std::string& canonical);
inline int sig_nid(const std::string& canonical);
// availability check
inline bool primitive_available(const std::string& canonical);
// KEM operations
inline kem_keypair kem_keygen(const std::string& canonical);
inline kem_encap_result kem_encapsulate(const std::string& canonical, const std::string& rawPublic);
inline std::string kem_decapsulate(const std::string& canonical, const std::string& rawPrivate, const std::string& ciphertext);
// KEM key loaders
inline EVP_PKEY* kem_resolve_public(const std::string& canonical, const std::string& rawPublic);
inline EVP_PKEY* kem_resolve_private(const std::string& canonical, const std::string& rawPrivate);
// Signature operations
inline sig_keypair sig_keygen(const std::string& canonical);
inline std::string sig_sign(const std::string& canonical, const std::string& rawPrivate, const std::string& data);
inline bool sig_verify(const std::string& canonical, const std::string& rawPublic, const std::string& signature, const std::string& data);
// Signature key loaders
inline EVP_PKEY* sig_resolve_public(const std::string& canonical, const std::string& rawPublic);
inline EVP_PKEY* sig_resolve_private(const std::string& canonical, const std::string& rawPrivate);
}
Supported algorithms
ML-KEM (KEM / key exchange)
| Canonical name | NID macro | Security level | Public key | Ciphertext | Shared secret |
|---|---|---|---|---|---|
ML-KEM-512 |
NID_ML_KEM_512 |
1 (≈ AES-128) | 800 B | 768 B | 32 B |
ML-KEM-768 |
NID_ML_KEM_768 |
3 (≈ AES-192) | 1184 B | 1088 B | 32 B |
ML-KEM-1024 |
NID_ML_KEM_1024 |
5 (≈ AES-256) | 1568 B | 1568 B | 32 B |
ML-DSA (signature)
| Canonical name | NID macro | Security level | Public key | Signature |
|---|---|---|---|---|
ML-DSA-44 |
EVP_PKEY_ML_DSA_44 |
2 (≈ AES-128) | 1312 B | 2420 B |
ML-DSA-65 |
EVP_PKEY_ML_DSA_65 |
3 (≈ AES-192) | 1952 B | 3309 B |
ML-DSA-87 |
EVP_PKEY_ML_DSA_87 |
5 (≈ AES-256) | 2592 B | 4627 B |
SLH-DSA (signature, hash-based)
| Canonical name | NID macro | Security level | Public key | Signature |
|---|---|---|---|---|
SLH-DSA-SHA2-128f |
EVP_PKEY_SLH_DSA_SHA2_128F |
1 | 32 B | 17 KB (fast) |
SLH-DSA-SHA2-128s |
EVP_PKEY_SLH_DSA_SHA2_128S |
1 | 32 B | 7.8 KB (small) |
SLH-DSA-SHA2-192f |
EVP_PKEY_SLH_DSA_SHA2_192F |
3 | 48 B | ~35 KB |
SLH-DSA-SHA2-192s |
EVP_PKEY_SLH_DSA_SHA2_192S |
3 | 48 B | ~16 KB |
SLH-DSA-SHA2-256f |
EVP_PKEY_SLH_DSA_SHA2_256F |
5 | 64 B | ~49 KB |
SLH-DSA-SHA2-256s |
EVP_PKEY_SLH_DSA_SHA2_256S |
5 | 64 B | ~25 KB |
SLH-DSA-SHAKE-128f |
EVP_PKEY_SLH_DSA_SHAKE_128F |
1 | 32 B | 17 KB |
SLH-DSA-SHAKE-128s |
EVP_PKEY_SLH_DSA_SHAKE_128S |
1 | 32 B | 7.8 KB |
SLH-DSA-SHAKE-192f |
EVP_PKEY_SLH_DSA_SHAKE_192F |
3 | 48 B | ~35 KB |
SLH-DSA-SHAKE-192s |
EVP_PKEY_SLH_DSA_SHAKE_192S |
3 | 48 B | ~16 KB |
SLH-DSA-SHAKE-256f |
EVP_PKEY_SLH_DSA_SHAKE_256F |
5 | 64 B | ~49 KB |
SLH-DSA-SHAKE-256s |
EVP_PKEY_SLH_DSA_SHAKE_256S |
5 | 64 B | ~25 KB |
f(fast) vss(small):fvariants have larger signatures but faster signing.svariants have smaller signatures but slower signing.
API reference
kem_nid(canonical)
// empl_pqc.h:88
inline int kem_nid(const std::string& canonical);
Returns the OpenSSL NID for the given ML-KEM canonical name, or 0 if the algorithm is not available on this OpenSSL build.
int nid = mpl_pqc::kem_nid("ML-KEM-768");
if (nid == 0) { /* OpenSSL too old */ }
sig_nid(canonical)
// empl_pqc.h:98
inline int sig_nid(const std::string& canonical);
Returns the OpenSSL NID for the given ML-DSA / SLH-DSA canonical name, or 0 if unavailable.
primitive_available(canonical)
// empl_pqc.h:122
inline bool primitive_available(const std::string& canonical);
Returns true if the algorithm is available on this OpenSSL build (i.e. both the NID is defined AND OpenSSL can instantiate a EVP_PKEY_CTX for it).
kem_keygen(canonical)
// empl_pqc.h:142
inline kem_keypair kem_keygen(const std::string& canonical);
Generates a fresh ML-KEM keypair. Throws std::runtime_error on failure (algorithm unavailable or OpenSSL error).
auto kp = mpl_pqc::kem_keygen("ML-KEM-768");
// kp.publicKey = 1184-byte raw key
// kp.privateKey = 2400-byte raw key
kem_encapsulate(canonical, rawPublic)
// empl_pqc.h:243
inline kem_encap_result kem_encapsulate(const std::string& canonical, const std::string& rawPublic);
Encapsulates a shared secret against the given public key. Returns the ciphertext to send to the holder of the private key, and the shared secret.
auto enc = mpl_pqc::kem_encapsulate("ML-KEM-768", kp.publicKey);
// enc.ciphertext = 1088 bytes
// enc.sharedSecret = 32 bytes
kem_decapsulate(canonical, rawPrivate, ciphertext)
// empl_pqc.h:268
inline std::string kem_decapsulate(const std::string& canonical, const std::string& rawPrivate, const std::string& ciphertext);
Decapsulates the ciphertext using the private key. Returns the shared secret. Throws on failure.
auto ss = mpl_pqc::kem_decapsulate("ML-KEM-768", kp.privateKey, enc.ciphertext);
// ss = 32-byte shared secret (matches enc.sharedSecret)
sig_keygen(canonical)
// empl_pqc.h:294
inline sig_keypair sig_keygen(const std::string& canonical);
Generates a fresh ML-DSA / SLH-DSA signature keypair. Throws on failure.
sig_sign(canonical, rawPrivate, data)
// empl_pqc.h:326
inline std::string sig_sign(const std::string& canonical, const std::string& rawPrivate, const std::string& data);
One-shot sign of data with the private key. Returns the signature bytes. Throws on failure.
Implementation note. OpenSSL 3.5 ML-DSA / SLH-DSA only expose the one-shot sign dispatch (
EVP_PKEY_sign_message_init+EVP_PKEY_sign). They do NOT implement streaming (sign_message_update/final) or the legacysign_init.empl_pqc.huses the one-shot API.
sig_verify(canonical, rawPublic, signature, data)
// empl_pqc.h:353
inline bool sig_verify(const std::string& canonical, const std::string& rawPublic, const std::string& signature, const std::string& data);
Verifies a signature. Returns true if valid. Throws std::runtime_error on context-initialization failure.
Key-loader functions
The kem_resolve_* and sig_resolve_* functions convert a raw public/private key blob into an EVP_PKEY* suitable for use with the OpenSSL EVP API directly. Callers must EVP_PKEY_free() the returned pointer.
EVP_PKEY* pkey = mpl_pqc::kem_resolve_public("ML-KEM-768", rawPub);
// ... use pkey ...
EVP_PKEY_free(pkey);
These are useful for advanced use cases (e.g. integrating with custom OpenSSL pipelines).
C-style aliases
For JS interop via empl.h, the header provides __mpl_pqc_* aliases:
// empl_pqc.h:381
using __mpl_pqc_kem_keypair = mpl_pqc::kem_keypair;
using __mpl_pqc_sig_keypair = mpl_pqc::sig_keypair;
using __mpl_pqc_kem_encap_result = mpl_pqc::kem_encap_result;
inline int __mpl_pqc_kem_nid(const std::string& s);
inline int __mpl_pqc_sig_nid(const std::string& s);
inline bool __mpl_pqc_primitive_available(const std::string& s);
inline __mpl_pqc_kem_keypair __mpl_pqc_kem_keygen(const std::string& s);
inline __mpl_pqc_sig_keypair __mpl_pqc_sig_keygen(const std::string& s);
inline __mpl_pqc_kem_encap_result __mpl_pqc_kem_encapsulate(const std::string& s, const std::string& p);
inline std::string __mpl_pqc_kem_decapsulate(const std::string& s, const std::string& k, const std::string& c);
inline std::string __mpl_pqc_sig_sign(const std::string& s, const std::string& k, const std::string& d);
inline bool __mpl_pqc_sig_verify(const std::string& s, const std::string& p, const std::string& g, const std::string& d);
Usage example
#include "empl_pqc.h"
// Check availability
if (!mpl_pqc::primitive_available("ML-KEM-768")) {
std::cerr << "ML-KEM-768 not available; need OpenSSL 3.5+\n";
return 1;
}
// Alice generates a keypair
auto aliceKp = mpl_pqc::kem_keygen("ML-KEM-768");
// Bob encapsulates a shared secret against Alice's public key
auto enc = mpl_pqc::kem_encapsulate("ML-KEM-768", aliceKp.publicKey);
// Bob sends enc.ciphertext to Alice
// Alice decapsulates to recover the shared secret
auto aliceSs = mpl_pqc::kem_decapsulate("ML-KEM-768", aliceKp.privateKey, enc.ciphertext);
// aliceSs == enc.sharedSecret (both 32 bytes)
assert(aliceSs == enc.sharedSecret);
Security considerations
- Algorithm selection. For new applications, prefer ML-KEM-768 + ML-DSA-65 (NIST security level 3, ≈ AES-192). Use ML-KEM-1024 + ML-DSA-87 only if 256-bit security is required.
- Side-channel resistance. OpenSSL's ML-KEM / ML-DSA implementations are constant-time but not formally verified against all known side-channel attacks. For high-assurance use, consider explicit constant-time wrappers.
- Hybrid deployment. PQC is still relatively new. Many deployments use hybrid schemes (e.g. X25519 + ML-KEM-768) to hedge against algorithmic breakthroughs. The C++ backend does NOT currently provide hybrid helpers — callers must compose them manually.
- Random seed. OpenSSL's keygen uses its internal CSPRNG (
RAND_bytes), which is cryptographically secure. Do NOT seed OpenSSL fromstd::mt19937_64(used bympl_crypto_random_uuidinempl_runtime.h). - Key serialization. The raw byte format is the canonical ML-KEM/ML-DSA/SLH-DSA byte string. There is no PEM/DER wrapper — convert to PEM via OpenSSL if needed for interchange with non-OpenSSL tools.
Cross-references
| Header | Provides |
|---|---|
empl_pqc.h (this) |
PQC primitives (KEM, signatures) |
empl_runtime.h |
Non-cryptographic randomness (mpl_crypto_random_uuid, mpl_crypto_random_bytes), OS info, ISO 8601 |
empl_core.h |
mpl_value (variant type), mpl_object (boxed objects), mpl_promise<T> (Promise semantics), mpl_throw_*_error |
empl.h |
JS adapter region, mpl_box_value, callable object model, SubtleCrypto implementation |
See also
cpp-impl.md— C++ stdlib referencemodules.md— full stdlib module listruntime.md— runtime utilitiesheaders.md— header hierarchy- NIST FIPS 203, 204, 205 — the standards documents
- OpenSSL EVP documentation — underlying API