Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSSEngine - Initial ALPN support #526

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions lib/jss.map
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ 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;
Java_org_mozilla_jss_nss_SSL_GetNextProto;
local:
*;
};
41 changes: 41 additions & 0 deletions org/mozilla/jss/nss/NextProtoResult.java
Original file line number Diff line number Diff line change
@@ -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) {
return null;
}

return new String(protocol);
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
}

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();
}
}
85 changes: 85 additions & 0 deletions org/mozilla/jss/nss/SSL.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand Down Expand Up @@ -816,6 +850,57 @@ 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 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)
Expand Down
14 changes: 14 additions & 0 deletions org/mozilla/jss/nss/SSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,20 @@ 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);

/**
* 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.
*
Expand Down
29 changes: 29 additions & 0 deletions org/mozilla/jss/ssl/SSLNextProtoState.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
85 changes: 85 additions & 0 deletions org/mozilla/jss/ssl/javax/JSSEngine.java
Original file line number Diff line number Diff line change
@@ -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.*;
Expand Down Expand Up @@ -186,6 +187,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 byte[][] alpn_protocols;

/**
* Constructor for a JSSEngine, providing no hints for an internal
* session reuse strategy and no key.
Expand Down Expand Up @@ -383,6 +389,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.getRawApplicationProtocols() != null) {
setApplicationProtocols(parsed.getRawApplicationProtocols());
}
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -905,6 +917,79 @@ public boolean getWantClientAuth() {
return want_client_auth;
}

/**
* 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;
}

/**
* 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 (byte[] protocol : alpn_protocols) {
length += 1 + protocol.length;
}

byte[] result = new byte[length];
int offset = 0;

// 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(protocol, 0, result, offset, protocol.length);
offset += protocol.length;
}

return result;
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Query whether or not the inbound side of this connection is closed.
*/
Expand Down
63 changes: 45 additions & 18 deletions org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,17 @@ 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) {
Copy link
Contributor

@frasertweedale frasertweedale Oct 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also check alpn_protocols.length > 0.

update: make that a MUST. SSLParameters class sets default value of protocols to empty array. This results in empty wire_data which results in SECFailure when setting ALPN data and the RuntimeException gets raised below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What SSLParameters class? We explicitly downcast into JSSParameters within JSSEngine so we can use a simple null check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the process of doing that downcast from SSLParameters to JSSParameters, we take the ALPN value as-is (see
https://github.com/dogtagpki/jss/pull/526/files#r503618760). So if we construct a JSSEngine with an "native" SSLParameters, and have not overridden the ALPN data, then we get an empty list. This is why the check is necessary.
This is not a theoretical issue - I hit it with my test program.

But even so, it is just a good defensive coding practice. If the type admits an invalid datum (it does), we should code up checks against it - or change the type (not practical here because String[] is imposed).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cipherboy I've tested this PR and it's almost there. We really do need this length check though.
Feel free to just apply this patch or modify the relevant commit: whichever you prefer:

commit 5e09241e923b426733c444ccb1ed9295726c3080
Author: Fraser Tweedale <[email protected]>
Date:   Tue Oct 13 12:45:46 2020 +1100

    ALPN: guard against empty array of protocol IDs
    
    javax.net.ssl.SSLParameters sets default ALPN protocol IDs to empty
    array.  When downcasting to JSSParameters, we take the value as-is,
    so we could also end up with an empty array.  Therefore when
    deciding whether to create the ALPN extension the non-null check is
    not sufficient.  Also check that the array is non-empty.

diff --git a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
index 93fe703c..f84ae800 100644
--- a/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
+++ b/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
@@ -347,7 +347,7 @@ public class JSSEngineReferenceImpl extends JSSEngine {
             throw new SSLException("Unable to enable SSL Handshake Callback on this SSLFDProxy instance.");
         }
 
-        if (alpn_protocols != null) {
+        if (alpn_protocols != null && alpn_protocols.length > 0) {
             byte[] wire_data = getALPNWireData();
             if (wire_data == null) {
                 throw new RuntimeException("JSSEngine.init(): ALPN wire data is NULL but alpn_protocols is non-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.");
}
cipherboy marked this conversation as resolved.
Show resolved Hide resolved

ret = SSL.SetNextProtoNeg(ssl_fd, wire_data);
if (ret != SSL.SECSuccess) {
throw new RuntimeException("JSSEngine.init(): Unable to set ALPN protocol list: " + errorText(PR.GetError()) + " " + ret);
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

private void initClient() throws SSLException {
Expand Down Expand Up @@ -937,6 +946,39 @@ private int putData(byte[] data, ByteBuffer[] buffers, int offset, int length) {
return data_index;
}

private void updateSession() {
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
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)
cipherboy marked this conversation as resolved.
Show resolved Hide resolved
{
session.setNextProtocol(ret.getProtocol());
} else {
session.setNextProtocol("");
}
}
} 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.");

Expand Down Expand Up @@ -1068,22 +1110,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;
}

Expand Down
Loading