From 12e4bc51c60107218b792da46991505539e291fc Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 28 Apr 2020 08:27:55 -0400 Subject: [PATCH 01/11] Add SSL.SetNextProtoNeg() Signed-off-by: Alexander Scheel --- lib/jss.map | 1 + org/mozilla/jss/nss/SSL.c | 26 ++++++++++++++++++++++++++ org/mozilla/jss/nss/SSL.java | 7 +++++++ 3 files changed, 34 insertions(+) diff --git a/lib/jss.map b/lib/jss.map index 3d4576425..848bbefaa 100644 --- a/lib/jss.map +++ b/lib/jss.map @@ -503,6 +503,7 @@ JSS_4.8.0 { global: Java_org_mozilla_jss_crypto_JSSOAEPParameterSpec_acquireNativeResources; Java_org_mozilla_jss_crypto_JSSOAEPParameterSpec_releaseNativeResources; +Java_org_mozilla_jss_nss_SSL_SetNextProtoNeg; local: *; }; diff --git a/org/mozilla/jss/nss/SSL.c b/org/mozilla/jss/nss/SSL.c index c54756ae8..cda853a14 100644 --- a/org/mozilla/jss/nss/SSL.c +++ b/org/mozilla/jss/nss/SSL.c @@ -816,6 +816,32 @@ Java_org_mozilla_jss_nss_SSL_KeyUpdate(JNIEnv *env, jclass clazz, return SSL_KeyUpdate(real_fd, requestUpdate == JNI_TRUE ? PR_TRUE : PR_FALSE); } +JNIEXPORT jint JNICALL +Java_org_mozilla_jss_nss_SSL_SetNextProtoNeg(JNIEnv *env, jclass clazz, + jobject fd, jbyteArray wire_data) +{ + PRFileDesc *real_fd = NULL; + uint8_t *data = NULL; + size_t data_length = 0; + SECStatus ret = SECFailure; + + PR_ASSERT(env != NULL && fd != NULL && wire_data != NULL); + PR_SetError(0, 0); + + if (JSS_PR_getPRFileDesc(env, fd, &real_fd) != PR_SUCCESS) { + return ret; + } + + if (!JSS_FromByteArray(env, wire_data, &data, &data_length)) { + return ret; + } + + ret = SSL_SetNextProtoNego(real_fd, data, data_length); + free(data); + + return ret; +} + JNIEXPORT jint JNICALL Java_org_mozilla_jss_nss_SSL_AttachClientCertCallback(JNIEnv *env, jclass clazz, jobject fd) diff --git a/org/mozilla/jss/nss/SSL.java b/org/mozilla/jss/nss/SSL.java index e9ff5e1e5..9ba9fe3a9 100644 --- a/org/mozilla/jss/nss/SSL.java +++ b/org/mozilla/jss/nss/SSL.java @@ -410,6 +410,13 @@ public synchronized static native int ConfigServerSessionIDCache(int maxCacheEnt */ public static native int KeyUpdate(SSLFDProxy fd, boolean requestUpdate); + /** + * Sets the next protocol negotiation (ALPN) in wire format. + * + * See also: SSL_SetNextProtoNego in /usr/include/nss3/ssl.h. + */ + public static native int SetNextProtoNeg(SSLFDProxy fd, byte[] wire_data); + /** * Use client authentication; set client certificate from SSLFDProxy. * From 326dc83642d95dafc241a4db0e5e10d4d7e67c31 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 28 Apr 2020 17:02:52 -0400 Subject: [PATCH 02/11] Add SSLNextProtoState enum Signed-off-by: Alexander Scheel --- org/mozilla/jss/ssl/SSLNextProtoState.java | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 org/mozilla/jss/ssl/SSLNextProtoState.java diff --git a/org/mozilla/jss/ssl/SSLNextProtoState.java b/org/mozilla/jss/ssl/SSLNextProtoState.java new file mode 100644 index 000000000..a1a3b04a0 --- /dev/null +++ b/org/mozilla/jss/ssl/SSLNextProtoState.java @@ -0,0 +1,29 @@ +package org.mozilla.jss.ssl; + +public enum SSLNextProtoState { + SSL_NEXT_PROTO_NO_SUPPORT (0), + SSL_NEXT_PROTO_NEGOTIATED (1), + SSL_NEXT_PROTO_NO_OVERLAP (2), + SSL_NEXT_PROTO_SELECTED (3), + SSL_NEXT_PROTO_EARLY_VALUE (4); + + private int value; + + private SSLNextProtoState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static SSLNextProtoState valueOf(int value) { + for (SSLNextProtoState type : SSLNextProtoState.values()) { + if (type.value == value) { + return type; + } + } + + return null; + } +} From d38661f42945694051b0f9ea4c83331fb90b2826 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 28 Apr 2020 18:19:48 -0400 Subject: [PATCH 03/11] Add SSL.GetNextProto() Signed-off-by: Alexander Scheel --- lib/jss.map | 1 + org/mozilla/jss/nss/NextProtoResult.java | 41 ++++++++++++++++ org/mozilla/jss/nss/SSL.c | 59 ++++++++++++++++++++++++ org/mozilla/jss/nss/SSL.java | 7 +++ org/mozilla/jss/util/java_ids.h | 6 +++ 5 files changed, 114 insertions(+) create mode 100644 org/mozilla/jss/nss/NextProtoResult.java diff --git a/lib/jss.map b/lib/jss.map index 848bbefaa..457808e0f 100644 --- a/lib/jss.map +++ b/lib/jss.map @@ -504,6 +504,7 @@ JSS_4.8.0 { Java_org_mozilla_jss_crypto_JSSOAEPParameterSpec_acquireNativeResources; Java_org_mozilla_jss_crypto_JSSOAEPParameterSpec_releaseNativeResources; Java_org_mozilla_jss_nss_SSL_SetNextProtoNeg; +Java_org_mozilla_jss_nss_SSL_GetNextProto; local: *; }; diff --git a/org/mozilla/jss/nss/NextProtoResult.java b/org/mozilla/jss/nss/NextProtoResult.java new file mode 100644 index 000000000..32b844d55 --- /dev/null +++ b/org/mozilla/jss/nss/NextProtoResult.java @@ -0,0 +1,41 @@ +package org.mozilla.jss.nss; + +import java.lang.StringBuilder; +import java.util.Arrays; + +import org.mozilla.jss.ssl.SSLNextProtoState; + +/** + * The fields in the NextProtoResult indicate whether a given SSL-enabled + * PRFileDesc has negotiated a next protocol (via ALPN) and if so, what it + * is. + * + * These object is returned by org.mozilla.jss.nss.SSL.GetNextProto(fd). + * This is a native method; note that updating the constructor will require + * modifying util/java_ids.h and nss/SSL.c + */ +public class NextProtoResult { + public SSLNextProtoState state; + public byte[] protocol; + + public NextProtoResult(int state_value, byte[] protocol) { + state = SSLNextProtoState.valueOf(state_value); + this.protocol = protocol; + } + + public String getProtocol() { + if (protocol == null || protocol.length == 0) { + return null; + } + + return new String(protocol); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("State: " + state + "\n"); + sb.append("Protocol: " + getProtocol() + " "); + sb.append(Arrays.toString(protocol) + "\n"); + return sb.toString(); + } +} diff --git a/org/mozilla/jss/nss/SSL.c b/org/mozilla/jss/nss/SSL.c index cda853a14..1a14b0109 100644 --- a/org/mozilla/jss/nss/SSL.c +++ b/org/mozilla/jss/nss/SSL.c @@ -140,6 +140,40 @@ jobject JSS_NewSSLPreliminaryChannelInfo(JNIEnv *env, jlong valuesSet, return result; } +jobject JSS_NewNextProtoResult(JNIEnv *env, int state, uint8_t *proto, + unsigned int proto_len) +{ + jclass resultClass; + jmethodID constructor; + jobject result = NULL; + jbyteArray protocol = NULL; + + PR_ASSERT(env != NULL); + + resultClass = (*env)->FindClass(env, NEXT_PROTO_CLASS_NAME); + if (resultClass == NULL) { + ASSERT_OUTOFMEM(env); + goto finish; + } + + constructor = (*env)->GetMethodID(env, resultClass, PLAIN_CONSTRUCTOR, + NEXT_PROTO_CONSTRUCTOR_SIG); + if (constructor == NULL) { + ASSERT_OUTOFMEM(env); + goto finish; + } + + if (proto != NULL) { + protocol = JSS_ToByteArray(env, proto, proto_len); + } + + result = (*env)->NewObject(env, resultClass, constructor, state, + protocol); + +finish: + return result; +} + JNIEXPORT jobject JNICALL Java_org_mozilla_jss_nss_SSL_ImportFD(JNIEnv *env, jclass clazz, jobject model, jobject fd) @@ -842,6 +876,31 @@ Java_org_mozilla_jss_nss_SSL_SetNextProtoNeg(JNIEnv *env, jclass clazz, return ret; } +JNIEXPORT jobject JNICALL +Java_org_mozilla_jss_nss_SSL_GetNextProto(JNIEnv *env, jclass clazz, + jobject fd) +{ + PRFileDesc *real_fd = NULL; + SSLNextProtoState state; + uint8_t proto[255] = {0}; + unsigned int proto_len; + SECStatus ret; + + PR_ASSERT(env != NULL && fd != NULL); + PR_SetError(0, 0); + + if (JSS_PR_getPRFileDesc(env, fd, &real_fd) != PR_SUCCESS) { + return NULL; + } + + ret = SSL_GetNextProto(real_fd, &state, proto, &proto_len, + sizeof(proto)); + if (ret != SECSuccess) { + return JSS_NewNextProtoResult(env, state, NULL, 0); + } + return JSS_NewNextProtoResult(env, state, proto, proto_len); +} + JNIEXPORT jint JNICALL Java_org_mozilla_jss_nss_SSL_AttachClientCertCallback(JNIEnv *env, jclass clazz, jobject fd) diff --git a/org/mozilla/jss/nss/SSL.java b/org/mozilla/jss/nss/SSL.java index 9ba9fe3a9..a1c892d08 100644 --- a/org/mozilla/jss/nss/SSL.java +++ b/org/mozilla/jss/nss/SSL.java @@ -417,6 +417,13 @@ public synchronized static native int ConfigServerSessionIDCache(int maxCacheEnt */ public static native int SetNextProtoNeg(SSLFDProxy fd, byte[] wire_data); + /** + * Gets the next negotiated protocol. + * + * See also: SSL_GetNextProto in /usr/include/nss3/ssl.h. + */ + public static native NextProtoResult GetNextProto(SSLFDProxy fd); + /** * Use client authentication; set client certificate from SSLFDProxy. * diff --git a/org/mozilla/jss/util/java_ids.h b/org/mozilla/jss/util/java_ids.h index de920dd77..06891262f 100644 --- a/org/mozilla/jss/util/java_ids.h +++ b/org/mozilla/jss/util/java_ids.h @@ -444,6 +444,12 @@ PR_BEGIN_EXTERN_C #define SSL_PRELIMINARY_CHANNEL_INFO_CLASS_NAME "org/mozilla/jss/nss/SSLPreliminaryChannelInfo" #define SSL_PRELIMINARY_CHANNEL_INFO_CONSTRUCTOR_SIG "(JIIZJZIZZII)V" +/* + * NextProtoResult classes + */ +#define NEXT_PROTO_CLASS_NAME "org/mozilla/jss/nss/NextProtoResult" +#define NEXT_PROTO_CONSTRUCTOR_SIG "(I[B)V" + PR_END_EXTERN_C #endif From 1f5f18af0bcd9eb86238b5c97ecb9d0caa7b7ecb Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 28 Apr 2020 08:27:07 -0400 Subject: [PATCH 04/11] Implement intial ALPN support in JSSEngine Signed-off-by: Alexander Scheel --- org/mozilla/jss/ssl/javax/JSSEngine.java | 63 +++++++++++++++++++ .../jss/ssl/javax/JSSEngineReferenceImpl.java | 41 +++++++++++- org/mozilla/jss/ssl/javax/JSSParameters.java | 29 +++++++++ org/mozilla/jss/ssl/javax/JSSSession.java | 10 +++ 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/org/mozilla/jss/ssl/javax/JSSEngine.java b/org/mozilla/jss/ssl/javax/JSSEngine.java index f24901cc0..b326d764f 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngine.java +++ b/org/mozilla/jss/ssl/javax/JSSEngine.java @@ -186,6 +186,11 @@ public abstract class JSSEngine extends javax.net.ssl.SSLEngine { */ private final static AtomicBoolean sessionCacheInitialized = new AtomicBoolean(); + /** + * Set of possible application protocols to negotiate. + */ + protected String[] alpn_protocols; + /** * Constructor for a JSSEngine, providing no hints for an internal * session reuse strategy and no key. @@ -383,6 +388,12 @@ public void setSSLParameters(SSLParameters params) { if (parsed.getHostname() != null) { setHostname(parsed.getHostname()); } + + // When we have a non-zero number of ALPNs, use them in the + // negotiation. + if (parsed.getApplicationProtocols() != null) { + setApplicationProtocols(parsed.getApplicationProtocols()); + } } /** @@ -905,6 +916,58 @@ public boolean getWantClientAuth() { return want_client_auth; } + /** + * Set a specific list of protocols to negotiate next for ALPN support. + */ + public void setApplicationProtocols(String[] protocols) { + alpn_protocols = protocols; + } + + /** + * Get the most recently negotiated application protocol. + * + * Note that NSS only allows selection on the initial handshake so + * this is implemented via a call to getHandshakeApplicationProtocol(). + */ + public String getApplicationProtocol() { + return getHandshakeApplicationProtocol(); + } + + /** + * Get the application protocol negotiated during the initial handshake. + */ + public String getHandshakeApplicationProtocol() { + if (session == null) { + return null; + } + + return session.getNextProtocol(); + } + + /** + * Helper method for implementations: encodes ALPN protocols into wire + * format (8-bit length prefixed byte encoding). + */ + public byte[] getALPNWireData() { + int length = 0; + for (String protocol : alpn_protocols) { + length += 1 + protocol.getBytes().length; + } + + byte[] result = new byte[length]; + int offset = 0; + + for (String protocol : alpn_protocols) { + byte[] p_bytes = protocol.getBytes(); + result[offset] = (byte) p_bytes.length; + offset += 1; + System.arraycopy(p_bytes, 0, result, offset, p_bytes.length); + offset += p_bytes.length; + } + + return result; + } + /** * Query whether or not the inbound side of this connection is closed. */ diff --git a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java index 47edf08a7..4990bd1b9 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java +++ b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java @@ -347,8 +347,14 @@ private void createBufferFD() throws SSLException { throw new SSLException("Unable to enable SSL Handshake Callback on this SSLFDProxy instance."); } - // Pass this ssl_fd to the session object so that we can use - // SSL methods to invalidate the session. + if (alpn_protocols != null) { + byte[] wire_data = getALPNWireData(); + + ret = SSL.SetNextProtoNeg(ssl_fd, wire_data); + if (ret == SSL.SECFailure) { + throw new RuntimeException("JSSEngine.init(): Unable to set ALPN protocol list."); + } + } } private void initClient() throws SSLException { @@ -937,6 +943,37 @@ private int putData(byte[] data, ByteBuffer[] buffers, int offset, int length) { return data_index; } + private void updateSession() { + if (ssl_fd == null) { + return; + } + + try { + PK11Cert[] peer_chain = SSL.PeerCertificateChain(ssl_fd); + session.setPeerCertificates(peer_chain); + + SSLChannelInfo info = SSL.GetChannelInfo(ssl_fd); + if (info == null) { + return; + } + + session.setId(info.getSessionID()); + session.refreshData(); + + NextProtoResult ret = SSL.GetNextProto(ssl_fd); + if (ret != null) { + // Only advertise the resulting protocol if we have one. + if (ret.state != SSLNextProtoState.SSL_NEXT_PROTO_NO_SUPPORT && + ret.state != SSLNextProtoState.SSL_NEXT_PROTO_NO_OVERLAP) + { + session.setNextProtocol(ret.getProtocol()); + } + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + private SSLException checkSSLAlerts() { debug("JSSEngine: Checking inbound and outbound SSL Alerts. Have " + ssl_fd.inboundAlerts.size() + " inbound and " + ssl_fd.outboundAlerts.size() + " outbound alerts."); diff --git a/org/mozilla/jss/ssl/javax/JSSParameters.java b/org/mozilla/jss/ssl/javax/JSSParameters.java index ce49db952..88f839dc3 100644 --- a/org/mozilla/jss/ssl/javax/JSSParameters.java +++ b/org/mozilla/jss/ssl/javax/JSSParameters.java @@ -26,6 +26,7 @@ public class JSSParameters extends SSLParameters { private SSLVersionRange range; private String alias; private String hostname; + private String[] appProtocols; public JSSParameters() { // Choose our default set of SSLParameters here; default to null @@ -54,6 +55,11 @@ public JSSParameters(SSLParameters downcast) { if (downcast.getNeedClientAuth()) { setNeedClientAuth(downcast.getNeedClientAuth()); } + + String[] alpn = downcast.getApplicationProtocols(downcast); + if (alpn != null) { + setApplicationProtocols(alpn); + } } public JSSParameters(String[] cipherSuites) { @@ -190,4 +196,27 @@ public String getHostname() { public void setHostname(String server_hostname) { hostname = server_hostname; } + + public void setApplicationProtocols(String[] protocols) throws IllegalArgumentException { + if (protocols == null) { + appProtocols = null; + return; + } + + int index = 0; + for (String protocol : protocols) { + if (protocol.length() > 255 || protocol.getBytes().length > 255) { + String msg = "Invalid application protocol " + protocol; + msg += ": standard allows up to 255 characters but was "; + msg += protocol.length(); + throw new IllegalArgumentException(msg); + } + } + + appProtocols = protocols; + } + + public String[] getApplicationProtocols() { + return appProtocols; + } } diff --git a/org/mozilla/jss/ssl/javax/JSSSession.java b/org/mozilla/jss/ssl/javax/JSSSession.java index 79d3431c7..753dd8889 100644 --- a/org/mozilla/jss/ssl/javax/JSSSession.java +++ b/org/mozilla/jss/ssl/javax/JSSSession.java @@ -40,6 +40,8 @@ public class JSSSession implements SSLSession, AutoCloseable { private boolean closed; + private String nextProtocol; + protected JSSSession(JSSEngine engine, int buffer_size) { this.parent = engine; @@ -292,4 +294,12 @@ public int getPeerPort() { public void setPeerPort(int port) { peerPort = port; } + + public void setNextProtocol(String protocol) { + nextProtocol = protocol; + } + + public String getNextProtocol() { + return nextProtocol; + } } From 784501aaa2ebe61c2d42c6f9ba1eafaf9147b1fd Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 28 Apr 2020 08:27:47 -0400 Subject: [PATCH 05/11] Add test for JSSEngine.getALPNWireData() Signed-off-by: Alexander Scheel --- org/mozilla/jss/tests/TestSSLEngine.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/org/mozilla/jss/tests/TestSSLEngine.java b/org/mozilla/jss/tests/TestSSLEngine.java index 302e7751e..304fe3dbb 100644 --- a/org/mozilla/jss/tests/TestSSLEngine.java +++ b/org/mozilla/jss/tests/TestSSLEngine.java @@ -1003,6 +1003,20 @@ public static void testNativeClientServer(String[] args) throws Exception { testJSSEToJSSHandshakes(ctx, server_alias); } + public static void testALPNEncoding() throws Exception { + JSSEngine eng = new JSSEngineReferenceImpl(); + + eng.setApplicationProtocols(new String[] { "http/1.1" }); + byte[] expectedHTTPOnly = new byte[] { 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31 }; + assert Arrays.equals(eng.getALPNWireData(), expectedHTTPOnly); + + eng = new JSSEngineReferenceImpl(); + + eng.setApplicationProtocols(new String[] { "http/1.1", "spdy/2" }); + byte[] expectedHTTPSpdy = new byte[] { 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x06, 0x73, 0x70, 0x64, 0x79, 0x2f, 0x32 }; + assert Arrays.equals(eng.getALPNWireData(), expectedHTTPSpdy); + } + public static void main(String[] args) throws Exception { // Args: // - nssdb @@ -1027,5 +1041,8 @@ public static void main(String[] args) throws Exception { System.out.println("Testing basic handshake with native TM..."); testNativeClientServer(args); + + System.out.println("Testing ALPN encoding..."); + testALPNEncoding(); } } From 31257d14db9d1d1909b08cee1e7f38f9e92d4871 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 28 Apr 2020 16:02:35 -0400 Subject: [PATCH 06/11] Add test for ALPN negotiation during handshake Signed-off-by: Alexander Scheel --- .../jss/ssl/javax/JSSEngineReferenceImpl.java | 7 ++-- org/mozilla/jss/tests/TestSSLEngine.java | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java index 4990bd1b9..f20ea0d64 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java +++ b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java @@ -349,10 +349,13 @@ private void createBufferFD() throws SSLException { if (alpn_protocols != null) { byte[] wire_data = getALPNWireData(); + if (wire_data == null) { + throw new RuntimeException("JSSEngine.init(): ALPN wire data is NULL but alpn_protocols is non-NULL."); + } ret = SSL.SetNextProtoNeg(ssl_fd, wire_data); - if (ret == SSL.SECFailure) { - throw new RuntimeException("JSSEngine.init(): Unable to set ALPN protocol list."); + if (ret != SSL.SECSuccess) { + throw new RuntimeException("JSSEngine.init(): Unable to set ALPN protocol list: " + errorText(PR.GetError()) + " " + ret); } } } diff --git a/org/mozilla/jss/tests/TestSSLEngine.java b/org/mozilla/jss/tests/TestSSLEngine.java index 304fe3dbb..df1a29ebd 100644 --- a/org/mozilla/jss/tests/TestSSLEngine.java +++ b/org/mozilla/jss/tests/TestSSLEngine.java @@ -978,6 +978,38 @@ public static void testPostHandshakeAuth(SSLContext ctx, String client_alias, St } } + public static void testALPNHandshake(SSLContext ctx, String server_alias) throws Exception { + JSSEngine client_eng = (JSSEngine) ctx.createSSLEngine(); + JSSParameters client_params = createParameters(); + client_params.setApplicationProtocols(new String[] { "http/1.1", "h2", "spdy/2" }); + client_eng.setSSLParameters(client_params); + client_eng.setUseClientMode(true); + + if (client_eng instanceof JSSEngineReferenceImpl) { + ((JSSEngineReferenceImpl) client_eng).setName("JSS Client for ALPN"); + } + + JSSEngine server_eng = (JSSEngine) ctx.createSSLEngine(); + JSSParameters server_params = createParameters(server_alias); + server_params.setApplicationProtocols(new String[] { "h2" }); + server_eng.setSSLParameters(server_params); + server_eng.setUseClientMode(false); + + if (server_eng instanceof JSSEngineReferenceImpl) { + ((JSSEngineReferenceImpl) server_eng).setName("JSS Server for ALPN"); + ((JSSEngineReferenceImpl) server_eng).enableSafeDebugLogging(7377); + } + + try { + testBasicHandshake(client_eng, server_eng, false); + assert(server_eng.getApplicationProtocol().equals("h2")); + } catch (Exception e) { + client_eng.cleanup(); + server_eng.cleanup(); + throw e; + } + } + public static void testBasicClientServer(String[] args) throws Exception { SSLContext ctx = SSLContext.getInstance("TLS", "Mozilla-JSS"); ctx.init(getKMs(), getTMs(), null); @@ -1001,6 +1033,7 @@ public static void testNativeClientServer(String[] args) throws Exception { testAllHandshakes(ctx, client_alias, server_alias, true); testPostHandshakeAuth(ctx, client_alias, server_alias); testJSSEToJSSHandshakes(ctx, server_alias); + testALPNHandshake(ctx, server_alias); } public static void testALPNEncoding() throws Exception { From 9c259f2cf6478d5048c32e50239d0e1256b5cb1d Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 6 Oct 2020 09:29:37 -0400 Subject: [PATCH 07/11] Introduce JDK8+ compatibility layer ALPN support merged in JDK9 and will eventually be backported into JDK8. However, not every consumer of JSS will necessarily upgrade to a newer JDK8 version. While support has landed upstream, Fedora 32 and Fedora 33 haven't yet seen a backport yet. This likely means that RHEL also won't see a backport. Introduce a small reflection-based compatibility layer to see if the class supports ALPN and if so, use the value from it. Signed-off-by: Alexander Scheel --- org/mozilla/jss/ssl/javax/JSSParameters.java | 3 ++- org/mozilla/jss/util/JDKCompat.java | 22 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 org/mozilla/jss/util/JDKCompat.java diff --git a/org/mozilla/jss/ssl/javax/JSSParameters.java b/org/mozilla/jss/ssl/javax/JSSParameters.java index 88f839dc3..d55ebd0bd 100644 --- a/org/mozilla/jss/ssl/javax/JSSParameters.java +++ b/org/mozilla/jss/ssl/javax/JSSParameters.java @@ -3,6 +3,7 @@ import javax.net.ssl.*; import java.util.*; +import org.mozilla.jss.util.JDKCompat; import org.mozilla.jss.ssl.*; /** @@ -56,7 +57,7 @@ public JSSParameters(SSLParameters downcast) { setNeedClientAuth(downcast.getNeedClientAuth()); } - String[] alpn = downcast.getApplicationProtocols(downcast); + String[] alpn = JDKCompat.SSLParametersHelper.getApplicationProtocols(downcast); if (alpn != null) { setApplicationProtocols(alpn); } diff --git a/org/mozilla/jss/util/JDKCompat.java b/org/mozilla/jss/util/JDKCompat.java new file mode 100644 index 000000000..4eeffaeab --- /dev/null +++ b/org/mozilla/jss/util/JDKCompat.java @@ -0,0 +1,22 @@ + +package org.mozilla.jss.util; + +import java.lang.reflect.Method; + +import javax.net.ssl.SSLParameters; + +public class JDKCompat { + public static class SSLParametersHelper { + public static String[] getApplicationProtocols(SSLParameters inst) { + try { + Method getter = inst.getClass().getMethod("getApplicationProtocols"); + Object result = getter.invoke(inst); + return (String[]) result; + } catch (NoSuchMethodException nsme) { + return null; + } catch (Throwable t) { + throw new RuntimeException(t.getMessage(), t); + } + } + } +} From a380911a74af4aa4bf06d510fdd278ad8e295cb1 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Wed, 7 Oct 2020 09:19:23 -0400 Subject: [PATCH 08/11] Use UTF-8 for ALPN data, call updateSession() It looks like UTF-8 is the desired encoding for ALPN data for compatibility with what the JCA and SunJSSE does: https://github.com/openjdk/jdk11u-dev/blob/master/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java#L100 Therefore we should make sure we encode our protocols the same way. Signed-off-by: Alexander Scheel --- org/mozilla/jss/ssl/javax/JSSEngine.java | 5 +++-- .../jss/ssl/javax/JSSEngineReferenceImpl.java | 17 +---------------- org/mozilla/jss/ssl/javax/JSSParameters.java | 5 +++-- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/org/mozilla/jss/ssl/javax/JSSEngine.java b/org/mozilla/jss/ssl/javax/JSSEngine.java index b326d764f..81ac09ad8 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngine.java +++ b/org/mozilla/jss/ssl/javax/JSSEngine.java @@ -1,5 +1,6 @@ package org.mozilla.jss.ssl.javax; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.*; @@ -951,14 +952,14 @@ public String getHandshakeApplicationProtocol() { public byte[] getALPNWireData() { int length = 0; for (String protocol : alpn_protocols) { - length += 1 + protocol.getBytes().length; + length += 1 + protocol.getBytes(StandardCharsets.UTF_8).length; } byte[] result = new byte[length]; int offset = 0; for (String protocol : alpn_protocols) { - byte[] p_bytes = protocol.getBytes(); + byte[] p_bytes = protocol.getBytes(StandardCharsets.UTF_8); result[offset] = (byte) p_bytes.length; offset += 1; System.arraycopy(p_bytes, 0, result, offset, p_bytes.length); diff --git a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java index f20ea0d64..479b0a36c 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java +++ b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java @@ -1108,22 +1108,7 @@ private void updateHandshakeState() { handshake_state = SSLEngineResult.HandshakeStatus.FINISHED; unknown_state_count = 0; - // Only update peer certificate chain when we've finished - // handshaking. - try { - PK11Cert[] peer_chain = SSL.PeerCertificateChain(ssl_fd); - session.setPeerCertificates(peer_chain); - } catch (Exception e) { - String msg = "Unable to get peer's certificate chain: "; - msg += e.getMessage(); - - seen_exception = true; - ssl_exception = new SSLException(msg, e); - } - - // Also update our session information here. - session.refreshData(); - + updateSession(); return; } diff --git a/org/mozilla/jss/ssl/javax/JSSParameters.java b/org/mozilla/jss/ssl/javax/JSSParameters.java index d55ebd0bd..bd014ce73 100644 --- a/org/mozilla/jss/ssl/javax/JSSParameters.java +++ b/org/mozilla/jss/ssl/javax/JSSParameters.java @@ -1,7 +1,8 @@ package org.mozilla.jss.ssl.javax; -import javax.net.ssl.*; +import java.nio.charset.StandardCharsets; import java.util.*; +import javax.net.ssl.*; import org.mozilla.jss.util.JDKCompat; import org.mozilla.jss.ssl.*; @@ -206,7 +207,7 @@ public void setApplicationProtocols(String[] protocols) throws IllegalArgumentEx int index = 0; for (String protocol : protocols) { - if (protocol.length() > 255 || protocol.getBytes().length > 255) { + if (protocol != "" && protocol.getBytes(StandardCharsets.UTF_8).length > 255) { String msg = "Invalid application protocol " + protocol; msg += ": standard allows up to 255 characters but was "; msg += protocol.length(); From 9b7b794e0778204465d233d7b177eb77a650a117 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Wed, 7 Oct 2020 10:24:59 -0400 Subject: [PATCH 09/11] Expose ALPN support in JSSSocket Since setting parameters via JSSParameters was already supported, technically no work is necessary to support ALPN in JSSSocket. However, we should extend JSSSocket with the new methods added to JSSEngine and implement them via calls to the underlying engine. Signed-off-by: Alexander Scheel --- org/mozilla/jss/ssl/javax/JSSSocket.java | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/org/mozilla/jss/ssl/javax/JSSSocket.java b/org/mozilla/jss/ssl/javax/JSSSocket.java index 7e2cfec07..0a114692c 100644 --- a/org/mozilla/jss/ssl/javax/JSSSocket.java +++ b/org/mozilla/jss/ssl/javax/JSSSocket.java @@ -664,6 +664,27 @@ public void setNeedClientAuth(boolean need) { engine.setNeedClientAuth(need); } + /** + * Set a specific list of protocols to negotiate next for ALPN support. + */ + public void setApplicationProtocols(String[] protocols) { + engine.setApplicationProtocols(protocols); + } + + /** + * Get the most recently negotiated application protocol. + */ + public String getApplicationProtocol() { + return engine.getApplicationProtocol(); + } + + /** + * Get the application protocol negotiated during the initial handshake. + */ + public String getHandshakeApplicationProtocol() { + return engine.getHandshakeApplicationProtocol(); + } + /** * Get the configuration of this SSLSocket as a JSSParameters object. * From 5998295908b86ccf522db2936a63506bc59c8ea6 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Fri, 9 Oct 2020 12:02:54 -0400 Subject: [PATCH 10/11] Support byte-encoded application protocols This will let GREASE'd ALPN values be handled correctly in JSS, despite the JDK not working well. Additionally, handle NSS's quirks w.r.t. ALPN protocol preference. Signed-off-by: Alexander Scheel --- org/mozilla/jss/nss/NextProtoResult.java | 2 +- org/mozilla/jss/ssl/javax/JSSEngine.java | 41 ++++++++--- .../jss/ssl/javax/JSSEngineReferenceImpl.java | 2 + org/mozilla/jss/ssl/javax/JSSParameters.java | 72 ++++++++++++++++--- org/mozilla/jss/tests/TestSSLEngine.java | 8 ++- 5 files changed, 102 insertions(+), 23 deletions(-) diff --git a/org/mozilla/jss/nss/NextProtoResult.java b/org/mozilla/jss/nss/NextProtoResult.java index 32b844d55..8324deae7 100644 --- a/org/mozilla/jss/nss/NextProtoResult.java +++ b/org/mozilla/jss/nss/NextProtoResult.java @@ -24,7 +24,7 @@ public NextProtoResult(int state_value, byte[] protocol) { } public String getProtocol() { - if (protocol == null || protocol.length == 0) { + if (protocol == null) { return null; } diff --git a/org/mozilla/jss/ssl/javax/JSSEngine.java b/org/mozilla/jss/ssl/javax/JSSEngine.java index 81ac09ad8..d2181c6da 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngine.java +++ b/org/mozilla/jss/ssl/javax/JSSEngine.java @@ -190,7 +190,7 @@ public abstract class JSSEngine extends javax.net.ssl.SSLEngine { /** * Set of possible application protocols to negotiate. */ - protected String[] alpn_protocols; + protected byte[][] alpn_protocols; /** * Constructor for a JSSEngine, providing no hints for an internal @@ -392,8 +392,8 @@ public void setSSLParameters(SSLParameters params) { // When we have a non-zero number of ALPNs, use them in the // negotiation. - if (parsed.getApplicationProtocols() != null) { - setApplicationProtocols(parsed.getApplicationProtocols()); + if (parsed.getRawApplicationProtocols() != null) { + setApplicationProtocols(parsed.getRawApplicationProtocols()); } } @@ -921,6 +921,15 @@ public boolean getWantClientAuth() { * Set a specific list of protocols to negotiate next for ALPN support. */ public void setApplicationProtocols(String[] protocols) { + JSSParameters parser = new JSSParameters(); + parser.setApplicationProtocols(protocols); + alpn_protocols = parser.getRawApplicationProtocols(); + } + + /** + * Set a specific list of protocols to negotiate next for ALPN support. + */ + public void setApplicationProtocols(byte[][] protocols) { alpn_protocols = protocols; } @@ -951,19 +960,31 @@ public String getHandshakeApplicationProtocol() { */ public byte[] getALPNWireData() { int length = 0; - for (String protocol : alpn_protocols) { - length += 1 + protocol.getBytes(StandardCharsets.UTF_8).length; + for (byte[] protocol : alpn_protocols) { + length += 1 + protocol.length; } byte[] result = new byte[length]; int offset = 0; - for (String protocol : alpn_protocols) { - byte[] p_bytes = protocol.getBytes(StandardCharsets.UTF_8); - result[offset] = (byte) p_bytes.length; + // XXX: Handle custom encoding left over from NPN draft: NSS's + // SSL_SetNextProtoNego takes the first protocol and helpfully puts + // it at the end of the list for us... So when we're validating using + // the default ALPN callback handler, switch the first to the last to + // ensure we're passing it in the caller's specified order. + byte[] last = alpn_protocols[alpn_protocols.length - 1]; + result[offset] = (byte) last.length; + offset += 1; + System.arraycopy(last, 0, result, offset, last.length); + offset += last.length; + + // Now copy the rest of the protocols. + for (int index = 0; index < alpn_protocols.length - 1; index++) { + byte[] protocol = alpn_protocols[index]; + result[offset] = (byte) protocol.length; offset += 1; - System.arraycopy(p_bytes, 0, result, offset, p_bytes.length); - offset += p_bytes.length; + System.arraycopy(protocol, 0, result, offset, protocol.length); + offset += protocol.length; } return result; diff --git a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java index 479b0a36c..93fe703c2 100644 --- a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java +++ b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java @@ -970,6 +970,8 @@ private void updateSession() { ret.state != SSLNextProtoState.SSL_NEXT_PROTO_NO_OVERLAP) { session.setNextProtocol(ret.getProtocol()); + } else { + session.setNextProtocol(""); } } } catch (Exception e) { diff --git a/org/mozilla/jss/ssl/javax/JSSParameters.java b/org/mozilla/jss/ssl/javax/JSSParameters.java index bd014ce73..0fac271a6 100644 --- a/org/mozilla/jss/ssl/javax/JSSParameters.java +++ b/org/mozilla/jss/ssl/javax/JSSParameters.java @@ -28,7 +28,7 @@ public class JSSParameters extends SSLParameters { private SSLVersionRange range; private String alias; private String hostname; - private String[] appProtocols; + private byte[][] appProtocols; public JSSParameters() { // Choose our default set of SSLParameters here; default to null @@ -205,20 +205,74 @@ public void setApplicationProtocols(String[] protocols) throws IllegalArgumentEx return; } - int index = 0; - for (String protocol : protocols) { - if (protocol != "" && protocol.getBytes(StandardCharsets.UTF_8).length > 255) { - String msg = "Invalid application protocol " + protocol; - msg += ": standard allows up to 255 characters but was "; - msg += protocol.length(); + byte[][] converted = new byte[protocols.length][]; + for (int index = 0; index < protocols.length; index++) { + if (protocols[index] == null) { + String msg = "Invalid application protocol (index: "; + msg += index + "): null"; throw new IllegalArgumentException(msg); } + + converted[index] = protocols[index].getBytes(StandardCharsets.UTF_8); } - appProtocols = protocols; + setApplicationProtocols(converted); } - public String[] getApplicationProtocols() { + public void setApplicationProtocols(byte[][] protocols) throws IllegalArgumentException { + if (protocols == null || protocols.length == 0) { + appProtocols = null; + return; + } + + int total_length = 0; + byte[][] result = new byte[protocols.length][]; + for (int index = 0; index < protocols.length; index++) { + byte[] protocol = protocols[index]; + if (protocol == null) { + String msg = "Invalid application protocol (index: "; + msg += index + "): null"; + throw new IllegalArgumentException(msg); + } + + if (protocol.length == 0 || protocol.length > 255) { + String msg = "Invalid application protocol (index: "; + msg += index + "): RFC 7301 allows up to 255 "; + msg += "characters but was " + protocol.length; + msg += ": " + new String(protocol); + throw new IllegalArgumentException(msg); + } + + result[index] = Arrays.copyOf(protocol, protocol.length); + total_length += 1 + protocol.length; + } + + // XXX: NSS's ssl3_ValidateAppProtocol doesn't validate the total + // length. Stop early. + if (total_length >= (1 << 16)) { + String msg = "Total length of encoded protocols exceeds encoded "; + msg += "maximum allowable by RFC 7301: " + total_length; + msg += ">= 65536"; + throw new IllegalArgumentException(msg); + } + + appProtocols = result; + } + + public byte[][] getRawApplicationProtocols() { return appProtocols; } + + public String[] getApplicationProtocols() { + if (appProtocols == null) { + return null; + } + + String[] result = new String[appProtocols.length]; + for (int index = 0; index < appProtocols.length; index++) { + result[index] = new String(appProtocols[index], StandardCharsets.UTF_8); + } + + return result; + } } diff --git a/org/mozilla/jss/tests/TestSSLEngine.java b/org/mozilla/jss/tests/TestSSLEngine.java index df1a29ebd..776da9fc0 100644 --- a/org/mozilla/jss/tests/TestSSLEngine.java +++ b/org/mozilla/jss/tests/TestSSLEngine.java @@ -1001,8 +1001,10 @@ public static void testALPNHandshake(SSLContext ctx, String server_alias) throws } try { - testBasicHandshake(client_eng, server_eng, false); - assert(server_eng.getApplicationProtocol().equals("h2")); + testInitialHandshake(client_eng, server_eng); + assert client_eng.getApplicationProtocol().equals("h2"); + assert server_eng.getApplicationProtocol().equals("h2"); + testClose(client_eng, server_eng); } catch (Exception e) { client_eng.cleanup(); server_eng.cleanup(); @@ -1046,7 +1048,7 @@ public static void testALPNEncoding() throws Exception { eng = new JSSEngineReferenceImpl(); eng.setApplicationProtocols(new String[] { "http/1.1", "spdy/2" }); - byte[] expectedHTTPSpdy = new byte[] { 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x06, 0x73, 0x70, 0x64, 0x79, 0x2f, 0x32 }; + byte[] expectedHTTPSpdy = new byte[] { 0x06, 0x73, 0x70, 0x64, 0x79, 0x2f, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31 }; assert Arrays.equals(eng.getALPNWireData(), expectedHTTPSpdy); } From f50bf1cf67d15135b5557dbc5f4780515df27a47 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Wed, 14 Oct 2020 09:44:51 -0400 Subject: [PATCH 11/11] Explicitly test null and zero-length ALPN protos Signed-off-by: Alexander Scheel --- org/mozilla/jss/tests/TestSSLEngine.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/org/mozilla/jss/tests/TestSSLEngine.java b/org/mozilla/jss/tests/TestSSLEngine.java index 776da9fc0..8e42f02f3 100644 --- a/org/mozilla/jss/tests/TestSSLEngine.java +++ b/org/mozilla/jss/tests/TestSSLEngine.java @@ -11,6 +11,7 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; @@ -1050,6 +1051,22 @@ public static void testALPNEncoding() throws Exception { eng.setApplicationProtocols(new String[] { "http/1.1", "spdy/2" }); byte[] expectedHTTPSpdy = new byte[] { 0x06, 0x73, 0x70, 0x64, 0x79, 0x2f, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31 }; assert Arrays.equals(eng.getALPNWireData(), expectedHTTPSpdy); + + // Handles default value + SSLParameters s_params = new SSLParameters(); + JSSParameters j_params = new JSSParameters(s_params); + assert j_params.getApplicationProtocols() == null; + + // Handles empty list + j_params = new JSSParameters(); + j_params.setApplicationProtocols(new String[0]); + assert j_params.getApplicationProtocols() == null; + + // Handles null list + j_params = new JSSParameters(); + String[] protos = null; + j_params.setApplicationProtocols(protos); + assert j_params.getApplicationProtocols() == null; } public static void main(String[] args) throws Exception {