Skip to content

Commit 43c2f32

Browse files
committed
Preserve verified TLS modes with legacy Connector/J
1 parent e7cb932 commit 43c2f32

3 files changed

Lines changed: 122 additions & 9 deletions

File tree

‎SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/ConnectionManager.java‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,27 @@ private String requiredMysqlFallbackTls(String extra) {
340340
verifyCertificate = true;
341341
}
342342
}
343-
return "&sslMode=" + (mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED") + "&requireSSL=true";
343+
String requiredMode = mode != null ? mode : verifyCertificate ? "VERIFY_CA" : "REQUIRED";
344+
if (verifyCertificate && requiredMode.equals("REQUIRED")) requiredMode = "VERIFY_CA";
345+
if (requiredMode.equals("VERIFY_IDENTITY") && !mysqlDriverSupportsSslMode()) {
346+
throw new IllegalArgumentException("MariaDB VERIFY_IDENTITY requires Connector/J hostname verification support");
347+
}
348+
return "&sslMode=" + requiredMode + "&requireSSL=true"
349+
+ (requiredMode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : "");
350+
}
351+
352+
/** Query the driver's supported JDBC properties without opening a connection. */
353+
private boolean mysqlDriverSupportsSslMode() {
354+
try {
355+
java.sql.Driver driver = (java.sql.Driver) Class.forName("com.mysql.cj.jdbc.Driver")
356+
.getDeclaredConstructor().newInstance();
357+
for (java.sql.DriverPropertyInfo property : driver.getPropertyInfo("jdbc:mysql://localhost/", null)) {
358+
if ("sslMode".equals(property.name)) return true;
359+
}
360+
return false;
361+
} catch (ReflectiveOperationException | SQLException | LinkageError failure) {
362+
throw new IllegalArgumentException("Unable to determine Connector/J TLS verification support", failure);
363+
}
344364
}
345365

346366
private void rejectConflictingPostgreSqlTlsOptions(String extra) {

‎SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java‎

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
import java.util.concurrent.Executors;
1212
import java.util.concurrent.TimeUnit;
1313
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.io.TempDir;
15+
import java.nio.file.Path;
16+
import javax.net.ssl.SSLSocket;
17+
import javax.net.ssl.SSLException;
18+
import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity;
1419

1520
class MariaDbFallbackTlsTest {
21+
@TempDir Path directory;
1622
@Test void sslEnabledFallbackRequiresTls() {
1723
assertTrue(fallback().buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED&requireSSL=true"));
1824
}
@@ -21,7 +27,12 @@ class MariaDbFallbackTlsTest {
2127
for (String mode : new String[]{"REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY"}) {
2228
ConnectionManager manager = fallback();
2329
manager.setStr("&sslMode=" + mode);
24-
assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true"));
30+
if (mode.equals("VERIFY_IDENTITY") && legacyDriver()) {
31+
assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"));
32+
} else {
33+
String certificateFlag = mode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : "";
34+
assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true" + certificateFlag));
35+
}
2536
}
2637
}
2738

@@ -40,7 +51,14 @@ class MariaDbFallbackTlsTest {
4051
@Test void legacyCertificateVerificationRemainsEnabled() {
4152
ConnectionManager manager = fallback();
4253
manager.setStr("&verifyServerCertificate=true&customOption=retained");
43-
assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true"));
54+
assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true"));
55+
}
56+
57+
@Test void explicitEncryptionRetainsLegacyCertificateVerification() {
58+
ConnectionManager manager = fallback();
59+
manager.setStr("&sslMode=REQUIRED&verifyServerCertificate=true");
60+
assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith(
61+
"&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true"));
4462
}
4563

4664
@Test void ordinaryMysqlNativeMariaAndSslDisabledRemainUnchanged() {
@@ -84,6 +102,76 @@ class MariaDbFallbackTlsTest {
84102
}
85103
}
86104

105+
@Test void identityVerificationFailsClosedOnLegacyDriver() {
106+
ConnectionManager manager = fallback();
107+
manager.setStr("&sslMode=VERIFY_IDENTITY");
108+
if (legacyDriver()) {
109+
assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"));
110+
} else {
111+
assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").contains("sslMode=VERIFY_IDENTITY"));
112+
}
113+
}
114+
115+
@Test void verifiedFallbackRejectsUntrustedTlsPeerBeforeAuthentication() throws Exception {
116+
var context = HttpTlsIdentity.loadOrCreate(directory.resolve("untrusted-server"), "localhost").serverContext();
117+
try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress())) {
118+
server.setSoTimeout(5000);
119+
var worker = Executors.newSingleThreadExecutor();
120+
try {
121+
var peer = worker.submit(() -> {
122+
try (var socket = server.accept()) {
123+
socket.setSoTimeout(5000);
124+
socket.getOutputStream().write(greeting(true));
125+
socket.getOutputStream().flush();
126+
byte[] header = socket.getInputStream().readNBytes(4);
127+
assertEquals(4, header.length);
128+
int length = (header[0] & 255) | ((header[1] & 255) << 8) | ((header[2] & 255) << 16);
129+
assertEquals(32, length, "Client must send a bounded SSL request before TLS");
130+
byte[] request = socket.getInputStream().readNBytes(length);
131+
assertEquals(32, request.length);
132+
assertTrue((request[1] & 8) != 0, "CLIENT_SSL must be set");
133+
try (SSLSocket tls = (SSLSocket) context.getSocketFactory().createSocket(
134+
socket, "localhost", socket.getPort(), true)) {
135+
tls.setUseClientMode(false);
136+
try {
137+
tls.startHandshake();
138+
return tls.getInputStream().read();
139+
} catch (SSLException rejected) {
140+
return -1;
141+
}
142+
}
143+
}
144+
});
145+
ConnectionManager manager = fallback();
146+
manager.setHost(server.getInetAddress().getHostAddress());
147+
manager.setPort(Integer.toString(server.getLocalPort()));
148+
manager.setStr("&sslMode=VERIFY_CA&verifyServerCertificate=false&enabledTLSProtocols=TLSv1.2&connectTimeout=2000&socketTimeout=2000");
149+
Properties properties = new Properties();
150+
properties.setProperty("user", "test-user");
151+
properties.setProperty("password", "test-only-not-a-secret");
152+
SQLException failure = assertThrows(SQLException.class, () ->
153+
new com.mysql.cj.jdbc.Driver().connect(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"), properties));
154+
assertTrue(certificateFailure(failure), "Expected certificate validation failure, got " + failure);
155+
assertEquals(-1, peer.get(5, TimeUnit.SECONDS), "No authentication packet may reach an untrusted TLS peer");
156+
} finally {
157+
worker.shutdownNow();
158+
}
159+
}
160+
}
161+
162+
private static boolean certificateFailure(Throwable failure) {
163+
for (Throwable current = failure; current != null; current = current.getCause()) {
164+
if (current instanceof java.security.cert.CertificateException
165+
|| current instanceof java.security.cert.CertPathBuilderException
166+
|| current instanceof java.security.cert.CertPathValidatorException) return true;
167+
}
168+
return false;
169+
}
170+
171+
private static boolean legacyDriver() {
172+
return "8.0.12".equals(com.mysql.cj.jdbc.Driver.class.getPackage().getImplementationVersion());
173+
}
174+
87175
private static ConnectionManager fallback() {
88176
ConnectionManager manager = new ConnectionManager("localhost", "3306", "test", "", "votes");
89177
manager.setDbType(DbType.MARIADB);
@@ -93,14 +181,16 @@ private static ConnectionManager fallback() {
93181

94182
// Protocol-10 greeting with protocol-41, secure connection and plugin authentication,
95183
// deliberately omitting CLIENT_SSL. No actual database or external network is needed.
96-
private static byte[] nonTlsGreeting() {
184+
private static byte[] nonTlsGreeting() { return greeting(false); }
185+
186+
private static byte[] greeting(boolean ssl) {
97187
ByteArrayOutputStream payload = new ByteArrayOutputStream();
98188
payload.write(10);
99189
payload.writeBytes("8.0.0-test\0".getBytes(StandardCharsets.US_ASCII));
100190
payload.writeBytes(new byte[]{1, 0, 0, 0});
101191
payload.writeBytes("abcdefgh".getBytes(StandardCharsets.US_ASCII));
102192
payload.write(0);
103-
int capabilities = 1 | 512 | 8192 | 32768 | 0x80000;
193+
int capabilities = 1 | 512 | 8192 | 32768 | 0x80000 | (ssl ? 2048 : 0);
104194
payload.write(capabilities & 255);
105195
payload.write((capabilities >>> 8) & 255);
106196
payload.write(45);

‎docs/shared-libraries.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,12 @@ Native MariaDB and custom MariaDB driver overrides retain the MariaDB scheme;
230230
the explicit fallback opt-out remains supported.
231231

232232
For MariaDB configured with `UseSSL=true`, Connector/J fallback requires TLS
233-
(`sslMode=REQUIRED` plus `requireSSL=true` for pre-8.0.13 drivers) instead of permitting a plaintext downgrade. Stronger
234-
`VERIFY_CA`/`VERIFY_IDENTITY` modes in `Line` remain supported; weaker or ambiguous
235-
`sslMode` overrides and options disabling `useSSL`/`requireSSL` are rejected. Legacy certificate verification requests remain
236-
verified. This requires encryption but does not claim server identity verification
233+
(`sslMode=REQUIRED` plus `requireSSL=true` for pre-8.0.13 drivers) instead of permitting a plaintext downgrade.
234+
`VERIFY_CA` also enables the legacy certificate-verification property.
235+
`VERIFY_IDENTITY` is accepted only when the actual driver advertises modern
236+
`sslMode` support, and otherwise fails before connecting. Weak or ambiguous
237+
`sslMode` overrides and options disabling `useSSL`/`requireSSL` are rejected.
238+
Legacy certificate verification requests remain verified even with an explicit
239+
`REQUIRED` mode. This requires encryption but does not claim server identity verification
237240
without the appropriate verified mode. Native MariaDB, ordinary MySQL settings,
238241
and explicitly disabled TLS remain unchanged.

0 commit comments

Comments
 (0)