Add support for ALPN when using openssl + NPN client mode and support for CipherSuiteFilter

Motivation:

To support HTTP2 we need APLN support. This was not provided before when using OpenSslEngine, so SSLEngine (JDK one) was the only bet.
Beside this CipherSuiteFilter was not supported

Modifications:

- Upgrade netty-tcnative and make use of new features to support ALPN and NPN in server and client mode.
- Guard against segfaults after the ssl pointer is freed
- support correctly different failure behaviours
- add support for CipherSuiteFilter

Result:

Be able to use OpenSslEngine for ALPN / NPN for server and client.
This commit is contained in:
Norman Maurer 2015-03-05 14:19:13 +01:00
parent ecf1b558d9
commit 97a3308c7e
15 changed files with 554 additions and 207 deletions

View File

@ -17,6 +17,7 @@
package io.netty.util.internal;
import java.nio.ByteBuffer;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
public final class EmptyArrays {
@ -34,7 +35,10 @@ public final class EmptyArrays {
public static final String[] EMPTY_STRINGS = new String[0];
public static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
public static final ByteBuffer[] EMPTY_BYTE_BUFFERS = new ByteBuffer[0];
public static final Certificate[] EMPTY_CERTIFICATES = new Certificate[0];
public static final X509Certificate[] EMPTY_X509_CERTIFICATES = new X509Certificate[0];
public static final javax.security.cert.X509Certificate[] EMPTY_JAVAX_X509_CERTIFICATES =
new javax.security.cert.X509Certificate[0];
private EmptyArrays() { }
}

View File

@ -26,10 +26,13 @@ import java.util.List;
public final class StringUtil {
public static final String NEWLINE;
public static final char DOUBLE_QUOTE = '\"';
public static final char COMMA = ',';
public static final char LINE_FEED = '\n';
public static final char CARRIAGE_RETURN = '\r';
public static final String EMPTY_STRING = "";
private static final String[] BYTE2HEX_PAD = new String[256];
private static final String[] BYTE2HEX_NOPAD = new String[256];
private static final String EMPTY_STRING = "";
static {
// Determine the newline character of the current platform.

View File

@ -60,8 +60,8 @@ public final class SpdyClient {
null, InsecureTrustManagerFactory.INSTANCE, null, IdentityCipherSuiteFilter.INSTANCE,
new ApplicationProtocolConfig(
Protocol.NPN,
SelectorFailureBehavior.FATAL_ALERT,
SelectedListenerFailureBehavior.FATAL_ALERT,
SelectorFailureBehavior.CHOOSE_MY_LAST_PROTOCOL,
SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL,
SelectedProtocol.SPDY_3_1.protocolName(),
SelectedProtocol.HTTP_1_1.protocolName()),
0, 0);

View File

@ -61,8 +61,8 @@ public final class SpdyServer {
ssc.certificate(), ssc.privateKey(), null, null, IdentityCipherSuiteFilter.INSTANCE,
new ApplicationProtocolConfig(
Protocol.NPN,
SelectorFailureBehavior.FATAL_ALERT,
SelectedListenerFailureBehavior.FATAL_ALERT,
SelectorFailureBehavior.CHOOSE_MY_LAST_PROTOCOL,
SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL,
SelectedProtocol.SPDY_3_1.protocolName(),
SelectedProtocol.HTTP_1_1.protocolName()),
0, 0);

View File

@ -79,6 +79,9 @@ public final class ApplicationProtocolConfig {
if (protocol == Protocol.NONE) {
throw new IllegalArgumentException("protocol (" + Protocol.NONE + ") must not be " + Protocol.NONE + '.');
}
if (supportedProtocols.isEmpty()) {
throw new IllegalArgumentException("supportedProtocols must be not empty");
}
}
/**

View File

@ -113,6 +113,14 @@ public final class OpenSsl {
return UNAVAILABILITY_CAUSE == null;
}
/**
* Returns {@code true} if the used version of openssl supports
* <a href="https://tools.ietf.org/html/rfc7301">ALPN</a>.
*/
public static boolean isAlpnSupported() {
return isAvailable() && SSL.version() >= 0x10002000L;
}
/**
* Ensure that <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and
* its OpenSSL support are available.

View File

@ -19,10 +19,19 @@ package io.netty.handler.ssl;
* OpenSSL version of {@link ApplicationProtocolNegotiator}.
*/
public interface OpenSslApplicationProtocolNegotiator extends ApplicationProtocolNegotiator {
// The current need for this interface is primarily for consistency with the JDK provider
// How the OpenSsl provider will provide extensibility to control the application selection
// and notification algorithms is not yet known (JNI, pure java, tcnative hooks, etc...).
// OpenSSL itself is currently not in compliance with the specification for the 2 supported
// protocols (ALPN, NPN) with respect to allowing the handshakes to fail during the application
// protocol negotiation process. Issue https://github.com/openssl/openssl/issues/188 has been created for this.
/**
* Returns the {@link ApplicationProtocolConfig.Protocol} which should be used.
*/
ApplicationProtocolConfig.Protocol protocol();
/**
* Get the desired behavior for the peer who selects the application protocol.
*/
ApplicationProtocolConfig.SelectorFailureBehavior selectorFailureBehavior();
/**
* Get the desired behavior for the peer who is notified of the selected protocol.
*/
ApplicationProtocolConfig.SelectedListenerFailureBehavior selectedListenerFailureBehavior();
}

View File

@ -45,7 +45,7 @@ public final class OpenSslClientContext extends OpenSslContext {
* Creates a new instance.
*/
public OpenSslClientContext() throws SSLException {
this(null, null, null, null, 0, 0);
this(null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
}
/**
@ -79,10 +79,13 @@ public final class OpenSslClientContext extends OpenSslContext {
* {@code null} to use the default.
*/
public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException {
this(certChainFile, trustManagerFactory, null, null, 0, 0);
this(certChainFile, trustManagerFactory, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
}
/**
* @deprecated use {@link #OpenSslClientContext(File, TrustManagerFactory, Iterable,
* CipherSuiteFilter, ApplicationProtocolConfig, long, long)}
*
* Creates a new instance.
*
* @param certChainFile an X.509 certificate chain file in PEM format
@ -97,10 +100,35 @@ public final class OpenSslClientContext extends OpenSslContext {
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
*/
@Deprecated
public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory, Iterable<String> ciphers,
ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout)
throws SSLException {
super(ciphers, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT);
this(certChainFile, trustManagerFactory, ciphers, IdentityCipherSuiteFilter.INSTANCE,
apn, sessionCacheSize, sessionTimeout);
}
/**
* Creates a new instance.
*
* @param certChainFile an X.509 certificate chain file in PEM format
* @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s
* that verifies the certificates sent from servers.
* {@code null} to use the default..
* @param ciphers the cipher suites to enable, in the order of preference.
* {@code null} to use the default cipher suites.
* @param cipherFilter a filter to apply over the supplied list of ciphers
* @param apn Provides a means to configure parameters related to application protocol negotiation.
* @param sessionCacheSize the size of the cache used for storing SSL session objects.
* {@code 0} to use the default value.
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
*/
public OpenSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory, Iterable<String> ciphers,
CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
long sessionCacheSize, long sessionTimeout)
throws SSLException {
super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT);
boolean success = false;
try {
if (certChainFile != null && !certChainFile.isFile()) {

View File

@ -31,12 +31,15 @@ import javax.net.ssl.X509ExtendedTrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import static io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
public abstract class OpenSslContext extends SslContext {
@ -48,10 +51,9 @@ public abstract class OpenSslContext extends SslContext {
protected static final int VERIFY_DEPTH = 10;
private final long aprPool;
@SuppressWarnings("unused")
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
private volatile int aprPoolDestroyed;
private final List<String> ciphers = new ArrayList<String>();
private final List<String> unmodifiableCiphers = Collections.unmodifiableList(ciphers);
private final List<String> unmodifiableCiphers;
private final long sessionCacheSize;
private final long sessionTimeout;
private final OpenSslApplicationProtocolNegotiator apn;
@ -59,6 +61,29 @@ public abstract class OpenSslContext extends SslContext {
protected final long ctx;
private final int mode;
static final OpenSslApplicationProtocolNegotiator NONE_PROTOCOL_NEGOTIATOR =
new OpenSslApplicationProtocolNegotiator() {
@Override
public ApplicationProtocolConfig.Protocol protocol() {
return ApplicationProtocolConfig.Protocol.NONE;
}
@Override
public List<String> protocols() {
return Collections.emptyList();
}
@Override
public SelectorFailureBehavior selectorFailureBehavior() {
return SelectorFailureBehavior.CHOOSE_MY_LAST_PROTOCOL;
}
@Override
public SelectedListenerFailureBehavior selectedListenerFailureBehavior() {
return SelectedListenerFailureBehavior.ACCEPT;
}
};
static {
List<String> ciphers = new ArrayList<String>();
// XXX: Make sure to sync this list with JdkSslEngineFactory.
@ -86,12 +111,13 @@ public abstract class OpenSslContext extends SslContext {
DESTROY_UPDATER = updater;
}
OpenSslContext(Iterable<String> ciphers, ApplicationProtocolConfig apnCfg, long sessionCacheSize,
long sessionTimeout, int mode) throws SSLException {
this(ciphers, toNegotiator(apnCfg, mode == SSL.SSL_MODE_SERVER), sessionCacheSize, sessionTimeout, mode);
OpenSslContext(Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apnCfg,
long sessionCacheSize, long sessionTimeout, int mode) throws SSLException {
this(ciphers, cipherFilter, toNegotiator(apnCfg), sessionCacheSize, sessionTimeout, mode);
}
OpenSslContext(Iterable<String> ciphers, OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize,
OpenSslContext(Iterable<String> ciphers, CipherSuiteFilter cipherFilter,
OpenSslApplicationProtocolNegotiator apn, long sessionCacheSize,
long sessionTimeout, int mode) throws SSLException {
OpenSsl.ensureAvailability();
@ -100,10 +126,11 @@ public abstract class OpenSslContext extends SslContext {
}
this.mode = mode;
final List<String> convertedCiphers;
if (ciphers == null) {
ciphers = DEFAULT_CIPHERS;
}
convertedCiphers = null;
} else {
convertedCiphers = new ArrayList<String>();
for (String c: ciphers) {
if (c == null) {
break;
@ -113,9 +140,12 @@ public abstract class OpenSslContext extends SslContext {
if (converted != null) {
c = converted;
}
this.ciphers.add(c);
convertedCiphers.add(c);
}
}
unmodifiableCiphers = Arrays.asList(checkNotNull(cipherFilter, "cipherFilter").filterCipherSuites(
convertedCiphers, DEFAULT_CIPHERS, OpenSsl.availableCipherSuites()));
this.apn = checkNotNull(apn, "apn");
@ -142,25 +172,33 @@ public abstract class OpenSslContext extends SslContext {
/* List the ciphers that are permitted to negotiate. */
try {
SSLContext.setCipherSuite(ctx, CipherSuiteConverter.toOpenSsl(this.ciphers));
SSLContext.setCipherSuite(ctx, CipherSuiteConverter.toOpenSsl(unmodifiableCiphers));
} catch (SSLException e) {
throw e;
} catch (Exception e) {
throw new SSLException("failed to set cipher suite: " + this.ciphers, e);
throw new SSLException("failed to set cipher suite: " + unmodifiableCiphers, e);
}
List<String> nextProtoList = apn.protocols();
/* Set next protocols for next protocol negotiation extension, if specified */
if (!nextProtoList.isEmpty()) {
// Convert the protocol list into a comma-separated string.
StringBuilder nextProtocolBuf = new StringBuilder();
for (String p: nextProtoList) {
nextProtocolBuf.append(p);
nextProtocolBuf.append(',');
}
nextProtocolBuf.setLength(nextProtocolBuf.length() - 1);
String[] protocols = nextProtoList.toArray(new String[nextProtoList.size()]);
int selectorBehavior = opensslSelectorFailureBehavior(apn.selectorFailureBehavior());
SSLContext.setNextProtos(ctx, nextProtocolBuf.toString());
switch (apn.protocol()) {
case NPN:
SSLContext.setNpnProtos(ctx, protocols, selectorBehavior);
break;
case ALPN:
SSLContext.setAlpnProtos(ctx, protocols, selectorBehavior);
break;
case NPN_AND_ALPN:
SSLContext.setNpnProtos(ctx, protocols, selectorBehavior);
SSLContext.setAlpnProtos(ctx, protocols, selectorBehavior);
break;
default:
throw new Error();
}
}
/* Set session cache size, if specified */
@ -193,6 +231,17 @@ public abstract class OpenSslContext extends SslContext {
}
}
private static int opensslSelectorFailureBehavior(SelectorFailureBehavior behavior) {
switch (behavior) {
case NO_ADVERTISE:
return SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE;
case CHOOSE_MY_LAST_PROTOCOL:
return SSL.SSL_SELECTOR_FAILURE_CHOOSE_MY_LAST_PROTOCOL;
default:
throw new Error();
}
}
@Override
public final List<String> cipherSuites() {
return unmodifiableCiphers;
@ -228,15 +277,8 @@ public abstract class OpenSslContext extends SslContext {
*/
@Override
public final SSLEngine newEngine(ByteBufAllocator alloc) {
List<String> protos = applicationProtocolNegotiator().protocols();
OpenSslEngineMap engineMap = engineMap();
final OpenSslEngine engine;
if (protos.isEmpty()) {
engine = new OpenSslEngine(ctx, alloc, null, isClient(), sessionContext(), engineMap);
} else {
engine = new OpenSslEngine(ctx, alloc, protos.get(protos.size() - 1), isClient(),
sessionContext(), engineMap);
}
final OpenSslEngine engine = new OpenSslEngine(ctx, alloc, isClient(), sessionContext(), apn, engineMap);
engineMap.add(engine);
return engine;
}
@ -312,35 +354,41 @@ public abstract class OpenSslContext extends SslContext {
* Translate a {@link ApplicationProtocolConfig} object to a
* {@link OpenSslApplicationProtocolNegotiator} object.
* @param config The configuration which defines the translation
* @param isServer {@code true} if a server {@code false} otherwise.
* @return The results of the translation
*/
static OpenSslApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config,
boolean isServer) {
static OpenSslApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config) {
if (config == null) {
return OpenSslDefaultApplicationProtocolNegotiator.INSTANCE;
return NONE_PROTOCOL_NEGOTIATOR;
}
switch (config.protocol()) {
case NONE:
return OpenSslDefaultApplicationProtocolNegotiator.INSTANCE;
return NONE_PROTOCOL_NEGOTIATOR;
case ALPN:
case NPN:
if (isServer) {
case NPN_AND_ALPN:
switch (config.selectedListenerFailureBehavior()) {
case CHOOSE_MY_LAST_PROTOCOL:
return new OpenSslNpnApplicationProtocolNegotiator(config.supportedProtocols());
case ACCEPT:
switch (config.selectorFailureBehavior()) {
case CHOOSE_MY_LAST_PROTOCOL:
case NO_ADVERTISE:
return new OpenSslDefaultApplicationProtocolNegotiator(
config);
default:
throw new UnsupportedOperationException(
new StringBuilder("OpenSSL provider does not support ")
.append(config.selectorFailureBehavior())
.append(" behavior").toString());
}
default:
throw new UnsupportedOperationException(
new StringBuilder("OpenSSL provider does not support ")
.append(config.selectedListenerFailureBehavior())
.append(" behavior").toString());
}
} else {
throw new UnsupportedOperationException("OpenSSL provider does not support client mode");
}
default:
throw new UnsupportedOperationException(new StringBuilder("OpenSSL provider does not support ")
.append(config.protocol()).append(" protocol").toString());
throw new Error();
}
}

View File

@ -15,21 +15,36 @@
*/
package io.netty.handler.ssl;
import java.util.Collections;
import java.util.List;
/**
* Provides no {@link ApplicationProtocolNegotiator} functionality for OpenSSL.
*/
final class OpenSslDefaultApplicationProtocolNegotiator implements OpenSslApplicationProtocolNegotiator {
static final OpenSslDefaultApplicationProtocolNegotiator INSTANCE =
new OpenSslDefaultApplicationProtocolNegotiator();
import static io.netty.util.internal.ObjectUtil.checkNotNull;
private OpenSslDefaultApplicationProtocolNegotiator() {
/**
* OpenSSL {@link ApplicationProtocolNegotiator} for ALPN and NPN.
*/
public final class OpenSslDefaultApplicationProtocolNegotiator implements OpenSslApplicationProtocolNegotiator {
private final ApplicationProtocolConfig config;
public OpenSslDefaultApplicationProtocolNegotiator(ApplicationProtocolConfig config) {
this.config = checkNotNull(config, "config");
}
@Override
public List<String> protocols() {
return Collections.emptyList();
return config.supportedProtocols();
}
@Override
public ApplicationProtocolConfig.Protocol protocol() {
return config.protocol();
}
@Override
public ApplicationProtocolConfig.SelectorFailureBehavior selectorFailureBehavior() {
return config.selectorFailureBehavior();
}
@Override
public ApplicationProtocolConfig.SelectedListenerFailureBehavior selectedListenerFailureBehavior() {
return config.selectedListenerFailureBehavior();
}
}

View File

@ -19,8 +19,8 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.util.internal.EmptyArrays;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import org.apache.tomcat.jni.Buffer;
@ -49,6 +49,8 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import static io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
import static javax.net.ssl.SSLEngineResult.Status.*;
@ -60,7 +62,9 @@ public final class OpenSslEngine extends SSLEngine {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(OpenSslEngine.class);
private static final Certificate[] EMPTY_CERTIFICATES = new Certificate[0];
private static final Certificate[] EMPTY_CERTIFICATES = EmptyArrays.EMPTY_CERTIFICATES;
private static final X509Certificate[] EMPTY_X509_CERTIFICATES = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES;
private static final SSLException ENGINE_CLOSED = new SSLException("engine closed");
private static final SSLException RENEGOTIATION_UNSUPPORTED = new SSLException("renegotiation unsupported");
private static final SSLException ENCRYPTED_PACKET_OVERSIZED = new SSLException("encrypted packet oversized");
@ -145,10 +149,9 @@ public final class OpenSslEngine extends SSLEngine {
private final boolean clientMode;
private final ByteBufAllocator alloc;
private final String fallbackApplicationProtocol;
private final OpenSslSessionContext sessionContext;
private final OpenSslEngineMap engineMap;
private final OpenSslApplicationProtocolNegotiator apn;
private final SSLSession session = new OpenSslSession();
/**
@ -158,8 +161,9 @@ public final class OpenSslEngine extends SSLEngine {
* @param alloc the {@link ByteBufAllocator} that will be used by this engine
*/
@Deprecated
public OpenSslEngine(long sslCtx, ByteBufAllocator alloc, String fallbackApplicationProtocol) {
this(sslCtx, alloc, fallbackApplicationProtocol, false, null, OpenSslEngineMap.EMPTY);
public OpenSslEngine(long sslCtx, ByteBufAllocator alloc,
@SuppressWarnings("unused") String fallbackApplicationProtocol) {
this(sslCtx, alloc, false, null, OpenSslContext.NONE_PROTOCOL_NEGOTIATOR, OpenSslEngineMap.EMPTY);
}
/**
@ -170,17 +174,18 @@ public final class OpenSslEngine extends SSLEngine {
* @param clientMode {@code true} if this is used for clients, {@code false} otherwise
* @param sessionContext the {@link OpenSslSessionContext} this {@link SSLEngine} belongs to.
*/
OpenSslEngine(long sslCtx, ByteBufAllocator alloc, String fallbackApplicationProtocol,
boolean clientMode, OpenSslSessionContext sessionContext, OpenSslEngineMap engineMap) {
OpenSslEngine(long sslCtx, ByteBufAllocator alloc,
boolean clientMode, OpenSslSessionContext sessionContext,
OpenSslApplicationProtocolNegotiator apn, OpenSslEngineMap engineMap) {
OpenSsl.ensureAvailability();
if (sslCtx == 0) {
throw new NullPointerException("sslCtx");
}
this.alloc = ObjectUtil.checkNotNull(alloc, "alloc");
this.alloc = checkNotNull(alloc, "alloc");
this.apn = checkNotNull(apn, "apn");
ssl = SSL.newSSL(sslCtx, !clientMode);
networkBIO = SSL.makeNetworkBIO(ssl);
this.fallbackApplicationProtocol = fallbackApplicationProtocol;
this.clientMode = clientMode;
this.sessionContext = sessionContext;
this.engineMap = engineMap;
@ -396,7 +401,7 @@ public final class OpenSslEngine extends SSLEngine {
// In handshake or close_notify stages, check if call to wrap was made
// without regard to the handshake status.
SSLEngineResult.HandshakeStatus handshakeStatus = getHandshakeStatus();
SSLEngineResult.HandshakeStatus handshakeStatus = handshakeStatus0();
if ((!handshakeFinished || engineClosed) && handshakeStatus == NEED_UNWRAP) {
return new SSLEngineResult(getEngineStatus(), NEED_UNWRAP, 0, 0);
@ -428,7 +433,7 @@ public final class OpenSslEngine extends SSLEngine {
shutdown();
}
return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), 0, bytesProduced);
return new SSLEngineResult(getEngineStatus(), handshakeStatus0(), 0, bytesProduced);
}
// There was no pending data in the network BIO -- encrypt any application data
@ -455,7 +460,7 @@ public final class OpenSslEngine extends SSLEngine {
int capacity = dst.remaining();
if (capacity < pendingNet) {
return new SSLEngineResult(
BUFFER_OVERFLOW, getHandshakeStatus(), bytesConsumed, bytesProduced);
BUFFER_OVERFLOW, handshakeStatus0(), bytesConsumed, bytesProduced);
}
// Write the pending data from the network BIO into the dst buffer
@ -465,12 +470,12 @@ public final class OpenSslEngine extends SSLEngine {
throw new SSLException(e);
}
return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced);
return new SSLEngineResult(getEngineStatus(), handshakeStatus0(), bytesConsumed, bytesProduced);
}
}
}
return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced);
return new SSLEngineResult(getEngineStatus(), handshakeStatus0(), bytesConsumed, bytesProduced);
}
public synchronized SSLEngineResult unwrap(
@ -520,7 +525,7 @@ public final class OpenSslEngine extends SSLEngine {
// In handshake or close_notify stages, check if call to unwrap was made
// without regard to the handshake status.
SSLEngineResult.HandshakeStatus handshakeStatus = getHandshakeStatus();
SSLEngineResult.HandshakeStatus handshakeStatus = handshakeStatus0();
if ((!handshakeFinished || engineClosed) && handshakeStatus == NEED_WRAP) {
return new SSLEngineResult(getEngineStatus(), NEED_WRAP, 0, 0);
}
@ -604,7 +609,7 @@ public final class OpenSslEngine extends SSLEngine {
if (pendingApp > 0) {
// Do we have enough room in dsts to write decrypted data?
if (capacity < pendingApp) {
return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), bytesConsumed, 0);
return new SSLEngineResult(BUFFER_OVERFLOW, handshakeStatus0(), bytesConsumed, 0);
}
// Write decrypted data to dsts buffers
@ -647,7 +652,7 @@ public final class OpenSslEngine extends SSLEngine {
closeInbound();
}
return new SSLEngineResult(getEngineStatus(), getHandshakeStatus(), bytesConsumed, bytesProduced);
return new SSLEngineResult(getEngineStatus(), handshakeStatus0(), bytesConsumed, bytesProduced);
}
public SSLEngineResult unwrap(final ByteBuffer[] srcs, final ByteBuffer[] dsts) throws SSLException {
@ -723,7 +728,14 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public String[] getEnabledCipherSuites() {
String[] enabled = SSL.getCiphers(ssl);
final String[] enabled;
synchronized (this) {
if (destroyed == 0) {
enabled = SSL.getCiphers(ssl);
} else {
return EmptyArrays.EMPTY_STRINGS;
}
}
if (enabled == null) {
return EmptyArrays.EMPTY_STRINGS;
} else {
@ -739,7 +751,7 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public void setEnabledCipherSuites(String[] cipherSuites) {
ObjectUtil.checkNotNull(cipherSuites, "cipherSuites");
checkNotNull(cipherSuites, "cipherSuites");
final StringBuilder buf = new StringBuilder();
for (String c: cipherSuites) {
@ -766,11 +778,18 @@ public final class OpenSslEngine extends SSLEngine {
buf.setLength(buf.length() - 1);
final String cipherSuiteSpec = buf.toString();
synchronized (this) {
if (destroyed == 0) {
try {
SSL.setCipherSuites(ssl, cipherSuiteSpec);
} catch (Exception e) {
throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e);
}
} else {
throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec);
}
}
}
@Override
@ -783,7 +802,15 @@ public final class OpenSslEngine extends SSLEngine {
List<String> enabled = new ArrayList<String>();
// Seems like there is no way to explict disable SSLv2Hello in openssl so it is always enabled
enabled.add(PROTOCOL_SSL_V2_HELLO);
int opts = SSL.getOptions(ssl);
int opts;
synchronized (this) {
if (destroyed == 0) {
opts = SSL.getOptions(ssl);
} else {
return enabled.toArray(new String[1]);
}
}
if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) {
enabled.add(PROTOCOL_TLS_V1);
}
@ -799,12 +826,7 @@ public final class OpenSslEngine extends SSLEngine {
if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) {
enabled.add(PROTOCOL_SSL_V3);
}
int size = enabled.size();
if (size == 0) {
return EmptyArrays.EMPTY_STRINGS;
} else {
return enabled.toArray(new String[size]);
}
return enabled.toArray(new String[enabled.size()]);
}
@Override
@ -834,6 +856,8 @@ public final class OpenSslEngine extends SSLEngine {
tlsv1_2 = true;
}
}
synchronized (this) {
if (destroyed == 0) {
// Enable all and then disable what we not want
SSL.setOptions(ssl, SSL.SSL_OP_ALL);
@ -852,6 +876,10 @@ public final class OpenSslEngine extends SSLEngine {
if (!tlsv1_2) {
SSL.setOptions(ssl, SSL.SSL_OP_NO_TLSv1_2);
}
} else {
throw new IllegalStateException("failed to enable protocols: " + protocols);
}
}
}
@Override
@ -915,7 +943,7 @@ public final class OpenSslEngine extends SSLEngine {
} else {
// if SSL_do_handshake returns > 0 it means the handshake was finished. This means we can update
// handshakeFinished directly and so eliminate uncessary calls to SSL.isInInit(...)
handshakeFinished = true;
handshakeFinished();
}
}
@ -927,10 +955,76 @@ public final class OpenSslEngine extends SSLEngine {
}
}
private void handshakeFinished() throws SSLException {
SelectedListenerFailureBehavior behavior = apn.selectedListenerFailureBehavior();
List<String> protocols = apn.protocols();
String applicationProtocol;
switch (apn.protocol()) {
case NONE:
break;
// We always need to check for applicationProtocol == null as the remote peer may not support
// the TLS extension or may have returned an empty selection.
case ALPN:
applicationProtocol = SSL.getAlpnSelected(ssl);
if (applicationProtocol != null) {
this.applicationProtocol = selectApplicationProtocol(protocols, behavior, applicationProtocol);
}
break;
case NPN:
applicationProtocol = SSL.getNextProtoNegotiated(ssl);
if (applicationProtocol != null) {
this.applicationProtocol = selectApplicationProtocol(protocols, behavior, applicationProtocol);
}
break;
case NPN_AND_ALPN:
applicationProtocol = SSL.getAlpnSelected(ssl);
if (applicationProtocol == null) {
applicationProtocol = SSL.getNextProtoNegotiated(ssl);
}
if (applicationProtocol != null) {
this.applicationProtocol = selectApplicationProtocol(protocols, behavior, applicationProtocol);
}
break;
default:
throw new Error();
}
handshakeFinished = true;
}
private static String selectApplicationProtocol(List<String> protocols,
SelectedListenerFailureBehavior behavior,
String applicationProtocol) throws SSLException {
applicationProtocol = applicationProtocol.replace(':', '_');
if (behavior == SelectedListenerFailureBehavior.ACCEPT) {
return applicationProtocol;
} else {
int size = protocols.size();
assert size > 0;
if (protocols.contains(applicationProtocol)) {
return applicationProtocol;
} else {
if (behavior == SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) {
return protocols.get(size - 1);
} else {
throw new SSLException("Unknown protocol " + applicationProtocol);
}
}
}
}
private SSLEngineResult.Status getEngineStatus() {
return engineClosed? CLOSED : OK;
}
private SSLEngineResult.HandshakeStatus handshakeStatus0() throws SSLException {
SSLEngineResult.HandshakeStatus status = getHandshakeStatus();
if (status == FINISHED) {
handshakeFinished();
}
return status;
}
@Override
public synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() {
if (accepted == 0 || destroyed != 0) {
@ -947,7 +1041,6 @@ public final class OpenSslEngine extends SSLEngine {
// No pending data to be sent to the peer
// Check to see if we have finished handshaking
if (SSL.isInInit(ssl) == 0) {
handshakeFinished = true;
return FINISHED;
}
@ -1088,8 +1181,15 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public byte[] getId() {
final byte[] id;
synchronized (OpenSslEngine.this) {
if (destroyed == 0) {
id = SSL.getSessionId(ssl);
} else {
id = EmptyArrays.EMPTY_BYTES;
}
}
// We don't cache that to keep memory usage to a minimum.
byte[] id = SSL.getSessionId(ssl);
if (id == null) {
// The id should never be null, if it was null then the SESSION itself was not valid.
throw new IllegalStateException("SSL session ID not available");
@ -1104,9 +1204,14 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public long getCreationTime() {
synchronized (OpenSslEngine.this) {
if (destroyed == 0) {
// We need ot multiple by 1000 as openssl uses seconds and we need milli-seconds.
return SSL.getTime(ssl) * 1000L;
}
return 0;
}
}
@Override
public long getLastAccessedTime() {
@ -1126,9 +1231,12 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public void putValue(String name, Object value) {
ObjectUtil.checkNotNull(name, "name");
ObjectUtil.checkNotNull(value, "value");
if (name == null) {
throw new NullPointerException("name");
}
if (value == null) {
throw new NullPointerException("value");
}
Map<String, Object> values = this.values;
if (values == null) {
// Use size of 2 to keep the memory overhead small
@ -1143,8 +1251,9 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public Object getValue(String name) {
ObjectUtil.checkNotNull(name, "name");
if (name == null) {
throw new NullPointerException("name");
}
if (values == null) {
return null;
}
@ -1153,8 +1262,9 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public void removeValue(String name) {
ObjectUtil.checkNotNull(name, "name");
if (name == null) {
throw new NullPointerException("name");
}
Map<String, Object> values = this.values;
if (values == null) {
return;
@ -1183,53 +1293,20 @@ public final class OpenSslEngine extends SSLEngine {
// these are lazy created to reduce memory overhead
Certificate[] c = peerCerts;
if (c == null) {
synchronized (OpenSslEngine.this) {
if (destroyed == 0) {
if (SSL.isInInit(ssl) != 0) {
throw new SSLPeerUnverifiedException("peer not verified");
}
c = peerCerts = initPeerCertChain();
} else {
c = peerCerts = EMPTY_CERTIFICATES;
}
}
}
return c;
}
private Certificate[] initPeerCertChain() throws SSLPeerUnverifiedException {
byte[][] chain = SSL.getPeerCertChain(ssl);
byte[] clientCert;
if (!clientMode) {
// if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer certificate.
// We use SSL_get_peer_certificate to get it in this case and add it to our array later.
//
// See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html
clientCert = SSL.getPeerCertificate(ssl);
} else {
clientCert = null;
}
if (chain == null && clientCert == null) {
throw new SSLPeerUnverifiedException("peer not verified");
}
int len = 0;
if (chain != null) {
len += chain.length;
}
int i = 0;
Certificate[] peerCerts;
if (clientCert != null) {
len++;
peerCerts = new Certificate[len];
peerCerts[i++] = new OpenSslX509Certificate(clientCert);
} else {
peerCerts = new Certificate[len];
}
if (chain != null) {
int a = 0;
for (; i < peerCerts.length; i++) {
peerCerts[i] = new OpenSslX509Certificate(chain[a++]);
}
}
return peerCerts;
}
@Override
public Certificate[] getLocalCertificates() {
// TODO: Find out how to get these
@ -1241,10 +1318,18 @@ public final class OpenSslEngine extends SSLEngine {
// these are lazy created to reduce memory overhead
X509Certificate[] c = x509PeerCerts;
if (c == null) {
final byte[][] chain;
synchronized (OpenSslEngine.this) {
if (destroyed == 0) {
if (SSL.isInInit(ssl) != 0) {
throw new SSLPeerUnverifiedException("peer not verified");
}
byte[][] chain = SSL.getPeerCertChain(ssl);
chain = SSL.getPeerCertChain(ssl);
} else {
c = x509PeerCerts = EMPTY_X509_CERTIFICATES;
return c;
}
}
if (chain == null) {
throw new SSLPeerUnverifiedException("peer not verified");
}
@ -1289,7 +1374,14 @@ public final class OpenSslEngine extends SSLEngine {
return INVALID_CIPHER;
}
if (cipher == null) {
String c = toJavaCipherSuite(SSL.getCipherForSSL(ssl));
final String c;
synchronized (OpenSslEngine.this) {
if (destroyed == 0) {
c = toJavaCipherSuite(SSL.getCipherForSSL(ssl));
} else {
c = INVALID_CIPHER;
}
}
if (c != null) {
cipher = c;
}
@ -1300,19 +1392,15 @@ public final class OpenSslEngine extends SSLEngine {
@Override
public String getProtocol() {
String applicationProtocol = OpenSslEngine.this.applicationProtocol;
if (applicationProtocol == null) {
applicationProtocol = SSL.getNextProtoNegotiated(ssl);
if (applicationProtocol == null) {
applicationProtocol = fallbackApplicationProtocol;
}
if (applicationProtocol != null) {
OpenSslEngine.this.applicationProtocol = applicationProtocol.replace(':', '_');
final String version;
synchronized (OpenSslEngine.this) {
if (destroyed == 0) {
version = SSL.getVersion(ssl);
} else {
OpenSslEngine.this.applicationProtocol = applicationProtocol = "";
return StringUtil.EMPTY_STRING;
}
}
String version = SSL.getVersion(ssl);
if (applicationProtocol.isEmpty()) {
if (applicationProtocol == null || applicationProtocol.isEmpty()) {
return version;
} else {
return version + ':' + applicationProtocol;
@ -1338,5 +1426,44 @@ public final class OpenSslEngine extends SSLEngine {
public int getApplicationBufferSize() {
return MAX_PLAINTEXT_LENGTH;
}
private Certificate[] initPeerCertChain() throws SSLPeerUnverifiedException {
byte[][] chain = SSL.getPeerCertChain(ssl);
final byte[] clientCert;
if (!clientMode) {
// if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer certificate.
// We use SSL_get_peer_certificate to get it in this case and add it to our array later.
//
// See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html
clientCert = SSL.getPeerCertificate(ssl);
} else {
clientCert = null;
}
if (chain == null && clientCert == null) {
throw new SSLPeerUnverifiedException("peer not verified");
}
int len = 0;
if (chain != null) {
len += chain.length;
}
int i = 0;
Certificate[] peerCerts;
if (clientCert != null) {
len++;
peerCerts = new Certificate[len];
peerCerts[i++] = new OpenSslX509Certificate(clientCert);
} else {
peerCerts = new Certificate[len];
}
if (chain != null) {
int a = 0;
for (; i < peerCerts.length; i++) {
peerCerts[i] = new OpenSslX509Certificate(chain[a++]);
}
}
return peerCerts;
}
}
}

View File

@ -22,7 +22,10 @@ import java.util.List;
/**
* OpenSSL {@link ApplicationProtocolNegotiator} for NPN.
*
* @deprecated use {@link OpenSslDefaultApplicationProtocolNegotiator}
*/
@Deprecated
public final class OpenSslNpnApplicationProtocolNegotiator implements OpenSslApplicationProtocolNegotiator {
private final List<String> protocols;
@ -34,8 +37,23 @@ public final class OpenSslNpnApplicationProtocolNegotiator implements OpenSslApp
this.protocols = checkNotNull(toList(protocols), "protocols");
}
@Override
public ApplicationProtocolConfig.Protocol protocol() {
return ApplicationProtocolConfig.Protocol.NPN;
}
@Override
public List<String> protocols() {
return protocols;
}
@Override
public ApplicationProtocolConfig.SelectorFailureBehavior selectorFailureBehavior() {
return ApplicationProtocolConfig.SelectorFailureBehavior.CHOOSE_MY_LAST_PROTOCOL;
}
@Override
public ApplicationProtocolConfig.SelectedListenerFailureBehavior selectedListenerFailureBehavior() {
return ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT;
}
}

View File

@ -64,11 +64,14 @@ public final class OpenSslServerContext extends OpenSslContext {
* {@code null} if it's not password-protected.
*/
public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException {
this(certChainFile, keyFile, keyPassword, null, null,
OpenSslDefaultApplicationProtocolNegotiator.INSTANCE, 0, 0);
this(certChainFile, keyFile, keyPassword, null, null, IdentityCipherSuiteFilter.INSTANCE,
NONE_PROTOCOL_NEGOTIATOR, 0, 0);
}
/**
* @deprecated use {@link #OpenSslServerContext(
* File, File, String, Iterable, CipherSuiteFilter, ApplicationProtocolConfig, long, long)}
*
* Creates a new instance.
*
* @param certChainFile an X.509 certificate chain file in PEM format
@ -83,12 +86,13 @@ public final class OpenSslServerContext extends OpenSslContext {
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
*/
@Deprecated
public OpenSslServerContext(
File certChainFile, File keyFile, String keyPassword,
Iterable<String> ciphers, ApplicationProtocolConfig apn,
long sessionCacheSize, long sessionTimeout) throws SSLException {
this(certChainFile, keyFile, keyPassword, null, ciphers,
toNegotiator(apn, false), sessionCacheSize, sessionTimeout);
toNegotiator(apn), sessionCacheSize, sessionTimeout);
}
/**
@ -133,13 +137,17 @@ public final class OpenSslServerContext extends OpenSslContext {
* {@code 0} to use the default value.
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
* @deprecated use {@link #OpenSslServerContext(
* File, File, String, TrustManagerFactory, Iterable,
* CipherSuiteFilter, ApplicationProtocolConfig, long, long)}
*/
@Deprecated
public OpenSslServerContext(
File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
Iterable<String> ciphers, ApplicationProtocolConfig config,
long sessionCacheSize, long sessionTimeout) throws SSLException {
this(certChainFile, keyFile, keyPassword, trustManagerFactory, ciphers,
toNegotiator(config, true), sessionCacheSize, sessionTimeout);
toNegotiator(config), sessionCacheSize, sessionTimeout);
}
/**
@ -156,13 +164,88 @@ public final class OpenSslServerContext extends OpenSslContext {
* {@code 0} to use the default value.
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
* @deprecated use {@link #OpenSslServerContext(
* File, File, String, TrustManagerFactory, Iterable,
* CipherSuiteFilter, OpenSslApplicationProtocolNegotiator, long, long)}
*/
@Deprecated
public OpenSslServerContext(
File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
Iterable<String> ciphers, OpenSslApplicationProtocolNegotiator apn,
long sessionCacheSize, long sessionTimeout) throws SSLException {
this(certChainFile, keyFile, keyPassword, trustManagerFactory, ciphers,
IdentityCipherSuiteFilter.INSTANCE, apn, sessionCacheSize, sessionTimeout);
}
super(ciphers, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER);
/**
* Creates a new instance.
*
* @param certChainFile an X.509 certificate chain file in PEM format
* @param keyFile a PKCS#8 private key file in PEM format
* @param keyPassword the password of the {@code keyFile}.
* {@code null} if it's not password-protected.
* @param ciphers the cipher suites to enable, in the order of preference.
* {@code null} to use the default cipher suites.
* @param cipherFilter a filter to apply over the supplied list of ciphers
* @param apn Provides a means to configure parameters related to application protocol negotiation.
* @param sessionCacheSize the size of the cache used for storing SSL session objects.
* {@code 0} to use the default value.
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
*/
public OpenSslServerContext(
File certChainFile, File keyFile, String keyPassword,
Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
long sessionCacheSize, long sessionTimeout) throws SSLException {
this(certChainFile, keyFile, keyPassword, null, ciphers, cipherFilter,
toNegotiator(apn), sessionCacheSize, sessionTimeout);
}
/**
* Creates a new instance.
*
* @param certChainFile an X.509 certificate chain file in PEM format
* @param keyFile a PKCS#8 private key file in PEM format
* @param keyPassword the password of the {@code keyFile}.
* {@code null} if it's not password-protected.
* @param ciphers the cipher suites to enable, in the order of preference.
* {@code null} to use the default cipher suites.
* @param cipherFilter a filter to apply over the supplied list of ciphers
* @param config Application protocol config.
* @param sessionCacheSize the size of the cache used for storing SSL session objects.
* {@code 0} to use the default value.
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
*/
public OpenSslServerContext(
File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig config,
long sessionCacheSize, long sessionTimeout) throws SSLException {
this(certChainFile, keyFile, keyPassword, trustManagerFactory, ciphers, cipherFilter,
toNegotiator(config), sessionCacheSize, sessionTimeout);
}
/**
* Creates a new instance.
*
* @param certChainFile an X.509 certificate chain file in PEM format
* @param keyFile a PKCS#8 private key file in PEM format
* @param keyPassword the password of the {@code keyFile}.
* {@code null} if it's not password-protected.
* @param ciphers the cipher suites to enable, in the order of preference.
* {@code null} to use the default cipher suites.
* @param cipherFilter a filter to apply over the supplied list of ciphers
* @param apn Application protocol negotiator.
* @param sessionCacheSize the size of the cache used for storing SSL session objects.
* {@code 0} to use the default value.
* @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
* {@code 0} to use the default value.
*/
public OpenSslServerContext(
File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory,
Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn,
long sessionCacheSize, long sessionTimeout) throws SSLException {
super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER);
OpenSsl.ensureAvailability();
checkNotNull(certChainFile, "certChainFile");

View File

@ -362,7 +362,7 @@ public abstract class SslContext {
case OPENSSL:
return new OpenSslServerContext(
keyCertChainFile, keyFile, keyPassword, trustManagerFactory,
ciphers, apn, sessionCacheSize, sessionTimeout);
ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
default:
throw new Error(provider.toString());
}
@ -655,7 +655,8 @@ public abstract class SslContext {
keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
case OPENSSL:
return new OpenSslClientContext(
trustCertChainFile, trustManagerFactory, ciphers, apn, sessionCacheSize, sessionTimeout);
trustCertChainFile, trustManagerFactory, ciphers, cipherFilter, apn,
sessionCacheSize, sessionTimeout);
}
// Should never happen!!
throw new Error();

View File

@ -605,7 +605,7 @@
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-tcnative</artifactId>
<version>1.1.32.Fork1</version>
<version>1.1.33.Fork1</version>
<classifier>${os.detected.classifier}</classifier>
<scope>compile</scope>
<optional>true</optional>