Skip to content

Commit 5f1c638

Browse files
committed
JNI/JSSE: Add ML-KEM (FIPS 203) TLS 1.3 named group support and ML-DSA (FIPS 204) support and tests
1 parent 57f35cf commit 5f1c638

80 files changed

Lines changed: 5317 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ and used by wolfSSL JNI/JSSE.
566566
| wolfssl.readWriteByteBufferPool.size | 16 | Integer | Sets the read/write per-thread ByteBuffer pool size |
567567
| wolfssl.readWriteByteBufferPool.bufferSize | 17408 | String | Sets the read/write per-thread ByteBuffer size |
568568
| wolfjsse.enabledCipherSuites | | String | Restricts enabled cipher suites |
569-
| wolfjsse.enabledSupportedCurves | | String | Restricts enabled ECC curves |
569+
| wolfjsse.enabledSupportedCurves | | String | Restricts enabled named groups (ECC curves and PQC/hybrid groups) |
570570
| wolfjsse.enabledSignatureAlgorithms | | String | Restricts enabled signature algorithms |
571571
| wolfjsse.keystore.type.required | | String | Restricts KeyStore type |
572572
| wolfjsse.clientSessionCache.disabled | | "true" | Disables client session cache |
@@ -617,16 +617,24 @@ changing this property. This should be a comma-delimited String. Example use:
617617
wolfjsse.enabledCipherSuites=TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
618618
```
619619

620-
**wolfjsse.enabledSupportedCurves (String)** - Allows setting of specific ECC
621-
curves to be enabled for SSL/TLS connections. This propogates down to the native
620+
**wolfjsse.enabledSupportedCurves (String)** - Allows setting of specific
621+
named groups (ECC curves and post-quantum / hybrid groups such as ML-KEM)
622+
to be enabled for SSL/TLS connections. This propogates down to the native
622623
wolfSSL API `wolfSSL_UseSupportedCurve()`. If invalid/bad values are found
623624
when processing this property, connection establishment will fail with an
624-
SSLException. This should be a comma-delimited String. Example use:
625+
SSLException. This should be a comma-delimited String. Both compact
626+
(`MLKEM768`, `X25519MLKEM768`) and IETF (`ML-KEM-768`, `SecP256r1MLKEM768`)
627+
spellings are recognized for PQC and hybrid groups when native wolfSSL was
628+
built with `--enable-mlkem`. Example use:
625629

626630
```
627631
wolfjsse.enabledSupportedCurves=secp256r1, secp521r1
628632
```
629633

634+
```
635+
wolfjsse.enabledSupportedCurves=X25519MLKEM768, SecP256r1MLKEM768, ML-KEM-768
636+
```
637+
630638
**wolfjsse.enabledSignatureAlgorithms (String)** - Allows restriction of the
631639
signature algorithms sent in the TLS ClientHello Signature Algorithms
632640
Extension. By using/setting this property, native wolfSSL will not populate
@@ -638,6 +646,14 @@ String of signature algorithm + MAC combinations. Example use:
638646
wolfjsse.enabledSignatureAlgorithms=RSA+SHA256:ECDSA+SHA256
639647
```
640648

649+
Standalone scheme tokens (no MAC component) are also accepted, including
650+
ED25519, ED448, and the FIPS 204 ML-DSA names (`ML-DSA-44`, `ML-DSA-65`,
651+
`ML-DSA-87`) when native wolfSSL was built with `--enable-mldsa`:
652+
653+
```
654+
wolfjsse.enabledSignatureAlgorithms=ML-DSA-87:ECDSA+SHA384
655+
```
656+
641657
**wolfjsse.keystore.type.required (String)** - Can be used to specify a KeyStore
642658
type that is required to be used. If this is set, wolfJSSE will not allow use
643659
of any KeyStore instances that are not of this type. One use of this option
@@ -724,6 +740,18 @@ advertised and used by the server if set.
724740
**jdk.tls.client.SignatureSchemes (String)** - Controls which signature algorithms are
725741
advertised and used by the client if set.
726742

743+
**jdk.tls.namedGroups (String)** - Comma-delimited list of TLS named groups
744+
to advertise. Honored alongside `SSLParameters.setNamedGroups()` API (added in
745+
JDK 20, JDK-8281236) and the `wolfjsse.enabledSupportedCurves` Security
746+
property. Recognizes both classical curves (`secp256r1`, `secp384r1`) and
747+
PQC / hybrid groups (`X25519MLKEM768`, `SecP256r1MLKEM768`, `ML-KEM-768`) when
748+
native wolfSSL was built with `--enable-mlkem`. `SSLParameters.setNamedGroups()`
749+
takes precedence over this property when both are set.
750+
751+
```
752+
java -Djdk.tls.namedGroups=X25519MLKEM768,secp256r1 ...
753+
```
754+
727755
**jdk.tls.useExtendedMasterSecret (boolean)** - Can be used to enable or
728756
disable the use of the Extended Master Secret (EMS) extension. This extension
729757
is enabled by default, unless explicitly disabled by setting this property to

examples/Client.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public void run(String[] args) {
7777
boolean useSecretCallback = false; /* enable TLS 1.3 secret cb */
7878
String keyLogFile = "sslkeylog.log"; /* output keylog file */
7979

80+
String pqcAlg = null; /* PQC named group, -pqc */
81+
8082
long session = 0; /* pointer to WOLFSSL_SESSION */
8183
boolean resumeSession = false; /* try one session resumption */
8284

@@ -219,11 +221,24 @@ public void run(String[] args) {
219221
} else if (arg.equals("-cid")) {
220222
useDtlsCid = 1;
221223

224+
} else if (arg.equals("-pqc")) {
225+
if (args.length < i+2) {
226+
printUsage();
227+
}
228+
pqcAlg = args[++i];
229+
222230
} else {
223231
printUsage();
224232
}
225233
}
226234

235+
/* PQC named groups are TLS 1.3 only */
236+
if (pqcAlg != null && sslVersion != 4) {
237+
System.out.println("-pqc requires -v 4 (TLS 1.3); " +
238+
"PQC named groups are TLS 1.3 only");
239+
System.exit(1);
240+
}
241+
227242
/* sort out DTLS versus TLS versions */
228243
if (doDTLS == 1) {
229244
if (sslVersion == 4) {
@@ -280,6 +295,29 @@ else if (sslVersion == 3) {
280295
/* create context */
281296
WolfSSLContext sslCtx = new WolfSSLContext(method);
282297

298+
/* Restrict TLS supported_groups extension to the single post
299+
* quantum group passed via -pqc */
300+
if (pqcAlg != null) {
301+
int pqcGid = WolfSSL.getNamedGroupFromString(pqcAlg);
302+
if (pqcGid == WolfSSL.WOLFSSL_NAMED_GROUP_INVALID) {
303+
System.out.println("Unknown -pqc group: " + pqcAlg);
304+
System.exit(1);
305+
}
306+
System.out.println("PQC enabled in native wolfSSL: " +
307+
WolfSSL.PQCEnabled());
308+
System.out.println("ML-KEM enabled: " + WolfSSL.MLKEMEnabled());
309+
System.out.println("ML-DSA enabled: " + WolfSSL.MLDSAEnabled());
310+
System.out.println("ML-KEM legacy IDs accepted: " +
311+
WolfSSL.MLKEMOldIdsEnabled());
312+
313+
ret = sslCtx.useSupportedCurves(new String[] { pqcAlg });
314+
if (ret != WolfSSL.SSL_SUCCESS) {
315+
System.out.println("Failed to set -pqc group " + pqcAlg +
316+
", ret = " + ret);
317+
System.exit(1);
318+
}
319+
}
320+
283321
/* set up PSK, if being used */
284322
if (usePsk == 1) {
285323

@@ -452,6 +490,21 @@ else if (sslVersion == 3) {
452490
/* create SSL object */
453491
ssl = new WolfSSLSession(sslCtx);
454492

493+
/* Pre-send TLS 1.3 key share for the -pqc group to avoid a
494+
* HelloRetryRequest if the server picks it. */
495+
if (pqcAlg != null) {
496+
int gid = WolfSSL.getNamedGroupFromString(pqcAlg);
497+
if (WolfSSL.isPQCNamedGroup(gid)) {
498+
ret = ssl.useKeyShare(gid);
499+
if (ret != WolfSSL.SSL_SUCCESS &&
500+
ret != WolfSSL.NOT_COMPILED_IN) {
501+
System.out.println("useKeyShare failed for " + pqcAlg +
502+
", ret = " + ret);
503+
System.exit(1);
504+
}
505+
}
506+
}
507+
455508
/* enable/load CRL functionality */
456509
if (WolfSSL.isEnabledCRL() == 1) {
457510
ret = ssl.enableCRL(WolfSSL.WOLFSSL_CRL_CHECKALL);
@@ -906,6 +959,10 @@ void printUsage() {
906959
System.out.println("-P\t\tPublic Key Callbacks");
907960
if (WolfSSL.secretCallbackEnabled())
908961
System.out.println("-tls13secretcb\tEnable TLS 1.3 secret callback");
962+
if (WolfSSL.PQCEnabled()) {
963+
System.out.println("-pqc <alg>\tKey Share with specified " +
964+
"post-quantum algorithm only, e.g. X25519MLKEM768");
965+
}
909966
System.exit(1);
910967
}
911968

examples/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,116 @@ This will write out generated CSRs to the following directory:
111111
examples/certs/generated/
112112
```
113113

114+
## Post Quantum (ML-KEM, ML-DSA) with ServerJSSE / ClientJSSE
115+
116+
wolfJSSE supports TLS 1.3 post-quantum key exchange (ML-KEM / FIPS 203) and
117+
post quantum certificate authentication (ML-DSA / FIPS 204) when native wolfSSL
118+
has been built with `--enable-mlkem` and `--enable-mldsa`. To additionally
119+
enable the pure (non-hybrid) `ML-KEM-512` / `ML-KEM-768` / `ML-KEM-1024` named
120+
groups, native wolfSSL also needs `--enable-tls-mlkem-standalone`; without it
121+
the PQ/T hybrid groups (e.g. `X25519MLKEM768`, `SECP384R1MLKEM1024`) still
122+
work but the standalone groups are rejected at the native layer.
123+
124+
**Native wolfSSL version requirement (ML-DSA cert auth):** the ML-DSA cert
125+
authentication path requires native wolfSSL containing PR
126+
[#10310](https://github.com/wolfSSL/wolfssl/pull/10310), which added ML-DSA
127+
SPKI / PKCS#8 DER support to `d2i_PUBKEY` / `d2i_PrivateKey`. That PR landed
128+
after the wolfSSL 5.9.1 release tag, so a post-5.9.1 stable release is required.
129+
The ML-KEM key-exchange path works fine on 5.9.1, only ML-DSA cert auth is
130+
gated. On older native wolfSSL the handshake will fail with an
131+
`SSLHandshakeException` (typically `error code: -125` on the verifier side and
132+
`-313` on the peer).
133+
134+
The `ServerJSSE.sh` and `ClientJSSE.sh` examples can use the `-pqc <alg>`
135+
option to specify a PQC named group for TLS 1.3 handshakes.
136+
137+
A PQC TLS 1.3 handshake can mix and match two independent pieces:
138+
139+
1. **Key exchange** -- pass `-pqc <named-group>` to use a post-quantum or
140+
PQ/T-hybrid named group instead of the classical default.
141+
2. **Certificate authentication** -- pass `-c` and/or `-A` to load ML-DSA
142+
entity keys and roots instead of the classical defaults
143+
(`server.jks` / `client.jks` / `ca-server.jks` / `ca-client.jks`).
144+
145+
The three subsections below show each common combination. All three
146+
require `-v 4` (TLS 1.3).
147+
148+
### PQ-Hybrid Key Exchange with Classical Certs
149+
150+
To use classical RSA/ECDSA certs and only switch the key exchange to a PQ/T
151+
hybrid group:
152+
153+
```
154+
$ cd <wolfssljni_root>
155+
$ ./examples/provider/ServerJSSE.sh -v 4 -pqc X25519MLKEM768
156+
$ ./examples/provider/ClientJSSE.sh -h 127.0.0.1 -v 4 -pqc X25519MLKEM768
157+
```
158+
159+
Other supported `-pqc` named groups (build flag dependent) include:
160+
161+
- **Pure ML-KEM (standalone, requires `--enable-tls-mlkem-standalone`):**
162+
`ML-KEM-512`, `ML-KEM-768`, `ML-KEM-1024`.
163+
- **PQ/T hybrids (default with `--enable-mlkem`):** `SECP256R1MLKEM768`,
164+
`SECP384R1MLKEM1024` (CNSA 2.0 level), plus the OQS-assigned hybrids
165+
`SECP256R1MLKEM512`, `SECP384R1MLKEM768`, `SECP521R1MLKEM1024`,
166+
`X25519MLKEM512`, `X448MLKEM768`.
167+
168+
See `./examples/provider/ServerJSSE.sh -?` for the full list at runtime.
169+
170+
### ML-DSA Server Cert Authentication
171+
172+
To replace the classical server cert with an ML-DSA cert, pass an ML-DSA
173+
keystore via `-c` (entity cert/key) on the server, and the matching truststore
174+
via `-A` (trusted CA) on the client. The client still uses the default
175+
classical `client.jks` for client auth:
176+
177+
```
178+
$ ./examples/provider/ServerJSSE.sh -v 4 \
179+
-pqc SECP384R1MLKEM1024 -l TLS_AES_256_GCM_SHA384 \
180+
-c "../provider/server-mldsa87.jks:wolfSSL test"
181+
$ ./examples/provider/ClientJSSE.sh -h 127.0.0.1 -v 4 \
182+
-pqc SECP384R1MLKEM1024 -l TLS_AES_256_GCM_SHA384 \
183+
-A "../provider/ca-mldsa87.jks:wolfSSL test"
184+
```
185+
186+
### Mutual ML-DSA Authentication
187+
188+
For mutual authentication with ML-DSA certs, ServerJSSE verifies the client
189+
by default, so just add `-c client-mldsa<N>.jks` on the client and the shared
190+
truststore (`-A ca-mldsa<N>.jks`) on both sides:
191+
192+
```
193+
$ ./examples/provider/ServerJSSE.sh -v 4 \
194+
-pqc SECP384R1MLKEM1024 -l TLS_AES_256_GCM_SHA384 \
195+
-c "../provider/server-mldsa87.jks:wolfSSL test" \
196+
-A "../provider/ca-mldsa87.jks:wolfSSL test"
197+
$ ./examples/provider/ClientJSSE.sh -h 127.0.0.1 -v 4 \
198+
-pqc SECP384R1MLKEM1024 -l TLS_AES_256_GCM_SHA384 \
199+
-c "../provider/client-mldsa87.jks:wolfSSL test" \
200+
-A "../provider/ca-mldsa87.jks:wolfSSL test"
201+
```
202+
203+
### Example Keystores
204+
205+
Example ML-DSA keystores can be found under `examples/provider/`:
206+
`server-mldsa{44,65,87}.jks`, `client-mldsa{44,65,87}.jks`, and
207+
`ca-mldsa{44,65,87}.jks` (password `wolfSSL test`). The server and client
208+
entity certs at each level are signed by the same root, so a single
209+
`ca-mldsa<N>.jks` truststore validates both sides.
210+
211+
JKS paths in the examples above are relative to the wrapper's working
212+
directory (`examples/build/`). Loading the ML-DSA private keys from a JKS
213+
requires JDK 24 or newer (JEP 497). To regenerate the keystores, run
214+
`./examples/provider/update-keystore-pqc.sh` (also requires JDK 24+).
215+
216+
### CNSA 2.0 Compliance
217+
218+
CNSA 2.0 (NSA Commercial National Security Algorithm Suite 2.0) mandates
219+
TLS 1.3 + ML-KEM-1024 (or a hybrid containing it) + AES-256-GCM + ML-DSA-87
220+
cert auth. The "Mutual ML-DSA Authentication" example above (level **87**,
221+
`SECP384R1MLKEM1024`, `TLS_AES_256_GCM_SHA384`) is the full CNSA 2.0
222+
recipe.
223+
114224
## Support
115225

116226
Please contact the wolfSSL support team at support@wolfssl.com with any

examples/Server.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public void run(String[] args) {
7676
int sendPskIdentityHint = 1; /* toggle sending PSK ident hint */
7777
int useDtlsCid = 0; /* use DTLS CID extension */
7878

79+
String pqcAlg = null; /* PQC named group, -pqc */
80+
7981
/* cert info */
8082
String serverCert = "../certs/server-cert.pem";
8183
String serverKey = "../certs/server-key.pem";
@@ -206,11 +208,24 @@ public void run(String[] args) {
206208
} else if (arg.equals("-cid")) {
207209
useDtlsCid = 1;
208210

211+
} else if (arg.equals("-pqc")) {
212+
if (args.length < i+2) {
213+
printUsage();
214+
}
215+
pqcAlg = args[++i];
216+
209217
} else {
210218
printUsage();
211219
}
212220
}
213221

222+
/* PQC named groups are TLS 1.3 only */
223+
if (pqcAlg != null && sslVersion != 4) {
224+
System.out.println("-pqc requires -v 4 (TLS 1.3); PQC named " +
225+
"groups are TLS 1.3 only");
226+
System.exit(1);
227+
}
228+
214229
/* sort out DTLS versus TLS versions */
215230
if (doDTLS == 1) {
216231
if (sslVersion == 4) {
@@ -267,6 +282,31 @@ else if (sslVersion == 3) {
267282
/* create context */
268283
WolfSSLContext sslCtx = new WolfSSLContext(method);
269284

285+
/* Single -pqc group, restrict the supported_groups extension to
286+
* the named group and pre-generate a TLS 1.3 key share for that
287+
* group per-session below so handshake completes without a
288+
* HelloRetryRequest when the client picks it. */
289+
if (pqcAlg != null) {
290+
int pqcGid = WolfSSL.getNamedGroupFromString(pqcAlg);
291+
if (pqcGid == WolfSSL.WOLFSSL_NAMED_GROUP_INVALID) {
292+
System.out.println("Unknown -pqc group: " + pqcAlg);
293+
System.exit(1);
294+
}
295+
System.out.println("PQC enabled in native wolfSSL: " +
296+
WolfSSL.PQCEnabled());
297+
System.out.println("ML-KEM enabled: " + WolfSSL.MLKEMEnabled());
298+
System.out.println("ML-DSA enabled: " + WolfSSL.MLDSAEnabled());
299+
System.out.println("ML-KEM legacy IDs accepted: " +
300+
WolfSSL.MLKEMOldIdsEnabled());
301+
302+
ret = sslCtx.useSupportedCurves(new String[] { pqcAlg });
303+
if (ret != WolfSSL.SSL_SUCCESS) {
304+
System.out.println("Failed to set -pqc group " + pqcAlg +
305+
", ret = " + ret);
306+
System.exit(1);
307+
}
308+
}
309+
270310
if (usePsk == 1) {
271311

272312
MyPskServerCallback pskServerCb = new MyPskServerCallback();
@@ -444,6 +484,22 @@ else if (sslVersion == 3) {
444484
/* create SSL object */
445485
WolfSSLSession ssl = new WolfSSLSession(sslCtx);
446486

487+
/* Pre-generate TLS 1.3 key share for the -pqc group so the
488+
* server can respond immediately if the client picks it,
489+
* completing handshake without a HRR round trip. */
490+
if (pqcAlg != null) {
491+
int gid = WolfSSL.getNamedGroupFromString(pqcAlg);
492+
if (WolfSSL.isPQCNamedGroup(gid)) {
493+
ret = ssl.useKeyShare(gid);
494+
if (ret != WolfSSL.SSL_SUCCESS &&
495+
ret != WolfSSL.NOT_COMPILED_IN) {
496+
System.out.println("useKeyShare failed for " +
497+
pqcAlg + ", ret = " + ret);
498+
System.exit(1);
499+
}
500+
}
501+
}
502+
447503
if (usePsk == 0 || cipherList != null || needDH == 1) {
448504
ret = ssl.setTmpDHFile(dhParam, WolfSSL.SSL_FILETYPE_PEM);
449505
if (ret != WolfSSL.SSL_SUCCESS) {
@@ -799,6 +855,10 @@ void printUsage() {
799855
System.out.println("-P\t\tPublic Key Callbacks");
800856
if (WolfSSL.isEnabledCRLMonitor() == 1)
801857
System.out.println("-m\t\tEnable CRL directory monitor");
858+
if (WolfSSL.PQCEnabled()) {
859+
System.out.println("-pqc <alg>\tKey Share with specified " +
860+
"post-quantum algorithm only, e.g. X25519MLKEM768");
861+
}
802862
System.exit(1);
803863
}
804864

0 commit comments

Comments
 (0)