1111import java .util .concurrent .Executors ;
1212import java .util .concurrent .TimeUnit ;
1313import 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
1520class 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 );
0 commit comments