Merge pull request #376 from cconlon/rsaServerCertChoice

JNI/JSSE: prefer RSA server cert when configured named groups have no ECC curves
pull/377/head
Ruby Martin 2026-07-07 13:05:15 -06:00 committed by GitHub
commit a43cdd9d6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 536 additions and 0 deletions

View File

@ -2189,6 +2189,32 @@ public class WolfSSL {
return PQC_GROUP_IDS.contains(Integer.valueOf(namedGroup));
}
/**
* Returns true if the given native named-group enum value identifies a
* classic elliptic curve group (SECT/SECP/Brainpool/X25519/X448/SM2),
* as opposed to an FFDHE or post-quantum group.
*
* The classification follows the IANA TLS Supported Groups registry
* partition (RFC 7919). Every value in the range 1-255 is treated as
* an elliptic curve group, including IANA-assigned curve IDs that do
* not yet have WOLFSSL_ECC_* constants on this class (ex:
* brainpoolP256r1tls13 through brainpoolP512r1tls13 at 31-33, GOST
* curves at 34-40). Values 256-511 are the FFDHE block and PQC
* standalone/hybrid groups sit at 512 and above, all returning false.
*
* @param namedGroup native named-group enum value, typically the
* result of {@link #getNamedGroupFromString(String)}
* or one of the WOLFSSL_ECC_* constants on this class.
* @return true if the group is in the elliptic curve range of the
* supported groups registry, false otherwise (including for
* {@link #WOLFSSL_NAMED_GROUP_INVALID}, FFDHE groups, and
* PQC standalone/hybrid groups).
*/
public static boolean isECCNamedGroup(int namedGroup) {
return (namedGroup > WOLFSSL_NAMED_GROUP_INVALID &&
namedGroup < WOLFSSL_FFDHE_2048);
}
@SuppressWarnings({"deprecation", "removal"})
@Override
protected void finalize() throws Throwable

View File

@ -291,6 +291,20 @@ public class WolfSSLEngineHelper {
keyAlgos.add("ML-DSA-87");
}
/* On server side, only prefer an ECC alias when an ECDSA certificate
* can actually complete a key exchange for this session. When the
* TLS named groups have been explicitly restricted to a list with
* no ECC curves (ex: FFDHE groups only via jdk.tls.namedGroups),
* ECDHE/ECDH cipher suites cannot be negotiated. */
if (!clientMode && keyAlgos.contains("EC") &&
!eccCertUsableWithNamedGroups()) {
WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO,
() -> "no ECC named groups configured for session, " +
"preferring non-ECC server cert/key types");
keyAlgos.remove("EC");
keyAlgos.add("EC");
}
String[] keyTypes = new String[keyAlgos.size()];
keyTypes = keyAlgos.toArray(keyTypes);
@ -338,6 +352,58 @@ public class WolfSSLEngineHelper {
return alias;
}
/**
* Return true if an ECDSA certificate is usable for key exchange on
* this session, given the configured TLS named groups and the
* SSLContext protocol version.
*
* Reads the named-groups configuration with the same source precedence
* as setLocalSupportedGroups(): SSLParameters.setNamedGroups(), then
* jdk.tls.namedGroups System property, then the
* wolfjsse.enabledSupportedCurves Security property.
*
* Returns true when no source is configured (native default groups
* include ECC curves), when the configured list contains at least one
* classic ECC curve, or when the SSLContext is (D)TLS 1.3-only, since
* TLS 1.3 certificate authentication is independent of the negotiated
* key exchange group.
*/
private boolean eccCertUsableWithNamedGroups() {
String[] groups =
WolfSSLParametersHelper.getNamedGroupsFromParams(this.params);
if (groups == null) {
groups = WolfSSLUtil.getJdkTlsNamedGroups();
}
if (groups == null) {
groups = WolfSSLUtil.getSupportedCurves();
}
if (groups == null) {
return true;
}
for (String name : groups) {
if (WolfSSL.isECCNamedGroup(
WolfSSL.getNamedGroupFromString(name))) {
return true;
}
}
/* No ECC curves configured. ECDHE/ECDH cannot complete on
* (D)TLS <= 1.2, but TLS 1.3 does not tie cert type to the group.
* Use the context protocol version, not the enabled protocols
* list: cert selection can run before the app finishes configuring
* the session (ex: SSLEngine.getSession() loads the cert), while
* a (D)TLS 1.3-only context can never negotiate lower. */
WolfSSL.TLS_VERSION ctxVersion =
this.authStore.getProtocolVersion();
return (ctxVersion == WolfSSL.TLS_VERSION.TLSv1_3 ||
ctxVersion == WolfSSL.TLS_VERSION.DTLSv1_3);
}
/**
* Loads the private key and certificate chain for this
* SSLSocket/SSLEngine to be used for performing authentication of

View File

@ -22,13 +22,17 @@
package com.wolfssl.provider.jsse.test;
import com.wolfssl.WolfSSL;
import com.wolfssl.WolfSSLContext;
import com.wolfssl.WolfSSLException;
import com.wolfssl.WolfSSLSession;
import com.wolfssl.provider.jsse.WolfSSLParametersHelper;
import com.wolfssl.provider.jsse.WolfSSLProvider;
import com.wolfssl.test.TimedTestWatcher;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.cert.Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
@ -41,6 +45,7 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for TLS named-groups (supported_groups) configuration in wolfJSSE.
@ -348,6 +353,400 @@ public class WolfSSLNamedGroupsTest {
}
}
/**
* A server whose KeyStore holds both RSA and ECC private keys must
* complete a TLS 1.2 handshake when jdk.tls.namedGroups is restricted
* to FFDHE groups only.
*
* Server certificate selection prefers an ECC alias over RSA when the
* KeyStore holds both. With no ECC curves in supported_groups, ECDHE
* suites cannot be negotiated on TLS 1.2, so an ECDSA certificate
* cannot complete any key exchange and the handshake previously failed
* with no shared cipher suite. The RSA alias must be preferred instead
* so a DHE_RSA suite can complete over the RFC 7919 group.
*/
@Test
public void testJdkTlsNamedGroupsFfdheOnlyTls12PrefersRsaCert()
throws Exception {
Assume.assumeTrue("TLS 1.2, RSA and ECC required in native wolfSSL",
WolfSSL.TLSv12Enabled() && WolfSSL.RsaEnabled() &&
WolfSSL.EccEnabled());
Assume.assumeTrue("DHE_RSA cipher suites not available",
dheRsaSuitesAvailable());
Assume.assumeTrue("ffdhe2048 named group not supported",
ffdhe2048GroupSupported());
synchronized (WolfSSLPQCTestUtil.GROUP_PROP_LOCK) {
Assume.assumeTrue("TLS 1.2 FFDHE handshakes not supported by " +
"native wolfSSL build", tls12FfdheHandshakeSupported());
String prevWolf =
WolfSSLPQCTestUtil.setCurvesProperty(null);
String prevJdk =
WolfSSLPQCTestUtil.setJdkNamedGroupsProperty("ffdhe2048");
try {
SSLContext ctx = tf.createSSLContext("TLSv1.2", PROVIDER);
SSLEngine server = ctx.createSSLEngine();
server.setUseClientMode(false);
server.setNeedClientAuth(false);
SSLEngine client =
ctx.createSSLEngine("wolfSSL named groups test", 11111);
client.setUseClientMode(true);
int ret = tf.testConnection(server, client,
suitesMatching("TLS_DHE_RSA_"),
new String[] { "TLSv1.2" }, APP_DATA);
assertEquals("TLS 1.2 handshake with FFDHE-only " +
"jdk.tls.namedGroups and a KeyStore holding both RSA " +
"and ECC keys must succeed", 0, ret);
String suite = client.getSession().getCipherSuite();
assertTrue("negotiated cipher suite must be DHE_RSA, " +
"got: " + suite, suite.startsWith("TLS_DHE_RSA_") ||
suite.startsWith("DHE-RSA-"));
}
finally {
WolfSSLPQCTestUtil.restoreCurvesProperty(prevWolf);
WolfSSLPQCTestUtil.restoreJdkNamedGroupsProperty(prevJdk);
}
}
}
/**
* True when native wolfSSL was compiled with DH support, using the
* presence of DHE_RSA cipher suites as a proxy. FFDHE named groups
* need DH for both TLS 1.2 DHE_RSA suites and TLS 1.3 FFDHE key
* shares.
*/
private static boolean dheRsaSuitesAvailable() {
for (String suite : WolfSSL.getCiphersIana()) {
if (suite.startsWith("TLS_DHE_RSA_")) {
return true;
}
}
return false;
}
/**
* Return compiled-in cipher suites whose IANA names start with any of
* the given prefixes.
*/
private static String[] suitesMatching(String... prefixes) {
ArrayList<String> matched = new ArrayList<>();
for (String suite : WolfSSL.getCiphersIana()) {
for (String prefix : prefixes) {
if (suite.startsWith(prefix)) {
matched.add(suite);
break;
}
}
}
return matched.toArray(new String[matched.size()]);
}
/* Cached result of tls12FfdheHandshakeSupported() */
private static Boolean tls12FfdheCapable = null;
/**
* True when this environment can complete a TLS 1.2 handshake with
* FFDHE-only named groups at all, probed with an RSA-only server
* KeyStore so that certificate selection plays no part.
*
* Caller must hold WolfSSLPQCTestUtil.GROUP_PROP_LOCK.
*/
private static boolean tls12FfdheHandshakeSupported() throws Exception {
if (tls12FfdheCapable != null) {
return tls12FfdheCapable.booleanValue();
}
String prevJdk =
WolfSSLPQCTestUtil.setJdkNamedGroupsProperty("ffdhe2048");
try {
SSLContext srvCtx = tf.createSSLContext("TLSv1.2", PROVIDER,
tf.createTrustManager("SunX509", tf.caClientJKS, PROVIDER),
tf.createKeyManager("SunX509", tf.serverRSAJKS, PROVIDER));
SSLContext cliCtx = tf.createSSLContext("TLSv1.2", PROVIDER,
tf.createTrustManager("SunX509", tf.caServerJKS, PROVIDER),
tf.createKeyManager("SunX509", tf.clientRSAJKS, PROVIDER));
SSLEngine server = srvCtx.createSSLEngine();
server.setUseClientMode(false);
server.setNeedClientAuth(false);
SSLEngine client =
cliCtx.createSSLEngine("wolfSSL named groups test", 11111);
client.setUseClientMode(true);
int ret = tf.testConnection(server, client,
suitesMatching("TLS_DHE_RSA_"),
new String[] { "TLSv1.2" }, APP_DATA);
tls12FfdheCapable = Boolean.valueOf(ret == 0);
}
finally {
WolfSSLPQCTestUtil.restoreJdkNamedGroupsProperty(prevJdk);
}
return tls12FfdheCapable.booleanValue();
}
/**
* True when native wolfSSL supports the ffdhe2048 TLS named group
* (HAVE_FFDHE_2048), probed the same way wolfJSSE applies configured
* groups to a session.
*/
private static boolean ffdhe2048GroupSupported() {
WolfSSLContext ctx = null;
WolfSSLSession ssl = null;
try {
ctx = new WolfSSLContext(WolfSSL.SSLv23_ClientMethod());
ssl = new WolfSSLSession(ctx);
return (ssl.useSupportedCurves(
new int[] { WolfSSL.WOLFSSL_FFDHE_2048 }) ==
WolfSSL.SSL_SUCCESS);
} catch (Exception e) {
return false;
} finally {
/* Best-effort cleanup, must not throw out of this probe */
if (ssl != null) {
try {
ssl.freeSSL();
} catch (Exception e) {
/* ignore */
}
}
if (ctx != null) {
try {
ctx.free();
} catch (Exception e) {
/* ignore */
}
}
}
}
/**
* Same server RSA-alias preference as
* testJdkTlsNamedGroupsFfdheOnlyTls12PrefersRsaCert, driven by the
* lowest-precedence configuration source, the
* wolfjsse.enabledSupportedCurves Security property.
*/
@Test
public void testWolfjsseCurvesPropertyFfdheOnlyTls12PrefersRsaCert()
throws Exception {
Assume.assumeTrue("TLS 1.2, RSA and ECC required in native wolfSSL",
WolfSSL.TLSv12Enabled() && WolfSSL.RsaEnabled() &&
WolfSSL.EccEnabled());
Assume.assumeTrue("DHE_RSA cipher suites not available",
dheRsaSuitesAvailable());
Assume.assumeTrue("ffdhe2048 named group not supported",
ffdhe2048GroupSupported());
synchronized (WolfSSLPQCTestUtil.GROUP_PROP_LOCK) {
Assume.assumeTrue("TLS 1.2 FFDHE handshakes not supported by " +
"native wolfSSL build", tls12FfdheHandshakeSupported());
String prevJdk =
WolfSSLPQCTestUtil.setJdkNamedGroupsProperty(null);
String prevWolf =
WolfSSLPQCTestUtil.setCurvesProperty("ffdhe2048");
try {
SSLContext ctx = tf.createSSLContext("TLSv1.2", PROVIDER);
SSLEngine server = ctx.createSSLEngine();
server.setUseClientMode(false);
server.setNeedClientAuth(false);
SSLEngine client =
ctx.createSSLEngine("wolfSSL named groups test", 11111);
client.setUseClientMode(true);
int ret = tf.testConnection(server, client,
suitesMatching("TLS_DHE_RSA_"),
new String[] { "TLSv1.2" }, APP_DATA);
assertEquals("TLS 1.2 handshake with FFDHE-only " +
"wolfjsse.enabledSupportedCurves and a KeyStore " +
"holding both RSA and ECC keys must succeed", 0, ret);
String suite = client.getSession().getCipherSuite();
assertTrue("negotiated cipher suite must be DHE_RSA, " +
"got: " + suite, suite.startsWith("TLS_DHE_RSA_") ||
suite.startsWith("DHE-RSA-"));
}
finally {
WolfSSLPQCTestUtil.restoreCurvesProperty(prevWolf);
WolfSSLPQCTestUtil.restoreJdkNamedGroupsProperty(prevJdk);
}
}
}
/**
* Same server RSA-alias preference as
* testJdkTlsNamedGroupsFfdheOnlyTls12PrefersRsaCert, driven by the
* highest-precedence configuration source, per-engine
* SSLParameters.setNamedGroups() (JDK 20+).
*/
@Test
public void testSetNamedGroupsFfdheOnlyTls12PrefersRsaCert()
throws Exception {
Assume.assumeTrue("Host JDK lacks SSLParameters.setNamedGroups",
jdkHasNamedGroups);
Assume.assumeTrue("TLS 1.2, RSA and ECC required in native wolfSSL",
WolfSSL.TLSv12Enabled() && WolfSSL.RsaEnabled() &&
WolfSSL.EccEnabled());
Assume.assumeTrue("DHE_RSA cipher suites not available",
dheRsaSuitesAvailable());
Assume.assumeTrue("ffdhe2048 named group not supported",
ffdhe2048GroupSupported());
synchronized (WolfSSLPQCTestUtil.GROUP_PROP_LOCK) {
Assume.assumeTrue("TLS 1.2 FFDHE handshakes not supported by " +
"native wolfSSL build", tls12FfdheHandshakeSupported());
String prevWolf =
WolfSSLPQCTestUtil.setCurvesProperty(null);
String prevJdk =
WolfSSLPQCTestUtil.setJdkNamedGroupsProperty(null);
try {
SSLContext ctx = tf.createSSLContext("TLSv1.2", PROVIDER);
SSLEngine server = ctx.createSSLEngine();
server.setUseClientMode(false);
server.setNeedClientAuth(false);
SSLEngine client =
ctx.createSSLEngine("wolfSSL named groups test", 11111);
client.setUseClientMode(true);
setEngineNamedGroups(server, new String[] { "ffdhe2048" });
setEngineNamedGroups(client, new String[] { "ffdhe2048" });
int ret = tf.testConnection(server, client,
suitesMatching("TLS_DHE_RSA_"),
new String[] { "TLSv1.2" }, APP_DATA);
assertEquals("TLS 1.2 handshake with FFDHE-only " +
"SSLParameters.setNamedGroups() and a KeyStore " +
"holding both RSA and ECC keys must succeed", 0, ret);
String suite = client.getSession().getCipherSuite();
assertTrue("negotiated cipher suite must be DHE_RSA, " +
"got: " + suite, suite.startsWith("TLS_DHE_RSA_") ||
suite.startsWith("DHE-RSA-"));
}
finally {
WolfSSLPQCTestUtil.restoreCurvesProperty(prevWolf);
WolfSSLPQCTestUtil.restoreJdkNamedGroupsProperty(prevJdk);
}
}
}
/**
* On a TLS 1.3-only session, FFDHE-only named groups must not change
* server certificate selection: TLS 1.3 negotiates the key exchange
* group independently of the certificate type, so the default ECC
* alias preference stays and the handshake completes with the ECDSA
* certificate over an ffdhe2048 key share.
*/
@Test
public void testJdkTlsNamedGroupsFfdheOnlyTls13KeepsEccCert()
throws Exception {
Assume.assumeTrue("TLS 1.3, RSA and ECC required in native wolfSSL",
WolfSSL.TLSv13Enabled() && WolfSSL.RsaEnabled() &&
WolfSSL.EccEnabled());
Assume.assumeTrue("ffdhe2048 named group not supported",
ffdhe2048GroupSupported());
synchronized (WolfSSLPQCTestUtil.GROUP_PROP_LOCK) {
String prevWolf =
WolfSSLPQCTestUtil.setCurvesProperty(null);
String prevJdk =
WolfSSLPQCTestUtil.setJdkNamedGroupsProperty("ffdhe2048");
try {
SSLContext ctx = tf.createSSLContext("TLSv1.3", PROVIDER);
SSLEngine server = ctx.createSSLEngine();
server.setUseClientMode(false);
server.setNeedClientAuth(false);
SSLEngine client =
ctx.createSSLEngine("wolfSSL named groups test", 11111);
client.setUseClientMode(true);
int ret = tf.testConnection(server, client, null,
new String[] { "TLSv1.3" }, APP_DATA);
assertEquals("TLS 1.3 handshake with FFDHE-only " +
"jdk.tls.namedGroups must succeed", 0, ret);
Certificate[] peerCerts =
client.getSession().getPeerCertificates();
String keyAlgo = peerCerts[0].getPublicKey().getAlgorithm();
assertEquals("TLS 1.3 server must keep default ECC cert " +
"preference with FFDHE-only named groups", "EC",
keyAlgo);
}
finally {
WolfSSLPQCTestUtil.restoreCurvesProperty(prevWolf);
WolfSSLPQCTestUtil.restoreJdkNamedGroupsProperty(prevJdk);
}
}
}
/**
* When the configured named groups contain at least one ECC curve
* alongside FFDHE groups, server certificate selection must keep the
* default ECC preference and negotiate an ECDHE_ECDSA suite on
* TLS 1.2.
*/
@Test
public void testJdkTlsNamedGroupsMixedEccFfdheTls12KeepsEccCert()
throws Exception {
Assume.assumeTrue("TLS 1.2, RSA and ECC required in native wolfSSL",
WolfSSL.TLSv12Enabled() && WolfSSL.RsaEnabled() &&
WolfSSL.EccEnabled());
synchronized (WolfSSLPQCTestUtil.GROUP_PROP_LOCK) {
String prevWolf =
WolfSSLPQCTestUtil.setCurvesProperty(null);
String prevJdk = WolfSSLPQCTestUtil.setJdkNamedGroupsProperty(
"secp256r1,ffdhe2048");
try {
SSLContext ctx = tf.createSSLContext("TLSv1.2", PROVIDER);
SSLEngine server = ctx.createSSLEngine();
server.setUseClientMode(false);
server.setNeedClientAuth(false);
SSLEngine client =
ctx.createSSLEngine("wolfSSL named groups test", 11111);
client.setUseClientMode(true);
int ret = tf.testConnection(server, client,
suitesMatching("TLS_ECDHE_ECDSA_", "TLS_DHE_RSA_"),
new String[] { "TLSv1.2" }, APP_DATA);
assertEquals("TLS 1.2 handshake with mixed ECC+FFDHE " +
"jdk.tls.namedGroups must succeed", 0, ret);
String suite = client.getSession().getCipherSuite();
assertTrue("negotiated cipher suite must be ECDHE_ECDSA " +
"when an ECC group is configured, got: " + suite,
suite.startsWith("TLS_ECDHE_ECDSA_") ||
suite.startsWith("ECDHE-ECDSA-"));
}
finally {
WolfSSLPQCTestUtil.restoreCurvesProperty(prevWolf);
WolfSSLPQCTestUtil.restoreJdkNamedGroupsProperty(prevJdk);
}
}
}
/**
* When both static properties are set, jdk.tls.namedGroups overrides
* wolfjsse.enabledSupportedCurves. A single source is applied per

View File

@ -517,6 +517,51 @@ public class WolfSSLTest {
assertFalse(WolfSSL.isPQCNamedGroup(-1));
}
@Test
public void test_isECCNamedGroup() {
/* Classic ECC curves return true, spanning the low and high ends
* of the curve ID range (SECT, SECP, Brainpool, X25519/X448, SM2). */
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_SECT163K1));
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_SECP256R1));
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_SECP384R1));
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_SECP521R1));
assertTrue(WolfSSL.isECCNamedGroup(
WolfSSL.WOLFSSL_ECC_BRAINPOOLP512R1));
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_X25519));
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_X448));
assertTrue(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ECC_SM2P256V1));
/* The whole registry range below the FFDHE block classifies as
* ECC (RFC 7919 partition), including IANA-assigned curve IDs
* without WOLFSSL_ECC_* constants yet: 31 is
* brainpoolP256r1tls13 (RFC 8734), 34 is GC256A (RFC 9189), and
* 255 is the top of the elliptic curve range. */
assertTrue(WolfSSL.isECCNamedGroup(31));
assertTrue(WolfSSL.isECCNamedGroup(34));
assertTrue(WolfSSL.isECCNamedGroup(255));
/* FFDHE groups and the reserved FFDHE block (256-511) return
* false. */
assertFalse(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_FFDHE_2048));
assertFalse(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_FFDHE_8192));
assertFalse(WolfSSL.isECCNamedGroup(511));
/* PQC standalone and hybrid groups return false, including
* hybrids with an ECDHE component. */
assertFalse(WolfSSL.isECCNamedGroup(WolfSSL.WOLFSSL_ML_KEM_768));
assertFalse(WolfSSL.isECCNamedGroup(
WolfSSL.WOLFSSL_X25519MLKEM768));
assertFalse(WolfSSL.isECCNamedGroup(
WolfSSL.WOLFSSL_SECP256R1MLKEM768));
/* Sentinel and out-of-range integers return false. */
assertFalse(WolfSSL.isECCNamedGroup(
WolfSSL.WOLFSSL_NAMED_GROUP_INVALID));
assertFalse(WolfSSL.isECCNamedGroup(0));
assertFalse(WolfSSL.isECCNamedGroup(-1));
}
@Test
public void test_PQC_FeatureDetect_NativeReturns() {