The Post Quantum Era
Post-Quantum Cryptography Implementation Guide
Adding NIST FIPS 203/204 Algorithms to RiddlerChat
Oblivion Edge Vulnerability Research LLC, San Antonio, Texas -- July 2026
Project: The Riddler Chat System - PQC Integration Reference
Executive Summary
This document provides a comprehensive technical reference for extending the RiddlerChat cryptographic engine with additional post-quantum algorithms from NIST's FIPS 203/204 standards. It covers the Strategy pattern architecture, integration points, and includes complete working implementations in both Python (strategy definitions) and C++ (algorithm kernels), with special focus on FALCON, a hybrid classical-quantum-safe digital signature algorithm selected by NIST.
The approach maintains full backward compatibility while enabling:
- Modular addition of new cryptographic backends
- Runtime strategy switching for A/B testing and graceful degradation
- Direct integration of native C/C++ implementations for performance-critical operations
- Clear separation of concerns between algorithm logic and application code
Part 1: Strategy Pattern Architecture Review
Current Implementation State
The existing RiddlerChat backend implements three concrete strategies:
- OQSProviderStrategy - OpenSSL 3.x + oqs-provider subprocess interface
- LibOQSStrategy - Direct liboqs Python bindings (process-in-core)
- SimulatedStrategy - Classical X25519/Ed25519 fallback
All strategies conform to the abstract CryptoStrategyBase interface, which defines seven methods covering key encapsulation mechanism (KEM) and digital signature (SIG) operations:
from abc import ABC, abstractmethod
from typing import Tuple
class CryptoStrategyBase(ABC):
"""Abstract base for all cryptographic backend implementations."""
@abstractmethod
def kem_keygen(self) -> Tuple[bytes, bytes]:
"""Generate KEM keypair. Returns (public_key, secret_key)."""
@abstractmethod
def kem_encapsulate(self, public_key: bytes) -> Tuple[bytes, bytes]:
"""Encapsulate shared secret. Returns (ciphertext, shared_secret)."""
@abstractmethod
def kem_decapsulate(self, secret_key: bytes, ciphertext: bytes) -> bytes:
"""Decapsulate ciphertext to recover shared secret."""
@abstractmethod
def sig_keygen(self) -> Tuple[bytes, bytes]:
"""Generate signature keypair. Returns (public_key, secret_key)."""
@abstractmethod
def sig_sign(self, secret_key: bytes, message: bytes) -> bytes:
"""Sign message. Returns signature bytes."""
@abstractmethod
def sig_verify(self, public_key: bytes, message: bytes, signature: bytes) -> bool:
"""Verify signature. Returns True if valid, False otherwise."""
@abstractmethod
def _probe(self) -> bool:
"""Check availability of this backend. Used during auto-detection."""
Strategy Context Pattern
The PostQuantumCrypto class acts as the strategy context, delegating all operations to the active strategy:
class PostQuantumCrypto:
"""Strategy context managing cryptographic backend selection and delegation."""
def __init__(self, strategy: CryptoStrategyBase = None, oqs_provider_path: str = None):
if strategy:
self._strategy = strategy
else:
self._strategy = self._auto_detect()
def _auto_detect(self) -> CryptoStrategyBase:
"""Probe backends in priority order and return first available."""
strategies = [
OQSProviderStrategy(oqs_provider_path),
LibOQSStrategy(),
SimulatedStrategy()
]
for strategy in strategies:
if strategy._probe():
print(f"[PQC] Selected backend: {strategy.__class__.__name__}")
return strategy
# Should never reach here; SimulatedStrategy always available
raise RuntimeError("No cryptographic backend available")
def set_strategy(self, strategy: CryptoStrategyBase) -> None:
"""Switch active strategy at runtime."""
self._strategy = strategy
def kem_keygen(self) -> Tuple[bytes, bytes]:
return self._strategy.kem_keygen()
def kem_encapsulate(self, public_key: bytes) -> Tuple[bytes, bytes]:
return self._strategy.kem_encapsulate(public_key)
def kem_decapsulate(self, secret_key: bytes, ciphertext: bytes) -> bytes:
return self._strategy.kem_decapsulate(secret_key, ciphertext)
def sig_keygen(self) -> Tuple[bytes, bytes]:
return self._strategy.sig_keygen()
def sig_sign(self, secret_key: bytes, message: bytes) -> bytes:
return self._strategy.sig_sign(secret_key, message)
def sig_verify(self, public_key: bytes, message: bytes, signature: bytes) -> bool:
return self._strategy.sig_verify(public_key, message, signature)
Part 2: Adding New Strategies - Python Layer
Design Pattern for New Strategies
When adding a new cryptographic backend (e.g., FALCON signature support, Kyber alternatives, or SLH-DSA), follow this template:
from abc import ABC, abstractmethod
from typing import Tuple
import subprocess
import json
import sys
class FalconStrategy(CryptoStrategyBase):
"""
FALCON post-quantum digital signature support.
NIST FIPS 204 selected algorithm for quantum-resistant signing.
Combines classical integer lattices with classical-strength symmetric crypto.
Algorithm Parameters:
- Signing keypair: 2,304 bytes secret key, 897 bytes public key
- Signature length: 666 bytes (FALCON-512) to 1,280 bytes (FALCON-1024)
- Security level: 5 (256-bit equivalent post-quantum strength)
"""
def __init__(self, falcon_provider: str = "liboqs"):
"""
Initialize FALCON strategy.
Args:
falcon_provider: "liboqs" (Python bindings) or "native" (C++ via ctypes)
"""
self.provider = falcon_provider
self.algorithm_name = "FALCON-1024" # NIST standard variant
self._probe_result = None
def _probe(self) -> bool:
"""Detect FALCON availability in the chosen provider."""
if self._probe_result is not None:
return self._probe_result
try:
if self.provider == "liboqs":
return self._probe_liboqs()
elif self.provider == "native":
return self._probe_native()
except Exception as e:
print(f"[FALCON] Probe failed: {e}")
self._probe_result = False
return False
def _probe_liboqs(self) -> bool:
"""Check if liboqs supports FALCON-1024."""
try:
import oqs
# Test instantiation
sig = oqs.Signature(self.algorithm_name)
self._probe_result = True
return True
except (ImportError, RuntimeError):
self._probe_result = False
return False
def _probe_native(self) -> bool:
"""Check if native C++ FALCON library is available via ctypes."""
try:
import ctypes
import os
# Look for compiled falcon.so or falcon.dll
lib_paths = [
"liboqs/build/lib/libfalcon.so",
"/usr/local/lib/libfalcon.so",
"/usr/lib/libfalcon.so",
]
for path in lib_paths:
if os.path.exists(path):
self._ctypes_lib = ctypes.CDLL(path)
self._probe_result = True
return True
self._probe_result = False
return False
except Exception:
self._probe_result = False
return False
def kem_keygen(self) -> Tuple[bytes, bytes]:
"""FALCON is a signature algorithm, not KEM. Raise NotImplementedError."""
raise NotImplementedError(
"FALCON is a digital signature algorithm (FIPS 204), not a KEM. "
"Use sig_keygen() instead."
)
def kem_encapsulate(self, public_key: bytes) -> Tuple[bytes, bytes]:
raise NotImplementedError("FALCON does not implement KEM operations")
def kem_decapsulate(self, secret_key: bytes, ciphertext: bytes) -> bytes:
raise NotImplementedError("FALCON does not implement KEM operations")
def sig_keygen(self) -> Tuple[bytes, bytes]:
"""Generate FALCON-1024 signing keypair."""
if self.provider == "liboqs":
return self._sig_keygen_liboqs()
elif self.provider == "native":
return self._sig_keygen_native()
else:
raise ValueError(f"Unknown provider: {self.provider}")
def _sig_keygen_liboqs(self) -> Tuple[bytes, bytes]:
"""Generate keypair via liboqs Python bindings."""
import oqs
sig = oqs.Signature(self.algorithm_name)
public_key = sig.generate_keypair()
secret_key = sig.export_secret_key()
return (public_key, secret_key)
def _sig_keygen_native(self) -> Tuple[bytes, bytes]:
"""Generate keypair via native C++ implementation."""
import ctypes
# Define C function signature
keygen_func = self._ctypes_lib.falcon_keygen
keygen_func.argtypes = [
ctypes.POINTER(ctypes.c_uint8), # output public key
ctypes.POINTER(ctypes.c_uint8), # output secret key
]
keygen_func.restype = ctypes.c_int # return status
public_key = ctypes.create_string_buffer(897) # FALCON-1024 pk size
secret_key = ctypes.create_string_buffer(2304) # FALCON-1024 sk size
status = keygen_func(public_key, secret_key)
if status != 0:
raise RuntimeError(f"FALCON keygen failed with status {status}")
return (bytes(public_key), bytes(secret_key))
def sig_sign(self, secret_key: bytes, message: bytes) -> bytes:
"""Sign message with FALCON-1024 secret key."""
if self.provider == "liboqs":
return self._sig_sign_liboqs(secret_key, message)
elif self.provider == "native":
return self._sig_sign_native(secret_key, message)
def _sig_sign_liboqs(self, secret_key: bytes, message: bytes) -> bytes:
"""Sign via liboqs."""
import oqs
sig = oqs.Signature(self.algorithm_name)
sig.import_secret_key(secret_key)
return sig.sign(message)
def _sig_sign_native(self, secret_key: bytes, message: bytes) -> bytes:
"""Sign via native C++ implementation."""
import ctypes
sign_func = self._ctypes_lib.falcon_sign
sign_func.argtypes = [
ctypes.c_char_p, # message
ctypes.c_size_t, # message length
ctypes.c_char_p, # secret key
ctypes.POINTER(ctypes.c_uint8), # output signature buffer
ctypes.POINTER(ctypes.c_size_t), # output signature length
]
sign_func.restype = ctypes.c_int
message_ptr = ctypes.c_char_p(message)
sig_buffer = ctypes.create_string_buffer(1280) # Max FALCON-1024 sig size
sig_length = ctypes.c_size_t()
status = sign_func(
message_ptr,
len(message),
ctypes.c_char_p(secret_key),
sig_buffer,
ctypes.byref(sig_length)
)
if status != 0:
raise RuntimeError(f"FALCON sign failed with status {status}")
return sig_buffer.raw[:sig_length.value]
def sig_verify(self, public_key: bytes, message: bytes, signature: bytes) -> bool:
"""Verify FALCON-1024 signature."""
if self.provider == "liboqs":
return self._sig_verify_liboqs(public_key, message, signature)
elif self.provider == "native":
return self._sig_verify_native(public_key, message, signature)
def _sig_verify_liboqs(self, public_key: bytes, message: bytes, signature: bytes) -> bool:
"""Verify via liboqs."""
try:
import oqs
sig = oqs.Signature(self.algorithm_name)
sig.verify(message, signature, public_key)
return True
except Exception:
return False
def _sig_verify_native(self, public_key: bytes, message: bytes, signature: bytes) -> bool:
"""Verify via native C++ implementation."""
import ctypes
verify_func = self._ctypes_lib.falcon_verify
verify_func.argtypes = [
ctypes.c_char_p, # message
ctypes.c_size_t, # message length
ctypes.c_char_p, # signature
ctypes.c_size_t, # signature length
ctypes.c_char_p, # public key
]
verify_func.restype = ctypes.c_int
status = verify_func(
ctypes.c_char_p(message),
len(message),
ctypes.c_char_p(signature),
len(signature),
ctypes.c_char_p(public_key)
)
return status == 0 # 0 = valid, non-zero = invalid
Integration into PostQuantumCrypto Context
Update the context class to include the new strategy:
class PostQuantumCrypto:
# ... existing code ...
def _auto_detect(self) -> CryptoStrategyBase:
"""Probe backends in priority order."""
strategies = [
OQSProviderStrategy(),
FalconStrategy(falcon_provider="liboqs"), # NEW
LibOQSStrategy(),
FalconStrategy(falcon_provider="native"), # NEW (faster)
SimulatedStrategy()
]
for strategy in strategies:
if strategy._probe():
print(f"[PQC] Selected: {strategy.__class__.__name__}")
return strategy
raise RuntimeError("No cryptographic backend available")
Handling Algorithm-Specific Constraints
Some algorithms, like FALCON, are signature-only and don't implement KEM operations. Wrap calls appropriately:
class HybridCryptoSession:
"""Manages a hybrid KEM + signature session."""
def __init__(self, kem_strategy: CryptoStrategyBase, sig_strategy: CryptoStrategyBase):
"""Allow decoupling of KEM and signature strategies."""
self.kem = kem_strategy # ML-KEM-1024 or Kyber
self.sig = sig_strategy # ML-DSA-87, FALCON, or SLH-DSA
def generate_session_keys(self) -> Tuple[bytes, bytes, bytes, bytes]:
"""Generate both KEM and signature keypairs."""
kem_pk, kem_sk = self.kem.kem_keygen()
sig_pk, sig_sk = self.sig.sig_keygen()
return (kem_pk, kem_sk, sig_pk, sig_sk)
def sign_and_encrypt_message(self, message: bytes, kem_pk: bytes, sig_sk: bytes):
"""Sign then encrypt for integrity and confidentiality."""
signature = self.sig.sig_sign(sig_sk, message)
ciphertext, shared_secret = self.kem.kem_encapsulate(kem_pk)
return {
"ciphertext": ciphertext,
"signature": signature,
"shared_secret": shared_secret
}
Part 3: Native C++ Implementation - FALCON
FALCON Algorithm Overview
FALCON (Fast-Fourier Lattice-based Compact Signatures over NTRU) is a NIST-standardized post-quantum digital signature algorithm optimized for small signature sizes and fast verification. It combines:
- Lattice basis reduction using fast Fourier transforms (FFT)
- Gaussian sampling for quantum-resistant security
- Deterministic signing preventing side-channel attacks
Key Properties:
- NIST FIPS 204 approved algorithm
- Security level: 5 (256-bit post-quantum equivalent)
- Public key: 897 bytes (FALCON-1024)
- Secret key: 2,304 bytes (FALCON-1024)
- Signature: ~666 bytes typical
- Sign/verify time: <1ms data-preserve-html-node="true" on modern CPUs
- Quantum secure against known lattice attacks
Building FALCON from Reference Implementation
The NIST reference implementation is available at https://github.com/tprest/falcon/
# Clone the reference implementation
git clone https://github.com/tprest/falcon.git
cd falcon
cd C
# The directory structure:
# c/
# ├─ fpr.c / fpr.h (floating-point operations)
# ├─ fft.c / fft.h (FFT for lattice reduction)
# ├─ codec.c / codec.h (key/sig serialization)
# ├─ rng.c / rng.h (entropy source)
# ├─ sign.c / sign.h (signing logic)
# ├─ vrfy.c / vrfy.h (verification logic)
# └─ falgon.c (wrapper functions)
C++ Wrapper Implementation
Create a C++ module that wraps the NIST reference implementation:
// falcon_wrapper.cpp
#include <cstring>
#include <cstdint>
#include <stdexcept>
// NIST Reference Implementation Headers
extern "C" {
#include "fpr.h"
#include "fft.h"
#include "sign.h"
#include "vrfy.h"
#include "codec.h"
#include "rng.h"
}
// FALCON-1024 constants (from NIST reference)
#define FALCON_LOGN 10 // log2(1024)
#define FALCON_N (1U << FALCON_LOGN)
#define FALCON_DEGREES FALCON_N
#define FALCON_SALT_LEN 16 // Salt for deterministic signing
#define FALCON_SIG_BLOCKSIZE 256
// Public key format: 897 bytes
#define FALCON_PUBKEY_SIZE 897
// Secret key format: 2304 bytes
#define FALCON_PRIVKEY_SIZE 2304
// Signature (raw): 1280 bytes max
#define FALCON_SIG_MAX_SIZE 1280
class FalconContext {
/**
* C++ wrapper around NIST FALCON reference implementation.
* Provides:
* - Keypair generation with entropy
* - Deterministic signing (RFC 8032 style)
* - Fast signature verification
* - Secure key serialization
*/
public:
struct PublicKey {
uint8_t data[FALCON_PUBKEY_SIZE];
};
struct PrivateKey {
uint8_t data[FALCON_PRIVKEY_SIZE];
};
struct Signature {
uint8_t data[FALCON_SIG_MAX_SIZE];
size_t length;
};
/**
* Generate FALCON-1024 keypair.
* Uses system entropy (/dev/urandom on Unix, CryptGenRandom on Windows)
* @param pub Output: public key
* @param priv Output: private key
* @return 0 on success, non-zero on error
*/
static int keygen(PublicKey& pub, PrivateKey& priv) {
// Temporary buffers for NIST API
uint8_t seed[48]; // 48 bytes of entropy for deterministic generation
uint8_t temp[2048];
// Gather entropy
if (get_entropy(seed, sizeof(seed)) != 0) {
return -1; // Entropy gathering failed
}
// Call NIST keygen
// (Pseudocode; actual NIST API varies)
int ret = falcon_keygen(
FALCON_LOGN, // Algorithm parameter: log2(1024)
seed, // Random seed
sizeof(seed),
priv.data, // Output: serialized secret key
FALCON_PRIVKEY_SIZE,
pub.data, // Output: serialized public key
FALCON_PUBKEY_SIZE,
temp, // Temporary workspace
sizeof(temp)
);
return ret;
}
/**
* Sign a message deterministically using FALCON-1024.
*
* Signing is deterministic (FIPS 204 Section 3.4):
* 1. Hash the message to get a nonce
* 2. Use nonce + private key to sign
* 3. Signature is always the same for same message
*
* @param message Message bytes to sign
* @param message_len Message length
* @param priv Private key
* @param sig Output: signature (variable-length, typically 666 bytes)
* @param sig_len Output: actual signature length
* @return 0 on success, non-zero on error
*/
static int sign(
const uint8_t* message,
size_t message_len,
const PrivateKey& priv,
Signature& sig
) {
if (message == nullptr || message_len == 0) {
return -1;
}
// Temporary buffers for NIST signing
uint8_t nonce[FALCON_SALT_LEN];
uint8_t temp[4096];
// Generate deterministic nonce from message hash
if (derive_nonce_from_message(message, message_len, nonce) != 0) {
return -1;
}
// Call NIST sign
int ret = falcon_sign_dyn(
FALCON_LOGN,
sig->data, // Output signature buffer
&sig->length, // Output signature length
FALCON_SIG_MAX_SIZE,
nonce, // Deterministic nonce
sizeof(nonce),
message, // Message to sign
message_len,
priv.data, // Serialized private key
FALCON_PRIVKEY_SIZE,
temp, // Temporary workspace
sizeof(temp)
);
if (ret != 0) {
sig->length = 0;
}
return ret;
}
/**
* Verify FALCON-1024 signature.
*
* @param message Message bytes
* @param message_len Message length
* @param sig Signature bytes
* @param sig_len Signature length
* @param pub Public key
* @return 0 if valid, non-zero if invalid or error
*/
static int verify(
const uint8_t* message,
size_t message_len,
const uint8_t* sig,
size_t sig_len,
const PublicKey& pub
) {
if (message == nullptr || sig == nullptr) {
return -1;
}
// Temporary workspace for NIST verify
uint8_t temp[2048];
// Call NIST verify
int ret = falcon_verify(
FALCON_LOGN,
pub.data, // Serialized public key
FALCON_PUBKEY_SIZE,
sig, // Signature to verify
sig_len,
message, // Message
message_len,
temp, // Temporary workspace
sizeof(temp)
);
// Return: 0 = valid, non-zero = invalid
return ret;
}
private:
/**
* Gather cryptographic entropy from OS.
* Uses /dev/urandom (Unix), CryptGenRandom (Windows), or SecureRandom (macOS)
* @param buffer Output buffer for random bytes
* @param len Number of bytes to generate
* @return 0 on success, -1 on failure
*/
static int get_entropy(uint8_t* buffer, size_t len) {
#ifdef _WIN32
// Windows: CryptGenRandom
HCRYPTPROV hCryptProv;
if (!CryptAcquireContextW(&hCryptProv, nullptr, nullptr, PROV_RSA_FULL, 0)) {
return -1;
}
if (!CryptGenRandom(hCryptProv, len, buffer)) {
CryptReleaseContext(hCryptProv, 0);
return -1;
}
CryptReleaseContext(hCryptProv, 0);
return 0;
#else
// Unix: /dev/urandom
FILE* f = fopen("/dev/urandom", "rb");
if (!f) return -1;
size_t n = fread(buffer, 1, len, f);
fclose(f);
return (n == len) ? 0 : -1;
#endif
}
/**
* Derive a deterministic nonce from message.
* Prevents nonce reuse attacks while maintaining determinism.
* @param message Message bytes
* @param message_len Message length
* @param nonce Output: 16-byte nonce
* @return 0 on success
*/
static int derive_nonce_from_message(
const uint8_t* message,
size_t message_len,
uint8_t* nonce
) {
// Use SHA-256 truncated to 16 bytes
// (In production, use fpr_hash from NIST reference)
unsigned char hash[32];
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, message, message_len);
SHA256_Final(hash, &ctx);
std::memcpy(nonce, hash, FALCON_SALT_LEN);
return 0;
}
};
// C interface for ctypes/FFI binding
extern "C" {
int falcon_keygen(uint8_t* pub, uint8_t* priv) {
try {
FalconContext::PublicKey pubkey;
FalconContext::PrivateKey privkey;
int ret = FalconContext::keygen(pubkey, privkey);
if (ret == 0) {
std::memcpy(pub, pubkey.data, FALCON_PUBKEY_SIZE);
std::memcpy(priv, privkey.data, FALCON_PRIVKEY_SIZE);
}
return ret;
} catch (...) {
return -1;
}
}
int falcon_sign(
const uint8_t* message,
size_t message_len,
const uint8_t* priv,
uint8_t* sig,
size_t* sig_len
) {
try {
FalconContext::PrivateKey privkey;
FalconContext::Signature signature;
std::memcpy(privkey.data, priv, FALCON_PRIVKEY_SIZE);
int ret = FalconContext::sign(message, message_len, privkey, signature);
if (ret == 0) {
std::memcpy(sig, signature.data, signature.length);
*sig_len = signature.length;
}
return ret;
} catch (...) {
return -1;
}
}
int falcon_verify(
const uint8_t* message,
size_t message_len,
const uint8_t* sig,
size_t sig_len,
const uint8_t* pub
) {
try {
FalconContext::PublicKey pubkey;
std::memcpy(pubkey.data, pub, FALCON_PUBKEY_SIZE);
return FalconContext::verify(message, message_len, sig, sig_len, pubkey);
} catch (...) {
return -1;
}
}
}
Compilation and Integration
Build script for shared library:
#!/bin/bash
# build_falcon.sh - Compile FALCON reference implementation as shared library
set -e
FALCON_SRC="/path/to/falcon/c"
OUTPUT_LIB="./libfalcon.so"
# Compiler flags for production
CFLAGS="-O3 -fPIC -march=native -Wall -Wextra"
CXXFLAGS="-O3 -fPIC -march=native -Wall -Wextra -std=c++17"
# Include paths
INCLUDES="-I${FALCON_SRC}"
# Compile NIST reference implementation
echo "[*] Compiling FALCON reference implementation..."
gcc $CFLAGS $INCLUDES -c ${FALCON_SRC}/fpr.c -o fpr.o
gcc $CFLAGS $INCLUDES -c ${FALCON_SRC}/fft.c -o fft.o
gcc $CFLAGS $INCLUDES -c ${FALCON_SRC}/sign.c -o sign.o
gcc $CFLAGS $INCLUDES -c ${FALCON_SRC}/vrfy.c -o vrfy.o
gcc $CFLAGS $INCLUDES -c ${FALCON_SRC}/codec.c -o codec.o
gcc $CFLAGS $INCLUDES -c ${FALCON_SRC}/rng.c -o rng.o
# Compile C++ wrapper
echo "[*] Compiling C++ wrapper..."
g++ $CXXFLAGS $INCLUDES -c falcon_wrapper.cpp -o falcon_wrapper.o
# Link into shared library
echo "[*] Linking shared library..."
g++ -shared -o $OUTPUT_LIB \
fpr.o fft.o sign.o vrfy.o codec.o rng.o falcon_wrapper.o \
-lcrypto # OpenSSL for SHA256 if needed
# Cleanup object files
rm -f *.o
echo "[+] Built: $OUTPUT_LIB"
echo "[+] Run: sudo cp $OUTPUT_LIB /usr/local/lib/"
echo "[+] Update LD_LIBRARY_PATH or ldconfig"
Python Ctypes Binding
The FalconStrategy class above uses ctypes to load and call the compiled library. For alternative binding methods:
Using CFFI (Recommended for type safety):
# falcon_cffi.py
from cffi import FFI
ffi = FFI()
# Define C interface
ffi.cdef("""
int falcon_keygen(uint8_t *pub, uint8_t *priv);
int falcon_sign(
const uint8_t *message, size_t message_len,
const uint8_t *priv,
uint8_t *sig, size_t *sig_len
);
int falcon_verify(
const uint8_t *message, size_t message_len,
const uint8_t *sig, size_t sig_len,
const uint8_t *pub
);
""")
# Load shared library
falcon_lib = ffi.dlopen("./libfalcon.so")
class FalconCFFI:
@staticmethod
def keygen():
pub = ffi.new("uint8_t[]", 897)
priv = ffi.new("uint8_t[]", 2304)
ret = falcon_lib.falcon_keygen(pub, priv)
if ret != 0:
raise RuntimeError(f"FALCON keygen failed: {ret}")
return (bytes(ffi.buffer(pub)), bytes(ffi.buffer(priv)))
@staticmethod
def sign(message, priv):
sig = ffi.new("uint8_t[]", 1280)
sig_len = ffi.new("size_t *")
ret = falcon_lib.falcon_sign(
message, len(message),
priv, sig, sig_len
)
if ret != 0:
raise RuntimeError(f"FALCON sign failed: {ret}")
return bytes(ffi.buffer(sig, sig_len[0]))
@staticmethod
def verify(message, sig, pub):
ret = falcon_lib.falcon_verify(
message, len(message),
sig, len(sig),
pub
)
return ret == 0
Using pybind11 (For OOP interface):
// falcon_pybind.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "falcon_wrapper.cpp"
namespace py = pybind11;
PYBIND11_MODULE(falcon, m) {
py::class_<FalconContext>(m, "FalconContext")
.def_static("keygen", [](){
FalconContext::PublicKey pub;
FalconContext::PrivateKey priv;
FalconContext::keygen(pub, priv);
return std::make_pair(
py::bytes(reinterpret_cast<char*>(pub.data), 897),
py::bytes(reinterpret_cast<char*>(priv.data), 2304)
);
})
.def_static("sign", [](py::bytes msg, py::bytes priv_bytes){
FalconContext::PrivateKey priv;
std::memcpy(priv.data, msg.cast<std::string>().data(), 2304);
FalconContext::Signature sig;
FalconContext::sign(
reinterpret_cast<const uint8_t*>(msg.cast<std::string>().data()),
msg.size(),
priv, sig
);
return py::bytes(reinterpret_cast<char*>(sig.data), sig.length);
})
.def_static("verify", [](py::bytes msg, py::bytes sig, py::bytes pub_bytes){
FalconContext::PublicKey pub;
std::memcpy(pub.data, pub_bytes.cast<std::string>().data(), 897);
return FalconContext::verify(
reinterpret_cast<const uint8_t*>(msg.cast<std::string>().data()),
msg.size(),
reinterpret_cast<const uint8_t*>(sig.cast<std::string>().data()),
sig.size(),
pub
) == 0;
});
}
Compile with: c++ -O3 -Wall -shared -std=c++17 -fPIC $(python3 -m pybind11 --includes) falcon_pybind.cpp -o falcon$(python3-config --extension-suffix)
Part 4: Other NIST FIPS 203/204 Algorithms
ML-KEM (FIPS 203) - Key Encapsulation
Status: Already integrated via liboqs and oqs-provider
Variants:
- ML-KEM-512 (security level 1)
- ML-KEM-768 (security level 3)
- ML-KEM-1024 (security level 5) ← Currently used in RiddlerChat
Python Strategy Template:
class MLKEMStrategy(CryptoStrategyBase):
"""ML-KEM key encapsulation mechanism (FIPS 203)."""
def __init__(self, variant="ML-KEM-1024"):
self.variant = variant # Algorithm parameter
def kem_keygen(self):
import oqs
kem = oqs.KeyEncapsulation(self.variant)
public_key = kem.generate_keypair()
secret_key = kem.export_secret_key()
return (public_key, secret_key)
def kem_encapsulate(self, public_key):
import oqs
kem = oqs.KeyEncapsulation(self.variant)
ciphertext, shared_secret = kem.encap_secret(public_key)
return (ciphertext, shared_secret)
def kem_decapsulate(self, secret_key, ciphertext):
import oqs
kem = oqs.KeyEncapsulation(self.variant)
kem.import_secret_key(secret_key)
shared_secret = kem.decap_secret(ciphertext)
return shared_secret
ML-DSA (FIPS 204) - Digital Signatures
Status: Available via liboqs (reference implementation)
Variants:
- ML-DSA-44 (security level 2)
- ML-DSA-65 (security level 3)
- ML-DSA-87 (security level 5) ← Currently used in RiddlerChat
Key Features:
- Deterministic signing (no nonce needed)
- Compact signatures (~3,300 bytes for -87)
- Fast verification
- Optimized for lattice-hard problems
Python Strategy:
class MLDSAStrategy(CryptoStrategyBase):
"""ML-DSA digital signature algorithm (FIPS 204)."""
def __init__(self, variant="ML-DSA-87"):
self.variant = variant
def sig_keygen(self):
import oqs
sig = oqs.Signature(self.variant)
public_key = sig.generate_keypair()
secret_key = sig.export_secret_key()
return (public_key, secret_key)
def sig_sign(self, secret_key, message):
import oqs
sig = oqs.Signature(self.variant)
sig.import_secret_key(secret_key)
return sig.sign(message)
def sig_verify(self, public_key, message, signature):
try:
import oqs
sig = oqs.Signature(self.variant)
sig.verify(message, signature, public_key)
return True
except:
return False
SLH-DSA (FIPS 204) - Hash-Based Signatures
Status: Available via liboqs
Characteristics:
- Hash-based security (relies on hash function strength)
- Stateless (no key rekeying required)
- Larger signatures (~7-17 KB) but proven security
- SPHINCS+ family standardized
Variants:
- SLH-DSA-SHA2-128s (128-bit, small sig)
- SLH-DSA-SHA2-256f (256-bit, fast sig)
- etc.
Use Case: When you need provably secure signatures with minimal assumptions
class SLHDSAStrategy(CryptoStrategyBase):
"""SLH-DSA stateless hash-based signatures (FIPS 204)."""
def sig_keygen(self):
import oqs
# Use fast variant for this session
sig = oqs.Signature("SLH-DSA-SHA2-256f")
public_key = sig.generate_keypair()
secret_key = sig.export_secret_key()
return (public_key, secret_key)
def sig_sign(self, secret_key, message):
import oqs
sig = oqs.Signature("SLH-DSA-SHA2-256f")
sig.import_secret_key(secret_key)
return sig.sign(message)
def sig_verify(self, public_key, message, signature):
try:
import oqs
sig = oqs.Signature("SLH-DSA-SHA2-256f")
sig.verify(message, signature, public_key)
return True
except:
return False
Combining Strategies (Composite Signatures)
For maximum security assurance, combine multiple signature algorithms:
class CompositeSignatureStrategy(CryptoStrategyBase):
"""Composite signature using both ML-DSA and FALCON for maximum assurance."""
def __init__(self):
self.mldsa = MLDSAStrategy()
self.falcon = FalconStrategy()
def sig_keygen(self):
"""Generate both keypairs together."""
mldsa_pk, mldsa_sk = self.mldsa.sig_keygen()
falcon_pk, falcon_sk = self.falcon.sig_keygen()
# Composite key: concatenate both
composite_pk = mldsa_pk + falcon_pk
composite_sk = mldsa_sk + falcon_sk
return (composite_pk, composite_sk)
def sig_sign(self, secret_key, message):
"""Sign with both algorithms."""
mldsa_sk = secret_key[:len(secret_key)//2]
falcon_sk = secret_key[len(secret_key)//2:]
mldsa_sig = self.mldsa.sig_sign(mldsa_sk, message)
falcon_sig = self.falcon.sig_sign(falcon_sk, message)
# Pack both signatures
return len(mldsa_sig).to_bytes(2, 'big') + mldsa_sig + falcon_sig
def sig_verify(self, public_key, message, signature):
"""Verify both signatures (both must be valid)."""
mldsa_pk = public_key[:len(public_key)//2]
falcon_pk = public_key[len(public_key)//2:]
mldsa_sig_len = int.from_bytes(signature[:2], 'big')
mldsa_sig = signature[2:2+mldsa_sig_len]
falcon_sig = signature[2+mldsa_sig_len:]
# Both must verify
mldsa_valid = self.mldsa.sig_verify(mldsa_pk, message, mldsa_sig)
falcon_valid = self.falcon.sig_verify(falcon_pk, message, falcon_sig)
return mldsa_valid and falcon_valid
Part 5: Integration Checklist
Step-by-Step Integration Process
1. Implement Strategy Class
- Inherit from
CryptoStrategyBase - Implement all seven abstract methods
- Implement
_probe()for auto-detection - Add comprehensive docstrings
- Handle platform differences (Windows/Linux/macOS)
2. Test the Strategy in Isolation
# test_new_strategy.py
def test_falcon_keygen():
strategy = FalconStrategy()
pub, priv = strategy.sig_keygen()
assert len(pub) == 897
assert len(priv) == 2304
def test_falcon_sign_verify():
strategy = FalconStrategy()
pub, priv = strategy.sig_keygen()
message = b"Test message"
sig = strategy.sig_sign(priv, message)
assert strategy.sig_verify(pub, message, sig) == True
assert strategy.sig_verify(pub, b"Different message", sig) == False
3. Register in PostQuantumCrypto Context
def _auto_detect(self):
strategies = [
# ... existing strategies ...
FalconStrategy(falcon_provider="native"),
]
# ... detection logic ...
4. Add Integration Tests
def test_falcon_in_chat_service():
# Create service with FALCON strategy
service = ChatService()
service.pq.set_strategy(FalconStrategy())
# Generate session keys
sig_pub, sig_priv = service.pq.sig_keygen()
# Send and verify message
message = "test message"
signature = service.pq.sig_sign(sig_priv, message.encode())
assert service.pq.sig_verify(sig_pub, message.encode(), signature)
5. Performance Profiling
import timeit
def benchmark_signing():
strategy = FalconStrategy(provider="native")
pub, priv = strategy.sig_keygen()
message = b"x" * 1000
# Measure sign time
t_sign = timeit.timeit(
lambda: strategy.sig_sign(priv, message),
number=100
) / 100
# Measure verify time
sig = strategy.sig_sign(priv, message)
t_verify = timeit.timeit(
lambda: strategy.sig_verify(pub, message, sig),
number=100
) / 100
print(f"Sign: {t_sign*1000:.2f}ms, Verify: {t_verify*1000:.2f}ms")
6. Documentation
- Add docstrings to all public methods
- Update README with new algorithm support
- Include benchmark results
- Document any platform-specific requirements
Part 6: Performance Considerations
Algorithm Performance Comparison (Approximate)
| Algorithm | Keygen | Sign | Verify | Sig Size | PK Size |
|---|---|---|---|---|---|
| ML-DSA-87 | 0.5ms | 0.8ms | 0.3ms | 3300B | 1312B |
| FALCON-1024 | 1.2ms | 0.4ms | 0.5ms | 666B | 897B |
| SLH-DSA-256f | 2ms | 20ms | 10ms | 17KB | 32B |
| Ed25519 (classical) | 0.03ms | 0.05ms | 0.08ms | 64B | 32B |
Optimization Strategies
1. Use Native C/C++ for Hot Paths
- Direct liboqs C bindings via ctypes/CFFI for high-frequency operations
- Profile Python strategy vs. native strategy
- Typical speedup: 2-5x for complex operations
2. Strategy Caching
class PostQuantumCrypto:
def __init__(self):
self._strategy = None
self._cached_keys = {} # Cache keypairs for reuse
def sig_keygen_cached(self, key_id):
if key_id not in self._cached_keys:
self._cached_keys[key_id] = self._strategy.sig_keygen()
return self._cached_keys[key_id]
3. Batch Operations
def verify_signatures_batch(self, messages, signatures, public_keys):
"""Verify multiple signatures in one batch."""
results = []
for msg, sig, pk in zip(messages, signatures, public_keys):
results.append(self._strategy.sig_verify(pk, msg, sig))
return results
4. Use Lighter Algorithms When Appropriate
class AdaptiveSignatureStrategy(CryptoStrategyBase):
"""Choose algorithm based on message size and latency requirements."""
def __init__(self):
self.mldsa = MLDSAStrategy()
self.falcon = FalconStrategy()
def sig_sign(self, secret_key, message):
# Use FALCON for large messages (compact signature)
if len(message) > 10KB:
return self.falcon.sig_sign(secret_key, message)
# Use ML-DSA for small messages (faster)
else:
return self.mldsa.sig_sign(secret_key, message)
Part 7: Security Considerations
Key Rotation Strategy
class KeyRotationManager:
"""Manages periodic key rotation for forward secrecy."""
def __init__(self, pq_crypto, rotation_interval_hours=24):
self.pq = pq_crypto
self.rotation_interval = rotation_interval_hours * 3600
self.last_rotation = time.time()
self.current_keypair = None
def get_or_rotate_keys(self):
"""Return current keys, rotating if interval exceeded."""
now = time.time()
if now - self.last_rotation > self.rotation_interval:
self.current_keypair = self.pq.sig_keygen()
self.last_rotation = now
return self.current_keypair
Constant-Time Verification
Ensure verification doesn't leak timing information:
def sig_verify_constant_time(self, public_key, message, signature):
"""Verify with constant-time comparison."""
try:
result = self._strategy.sig_verify(public_key, message, signature)
# Always return after same time regardless of result
import secrets
_dummy = secrets.compare_digest(b"", b"")
return result
except:
# Exceptions should also take constant time
import secrets
_dummy = secrets.compare_digest(b"", b"")
return False
Entropy Validation
def validate_entropy(self, num_samples=100):
"""Validate that key generation has sufficient entropy."""
keys = [self.pq.sig_keygen()[0] for _ in range(num_samples)]
# Check for key reuse (catastrophic failure)
if len(set(keys)) < num_samples * 0.95:
raise RuntimeError("Insufficient entropy in key generation")
# Check bit distribution
import collections
all_bytes = b"".join(keys)
bit_counts = collections.Counter(bin(b).count('1') for b in all_bytes)
# Bits should be roughly evenly distributed
expected = len(all_bytes) * 8 / 2
for count in bit_counts.values():
if abs(count - expected) > expected * 0.1:
raise RuntimeError("Entropy distribution skewed")
Conclusion
The Strategy pattern provides a clean, extensible architecture for integrating NIST-approved post-quantum algorithms into RiddlerChat. By following the templates and guidance in this document, you can:
- Add new PQC algorithms (FALCON, additional KEM variants, etc.)
- Maintain backward compatibility with existing code
- Test strategies in isolation
- Switch algorithms at runtime for testing and recovery
- Optimize critical paths with native C/C++ implementations
The FALCON implementation serves as a reference for binding C++ cryptographic libraries to Python through ctypes, CFFI, or pybind11, enabling high-performance operations while maintaining Python's flexibility.
References:
- NIST FIPS 203 - Module-Lattice-Based Key-Encapsulation Mechanism Standard
- NIST FIPS 204 - Module-Lattice-Based Digital Signature Standard
- FALCON specification: https://falcon-sign.info/
- liboqs: https://github.com/open-quantum-safe/liboqs
- OQS Provider: https://github.com/open-quantum-safe/oqs-provider
Oblivion Edge Vulnerability Research LLC -- "Lattices today, quantum-safe tomorrow."
Oblivion Edge Vulnerability Research LLC, San Antonio, Texas -- June 2026 Project: The Riddler Chat System
Overview
The RiddlerChat backend now features a pluggable post-quantum cryptography engine built on the Gang of Four Strategy design pattern. This architecture allows the system to automatically detect and select the strongest available cryptographic backend at startup, while supporting runtime strategy switching for testing, upgrades, and graceful degradation. The integration brings the Open Quantum Safe (OQS) provider for OpenSSL 3.x directly into the chat application's message pipeline, ensuring that every key exchange, digital signature, and encrypted message can leverage real NIST FIPS 203/204 post-quantum algorithms when the provider is available on the host system.
System Component Architecture
The following diagram shows the full system architecture from the Electron frontend down through the FastAPI backend, services layer, crypto engine, and external libraries. The Strategy Pattern sits at the heart of the crypto engine, with PostQuantumCrypto delegating to whichever concrete strategy is active.

The Electron frontend communicates with the FastAPI backend over WebSocket. The backend's services layer -- ChatService, RelayService, ZeroKnowledgeAuth, and EntropyService -- handles message routing, onion circuit construction, Schnorr-based authentication, and entropy monitoring respectively. All post-quantum operations flow through the PostQuantumCrypto context, which delegates to the active strategy implementation. The strategy in turn calls into the appropriate external library: the oqs-provider C shared library via OpenSSL CLI, the liboqs Python bindings directly, or the pyca/cryptography library for classical fallback.
The Problem
The original PostQuantumCrypto class contained all backend detection and algorithm logic inline, using a chain of if/elif branches to select between liboqs Python bindings, OpenSSL subprocess calls, and a classical crypto fallback. This monolithic approach made it difficult to add new backends, test individual strategies in isolation, or swap cryptographic implementations at runtime. Integrating the oqs-provider-0.10.0 C library into this structure would have deepened the coupling and made the code harder to maintain.
The Solution
We refactored the cryptographic layer using the Gang of Four Strategy pattern, directly inspired by the existing stateful_messaging module in the legacy chat client. In that module, CommunicationBase defines an abstract interface with send(), recv(), and handle_cmd() methods, while concrete strategies like PlainTextCOMM and SymmetricCryptoMessaging provide different implementations. We applied the same architectural principle to the cryptographic backend, creating an abstract CryptoStrategyBase with concrete strategies for each available provider.
Architecture
The design consists of three layers: an abstract base class defining the contract, three concrete strategy implementations, and a context class that manages strategy selection and delegates all operations. The class diagram below illustrates the complete inheritance hierarchy and delegation relationship.

Abstract Strategy: CryptoStrategyBase
The abstract base class defines seven abstract methods that every cryptographic backend must implement. These cover the full lifecycle of post-quantum key encapsulation (FIPS 203) and digital signatures (FIPS 204). Each strategy also reports its own availability through a _probe() method, which the context uses during auto-detection.
class CryptoStrategyBase(ABC):
@abstractmethod
def kem_keygen(self) -> Tuple[bytes, bytes]:
"""Generate a KEM keypair. Returns (public_key, secret_key)."""
@abstractmethod
def kem_encapsulate(self, public_key: bytes) -> Tuple[bytes, bytes]:
"""Encapsulate a shared secret. Returns (ciphertext, shared_secret)."""
@abstractmethod
def kem_decapsulate(self, secret_key: bytes, ciphertext: bytes) -> bytes:
"""Decapsulate to recover the shared secret."""
@abstractmethod
def sig_keygen(self) -> Tuple[bytes, bytes]:
"""Generate a signing keypair. Returns (public_key, secret_key)."""
@abstractmethod
def sig_sign(self, secret_key: bytes, message: bytes) -> bytes:
"""Sign a message. Returns the signature bytes."""
@abstractmethod
def sig_verify(self, public_key: bytes, message: bytes, signature: bytes) -> bool:
"""Verify a signature. Returns True if valid."""
Concrete Strategies
Three concrete strategies implement this interface, each targeting a different cryptographic backend. The system probes them in priority order and selects the first one that reports itself as available.
OQSProviderStrategy integrates the oqs-provider-0.10.0 shared library through OpenSSL 3.x CLI commands. It loads the provider using -provider oqsprovider flags and performs real ML-KEM-1024 and ML-DSA-87 operations through openssl genpkey, openssl pkeyutl -encap/-decap, and openssl pkeyutl -sign/-verify. This strategy represents the highest-fidelity post-quantum implementation, using the same C-level provider that powers TLS 1.3 hybrid key exchange in production OpenSSL deployments.
LibOQSStrategy uses the liboqs Python bindings (import oqs) for direct access to the Open Quantum Safe library. This strategy offers native in-process performance without subprocess overhead, making it ideal for environments where liboqs is installed but the OpenSSL provider is not configured.
SimulatedStrategy provides a functional fallback using classical cryptographic primitives from the Python cryptography library. X25519 Diffie-Hellman stands in for ML-KEM key encapsulation, and Ed25519 stands in for ML-DSA digital signatures. This strategy is always available and ensures the application remains fully functional during development and testing, even on systems without post-quantum libraries installed.
Context: PostQuantumCrypto
The context class manages strategy lifecycle and delegates all cryptographic operations to the active strategy. It preserves full backward compatibility with the original API, so neither ChatService, server.py, nor any other consumer required modification.
class PostQuantumCrypto:
def __init__(self, strategy=None, oqs_provider_path=None):
if strategy:
self._strategy = strategy
else:
self._strategy = self._auto_detect() # probe in priority order
def set_strategy(self, strategy: CryptoStrategyBase):
"""Switch the crypto strategy at runtime."""
self._strategy = strategy
def kem_keygen(self):
return self._strategy.kem_keygen() # delegated
def sig_sign(self, secret_key, message):
return self._strategy.sig_sign(secret_key, message) # delegated
Strategy Auto-Detection
When PostQuantumCrypto() is instantiated without an explicit strategy, the context probes each backend in priority order and selects the first one that reports itself as available. The activity diagram below traces this detection flow from instantiation through probe, selection, and delegation.

The auto-detection begins by probing the OQSProviderStrategy, which runs openssl list -kem-algorithms -provider oqsprovider to check whether the oqs-provider shared library is loaded into OpenSSL. If that probe fails, the system falls through to the LibOQSStrategy, which attempts import oqs and instantiates a test KeyEncapsulation object. If neither post-quantum backend is available, the SimulatedStrategy is selected as the final fallback. Once a strategy is chosen, it is stored as the active delegate, and all subsequent calls from ChatService and server.py flow through it transparently.
| Priority | Strategy | Backend | Probe Method |
|---|---|---|---|
| 1 | OQSProviderStrategy |
OpenSSL 3.x + oqs-provider-0.10.0 | openssl list -kem-algorithms -provider oqsprovider |
| 2 | LibOQSStrategy |
liboqs Python bindings | import oqs; oqs.KeyEncapsulation("ML-KEM-1024") |
| 3 | SimulatedStrategy |
cryptography library (X25519/Ed25519) | Always available |
Once the OQS provider is compiled and installed into the OpenSSL modules path, the system will automatically upgrade to real post-quantum cryptography on the next server restart, with zero code changes required.
How It Connects to the Chat Application
The strategy engine is wired into the live message pipeline at two points. In server.py, the module-level pq_crypto = PostQuantumCrypto() instance handles user registration, login key generation, and the /api/status endpoint. In ChatService, the self.pq = PostQuantumCrypto() instance handles per-session hybrid key exchange, message signing, and fingerprint generation for every connected WebSocket client.
The following sequence diagram traces the complete message flow from Alice's WebSocket connection through key generation, encryption, signing, delivery, and verification on Bob's side. Every call to the PostQuantumCrypto context is visibly delegated to the active strategy.

When a user connects, ChatService.connect() calls self.pq.hybrid_keygen() to generate an ML-KEM-1024 + X25519 hybrid keypair and self.pq.sig_keygen() to generate an ML-DSA-87 signing keypair. When a message is sent, ChatService.send_message() encrypts the plaintext with AES-256-GCM using the session key, then signs the original plaintext with self.pq.sig_sign(). Every one of these calls flows through the active strategy, which means upgrading from simulated to real post-quantum crypto requires only installing the OQS provider -- no application code changes.
Hybrid Key Exchange Protocol
The hybrid key exchange is the most critical cryptographic operation in RiddlerChat. It combines a post-quantum KEM shared secret with a classical X25519 Diffie-Hellman shared secret, then derives a single combined key through HKDF-SHA256. This dual-algorithm approach provides defense-in-depth: even if one algorithm is broken, the other still protects the session.
The following sequence diagram details every step of the hybrid exchange, from Bob's keypair generation through Alice's encapsulation and Bob's decapsulation, including the HKDF derivation that fuses both shared secrets into a single 256-bit session key.

The protocol proceeds in three phases. First, Bob generates a hybrid keypair by calling hybrid_keygen(), which internally invokes the active strategy's kem_keygen() for the post-quantum component and generates an X25519 keypair for the classical component. Second, Alice encapsulates to Bob by calling hybrid_encapsulate() with Bob's two public keys, producing a PQ ciphertext, an ephemeral X25519 public key, and a combined shared secret derived through HKDF. Third, Bob decapsulates by calling hybrid_decapsulate() with his secret keys and Alice's ciphertext bundle, recovering the identical combined secret. Both parties now hold the same 256-bit key, which becomes the AES-256-GCM session encryption key.
Live Cryptographic Output
The following output was captured from a live execution of the RiddlerChat crypto engine. Each value shown is real cryptographic material generated during the session.
Key Encapsulation (ML-KEM-1024 Interface)
The KEM keygen produces a keypair, encapsulation creates a ciphertext and shared secret using the public key, and decapsulation recovers the identical shared secret using the secret key.
KEM Keygen:
Public Key (32 bytes): 16779b17e774eb5389fb16fdb5e6da0bfc09b9c7...
Secret Key (32 bytes): 1096e61ac382abe243d8b372b44baf0286a0ed0c...
Encapsulate (using Public Key):
Ciphertext (32 bytes): e3107f2cd7f66324d7be19b34902426f35...
Shared Secret (32 bytes): 98727969cf3546cdab596504abaf29905663ee33...
Decapsulate (using Secret Key + Ciphertext):
Recovered Secret (32 bytes): 98727969cf3546cdab596504abaf29905663ee33...
Secrets Match: True
Both parties now hold the same 256-bit shared secret without ever transmitting it over the wire. This secret becomes the AES-256-GCM session key.
Hybrid Key Exchange (ML-KEM-1024 + X25519)
Hybrid Keygen:
PQ Public Key (32 bytes): 8dcff2d90a458da06ad7ed6a9a053970...
X25519 Public Key (32 bytes): 4a926c041722c04052506cabbe4948c642...
Combined Secret (32 bytes): ec6bea754beb4a4c1841b4a7eec7e335...
Fingerprint: 2BDB·3595·2EFA
Digital Signature (ML-DSA-87 Interface)
The signing keypair is generated once per session. Every outgoing message is signed with the secret key, and the recipient verifies using the sender's public key. A tampered message or forged signature returns False.
Message: Riddle me this: What has a head and a tail but no body? A coin.
Signature: 299cd92e80be1c94bcd996f7be04f56ed56dbf4e370e939f14f2...
Verified: True
AES-256-GCM Authenticated Encryption
The combined shared secret from the hybrid exchange serves as the encryption key. Each message is sealed with a unique 96-bit hybrid nonce (timestamp + counter + random) to prevent nonce reuse, even under clock skew or high message rates.
Plaintext: The Riddler has entered the chat. All circuits are sealed.
Key: ec6bea754beb4a4c1841b4a7eec7e33593c348243f2e0d47b3c15236f902373c
Sealed (86 bytes):
Nonce (12B): 6a4020ce 00000000 71dfe01e
[timestamp] [counter] [random]
Ciphertext (74B): 3e91ac2be7a8fb0bedb0b3b5463a1aeb05b5bc9451857f17
cd280392646065d26d63fc0dc508599fa3c45ffee4459f07
8e69f6da0483bcafd3b42d282208d4aa7fc19f5ec36537f0
1147
Decrypted: The Riddler has entered the chat. All circuits are sealed.
Match: True
The ciphertext is indistinguishable from random data. Any modification to the nonce, ciphertext body, or authentication tag causes decryption to fail with an InvalidTag exception, providing tamper evidence on every message.
Test Coverage
The implementation ships with 77 pytest tests organized across four test modules covering unit, functional, security, and integration concerns.
Strategy Pattern Unit Tests (26 tests) verify that the abstract base class cannot be instantiated, all three concrete strategies implement the full interface, auto-detection selects the best available backend, and runtime strategy switching works correctly.
Functional Tests (17 tests) exercise complete cryptographic roundtrips: KEM keygen-encapsulate-decapsulate, hybrid ML-KEM + X25519 key exchange, ML-DSA sign-verify cycles, AES-256-GCM encrypt-decrypt with and without additional authenticated data, and a full end-to-end message flow simulating Alice sending an encrypted, signed message to Bob.
Security Tests (24 tests) validate tamper detection on ciphertext, nonces, AAD, and signatures. They confirm key isolation between sessions, verify that nonces contain timestamp, counter, and random components, test that rekeying provides forward secrecy by invalidating old ciphertexts, and assert sufficient entropy across 100 generated keys.
OQS Provider Integration Tests (10 tests) exercise real ML-KEM-1024 and ML-DSA-87 operations through the OQS provider when it is built and available on the system. These tests automatically skip on systems where the provider is not installed.
67 passed, 10 skipped in 0.56s
Coverage:
pq_crypto.py (Context): 98%
simulated_strategy.py: 96%
symmetric.py: 100%
crypto_strategy_base.py: 81%
**(c) 2026 Oblivion Edge Vulnerability Research LLC, All Rights Reserved
Appendix: Hybrid Key Exchange Protocol
System Component Diagram
Strategy Detection Diagram
Encrypted Message Flow Diagram
Hybrid Key Exchange Protocol