Post-Quantum Hybrid Key Exchange
Post-Quantum Cryptography
This document describes the post-quantum cryptography (PQC) support in GlaSSLess, including the standalone algorithms (ML-KEM, ML-DSA, SLH-DSA, LMS/HSS) and the hybrid key exchange schemes that combine classical and post-quantum cryptography.
Requirements
-
OpenSSL 3.5+ for lattice-based PQC algorithms (ML-KEM, ML-DSA, SLH-DSA)
-
OpenSSL 3.6+ for hash-based PQC signatures (LMS/HSS)
-
Java 25+ with
--enable-native-accessflag
Standalone PQC Algorithms
GlaSSLess supports the NIST-standardized post-quantum algorithms:
| Algorithm | Standard | Type | Variants |
|---|---|---|---|
| ML-KEM | FIPS 203 | Key Encapsulation | ML-KEM-512, ML-KEM-768, ML-KEM-1024 |
| ML-DSA | FIPS 204 | Digital Signature | ML-DSA-44, ML-DSA-65, ML-DSA-87 |
| SLH-DSA | FIPS 205 | Digital Signature | 12 variants (SHA2/SHAKE, 128/192/256, s/f) |
| LMS/HSS | SP 800-208 | Digital Signature (verification only) | RFC 8554 |
LMS/HSS Hash-Based Signatures
LMS (Leighton-Micali Signature) / HSS (Hierarchical Signature System) is a stateful hash-based signature scheme standardized in RFC 8554 and NIST SP 800-208. Unlike ML-DSA and SLH-DSA, LMS is stateful -- each private key can only produce a limited number of signatures, and state must be carefully managed to avoid key reuse.
GlaSSLess supports verification only . Signing and key generation are not supported because state management must be handled by the key owner.
-
Signature algorithm :
LMS(aliases:HSS,id-alg-hss-lms-hashsig) -
KeyFactory :
LMS-- loads public keys from X.509/SubjectPublicKeyInfo encoding -
Requires : OpenSSL 3.6+ with LMS support enabled
See Usage Guide for code examples.
Hybrid Key Encapsulation Mechanisms
Hybrid KEMs combine a classical key exchange algorithm (X25519 or X448) with ML-KEM to provide security against both classical and quantum computers. This "hybrid" approach ensures that the key exchange remains secure even if one of the underlying algorithms is broken.
Supported Hybrid KEMs
| Algorithm | Classical Component | PQC Component | Shared Secret Size | Ciphertext Size |
|---|---|---|---|---|
| X25519MLKEM768 | X25519 | ML-KEM-768 | 64 bytes | 1120 bytes |
| X448MLKEM1024 | X448 | ML-KEM-1024 | 64 bytes | 1632 bytes |
Unsupported Variants
The following hybrid KEMs are defined in standards but not currently supported in GlaSSLess:
| Algorithm | Classical Component | PQC Component | Status |
|---|---|---|---|
| SecP256r1MLKEM768 | P-256 (secp256r1) | ML-KEM-768 | Blocked by OpenSSL |
| SecP384r1MLKEM1024 | P-384 (secp384r1) | ML-KEM-1024 | Blocked by OpenSSL |
Technical Details
OpenSSL 3.5.x supports these algorithms for in-memory operations but lacks key serialization support:
| Feature | X25519MLKEM768 | SecP256r1MLKEM768 |
|---|---|---|
| Key generation | Yes | Yes |
| Encapsulation | Yes | Yes |
| Decapsulation | Yes | Yes |
|
|
Yes | No |
|
|
Yes | No |
| DER/PEM encoding | No | No |
|
|
Yes | No |
|
|
Yes | No |
Java's JCA model requires keys to be serializable via Key.getEncoded() and reconstructable via KeyFactory. Without OpenSSL serialization support, GlaSSLess cannot:
-
Implement
getEncoded()for public/private keys -
Support
KeyFactory.generatePublic()/generatePrivate() -
Enable key persistence or transmission between JVMs
The XDH-based hybrids (X25519MLKEM768, X448MLKEM1024) work because OpenSSL provides raw key export functions for them.
Future Support
EC-based hybrid KEMs will be added to GlaSSLess when OpenSSL adds proper key serialization support, expected in a future 3.5.x patch or 3.6 release. Track progress at:
-
OpenSSL GitHub Issues - search for "hybrid KEM"
Usage Examples
Basic Hybrid Key Exchange
import javax.crypto.KEM;
import java.security.*;
import net.glassless.provider.GlaSSLessProvider;
Security.addProvider(new GlaSSLessProvider());
// Server: Generate hybrid key pair
KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519MLKEM768", PROVIDER_NAME);
KeyPair serverKeyPair = kpg.generateKeyPair();
// Server sends public key to client...
PublicKey serverPublicKey = serverKeyPair.getPublic();
// Client: Encapsulate a shared secret
KEM kem = KEM.getInstance("X25519MLKEM768", PROVIDER_NAME);
KEM.Encapsulator encapsulator = kem.newEncapsulator(serverPublicKey);
KEM.Encapsulated encapsulated = encapsulator.encapsulate();
// Client has: shared secret and ciphertext to send to server
SecretKey clientSharedSecret = encapsulated.key();
byte[] ciphertext = encapsulated.encapsulation();
// Client sends ciphertext to server...
// Server: Decapsulate to recover the same shared secret
KEM.Decapsulator decapsulator = kem.newDecapsulator(serverKeyPair.getPrivate());
SecretKey serverSharedSecret = decapsulator.decapsulate(ciphertext);
// Both parties now have the same 64-byte shared secret
assert Arrays.equals(clientSharedSecret.getEncoded(), serverSharedSecret.getEncoded());
Partial Key Extraction
You can extract a specific portion of the shared secret directly during encapsulation:
// Extract only the first 32 bytes as an AES key
KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 32, "AES");
SecretKey aesKey = encapsulated.key(); // 32-byte AES key
Checking Algorithm Availability
import net.glassless.provider.internal.OpenSSLCrypto;
// Check if hybrid KEMs are available
if (OpenSSLCrypto.isAlgorithmAvailable("KEYMGMT", "X25519MLKEM768")) {
System.out.println("X25519MLKEM768 is available");
} else {
System.out.println("X25519MLKEM768 requires OpenSSL 3.5+");
}
Key Serialization
Hybrid KEM keys use a custom encoding format (not standard ASN.1) due to OpenSSL limitations:
// Get encoded key bytes
byte[] publicKeyBytes = serverKeyPair.getPublic().getEncoded();
byte[] privateKeyBytes = serverKeyPair.getPrivate().getEncoded();
// Reconstruct keys using KeyFactory
KeyFactory kf = KeyFactory.getInstance("X25519MLKEM768", PROVIDER_NAME);
// For public key
X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey reconstructedPublic = kf.generatePublic(pubSpec);
// For private key
PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey reconstructedPrivate = kf.generatePrivate(privSpec);
TLS 1.3 Integration
JDK Version Support
| JDK Version | Hybrid TLS Support | GlaSSLess Role |
|---|---|---|
| JDK 24–26 | None — JSSE has a hardcoded named group list with no hybrid groups |
Application-layer hybrid KEM only (see Application-Layer Hybrid Key Exchange ) |
| JDK 27+ (JEP 527) |
Native — |
FIPS compliance: routes ML-KEM operations through OpenSSL's FIPS-validated module |
JDK 24–26: Why Hybrid TLS Is Not Possible
JDK 25/26 JSSE has three limitations that cannot be worked around by a JCA provider like GlaSSLess:
-
Hardcoded named groups. The
NamedGroupenum insun.security.sslis internal. There is no SPI to register new TLS named groups. -
No KEM-based key exchange. The TLS handshake code only knows
KeyAgreement(interactive, both sides contribute). It does not use thejavax.crypto.KEMAPI (asymmetric: encapsulate/decapsulate). -
No extensible handshake logic. The JSSE handshake state machine is in
sun.security.ssl, which is not a public API. It cannot be subclassed or extended.
JDK 27+: JEP 527 Integration
JEP 527 modifies JSSE itself to support hybrid post-quantum key exchange. It builds on JEP 496 (ML-KEM), which was delivered in JDK 24.
What JEP 527 Changes
-
Adds hybrid named groups (
X25519MLKEM768,SecP256r1MLKEM768,SecP384r1MLKEM1024) to JSSE's internal registry -
Teaches the TLS handshake to use the
javax.crypto.KEMSPI for key exchange — not justKeyAgreement -
Makes
X25519MLKEM768the default preferred named group — applications get PQ protection without code changes -
Allows configuration via
SSLParameters.setNamedGroups()and thejdk.tls.namedGroupssystem property
Architecture: How JSSE Resolves Providers
JSSE uses a layered architecture for hybrid key exchange. Critically, the component algorithm lookups go through standard JCA provider resolution :
JSSE TLS 1.3 Handshake
└─ NamedGroup.X25519MLKEM768
└─ HybridProvider (internal to JSSE, not in system provider list)
└─ Hybrid.KEMImpl(left="ML-KEM-768", right="X25519")
├─ KEM.getInstance("ML-KEM") ← standard JCA lookup
└─ KEM.getInstance("DH", HybridProvider) ← X25519-as-KEM wrapper
└─ Hybrid.KeyPairGeneratorImpl
├─ KeyPairGenerator.getInstance("ML-KEM-768") ← standard JCA lookup
└─ KeyPairGenerator.getInstance("X25519") ← standard JCA lookup
The JDK source explicitly notes: "This is done to work with 3rd-party providers that only have 'ML-KEM' KEM algorithm."
This means that when GlaSSLess is registered at a higher priority than SunJCE, the ML-KEM and X25519 operations within the TLS handshake are routed through GlaSSLess — and therefore through OpenSSL.
FIPS Compliance Use Case
JDK 27's SunJCE includes a pure-Java ML-KEM implementation that is performant but not FIPS-validated . For organizations that require FIPS 140-3 compliance, GlaSSLess provides the bridge between JSSE's TLS protocol handling and OpenSSL's FIPS-validated cryptographic module.
import java.security.Security;
import net.glassless.provider.GlaSSLessProvider;
// Register GlaSSLess at highest priority
// ML-KEM and X25519 operations in TLS handshakes will now use
// OpenSSL's FIPS-validated provider instead of SunJCE
Security.insertProviderAt(new GlaSSLessProvider(), 1);
// Verify FIPS mode is active
GlaSSLessProvider provider = (GlaSSLessProvider) Security.getProvider(PROVIDER_NAME);
assert provider.isFIPSMode() : "OpenSSL FIPS provider is not enabled";
// From this point, all TLS 1.3 connections that negotiate X25519MLKEM768
// will use OpenSSL's FIPS-validated ML-KEM implementation
|
NOTE
|
When GlaSSLess is at position 1, it will also intercept PBE algorithms used for PKCS12 keystore operations. Load keystores before registering GlaSSLess, or register it at a position that only overrides the algorithms you need. |
Performance Considerations
The full hybrid X25519MLKEM768 operation (keygen + encapsulate + decapsulate) was benchmarked across three approaches. Each iteration performs the complete key exchange that would occur during a TLS 1.3 handshake.
ML-KEM-768 in Isolation
When benchmarking ML-KEM-768 alone, JDK 27's pure-Java implementation (SunJCE) benefits from JIT compilation and inlining:
| Provider | ML-KEM-768 (keygen + encaps + decaps) | Relative |
|---|---|---|
| SunJCE (Java) | ~111 µs/op | 1.0x (baseline) |
| GlaSSLess (OpenSSL via FFM) | ~151 µs/op | 0.74x |
However, ML-KEM is only one component of the hybrid key exchange. The full picture is different.
Full Hybrid X25519MLKEM768
| Approach | avg (µs/op) | p50 | p99 | vs baseline |
|---|---|---|---|---|
|
SunJCE decomposed (JDK 27 default)
ML-KEM-768 via SunJCE + X25519 via SunEC |
353.8 |346.6 |447.4 |1.00x | |||
|
GlaSSLess decomposed (current behavior with GlaSSLess at pos 1)
ML-KEM-768 via GlaSSLess + X25519 via GlaSSLess |
290.1 |287.3 |317.6 | 1.22x faster | |||
|
GlaSSLess composite (single OpenSSL call)
X25519MLKEM768 as one native |
217.7 |216.3 |226.3 | 1.62x faster |
Key findings:
-
GlaSSLess decomposed is already 22% faster than the JDK 27 default. While ML-KEM alone is slower via FFM, the X25519 component benefits significantly from OpenSSL's optimized native implementation, and the combined result favors GlaSSLess.
-
The composite approach is 62% faster than the JDK 27 default. OpenSSL performs the X25519 key agreement, ML-KEM encapsulation, and secret concatenation entirely in native code — a single FFM crossing instead of ~20 separate native calls across 3 Arena lifecycles.
-
Tail latency improves dramatically. The p99 drops from 447 µs (SunJCE) to 226 µs (composite) — fewer FFM crossings mean fewer opportunities for GC pauses or scheduling jitter to affect the operation.
Why the Composite Approach Is Faster
JDK 27's HybridProvider decomposes X25519MLKEM768 into separate ML-KEM-768 and X25519 JCA service calls. When GlaSSLess serves both components, each operation involves its own Arena lifecycle, key loading, and cleanup — roughly 20 FFM native calls total. GlaSSLess's composite X25519MLKEM768 KEM delegates to a single OpenSSL EVP_PKEY_encapsulate call (~7 FFM crossings, 1 Arena), letting OpenSSL handle the hybrid construction natively.
However, the composite approach is not currently used by JSSE because HybridProvider is pinned as the provider for hybrid named groups. JSSE resolves KEM.getInstance("X25519MLKEM768", HybridProvider.PROVIDER), bypassing standard JCA provider lookup at the hybrid level. Standard JCA lookup only occurs for the component algorithms (ML-KEM, X25519) inside HybridProvider.
Recommendation
Use GlaSSLess when FIPS-validated cryptography is required — it provides both compliance and a performance improvement over the JDK 27 default for the full hybrid operation. Even in the current decomposed mode, GlaSSLess at position 1 delivers a 22% speedup.
Why Hybrid Key Exchange Matters
Quantum computing poses an increasing threat to widely-deployed public-key encryption algorithms like RSA and ECDH. Even though large-scale quantum computers don't yet exist, adversaries could harvest encrypted data today and decrypt it later ("harvest now, decrypt later" attacks). Hybrid key exchange provides immediate protection by combining classical algorithms with quantum-resistant ones.
JDK 24–26 Workarounds
Until JDK 27, you can achieve post-quantum key exchange through:
-
Application-layer hybrid KEM — perform a GlaSSLess hybrid KEM exchange over a classical TLS connection (see below)
-
Reverse proxy — use nginx or haproxy compiled against OpenSSL 3.5+ for TLS termination with
X25519MLKEM768
Application-Layer Hybrid Key Exchange
For applications on JDK 24–26 that need quantum-resistant key exchange today, perform the hybrid KEM exchange at the application layer after establishing a classical TLS connection:
// Step 1: Establish classical TLS 1.3 connection
SSLSocket socket = (SSLSocket) sslContext.getSocketFactory()
.createSocket("server.example.com", 443);
socket.startHandshake();
// Step 2: Perform hybrid key exchange over the TLS channel
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
// Server generates and sends hybrid public key
KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519MLKEM768", PROVIDER_NAME);
KeyPair keyPair = kpg.generateKeyPair();
out.writeObject(keyPair.getPublic().getEncoded());
// Client encapsulates and sends ciphertext back
byte[] serverPubKeyBytes = (byte[]) in.readObject();
// ... reconstruct public key and encapsulate ...
out.writeObject(ciphertext);
// Both sides derive application keys from both:
// - TLS session keys (classical security)
// - Hybrid KEM shared secret (quantum resistance)
Security Considerations
Why Hybrid?
Hybrid key exchange provides defense-in-depth:
-
If ML-KEM is broken : Classical X25519/X448 still provides security
-
If X25519/X448 is broken by quantum computers : ML-KEM provides security
-
Cryptographic agility : Easy to transition as standards evolve
Key Sizes
| Algorithm | Public Key | Private Key | Ciphertext | Shared Secret |
|---|---|---|---|---|
| X25519MLKEM768 | 1216 bytes | 2432 bytes | 1120 bytes | 64 bytes |
| X448MLKEM1024 | 1664 bytes | 3168 bytes | 1632 bytes | 64 bytes |
Performance
Hybrid KEMs are slower than classical key exchange due to the additional ML-KEM operations. Typical performance (varies by hardware):
| Operation | X25519MLKEM768 | X448MLKEM1024 |
|---|---|---|
| Key Generation | ~0.1ms | ~0.15ms |
| Encapsulation | ~0.1ms | ~0.15ms |
| Decapsulation | ~0.1ms | ~0.15ms |
Standards Compliance
The hybrid KEMs follow the construction specified in:
-
draft-ietf-tls-hybrid-design for TLS integration
-
draft-ounsworth-cfrg-kem-combiners for KEM combination