From bf134302793fd4a9882908e5a5e2c099ce1f9216 Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Wed, 29 Jul 2026 09:40:42 -0600 Subject: [PATCH] JNI/JSSE: return full peer cert chain from getPeerCertificates() --- native/com_wolfssl_WolfSSL.c | 13 + native/com_wolfssl_WolfSSL.h | 8 + native/com_wolfssl_WolfSSLSession.c | 78 ++++ native/com_wolfssl_WolfSSLSession.h | 8 + src/java/com/wolfssl/WolfSSL.java | 11 + src/java/com/wolfssl/WolfSSLSession.java | 157 ++++++++ .../jsse/WolfSSLImplementSSLSession.java | 216 ++++++----- .../wolfssl/provider/jsse/WolfSSLX509X.java | 8 + .../jsse/test/WolfSSLSessionTest.java | 356 ++++++++++++++++++ .../jsse/test/WolfSSLTestFactory.java | 130 +++++++ .../com/wolfssl/test/WolfSSLSessionTest.java | 219 +++++++++++ src/test/com/wolfssl/test/WolfSSLTest.java | 12 + 12 files changed, 1113 insertions(+), 103 deletions(-) diff --git a/native/com_wolfssl_WolfSSL.c b/native/com_wolfssl_WolfSSL.c index 0b21ad7a2..70c6c22c0 100644 --- a/native/com_wolfssl_WolfSSL.c +++ b/native/com_wolfssl_WolfSSL.c @@ -1413,6 +1413,19 @@ JNIEXPORT jboolean JNICALL Java_com_wolfssl_WolfSSL_trustPeerCertEnabled #endif } +JNIEXPORT jboolean JNICALL Java_com_wolfssl_WolfSSL_sessionCertsEnabled + (JNIEnv* jenv, jclass jcl) +{ + (void)jenv; + (void)jcl; + +#ifdef SESSION_CERTS + return JNI_TRUE; +#else + return JNI_FALSE; +#endif +} + JNIEXPORT jboolean JNICALL Java_com_wolfssl_WolfSSL_sessionTicketEnabled (JNIEnv* jenv, jclass jcl) { diff --git a/native/com_wolfssl_WolfSSL.h b/native/com_wolfssl_WolfSSL.h index 4fcd6241e..c6d6741fd 100644 --- a/native/com_wolfssl_WolfSSL.h +++ b/native/com_wolfssl_WolfSSL.h @@ -941,6 +941,14 @@ JNIEXPORT jboolean JNICALL Java_com_wolfssl_WolfSSL_certReqEnabled JNIEXPORT jboolean JNICALL Java_com_wolfssl_WolfSSL_trustPeerCertEnabled (JNIEnv *, jclass); +/* + * Class: com_wolfssl_WolfSSL + * Method: sessionCertsEnabled + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_com_wolfssl_WolfSSL_sessionCertsEnabled + (JNIEnv *, jclass); + /* * Class: com_wolfssl_WolfSSL * Method: sessionTicketEnabled diff --git a/native/com_wolfssl_WolfSSLSession.c b/native/com_wolfssl_WolfSSLSession.c index 45e48c430..993a7ec77 100644 --- a/native/com_wolfssl_WolfSSLSession.c +++ b/native/com_wolfssl_WolfSSLSession.c @@ -3412,6 +3412,84 @@ JNIEXPORT jlong JNICALL Java_com_wolfssl_WolfSSLSession_getPeerCertificate #endif } +JNIEXPORT jobjectArray JNICALL Java_com_wolfssl_WolfSSLSession_getPeerCertificateChainDER + (JNIEnv* jenv, jobject jcl, jlong sslPtr) +{ +#ifdef SESSION_CERTS + WOLFSSL* ssl = (WOLFSSL*)(uintptr_t)sslPtr; + WOLFSSL_X509_CHAIN* chain = NULL; + jclass byteArrayClass = NULL; + jobjectArray chainArr = NULL; + jbyteArray derArr = NULL; + unsigned char* der = NULL; + int chainSz = 0; + int derSz = 0; + int i = 0; + (void)jcl; + + if (jenv == NULL || ssl == NULL) { + return NULL; + } + + chain = wolfSSL_get_peer_chain(ssl); + if (chain == NULL) { + return NULL; + } + + chainSz = wolfSSL_get_chain_count(chain); + if (chainSz <= 0) { + return NULL; + } + + byteArrayClass = (*jenv)->FindClass(jenv, "[B"); + if (byteArrayClass == NULL) { + return NULL; + } + + chainArr = (*jenv)->NewObjectArray(jenv, chainSz, byteArrayClass, NULL); + if (chainArr == NULL) { + return NULL; + } + + /* Appended in wire order, peer cert first. Native wolfSSL drops certs + * of MAX_X509_SIZE or larger and anything past MAX_CHAIN_DEPTH, so the + * chain can be incomplete and may not start with the peer cert. */ + for (i = 0; i < chainSz; i++) { + + der = wolfSSL_get_chain_cert(chain, i); + derSz = wolfSSL_get_chain_length(chain, i); + + if (der == NULL || derSz <= 0) { + /* Chain not usable if any entry is missing */ + return NULL; + } + + derArr = (*jenv)->NewByteArray(jenv, derSz); + if (derArr == NULL) { + return NULL; + } + + (*jenv)->SetByteArrayRegion(jenv, derArr, 0, derSz, (jbyte*)der); + if ((*jenv)->ExceptionCheck(jenv)) { + return NULL; + } + + (*jenv)->SetObjectArrayElement(jenv, chainArr, i, derArr); + (*jenv)->DeleteLocalRef(jenv, derArr); + if ((*jenv)->ExceptionCheck(jenv)) { + return NULL; + } + } + + return chainArr; +#else + (void)jenv; + (void)jcl; + (void)sslPtr; + return NULL; +#endif +} + JNIEXPORT jstring JNICALL Java_com_wolfssl_WolfSSLSession_getPeerX509Issuer (JNIEnv* jenv, jobject jcl, jlong sslPtr, jlong x509Ptr) { diff --git a/native/com_wolfssl_WolfSSLSession.h b/native/com_wolfssl_WolfSSLSession.h index 04ad4e4f2..a5644f6d3 100644 --- a/native/com_wolfssl_WolfSSLSession.h +++ b/native/com_wolfssl_WolfSSLSession.h @@ -455,6 +455,14 @@ JNIEXPORT jint JNICALL Java_com_wolfssl_WolfSSLSession_sessionReused JNIEXPORT jlong JNICALL Java_com_wolfssl_WolfSSLSession_getPeerCertificate (JNIEnv *, jobject, jlong); +/* + * Class: com_wolfssl_WolfSSLSession + * Method: getPeerCertificateChainDER + * Signature: (J)[[B + */ +JNIEXPORT jobjectArray JNICALL Java_com_wolfssl_WolfSSLSession_getPeerCertificateChainDER + (JNIEnv *, jobject, jlong); + /* * Class: com_wolfssl_WolfSSLSession * Method: getPeerX509Issuer diff --git a/src/java/com/wolfssl/WolfSSL.java b/src/java/com/wolfssl/WolfSSL.java index 2d1e3665b..050230f35 100644 --- a/src/java/com/wolfssl/WolfSSL.java +++ b/src/java/com/wolfssl/WolfSSL.java @@ -1235,6 +1235,17 @@ protected static byte[] fileToBytes(File file) */ public static native boolean trustPeerCertEnabled(); + /** + * Tests if native wolfSSL has been compiled with SESSION_CERTS. + * If defined, wolfSSL stores the cert chain sent by the peer, allowing + * full chain to be returned from + * WolfSSLSession.getPeerCertificateChainDER(). + * + * @return true if enabled, otherwise false if SESSION_CERTS has not + * been defined. + */ + public static native boolean sessionCertsEnabled(); + /** * Tests if native session ticket support has been compiled into wolfSSL * with HAVE_SESSION_TICKET. diff --git a/src/java/com/wolfssl/WolfSSLSession.java b/src/java/com/wolfssl/WolfSSLSession.java index 34adb4877..8da0de939 100644 --- a/src/java/com/wolfssl/WolfSSLSession.java +++ b/src/java/com/wolfssl/WolfSSLSession.java @@ -630,6 +630,7 @@ private static native byte[] dtlsCidParseNative(byte[] msg, int msgSz, private native InetSocketAddress dtlsGetPeer(long ssl); private native int sessionReused(long ssl); private native long getPeerCertificate(long ssl); + private native byte[][] getPeerCertificateChainDER(long ssl); private native String getPeerX509Issuer(long ssl, long x509); private native String getPeerX509Subject(long ssl, long x509); private native String getPeerX509AltName(long ssl, long x509); @@ -3341,6 +3342,162 @@ public long getPeerCertificate() } } + /** + * Gets the DER encoding of the peer certificate. + * + * Requires native wolfSSL to be compiled with KEEP_PEER_CERT, + * auto defined by "--enable-jni". + * + * @return DER encoding of the peer cert, or null if the peer did not send + * a certificate or it is not available. Returned encoding is a + * copy, no native memory needs to be freed by caller. + * @throws IllegalStateException WolfSSLSession has been freed + * @throws WolfSSLJNIException Internal JNI error + * @see WolfSSLSession#getPeerCertificateChainDER() + */ + public byte[] getPeerCertificateDER() + throws IllegalStateException, WolfSSLJNIException { + + confirmObjectIsActive(); + + synchronized (sslLock) { + WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI, + WolfSSLDebug.INFO, this.sslPtr, + () -> "entered getPeerCertificateDER()"); + + return getPeerCertificateDERLocked(); + } + } + + /** + * Get the DER encoding of the peer certificate, caller must already hold + * sslLock and have called confirmObjectIsActive(). The public method + * cannot be used from inside sslLock, confirmObjectIsActive() is + * synchronized on this object and would invert this class's lock order. + * + * @return DER encoding of the peer cert, or null if not available + */ + private byte[] getPeerCertificateDERLocked() { + + long x509 = getPeerCertificate(this.sslPtr); + + if (x509 == 0) { + return null; + } + + try { + return WolfSSL.x509_getDer(x509); + + } finally { + /* wolfSSL 5.3.0 and later hand back a new WOLFSSL_X509 the + * caller owns, earlier versions internal memory */ + if (WolfSSL.getLibVersionHex() >= 0x05003000) { + WolfSSLCertificate.freeX509(x509); + } + } + } + + /** + * Gets the DER encoding of each certificate in the peer cert chain, + * as sent by the peer during the handshake. + * + * Index zero holds the peer cert, followed by any intermediate CA certs + * the peer chose to send, in the order received. If native wolfSSL + * stored no chain, for example when built without SESSION_CERTS, the + * peer cert alone is returned. + * + * Native wolfSSL skips certs of MAX_X509_SIZE or larger when storing the + * peer chain, and drops anything past MAX_CHAIN_DEPTH, so an oversized + * or deep intermediate CA cert will be missing. An oversized peer cert + * is skipped too, but is restored to index zero from the separate + * KEEP_PEER_CERT copy. Without KEEP_PEER_CERT there is no copy to check + * index zero against, so null is returned rather than a chain that might + * start at a CA. + * + * On a resumed session no Certificate message is received, so native + * wolfSSL reports the chain stored in the resumed session. Callers + * needing the certs from the original handshake should cache them. + * + * This method makes several native calls, one of which copies the peer + * cert, so callers should cache the result rather than call this + * repeatedly to avoid performance penalties. + * + * @return two dimensional byte array holding the DER encoding of each + * certificate in the peer's chain, or null if the peer sent no + * certificates. Returned arrays are copies, no native memory + * needs to be freed by caller. + * @throws IllegalStateException WolfSSLSession has been freed + * @throws WolfSSLJNIException Internal JNI error + * @see WolfSSLSession#getPeerCertificateDER() + * @see WolfSSLSession#getPeerCertificate() + */ + public byte[][] getPeerCertificateChainDER() + throws IllegalStateException, WolfSSLJNIException { + + byte[] peerDer = null; + byte[][] chain = null; + + confirmObjectIsActive(); + + synchronized (sslLock) { + WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI, + WolfSSLDebug.INFO, this.sslPtr, + () -> "entered getPeerCertificateChainDER()"); + + chain = getPeerCertificateChainDER(this.sslPtr); + peerDer = getPeerCertificateDERLocked(); + chain = ensurePeerCertFirst(chain, peerDer); + + final int chainSz = (chain == null) ? 0 : chain.length; + WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI, + WolfSSLDebug.INFO, this.sslPtr, + () -> "peer chain size: " + chainSz); + + return chain; + } + } + + /** + * Place the peer certificate at index zero of the stored chain, which + * native wolfSSL leaves starting at a CA when the peer cert was too + * large to store. + * + * @param chain DER encoding of each cert in the chain stored by native + * wolfSSL, may be null or empty + * @param peerDer DER encoding of the peer's own cert, may be null + * + * @return chain holding the peer cert at index zero, or null if no cert + * is available or the peer cert cannot be confirmed to be first + */ + private static byte[][] ensurePeerCertFirst(byte[][] chain, + byte[] peerDer) { + + byte[][] fullChain = null; + + if (chain == null || chain.length == 0) { + if (peerDer == null) { + return null; + } + return new byte[][] { peerDer }; + } + + if (peerDer == null) { + /* No peer cert to check chain[0] against, fail closed rather + * than report CA as peer. */ + return null; + } + + if (Arrays.equals(peerDer, chain[0])) { + return chain; + } + + fullChain = new byte[chain.length + 1][]; + fullChain[0] = peerDer; + System.arraycopy(chain, 0, fullChain, 1, chain.length); + + return fullChain; + } + /** * Gets the peer X509 certificate's issuer information. * diff --git a/src/java/com/wolfssl/provider/jsse/WolfSSLImplementSSLSession.java b/src/java/com/wolfssl/provider/jsse/WolfSSLImplementSSLSession.java index b938ff8a8..7e09860b9 100644 --- a/src/java/com/wolfssl/provider/jsse/WolfSSLImplementSSLSession.java +++ b/src/java/com/wolfssl/provider/jsse/WolfSSLImplementSSLSession.java @@ -490,16 +490,92 @@ public String[] getValueNames() { return binding.keySet().toArray(new String[binding.keySet().size()]); } + /** + * Get DER encoding of each cert the peer sent, peer cert first. + * Ordering and fallback are handled by WolfSSLSession. + * + * @return DER encoded peer chain, or null if the peer sent no certs + */ + private byte[][] getPeerCertificateChainDER() { + + try { + return this.ssl.getPeerCertificateChainDER(); + + } catch (IllegalStateException | WolfSSLJNIException ex) { + WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, + () -> "Error getting peer certificate chain: " + + ex.getMessage()); + return null; + } + } + + private boolean isSessionReused() { + + try { + if (this.ssl.sessionReused() == 1) { + return true; + } + } catch (Exception ex) { + WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, + () -> "Error checking session reuse: " + ex.getMessage()); + } + + return false; + } + + /** + * Convert an array of DER encoded certificates into an array of + * java.security.cert.X509Certificate objects. + * + * @param derChain array holding the DER encoding of each certificate + * + * @return X509Certificate array matching the order of the input array + * + * @throws SSLPeerUnverifiedException if error converting any of the + * DER encodings into an X509Certificate object + */ + private static X509Certificate[] derChainToX509Certificates( + byte[][] derChain) throws SSLPeerUnverifiedException { + + CertificateFactory cf; + X509Certificate[] certs; + + try { + cf = CertificateFactory.getInstance("X.509"); + } catch (CertificateException ex) { + throw new SSLPeerUnverifiedException( + "Error getting CertificateFactory instance"); + } + + certs = new X509Certificate[derChain.length]; + + for (int i = 0; i < derChain.length; i++) { + if (derChain[i] == null) { + throw new SSLPeerUnverifiedException( + "Null DER encoding in peer certificate chain"); + } + + try { + certs[i] = (X509Certificate)cf.generateCertificate( + new ByteArrayInputStream(derChain[i])); + } catch (CertificateException ex) { + throw new SSLPeerUnverifiedException( + "Error generating X509Certificate from DER encoding"); + } + } + + return certs; + } + /** * Get peer certificates for this session * - * This method first tries to call down to native wolfSSL with - * ssl.getPeerCertificate(). If that succeeds, it caches the peer - * certificate inside this object (this.peerCerts) so that in a resumed - * session when this method is called, the caller will still have access - * to the original certificate (matches SunJSSE behavior). If calling - * ssl.getPeerCertificate() fails, then we return the cached cert if - * we have it. + * Certificates are returned in the order sent by the peer, index zero + * holding the peer's own certificate. Certs from the handshake that + * created this session are cached, so a resumed session still reports + * them. Native wolfSSL only stores the full chain when compiled with + * SESSION_CERTS, included with "--enable-jni", otherwise only the peer + * cert is returned. * * @return Certificate array of peer certs for session. Actual subclass * type returned is X509Certificate[] to match SunJSSE behavior @@ -509,12 +585,8 @@ public String[] getValueNames() { */ public synchronized Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { - long x509; - WolfSSLX509 cert; - CertificateFactory cf; - ByteArrayInputStream der; - X509Certificate exportCert; + byte[][] derChain; if (ssl == null) { throw new SSLPeerUnverifiedException( @@ -531,25 +603,23 @@ public synchronized Certificate[] getPeerCertificates() "peer not authenticated (no client auth requested)"); } - try { - x509 = this.ssl.getPeerCertificate(); - } catch (IllegalStateException | WolfSSLJNIException ex) { - WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, - () -> "Error getting peer certificate: " - + ex.getMessage()); - x509 = 0; + /* Resumed session receives no Certificate message, use the certs + * cached from the handshake that created this session */ + if (this.peerCerts != null && isSessionReused()) { + WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, + () -> "Session resumed, returning cached peer certs"); + return this.peerCerts.clone(); } - /* if no peer cert, throw SSLPeerUnverifiedException */ - if (x509 == 0) { + derChain = getPeerCertificateChainDER(); + + if (derChain == null) { WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, - () -> "ssl.getPeerCertificates() returned null, trying " + - "cached cert"); + () -> "No peer certs available, trying cached certs"); if (this.peerCerts != null) { - /* If peer cert is already cached, just return that */ WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, - () -> "peer cert already cached, returning it"); + () -> "peer certs already cached, returning them"); return this.peerCerts.clone(); } else { @@ -559,53 +629,8 @@ public synchronized Certificate[] getPeerCertificates() } } - try { - /* wolfSSL starting with 5.3.0 returns a new WOLFSSL_X509 - * structure from wolfSSL_get_peer_certificate(). In that case, - * we need to free the pointer when finished. Prior to 5.3.0, - * this memory was freed internally by wolfSSL since the API - * only returned a pointer to internal memory */ - if (WolfSSL.getLibVersionHex() >= 0x05003000) { - cert = new WolfSSLX509(x509, true); - } - else { - cert = new WolfSSLX509(x509, false); - } - } catch (WolfSSLException ex) { - throw new SSLPeerUnverifiedException("Error creating certificate"); - } - - /* convert WolfSSLX509 into X509Certificate so we can release - * our native memory */ - try { - cf = CertificateFactory.getInstance("X.509"); - } catch (CertificateException ex) { - cert.free(); - throw new SSLPeerUnverifiedException( - "Error getting CertificateFactory instance"); - } - - try { - der = new ByteArrayInputStream(cert.getEncoded()); - } catch (CertificateEncodingException ex) { - cert.free(); - throw new SSLPeerUnverifiedException( - "Error getting encoded DER from WolfSSLX509 object"); - } - - try { - exportCert = (X509Certificate)cf.generateCertificate(der); - } catch (CertificateException ex) { - cert.free(); - throw new SSLPeerUnverifiedException( - "Error generating X509Certificdate from DER encoding"); - } - - /* release native memory */ - cert.free(); - - /* cache peer cert for use by app in resumed session */ - this.peerCerts = new X509Certificate[] { exportCert }; + /* cache peer certs for use by app in resumed session */ + this.peerCerts = derChainToX509Certificates(derChain); return this.peerCerts.clone(); } @@ -621,49 +646,34 @@ public Certificate[] getLocalCertificates() { public synchronized javax.security.cert.X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { - long peerX509 = 0; - WolfSSLX509X x509; + Certificate[] javaCerts; + javax.security.cert.X509Certificate[] certs; - if (ssl == null) { - throw new SSLPeerUnverifiedException("handshake not done"); - } + /* Get peer cert chain from native wolfSSL, or a cached copy if this + * is a resumed session and we had one from the original handshake. */ + javaCerts = getPeerCertificates(); - /* Throw if server side with no client auth requested */ - if (this.side == WolfSSL.WOLFSSL_SERVER_END && - !this.clientAuthRequested) { - throw new SSLPeerUnverifiedException( - "peer not authenticated (no client auth requested)"); - } + certs = new javax.security.cert.X509Certificate[javaCerts.length]; try { - peerX509 = this.ssl.getPeerCertificate(); - if (peerX509 == 0) { - throw new SSLPeerUnverifiedException("No peer certificate"); - } - - /* wolfSSL starting with 5.3.0 returns a new WOLFSSL_X509 - * structure from wolfSSL_get_peer_certificate(). In that case, - * we need to free the pointer when finished. Prior to 5.3.0, - * this memory was freed internally by wolfSSL since the API - * only returned a pointer to internal memory */ - if (WolfSSL.getLibVersionHex() >= 0x05003000) { - x509 = new WolfSSLX509X(peerX509, true); + for (int i = 0; i < javaCerts.length; i++) { + certs[i] = new WolfSSLX509X(javaCerts[i].getEncoded()); } - else { - x509 = new WolfSSLX509X(peerX509, false); + } catch (CertificateEncodingException | WolfSSLException ex) { + /* Release certs, array is discarded */ + for (int i = 0; i < certs.length; i++) { + if (certs[i] != null) { + ((WolfSSLX509X)certs[i]).free(); + } } - - return new javax.security.cert.X509Certificate[] { - (javax.security.cert.X509Certificate)x509 }; - - } catch (IllegalStateException | WolfSSLJNIException | - WolfSSLException ex) { WolfSSLDebug.log(getClass(), WolfSSLDebug.ERROR, () -> "Error getting peer certificate chain: " + ex.getMessage()); throw new SSLPeerUnverifiedException( "Error getting peer certificate chain: " + ex.getMessage()); } + + return certs; } @Override diff --git a/src/java/com/wolfssl/provider/jsse/WolfSSLX509X.java b/src/java/com/wolfssl/provider/jsse/WolfSSLX509X.java index b871bc9a6..229bac9f1 100644 --- a/src/java/com/wolfssl/provider/jsse/WolfSSLX509X.java +++ b/src/java/com/wolfssl/provider/jsse/WolfSSLX509X.java @@ -211,6 +211,14 @@ public PublicKey getPublicKey() { return this.cert.getPublicKey(); } + /** + * Free native resources used by this object, mirrors WolfSSLX509.free(). + * Otherwise released by the finalizer. + */ + public void free() { + this.cert.free(); + } + @Override @SuppressWarnings("removal") protected void finalize() throws Throwable { diff --git a/src/test/com/wolfssl/provider/jsse/test/WolfSSLSessionTest.java b/src/test/com/wolfssl/provider/jsse/test/WolfSSLSessionTest.java index 3bd7520c9..5b057725b 100644 --- a/src/test/com/wolfssl/provider/jsse/test/WolfSSLSessionTest.java +++ b/src/test/com/wolfssl/provider/jsse/test/WolfSSLSessionTest.java @@ -21,6 +21,8 @@ package com.wolfssl.provider.jsse.test; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; @@ -29,7 +31,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; +import java.io.FileInputStream; import java.io.IOException; +import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.Provider; @@ -62,7 +66,9 @@ import com.wolfssl.WolfSSL; import com.wolfssl.WolfSSLException; +import com.wolfssl.WolfSSLJNIException; import com.wolfssl.provider.jsse.WolfSSLProvider; +import com.wolfssl.provider.jsse.WolfSSLX509X; import com.wolfssl.test.TimedTestWatcher; public class WolfSSLSessionTest { @@ -344,6 +350,356 @@ public void testBinding() } } + /** + * Test that SSLSession.getPeerCertificates() returns the full cert chain + * sent by the peer, peer cert first, matching expected JSSE behavior. + */ + @Test + @SuppressWarnings("removal") + public void testGetPeerCertificatesReturnsFullChain() + throws NoSuchAlgorithmException, KeyManagementException, + KeyStoreException, CertificateException, IOException, + NoSuchProviderException, UnrecoverableKeyException { + + int ret; + byte[][] derChain; + SSLContext ctxClient; + SSLContext ctxServer; + SSLEngine client; + SSLEngine server; + Certificate[] expectedCerts; + KeyStore serverStore; + KeyStore clientStore; + javax.security.cert.X509Certificate[] oldPeerCerts; + + /* Server key store holds server cert followed by the CA signer, + * both are sent to the client during the handshake */ + serverStore = KeyStore.getInstance(tf.keyStoreType); + try (FileInputStream stream = new FileInputStream(tf.serverRSAJKS)) { + serverStore.load(stream, WolfSSLTestFactory.jksPass); + } + expectedCerts = serverStore.getCertificateChain("server-rsa"); + assertNotNull(expectedCerts); + + /* Sanity check that the test key store still holds a chain, if it + * only has the peer cert this test can not detect a regression */ + Assume.assumeTrue(expectedCerts.length > 1); + + Assume.assumeTrue(WolfSSL.sessionCertsEnabled()); + + ctxClient = tf.createSSLContext("TLS", engineProvider, + tf.createTrustManager("SunX509", tf.caServerJKS, engineProvider), + tf.createKeyManager("SunX509", tf.clientRSAJKS, engineProvider)); + ctxServer = tf.createSSLContext("TLS", engineProvider, + tf.createTrustManager("SunX509", tf.caClientJKS, engineProvider), + tf.createKeyManager("SunX509", tf.serverRSAJKS, engineProvider)); + + client = ctxClient.createSSLEngine("wolfSSL client test", 11111); + server = ctxServer.createSSLEngine(); + + client.setUseClientMode(true); + server.setUseClientMode(false); + /* Client auth on to cover server side. client-rsa.jks has one self + * signed cert, so that leg checks the leaf only. */ + server.setNeedClientAuth(true); + + ret = tf.testConnection(server, client, null, null, + "Test peer cert chain"); + if (ret != 0) { + fail("failed to connect"); + } + + checkPeerChain(client.getSession().getPeerCertificates(), + expectedCerts, "client side"); + + /* Deprecated getPeerCertificateChain() reports the same chain */ + oldPeerCerts = client.getSession().getPeerCertificateChain(); + assertNotNull(oldPeerCerts); + derChain = new byte[oldPeerCerts.length][]; + try { + for (int i = 0; i < oldPeerCerts.length; i++) { + derChain[i] = oldPeerCerts[i].getEncoded(); + } + } catch (javax.security.cert.CertificateEncodingException e) { + fail("failed to get encoding of peer certificate: " + e); + } finally { + freeX509XCerts(oldPeerCerts); + } + checkPeerChainDER(derChain, expectedCerts, + "client side deprecated API"); + + /* Server side sees the client's chain from the same code path */ + clientStore = KeyStore.getInstance(tf.keyStoreType); + try (FileInputStream stream = + new FileInputStream(tf.clientRSAJKS)) { + clientStore.load(stream, WolfSSLTestFactory.jksPass); + } + expectedCerts = clientStore.getCertificateChain("client-rsa"); + assertNotNull(expectedCerts); + + checkPeerChain(server.getSession().getPeerCertificates(), + expectedCerts, "server side"); + } + + /** + * Test that a resumed session still reports the certificates from the + * handshake that created it. No Certificate message is received on a + * resumed connection, so wolfJSSE serves them from the certs cached during + * the original handshake. + */ + @Test + public void testGetPeerCertificatesOnResumedSession() + throws NoSuchAlgorithmException, KeyManagementException, + KeyStoreException, CertificateException, IOException, + NoSuchProviderException, UnrecoverableKeyException { + + int ret; + SSLContext ctx; + SSLEngine client; + SSLEngine server; + Certificate[] firstCerts; + Certificate[] resumedCerts; + byte[] firstId; + + /* Make sure session cache is not disabled for resume test */ + String originalProp = Security.getProperty( + "wolfjsse.clientSessionCache.disabled"); + Security.setProperty("wolfjsse.clientSessionCache.disabled", "false"); + + try { + + ctx = tf.createSSLContext("TLS", engineProvider); + + client = ctx.createSSLEngine("wolfSSL peer chain test", 11111); + server = ctx.createSSLEngine(); + client.setUseClientMode(true); + server.setUseClientMode(false); + server.setNeedClientAuth(false); + + ret = tf.testConnection(server, client, null, null, + "Test resume 1"); + if (ret != 0) { + fail("failed to connect"); + } + + try { + firstCerts = client.getSession().getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + fail("failed to get peer certificates: " + e); + return; + } + assertNotNull(firstCerts); + firstId = client.getSession().getId(); + + /* Second connection off the same context, resumes the session */ + client = ctx.createSSLEngine("wolfSSL peer chain test", 11111); + server = ctx.createSSLEngine(); + client.setUseClientMode(true); + server.setUseClientMode(false); + server.setNeedClientAuth(false); + + ret = tf.testConnection(server, client, null, null, + "Test resume 2"); + if (ret != 0) { + fail("failed to connect for resumption"); + } + + try { + resumedCerts = client.getSession().getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + fail("failed to get resumed peer certificates: " + e); + return; + } + + /* Matching session IDs confirm the session resumed */ + assertArrayEquals("second connection did not resume the session", + firstId, client.getSession().getId()); + + assertNotNull(resumedCerts); + assertEquals("resumed session peer chain length changed", + firstCerts.length, resumedCerts.length); + + for (int i = 0; i < firstCerts.length; i++) { + assertArrayEquals("resumed session cert mismatch at index " + i, + firstCerts[i].getEncoded(), resumedCerts[i].getEncoded()); + } + + } finally { + if (originalProp != null && !originalProp.isEmpty()) { + Security.setProperty( + "wolfjsse.clientSessionCache.disabled", originalProp); + } + } + } + + /** + * Verify peer certs match the expected chain. + */ + private void checkPeerChain(Certificate[] peerCerts, + Certificate[] expectedCerts, String desc) + throws CertificateException { + + byte[][] derChain; + + assertNotNull(peerCerts); + + derChain = new byte[peerCerts.length][]; + for (int i = 0; i < peerCerts.length; i++) { + derChain[i] = peerCerts[i].getEncoded(); + } + + checkPeerChainDER(derChain, expectedCerts, desc); + } + + /** + * Verify DER encoded peer certs match the expected chain. Only the peer + * cert is available when native wolfSSL has not been compiled with + * SESSION_CERTS. + */ + private void checkPeerChainDER(byte[][] derChain, + Certificate[] expectedCerts, String desc) + throws CertificateException { + + assertNotNull(derChain); + assertEquals(desc + ": unexpected peer chain length", + expectedCerts.length, derChain.length); + + for (int i = 0; i < expectedCerts.length; i++) { + assertArrayEquals(desc + ": cert mismatch at index " + i, + expectedCerts[i].getEncoded(), derChain[i]); + } + } + + /** + * Test that SSLSession.getPeerCertificates() reports the peer cert at + * index zero when the peer cert is large. + * + * Native wolfSSL skips certs of MAX_X509_SIZE or larger when storing the + * peer chain. The peer's own cert is skipped the same way, which would + * otherwise leave the issuing CA at index zero and make both + * getPeerCertificates()[0] and getPeerPrincipal() report the CA. + * + * The generated leaf clears the 2048 byte MAX_X509_SIZE default, so the + * skip is exercised on default native wolfSSL builds. Builds raising + * MAX_X509_SIZE, for example those enabling ML-DSA (8192), store the leaf + * normally and exercise the ordinary path, since native wolfSSL cannot + * generate a cert larger than WC_MAX_X509_GEN (4096 bytes). + */ + @Test + @SuppressWarnings("removal") + public void testGetPeerCertificatesLargePeerCert() + throws NoSuchAlgorithmException, KeyManagementException, + KeyStoreException, CertificateException, IOException, + NoSuchProviderException, UnrecoverableKeyException, + WolfSSLException, WolfSSLJNIException { + + int ret; + KeyStore[] stores; + X509Certificate leafCert; + SSLContext ctxClient; + SSLContext ctxServer; + SSLEngine client; + SSLEngine server; + SSLSession session; + Certificate[] peerCerts; + + Assume.assumeFalse(WolfSSLTestFactory.isAndroid()); + /* Without SESSION_CERTS no chain stored, skip test */ + Assume.assumeTrue(WolfSSL.sessionCertsEnabled()); + + stores = tf.generateLargeLeafChain(); + leafCert = (X509Certificate)stores[0].getCertificateChain( + WolfSSLTestFactory.largeLeafAlias)[0]; + + /* Sanity check the leaf really is over the MAX_X509_SIZE default, + * otherwise this duplicates the normal peer chain test */ + if (leafCert.getEncoded().length <= 2048) { + fail("generated leaf certificate too small to test large peer " + + "certificate handling, size " + + leafCert.getEncoded().length); + } + + ctxServer = tf.createSSLContext("TLS", engineProvider, + tf.createTrustManager("SunX509", stores[1], engineProvider), + tf.createKeyManager("SunX509", stores[0], engineProvider)); + /* No client key material, NoDefaults avoids both the test factory + * default and the WolfSSLAuthStore default substitution */ + ctxClient = tf.createSSLContextNoDefaults("TLS", engineProvider, + tf.createTrustManager("SunX509", stores[1], engineProvider), null); + + client = ctxClient.createSSLEngine("large.example.com", 11111); + server = ctxServer.createSSLEngine(); + + client.setUseClientMode(true); + server.setUseClientMode(false); + server.setNeedClientAuth(false); + + ret = tf.testConnection(server, client, null, null, + "Test large peer cert"); + if (ret != 0) { + fail("failed to connect with large peer certificate"); + } + + session = client.getSession(); + + try { + peerCerts = session.getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + fail("failed to get peer certificates: " + e); + return; + } + + assertNotNull(peerCerts); + /* [leaf, CA] whether or not native skipped the leaf, so a silent + * fall back to the peer cert alone shows up here */ + assertEquals("unexpected peer chain length", 2, peerCerts.length); + + /* Peer cert must be first */ + assertArrayEquals("peer cert at index 0 was not the peer's own cert", + leafCert.getEncoded(), peerCerts[0].getEncoded()); + + /* getPeerPrincipal() re-reads index zero */ + try { + assertEquals("getPeerPrincipal() did not match peer cert subject", + leafCert.getSubjectX500Principal(), session.getPeerPrincipal()); + } catch (SSLPeerUnverifiedException e) { + fail("failed to get peer principal: " + e); + } + + /* Deprecated API reports the same peer cert */ + javax.security.cert.X509Certificate[] oldCerts = null; + try { + oldCerts = session.getPeerCertificateChain(); + assertNotNull(oldCerts); + assertArrayEquals("deprecated API peer cert at index 0 was not " + + "the peer's own cert", leafCert.getEncoded(), + oldCerts[0].getEncoded()); + } catch (SSLPeerUnverifiedException | + javax.security.cert.CertificateEncodingException e) { + fail("failed to get peer certificate chain: " + e); + } finally { + freeX509XCerts(oldCerts); + } + } + + /** + * Free native memory held by WolfSSLX509X certs from the deprecated + * getPeerCertificateChain(), rather than waiting on the finalizer. + */ + @SuppressWarnings("removal") + private void freeX509XCerts(javax.security.cert.X509Certificate[] certs) { + + if (certs == null) { + return; + } + + for (int i = 0; i < certs.length; i++) { + if (certs[i] instanceof WolfSSLX509X) { + ((WolfSSLX509X)certs[i]).free(); + } + } + } + @Test public void testSessionContext() throws NoSuchAlgorithmException, KeyManagementException, diff --git a/src/test/com/wolfssl/provider/jsse/test/WolfSSLTestFactory.java b/src/test/com/wolfssl/provider/jsse/test/WolfSSLTestFactory.java index d5f92fca6..35a662d25 100644 --- a/src/test/com/wolfssl/provider/jsse/test/WolfSSLTestFactory.java +++ b/src/test/com/wolfssl/provider/jsse/test/WolfSSLTestFactory.java @@ -1012,6 +1012,136 @@ protected KeyStore generateSelfSignedCertJKS(String commonName, return store; } + /** + * Alias used for the leaf entry in the KeyStore returned by + * generateLargeLeafChain(). + */ + protected final static String largeLeafAlias = "large-leaf"; + + /** + * Generate a [leaf, CA] certificate chain where the leaf is larger than + * the default native wolfSSL MAX_X509_SIZE of 2048 bytes. + * + * Native wolfSSL skips certificates of MAX_X509_SIZE or larger when + * storing the peer chain, including the peer's own certificate, which + * leaves the stored chain starting at the issuing CA. This chain is used + * to check that SSLSession.getPeerCertificates() still reports the peer + * certificate at index zero in that case. + * + * The leaf is grown with Subject Alt Names. Native wolfSSL cannot + * generate certificates larger than WC_MAX_X509_GEN (4096 bytes), so the + * leaf only crosses MAX_X509_SIZE on builds leaving it at the 2048 byte + * default. Builds raising it, such as those enabling ML-DSA (8192), store + * this leaf into the peer chain normally and exercise the ordinary path. + * + * @return two element KeyStore array. Index zero holds the leaf private + * key with the [leaf, CA] chain under alias largeLeafAlias. + * Index one holds the CA certificate, for use as a trust store. + * + * @throws CertificateException on error generating certificates + * @throws WolfSSLException on native wolfSSL error + * @throws WolfSSLJNIException on native JNI error + * @throws NoSuchAlgorithmException on error generating key pairs + * @throws IOException on error loading KeyStore objects + * @throws KeyStoreException on error loading KeyStore objects + */ + protected KeyStore[] generateLargeLeafChain() + throws CertificateException, WolfSSLException, + NoSuchAlgorithmException, IOException, KeyStoreException, + WolfSSLJNIException { + + int i; + /* Sized to stay under WC_MAX_X509_GEN while clearing the 2048 byte + * default MAX_X509_SIZE */ + final int numAltNames = 80; + final String leafCN = "large.example.com"; + + KeyPairGenerator kpg = null; + KeyPair caPair = null; + KeyPair leafPair = null; + WolfSSLCertificate caX509 = null; + WolfSSLCertificate leafX509 = null; + WolfSSLX509Name caName = null; + WolfSSLX509Name leafName = null; + X509Certificate caCert = null; + X509Certificate leafCert = null; + KeyStore keyStore = null; + KeyStore trustStore = null; + + Instant now = Instant.now(); + final Date notBefore = Date.from(now); + final Date notAfter = Date.from(now.plus(Duration.ofDays(365))); + + try { + + kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + + /* Self-signed CA */ + caPair = kpg.generateKeyPair(); + caX509 = new WolfSSLCertificate(); + caName = generateTestSubjectName("Large Leaf Test CA"); + + caX509.setNotBefore(notBefore); + caX509.setNotAfter(notAfter); + caX509.setSerialNumber(BigInteger.valueOf(9001)); + caX509.setSubjectName(caName); + caX509.setPublicKey(caPair.getPublic()); + caX509.addExtension(WolfSSL.NID_basic_constraints, true, true); + caX509.signCert(caPair.getPrivate(), "SHA256"); + caCert = caX509.getX509Certificate(); + + /* Leaf signed by the CA above, grown with Subject Alt Names */ + leafPair = kpg.generateKeyPair(); + leafX509 = new WolfSSLCertificate(); + leafName = generateTestSubjectName(leafCN); + + leafX509.setNotBefore(notBefore); + leafX509.setNotAfter(notAfter); + leafX509.setSerialNumber(BigInteger.valueOf(9002)); + leafX509.setSubjectName(leafName); + leafX509.setIssuerName(caCert); + leafX509.setPublicKey(leafPair.getPublic()); + leafX509.addExtension(WolfSSL.NID_basic_constraints, false, true); + + /* CN first, CN is ignored once SAN is present (RFC 6125) */ + leafX509.addAltName(leafCN, WolfSSL.ASN_DNS_TYPE); + for (i = 0; i < numAltNames; i++) { + leafX509.addAltName("host" + i + ".large.example.com", + WolfSSL.ASN_DNS_TYPE); + } + + leafX509.signCert(caPair.getPrivate(), "SHA256"); + leafCert = leafX509.getX509Certificate(); + + keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, jksPass); + keyStore.setKeyEntry(largeLeafAlias, leafPair.getPrivate(), jksPass, + new X509Certificate[] { leafCert, caCert }); + + trustStore = KeyStore.getInstance(keyStoreType); + trustStore.load(null, jksPass); + trustStore.setCertificateEntry("large-ca", caCert); + + return new KeyStore[] { keyStore, trustStore }; + + } finally { + /* Free native memory */ + if (caName != null) { + caName.free(); + } + if (leafName != null) { + leafName.free(); + } + if (caX509 != null) { + caX509.free(); + } + if (leafX509 != null) { + leafX509.free(); + } + } + } + /** * Returns the DER encoded buffer of the certificate * @param alias lookup alias in allJKS diff --git a/src/test/com/wolfssl/test/WolfSSLSessionTest.java b/src/test/com/wolfssl/test/WolfSSLSessionTest.java index 80fc0a3a4..811540976 100644 --- a/src/test/com/wolfssl/test/WolfSSLSessionTest.java +++ b/src/test/com/wolfssl/test/WolfSSLSessionTest.java @@ -55,6 +55,7 @@ import com.wolfssl.WolfSSL; import com.wolfssl.WolfSSLDebug; +import com.wolfssl.WolfSSLCertificate; import com.wolfssl.WolfSSLContext; import com.wolfssl.WolfSSLException; import com.wolfssl.WolfSSLJNIException; @@ -1255,6 +1256,224 @@ public Void call() throws Exception { } } + @Test + public void test_WolfSSLSession_getPeerCertificateChainDER() + throws WolfSSLJNIException { + + int ret, err; + WolfSSLSession ssl = null; + Socket cliSock = null; + byte[][] chain = null; + WolfSSLContext srvCtx = null; + WolfSSLContext cliCtx = null; + ServerSocket srvSocket = null; + ExecutorService es = null; + + try { + /* Create ServerSocket first to get ephemeral port, set + * 10 sec timeout on accept() to prevent test hangs. */ + srvSocket = new ServerSocket(0); + srvSocket.setSoTimeout(10000); + + final int port = srvSocket.getLocalPort(); + final ServerSocket finalSrvSocket = srvSocket; + + /* Server loads server-cert.pem, which holds the server cert + * followed by the CA that signed it. The client should get both + * back from the peer chain. */ + srvCtx = createAndSetupWolfSSLContext(srvCert, srvKey, + WolfSSL.SSL_FILETYPE_PEM, caCert, + WolfSSL.TLSv1_2_ServerMethod()); + cliCtx = createAndSetupWolfSSLContext(cliCert, cliKey, + WolfSSL.SSL_FILETYPE_PEM, caCert, + WolfSSL.TLSv1_2_ClientMethod()); + final WolfSSLContext finalSrvCtx = srvCtx; + + ssl = new WolfSSLSession(cliCtx); + + /* Peer chain should not be available before the handshake */ + chain = ssl.getPeerCertificateChainDER(); + if (chain != null) { + fail("Peer chain should be null before connection"); + } + + /* Latch to signal when server is ready to accept */ + final CountDownLatch serverReady = new CountDownLatch(1); + + /* Start server thread */ + es = Executors.newSingleThreadExecutor(); + Future serverFuture = es.submit(new Callable() { + @Override + public Void call() throws Exception { + int ret; + int err; + Socket server = null; + WolfSSLSession srvSes = null; + + try { + /* Signal that we're ready to accept */ + serverReady.countDown(); + server = finalSrvSocket.accept(); + srvSes = new WolfSSLSession(finalSrvCtx); + + ret = srvSes.setFd(server); + if (ret != WolfSSL.SSL_SUCCESS) { + throw new Exception( + "WolfSSLSession.setFd() failed: " + ret); + } + + do { + ret = srvSes.accept(); + err = srvSes.getError(ret); + } while (ret != WolfSSL.SSL_SUCCESS && + (err == WolfSSL.SSL_ERROR_WANT_READ || + err == WolfSSL.SSL_ERROR_WANT_WRITE)); + + if (ret != WolfSSL.SSL_SUCCESS) { + throw new Exception( + "WolfSSLSession.accept() failed: " + ret); + } + + srvSes.shutdownSSL(); + + } finally { + if (srvSes != null) { + srvSes.freeSSL(); + } + if (server != null) { + server.close(); + } + finalSrvSocket.close(); + } + + return null; + } + }); + + /* Wait for server thread to be ready before connecting */ + if (!serverReady.await(5, TimeUnit.SECONDS)) { + fail("Server thread did not become ready in time"); + } + + /* Client connects to local server */ + cliSock = new Socket("localhost", port); + + ret = ssl.setFd(cliSock); + if (ret != WolfSSL.SSL_SUCCESS) { + fail("Failed to set file descriptor"); + } + + 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)); + + if (ret != WolfSSL.SSL_SUCCESS) { + fail("Failed WolfSSL.connect() to localhost"); + } + + /* Wait for server thread to complete and check for errors */ + serverFuture.get(5, TimeUnit.SECONDS); + + chain = ssl.getPeerCertificateChainDER(); + + int expectedSz = 1; + + if (chain == null) { + fail("Peer certificate chain should not be null"); + } + + /* server-cert.pem holds the server cert plus the CA that signed + * it. Without SESSION_CERTS native wolfSSL stores no chain and + * only the peer cert is returned. */ + if (WolfSSL.sessionCertsEnabled()) { + expectedSz = 2; + } + if (chain.length != expectedSz) { + fail("Expected " + expectedSz + " certs in peer chain, got " + + chain.length); + } + + for (int i = 0; i < chain.length; i++) { + if (chain[i] == null || chain[i].length == 0) { + fail("Peer chain entry " + i + " was empty"); + } + } + + byte[] peerDer = ssl.getPeerCertificateDER(); + if (peerDer == null) { + fail("Unable to get peer certificate DER"); + } + if (!Arrays.equals(peerDer, chain[0])) { + fail("First cert in chain did not match peer cert"); + } + + if (WolfSSL.sessionCertsEnabled()) { + /* Second cert is the CA that signed the server cert, which + * the test setup also loads as the client trusted CA */ + byte[] caDer = getDerFromCertFile(caCert); + if (caDer == null) { + fail("Unable to read CA certificate DER"); + } + if (!Arrays.equals(caDer, chain[1])) { + fail("Second cert in chain was not the issuing CA"); + } + } + + ssl.shutdownSSL(); + + } catch (Exception e) { + e.printStackTrace(); + fail("Failed getPeerCertificateChainDER test: " + e.getMessage()); + + } finally { + /* Clean up resources */ + if (ssl != null) { + ssl.freeSSL(); + } + if (cliSock != null) { + try { cliSock.close(); } catch (Exception e) { } + } + if (srvSocket != null) { + try { srvSocket.close(); } catch (Exception e) { } + } + if (es != null) { + es.shutdown(); + try { + es.awaitTermination(5, TimeUnit.SECONDS); + } catch (Exception e) { } + } + if (cliCtx != null) { + cliCtx.free(); + } + if (srvCtx != null) { + srvCtx.free(); + } + } + + } + + /** + * Read a PEM certificate file and return its DER encoding. + */ + private byte[] getDerFromCertFile(String path) + throws WolfSSLException, WolfSSLJNIException { + + byte[] der = null; + WolfSSLCertificate cert = null; + + cert = new WolfSSLCertificate(path, WolfSSL.SSL_FILETYPE_PEM); + try { + der = cert.getDer(); + } finally { + cert.free(); + } + + return der; + } + @Test public void test_WolfSSLSession_useSecureRenegotiation() throws WolfSSLJNIException { diff --git a/src/test/com/wolfssl/test/WolfSSLTest.java b/src/test/com/wolfssl/test/WolfSSLTest.java index c20272f33..78dfc3df4 100644 --- a/src/test/com/wolfssl/test/WolfSSLTest.java +++ b/src/test/com/wolfssl/test/WolfSSLTest.java @@ -582,6 +582,18 @@ public void test_PQC_FeatureDetect_NativeReturns() { } } + @Test + public void test_SessionCerts_FeatureDetect_NativeReturns() { + + /* Native call should be reachable, a broken JNI binding shows up + * as UnsatisfiedLinkError. */ + boolean sessionCerts = WolfSSL.sessionCertsEnabled(); + + if (sessionCerts) { + /* N/A, keep to prevent unused var warning */ + } + } + /* Logging callback used by the setLoggingCb test. Uses a shared static * counter so invocations across swapped instances accumulate. */ static class TestLoggingCallback implements WolfSSLLoggingCallback {