From 62b90e0328d6adf4bcd5aeb16e154feca47a1274 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Tue, 20 Oct 2026 16:15:05 -0600 Subject: [PATCH] JNI: return original wolfSSL error from getError() instead of SSL_ERROR_SYSCALL --- native/com_wolfssl_WolfSSL.h | 2 + native/com_wolfssl_WolfSSLSession.c | 16 +- src/java/com/wolfssl/WolfSSL.java | 2 + src/java/com/wolfssl/WolfSSLSession.java | 5 + .../provider/jsse/test/WolfSSLEngineTest.java | 70 +++++++++ .../provider/jsse/test/WolfSSLSocketTest.java | 142 ++++++++++++++++-- .../com/wolfssl/test/WolfSSLSessionTest.java | 109 +++++++++++++- 7 files changed, 326 insertions(+), 20 deletions(-) diff --git a/native/com_wolfssl_WolfSSL.h b/native/com_wolfssl_WolfSSL.h index 27a33d67..72a05e4e 100644 --- a/native/com_wolfssl_WolfSSL.h +++ b/native/com_wolfssl_WolfSSL.h @@ -101,6 +101,8 @@ extern "C" { #define com_wolfssl_WolfSSL_UNKNOWN_ALPN_PROTOCOL_NAME_E -405L #undef com_wolfssl_WolfSSL_APP_DATA_READY #define com_wolfssl_WolfSSL_APP_DATA_READY -441L +#undef com_wolfssl_WolfSSL_MATCH_SUITE_ERROR +#define com_wolfssl_WolfSSL_MATCH_SUITE_ERROR -501L #undef com_wolfssl_WolfSSL_WOLFSSL_CRL_CHECKALL #define com_wolfssl_WolfSSL_WOLFSSL_CRL_CHECKALL 1L #undef com_wolfssl_WolfSSL_WOLFSSL_OCSP_URL_OVERRIDE diff --git a/native/com_wolfssl_WolfSSLSession.c b/native/com_wolfssl_WolfSSLSession.c index edea56d2..1b7ca84e 100644 --- a/native/com_wolfssl_WolfSSLSession.c +++ b/native/com_wolfssl_WolfSSLSession.c @@ -2209,6 +2209,8 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLSession_getError (JNIEnv* jenv, jobject jcl, jlong sslPtr, jint ret) { WOLFSSL* ssl = (WOLFSSL*)(uintptr_t)sslPtr; + int err = 0; + int state = 0; (void)jcl; if (jenv == NULL) { @@ -2216,7 +2218,19 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLSession_getError } /* wolfSSL checks ssl for NULL */ - return wolfSSL_get_error(ssl, ret); + err = wolfSSL_get_error(ssl, ret); + + /* With OPENSSL_EXTRA, wolfSSL_get_error() converts MATCH_SUITE_ERROR and + * SOCKET_PEER_CLOSED_E to SSL_ERROR_SYSCALL. Return the original error + * instead. */ + if (err == SSL_ERROR_SYSCALL) { + state = wolfSSL_state(ssl); + if (state == MATCH_SUITE_ERROR || state == SOCKET_PEER_CLOSED_E) { + err = state; + } + } + + return err; } JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLSession_setSession diff --git a/src/java/com/wolfssl/WolfSSL.java b/src/java/com/wolfssl/WolfSSL.java index 513701fb..0ccc8156 100644 --- a/src/java/com/wolfssl/WolfSSL.java +++ b/src/java/com/wolfssl/WolfSSL.java @@ -230,6 +230,8 @@ public enum TLS_VERSION { public static final int UNKNOWN_ALPN_PROTOCOL_NAME_E = -405; /** DTLS application data ready for read */ public static final int APP_DATA_READY = -441; + /** Unable to match a cipher suite with peer */ + public static final int MATCH_SUITE_ERROR = -501; /* extra definitions from ssl.h */ /** CertManager: check all cert CRLs */ diff --git a/src/java/com/wolfssl/WolfSSLSession.java b/src/java/com/wolfssl/WolfSSLSession.java index 1dba7692..e35a7d62 100644 --- a/src/java/com/wolfssl/WolfSSLSession.java +++ b/src/java/com/wolfssl/WolfSSLSession.java @@ -2039,6 +2039,11 @@ public int sendUserCanceled() throws IllegalStateException { * After getError() is called and returns the unique error * code, getErrorString() may be called to get a human- * readable error string. + *

+ * When native wolfSSL is compiled with OPENSSL_EXTRA, wolfSSL_get_error() + * converts MATCH_SUITE_ERROR and SSL_ERROR_SOCKET_PEER_CLOSED to + * SSL_ERROR_SYSCALL. This method returns the original error code for those + * cases instead, to match builds without OPENSSL_EXTRA. * * @param ret return value of the previous function which resulted * in an error return code. diff --git a/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java b/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java index e4c766b4..32683ad5 100644 --- a/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java +++ b/src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java @@ -239,6 +239,76 @@ public void testSSLEngineSetCipher() } + /** + * A handshake with no cipher suite in common must report the native + * MATCH_SUITE_ERROR from the server SSLEngine. Native wolfSSL compiled + * with OPENSSL_EXTRA converts that error to SSL_ERROR_SYSCALL in + * wolfSSL_get_error(), which would otherwise be reported instead. + */ + @Test + public void testCipherSuiteMismatchReportsMatchSuiteError() + throws Exception { + + final String srvSuite = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + final String cliSuite = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; + + Assume.assumeTrue("TLS 1.2 and RSA required in native wolfSSL", + WolfSSL.TLSv12Enabled() && WolfSSL.RsaEnabled()); + List available = Arrays.asList(WolfSSL.getCiphersIana()); + Assume.assumeTrue("ECDHE_RSA AES-GCM cipher suites not available", + available.contains(srvSuite) && available.contains(cliSuite)); + + this.ctx = tf.createSSLContext("TLSv1.2", engineProvider); + SSLEngine server = this.ctx.createSSLEngine(); + server.setUseClientMode(false); + server.setEnabledProtocols(new String[] { "TLSv1.2" }); + server.setEnabledCipherSuites(new String[] { srvSuite }); + + SSLEngine client = this.ctx.createSSLEngine("wolfSSL test", 11111); + client.setUseClientMode(true); + client.setEnabledProtocols(new String[] { "TLSv1.2" }); + client.setEnabledCipherSuites(new String[] { cliSuite }); + + ByteBuffer empty = ByteBuffer.allocate(0); + ByteBuffer cliToSrv = ByteBuffer.allocate( + client.getSession().getPacketBufferSize()); + ByteBuffer srvToCli = ByteBuffer.allocate( + server.getSession().getPacketBufferSize()); + ByteBuffer appData = ByteBuffer.allocate( + server.getSession().getApplicationBufferSize()); + SSLHandshakeException srvException = null; + + for (int i = 0; i < 20 && srvException == null; i++) { + client.wrap(empty, cliToSrv); + cliToSrv.flip(); + try { + server.unwrap(cliToSrv, appData); + Runnable task; + while ((task = server.getDelegatedTask()) != null) { + task.run(); + } + server.wrap(empty, srvToCli); + } catch (SSLHandshakeException e) { + srvException = e; + } + cliToSrv.compact(); + + srvToCli.flip(); + try { + client.unwrap(srvToCli, appData); + } catch (SSLException e) { + /* expected once client processes handshake_failure alert */ + } + srvToCli.compact(); + } + + assertNotNull("Server handshake should fail with no common suite", + srvException); + String msg = srvException.getMessage(); + assertTrue("Server error should report MATCH_SUITE_ERROR, got: " + + msg, msg.contains(String.valueOf(WolfSSL.MATCH_SUITE_ERROR))); + } + @Test public void testCipherConnectionTLS() throws NoSuchProviderException, NoSuchAlgorithmException, diff --git a/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java b/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java index f586bfad..70ee11c7 100644 --- a/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java +++ b/src/test/com/wolfssl/provider/jsse/test/WolfSSLSocketTest.java @@ -41,6 +41,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicIntegerArray; @@ -461,7 +462,7 @@ public void testLegacyEphemeralDHKeySizeAllowsValidDheHandshake() /* Probe without legacy property first, skip if build cannot * complete ffdhe2048 DHE handshake. */ try { - dheSocketHandshake(dheSuites); + tls12SocketHandshake(dheSuites, dheSuites); } catch (Exception e) { Assume.assumeNoException( "TLS 1.2 ffdhe2048 DHE handshake not supported by " + @@ -471,7 +472,7 @@ public void testLegacyEphemeralDHKeySizeAllowsValidDheHandshake() /* Legacy property must not cause checkKeySize to reject same * valid handshake. */ System.setProperty("jdk.tls.ephemeralDHKeySize", "legacy"); - String suite = dheSocketHandshake(dheSuites); + String suite = tls12SocketHandshake(dheSuites, dheSuites); assertTrue("negotiated cipher suite must be DHE_RSA, got: " + suite, suite.startsWith("TLS_DHE_RSA_") || suite.startsWith("DHE-RSA-")); @@ -489,23 +490,27 @@ public void testLegacyEphemeralDHKeySizeAllowsValidDheHandshake() } } - /* Complete a TLS 1.2 DHE_RSA SSLSocket handshake with the given suites - * and return the negotiated cipher suite. Throws on handshake failure. */ - private String dheSocketHandshake(String[] dheSuites) throws Exception { - SSLContext dheCtx = tf.createSSLContext("TLSv1.2", "wolfJSSE"); + /* Complete a TLS 1.2 SSLSocket handshake with the given server and + * client cipher suites and return the negotiated cipher suite. On + * failure throws the server exception if the server handshake failed, + * otherwise the client exception. */ + private String tls12SocketHandshake(String[] srvSuites, + String[] cliSuites) throws Exception { + + SSLContext ctx = tf.createSSLContext("TLSv1.2", "wolfJSSE"); SSLServerSocket ss = null; SSLSocket cs = null; SSLSocket server = null; ExecutorService es = Executors.newSingleThreadExecutor(); try { - ss = (SSLServerSocket)dheCtx.getServerSocketFactory() + ss = (SSLServerSocket)ctx.getServerSocketFactory() .createServerSocket(0); - ss.setEnabledCipherSuites(dheSuites); + ss.setEnabledCipherSuites(srvSuites); ss.setEnabledProtocols(new String[] { "TLSv1.2" }); - cs = (SSLSocket)dheCtx.getSocketFactory().createSocket(); - cs.setEnabledCipherSuites(dheSuites); + cs = (SSLSocket)ctx.getSocketFactory().createSocket(); + cs.setEnabledCipherSuites(cliSuites); cs.setEnabledProtocols(new String[] { "TLSv1.2" }); cs.connect(new InetSocketAddress(ss.getLocalPort())); @@ -517,8 +522,26 @@ private String dheSocketHandshake(String[] dheSuites) throws Exception { return null; }); - cs.startHandshake(); - f.get(10, TimeUnit.SECONDS); + Exception cliEx = null; + try { + cs.startHandshake(); + } catch (Exception e) { + cliEx = e; + /* Close client so a still waiting server fails fast */ + cs.close(); + } + + try { + f.get(10, TimeUnit.SECONDS); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + throw (cause instanceof Exception) ? (Exception)cause : e; + } catch (TimeoutException e) { + throw (cliEx != null) ? cliEx : e; + } + if (cliEx != null) { + throw cliEx; + } return cs.getSession().getCipherSuite(); } @@ -547,6 +570,101 @@ private static String[] dheRsaIanaSuites() { return matched.toArray(new String[matched.size()]); } + /** + * A handshake with no cipher suite in common must report the native + * MATCH_SUITE_ERROR from the server. Native wolfSSL compiled with + * OPENSSL_EXTRA converts that error to SSL_ERROR_SYSCALL in + * wolfSSL_get_error(), which reads as an I/O error instead. + */ + @Test + public void testCipherSuiteMismatchReportsMatchSuiteError() + throws Exception { + + final String srvSuite = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + final String cliSuite = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"; + + Assume.assumeTrue("TLS 1.2 and RSA required in native wolfSSL", + WolfSSL.TLSv12Enabled() && WolfSSL.RsaEnabled()); + List available = Arrays.asList(WolfSSL.getCiphersIana()); + Assume.assumeTrue("ECDHE_RSA AES-GCM cipher suites not available", + available.contains(srvSuite) && available.contains(cliSuite)); + + try { + tls12SocketHandshake(new String[] { srvSuite }, + new String[] { cliSuite }); + fail("Handshake should fail with no common cipher suite"); + } catch (SSLHandshakeException e) { + String msg = e.getMessage(); + assertTrue("Server error should report MATCH_SUITE_ERROR, " + + "got: " + msg, msg.contains("error code: " + + WolfSSL.MATCH_SUITE_ERROR + ",")); + } + } + + /** + * When the peer closes the TCP connection without sending close_notify, + * InputStream.read() must return the data already received and then + * end of stream. Native wolfSSL returns 0 with SOCKET_PEER_CLOSED_E, + * which wolfSSL_get_error() converts to SSL_ERROR_SYSCALL when compiled + * with OPENSSL_EXTRA, and read() used to return 0 in that case. + */ + @Test + public void testReadReturnsEndOfStreamAfterPeerClose() throws Exception { + + Assume.assumeTrue("TLS 1.2 required in native wolfSSL", + WolfSSL.TLSv12Enabled()); + + final SSLContext ctx = tf.createSSLContext("TLSv1.2", "wolfJSSE"); + final byte[] msg = "hello".getBytes(StandardCharsets.UTF_8); + final ServerSocket ss = new ServerSocket(0); + SSLSocket cs = null; + ExecutorService es = Executors.newSingleThreadExecutor(); + + try { + /* Layer TLS over a plain socket so the server can close TCP + * without sending close_notify */ + Future f = es.submit(() -> { + Socket s = ss.accept(); + SSLSocket server = null; + try { + server = (SSLSocket)ctx.getSocketFactory() + .createSocket(s, "localhost", s.getPort(), false); + server.setUseClientMode(false); + server.startHandshake(); + server.getOutputStream().write(msg); + } + finally { + /* Close TCP first so server.close() skips close_notify */ + s.close(); + if (server != null) { + server.close(); + } + } + return null; + }); + + cs = (SSLSocket)ctx.getSocketFactory().createSocket( + "localhost", ss.getLocalPort()); + cs.setSoTimeout(10000); + cs.startHandshake(); + f.get(10, TimeUnit.SECONDS); + + InputStream in = cs.getInputStream(); + byte[] buf = new byte[msg.length]; + assertEquals("read() should return data sent before close", + msg.length, in.read(buf, 0, buf.length)); + assertEquals("read() should return end of stream after peer " + + "closed connection", -1, in.read(buf, 0, buf.length)); + } + finally { + es.shutdownNow(); + if (cs != null) { + cs.close(); + } + ss.close(); + } + } + @Test public void testGetSupportedProtocols() throws NoSuchProviderException, NoSuchAlgorithmException { diff --git a/src/test/com/wolfssl/test/WolfSSLSessionTest.java b/src/test/com/wolfssl/test/WolfSSLSessionTest.java index 288e4fb2..96c6e091 100644 --- a/src/test/com/wolfssl/test/WolfSSLSessionTest.java +++ b/src/test/com/wolfssl/test/WolfSSLSessionTest.java @@ -767,14 +767,17 @@ public Void call() throws Exception { } /* Accept one TLS connection on a background thread. A server-side - * handshake failure is expected once the client rejects the cert. */ - private Future runOneShotTlsServer(final ServerSocket srvSocket, + * handshake failure is expected in the tests using this. Returns + * {ret, getError(ret)} from the last accept(), or null if an exception + * was thrown first. */ + private Future runOneShotTlsServer(final ServerSocket srvSocket, final WolfSSLContext srvCtx, ExecutorService es) { - return es.submit(new Callable() { + return es.submit(new Callable() { @Override - public Void call() { + public int[] call() { Socket server = null; WolfSSLSession srvSes = null; + int[] result = null; try { server = srvSocket.accept(); server.setSoTimeout(10000); @@ -787,8 +790,9 @@ public Void call() { } while (ret != WolfSSL.SSL_SUCCESS && (err == WolfSSL.SSL_ERROR_WANT_READ || err == WolfSSL.SSL_ERROR_WANT_WRITE)); + result = new int[] { ret, err }; } catch (Exception e) { - /* expected once the client rejects the certificate */ + /* expected once the client rejects the handshake */ } finally { if (srvSes != null) { try { @@ -801,7 +805,7 @@ public Void call() { } catch (Exception e) { } } } - return null; + return result; } }); } @@ -816,7 +820,7 @@ private void assertHandshakeFailsWithVerifyCallback( WolfSSLContext srvCtx = null; WolfSSLContext cliCtx = null; ExecutorService es = null; - Future srvFuture = null; + Future srvFuture = null; Socket cliSock = null; WolfSSLSession ssl = null; @@ -1101,6 +1105,97 @@ public Void call() throws Exception { } } + @Test + public void test_WolfSSLSession_getErrorAfterCipherSuiteMismatch() + throws Exception { + + Assume.assumeTrue("TLS 1.2 not compiled into native wolfSSL", + WolfSSL.TLSv12Enabled()); + Assume.assumeTrue("ECDHE-RSA AES-GCM cipher suites not available", + suiteAvailable("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") && + suiteAvailable("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")); + + ServerSocket srvSocket = null; + WolfSSLContext srvCtx = null; + WolfSSLContext cliCtx = null; + ExecutorService es = null; + Future srvFuture = null; + Socket cliSock = null; + WolfSSLSession ssl = null; + + try { + srvCtx = createAndSetupWolfSSLContext( + srvCert, srvKey, WolfSSL.SSL_FILETYPE_PEM, cliCert, + WolfSSL.TLSv1_2_ServerMethod()); + cliCtx = createAndSetupWolfSSLContext( + cliCert, cliKey, WolfSSL.SSL_FILETYPE_PEM, caCert, + WolfSSL.TLSv1_2_ClientMethod()); + assertEquals("server setCipherList", WolfSSL.SSL_SUCCESS, + srvCtx.setCipherList("ECDHE-RSA-AES128-GCM-SHA256")); + assertEquals("client setCipherList", WolfSSL.SSL_SUCCESS, + cliCtx.setCipherList("ECDHE-RSA-AES256-GCM-SHA384")); + + srvSocket = new ServerSocket(0); + srvSocket.setSoTimeout(10000); + es = Executors.newSingleThreadExecutor(); + srvFuture = runOneShotTlsServer(srvSocket, srvCtx, es); + + cliSock = new Socket("localhost", srvSocket.getLocalPort()); + cliSock.setSoTimeout(10000); + ssl = new WolfSSLSession(cliCtx); + assertEquals("client setFd", WolfSSL.SSL_SUCCESS, + ssl.setFd(cliSock)); + + int ret, err; + do { + ret = ssl.connect(); + err = ssl.getError(ret); + } while (ret != WolfSSL.SSL_SUCCESS && + (err == WolfSSL.SSL_ERROR_WANT_READ || + err == WolfSSL.SSL_ERROR_WANT_WRITE)); + assertNotEquals("client handshake should fail with no " + + "common cipher suite", WolfSSL.SSL_SUCCESS, ret); + + int[] srv = srvFuture.get(10, TimeUnit.SECONDS); + assertNotNull("server accept() should return a result", srv); + assertNotEquals("server handshake should fail with no " + + "common cipher suite", WolfSSL.SSL_SUCCESS, srv[0]); + + /* Not converted to SSL_ERROR_SYSCALL even when native wolfSSL + * is compiled with OPENSSL_EXTRA */ + assertEquals("server getError() should be MATCH_SUITE_ERROR", + WolfSSL.MATCH_SUITE_ERROR, srv[1]); + + } finally { + if (ssl != null) { + ssl.freeSSL(); + } + if (cliSock != null) { + cliSock.close(); + } + /* Wait for server thread before freeing its context */ + if (srvFuture != null) { + try { + srvFuture.get(10, TimeUnit.SECONDS); + } catch (Exception e) { + /* ignore on purpose */ + } + } + if (es != null) { + es.shutdownNow(); + } + if (srvSocket != null) { + srvSocket.close(); + } + if (cliCtx != null) { + cliCtx.free(); + } + if (srvCtx != null) { + srvCtx.free(); + } + } + } + /* True if the given IANA cipher suite is compiled into native wolfSSL. */ private static boolean suiteAvailable(String ianaName) { for (String suite : WolfSSL.getCiphersIana()) {