* Issue #3863 - Enforce use of SNI. Introduced SslContextFactory.rejectUnmatchedSNIHost (default false) so that if no SNI is sent, or SNI does not match a certificate, then the TLS handshake is aborted. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> * Issue #3863 - Enforce use of SNI. Updates after review. Introduced SslContextFactory.SNISelector to allow application to write their custom logic to select a certificate based on SNI information. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> * Issue #3863 Enforce SNI Added two sniRequired fields - one at SslContextLevel and the other at the SecureRequestCustomizer. This allows rejection either at TLS handshake or by 400 response. Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3863 Enforce SNI cleanups from review Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3863 Enforce SNI improved comments Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3863 Enforce SNI syntax sugar Signed-off-by: Greg Wilkins <gregw@webtide.com> * Issue #3863 SNI Updates from review. Extra test for sniSelector function Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
parent
869c3b51ce
commit
e09444eeb5
|
@ -26,6 +26,7 @@
|
|||
<Set name="sslSessionTimeout"><Property name="jetty.sslContext.sslSessionTimeout" default="-1"/></Set>
|
||||
<Set name="RenegotiationAllowed"><Property name="jetty.sslContext.renegotiationAllowed" default="true"/></Set>
|
||||
<Set name="RenegotiationLimit"><Property name="jetty.sslContext.renegotiationLimit" default="5"/></Set>
|
||||
<Set name="SniRequired"><Property name="jetty.sslContext.sniRequired" default="false"/></Set>
|
||||
|
||||
<!-- Example of how to configure a PKIX Certificate Path revocation Checker
|
||||
<Call id="pkixPreferCrls" class="java.security.cert.PKIXRevocationChecker$Option" name="valueOf"><Arg>PREFER_CRLS</Arg></Call>
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<Call name="addCustomizer">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.server.SecureRequestCustomizer">
|
||||
<Arg name="sniRequired" type="boolean"><Property name="jetty.ssl.sniRequired" default="false"/></Arg>
|
||||
<Arg name="sniHostCheck" type="boolean"><Property name="jetty.ssl.sniHostCheck" default="true"/></Arg>
|
||||
<Arg name="stsMaxAgeSeconds" type="int"><Property name="jetty.ssl.stsMaxAgeSeconds" default="-1"/></Arg>
|
||||
<Arg name="stsIncludeSubdomains" type="boolean"><Property name="jetty.ssl.stsIncludeSubdomains" default="false"/></Arg>
|
||||
|
|
|
@ -46,6 +46,12 @@ basehome:modules/ssl/keystore|etc/keystore
|
|||
## Connect Timeout in milliseconds
|
||||
# jetty.ssl.connectTimeout=15000
|
||||
|
||||
## Whether SNI is required for all secure connections. Rejections are in TLS handshakes.
|
||||
# jetty.sslContext.sniRequired=false
|
||||
|
||||
## Whether SNI is required for all secure connections. Rejections are in HTTP 400 response.
|
||||
# jetty.ssl.sniRequired=false
|
||||
|
||||
## Whether request host names are checked to match any SNI names
|
||||
# jetty.ssl.sniHostCheck=true
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
|
|||
|
||||
private String sslSessionAttribute = "org.eclipse.jetty.servlet.request.ssl_session";
|
||||
|
||||
private boolean _sniRequired;
|
||||
private boolean _sniHostCheck;
|
||||
private long _stsMaxAge = -1;
|
||||
private boolean _stsIncludeSubDomains;
|
||||
|
@ -82,6 +83,22 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
|
|||
@Name("stsMaxAgeSeconds") long stsMaxAgeSeconds,
|
||||
@Name("stsIncludeSubdomains") boolean stsIncludeSubdomains)
|
||||
{
|
||||
this(false, sniHostCheck, stsMaxAgeSeconds, stsIncludeSubdomains);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sniRequired True if a SNI certificate is required.
|
||||
* @param sniHostCheck True if the SNI Host name must match.
|
||||
* @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
|
||||
* @param stsIncludeSubdomains If true, a include subdomain property is sent with any Strict-Transport-Security header
|
||||
*/
|
||||
public SecureRequestCustomizer(
|
||||
@Name("sniRequired") boolean sniRequired,
|
||||
@Name("sniHostCheck") boolean sniHostCheck,
|
||||
@Name("stsMaxAgeSeconds") long stsMaxAgeSeconds,
|
||||
@Name("stsIncludeSubdomains") boolean stsIncludeSubdomains)
|
||||
{
|
||||
_sniRequired = sniRequired;
|
||||
_sniHostCheck = sniHostCheck;
|
||||
_stsMaxAge = stsMaxAgeSeconds;
|
||||
_stsIncludeSubDomains = stsIncludeSubdomains;
|
||||
|
@ -89,7 +106,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
|
|||
}
|
||||
|
||||
/**
|
||||
* @return True if the SNI Host name must match.
|
||||
* @return True if the SNI Host name must match when there is an SNI certificate.
|
||||
*/
|
||||
public boolean isSniHostCheck()
|
||||
{
|
||||
|
@ -97,13 +114,31 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
|
|||
}
|
||||
|
||||
/**
|
||||
* @param sniHostCheck True if the SNI Host name must match.
|
||||
* @param sniHostCheck True if the SNI Host name must match when there is an SNI certificate.
|
||||
*/
|
||||
public void setSniHostCheck(boolean sniHostCheck)
|
||||
{
|
||||
_sniHostCheck = sniHostCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if SNI is required, else requests will be rejected with 400 response.
|
||||
* @see SslContextFactory.Server#isSniRequired()
|
||||
*/
|
||||
public boolean isSniRequired()
|
||||
{
|
||||
return _sniRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sniRequired True if SNI is required, else requests will be rejected with 400 response.
|
||||
* @see SslContextFactory.Server#setSniRequired(boolean)
|
||||
*/
|
||||
public void setSniRequired(boolean sniRequired)
|
||||
{
|
||||
_sniRequired = sniRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
|
||||
*/
|
||||
|
@ -225,19 +260,23 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
|
|||
{
|
||||
SSLSession sslSession = sslEngine.getSession();
|
||||
|
||||
if (_sniHostCheck)
|
||||
if (_sniHostCheck || _sniRequired)
|
||||
{
|
||||
String name = request.getServerName();
|
||||
X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509);
|
||||
|
||||
if (x509 != null && !x509.matches(name))
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Host {} with SNI {}", name, x509);
|
||||
|
||||
if (x509 == null)
|
||||
{
|
||||
if (_sniRequired)
|
||||
throw new BadMessageException(400, "SNI required");
|
||||
}
|
||||
else if (_sniHostCheck && !x509.matches(name))
|
||||
{
|
||||
LOG.warn("Host {} does not match SNI {}", name, x509);
|
||||
throw new BadMessageException(400, "Host does not match SNI");
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Host {} matched SNI {}", name, x509);
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
@ -31,9 +31,11 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.function.Consumer;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
@ -57,6 +59,7 @@ import org.eclipse.jetty.server.SslConnectionFactory;
|
|||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
@ -67,6 +70,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class SniSslConnectionFactoryTest
|
||||
{
|
||||
|
@ -76,7 +81,7 @@ public class SniSslConnectionFactoryTest
|
|||
private int _port;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception
|
||||
public void before()
|
||||
{
|
||||
_server = new Server();
|
||||
|
||||
|
@ -114,12 +119,18 @@ public class SniSslConnectionFactoryTest
|
|||
|
||||
protected void start(String keystorePath) throws Exception
|
||||
{
|
||||
File keystoreFile = new File(keystorePath);
|
||||
start(ssl -> ssl.setKeyStorePath(keystorePath));
|
||||
}
|
||||
|
||||
protected void start(Consumer<SslContextFactory.Server> sslConfig) throws Exception
|
||||
{
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslConfig.accept(sslContextFactory);
|
||||
|
||||
File keystoreFile = sslContextFactory.getKeyStoreResource().getFile();
|
||||
if (!keystoreFile.exists())
|
||||
throw new FileNotFoundException(keystoreFile.getAbsolutePath());
|
||||
|
||||
SslContextFactory sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath());
|
||||
sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
|
||||
sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
|
||||
|
||||
|
@ -219,6 +230,79 @@ public class SniSslConnectionFactoryTest
|
|||
assertThat(response, Matchers.containsString("Host does not match SNI"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongSNIRejectedConnection() throws Exception
|
||||
{
|
||||
start(ssl ->
|
||||
{
|
||||
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
|
||||
// Do not allow unmatched SNI.
|
||||
ssl.setSniRequired(true);
|
||||
});
|
||||
|
||||
// Wrong SNI host.
|
||||
assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null));
|
||||
|
||||
// No SNI host.
|
||||
assertThrows(SSLHandshakeException.class, () -> getResponse(null, "wrong.com", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongSNIRejectedBadRequest() throws Exception
|
||||
{
|
||||
start(ssl ->
|
||||
{
|
||||
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
|
||||
// Do not allow unmatched SNI.
|
||||
ssl.setSniRequired(false);
|
||||
_httpsConfiguration.getCustomizers().stream()
|
||||
.filter(SecureRequestCustomizer.class::isInstance)
|
||||
.map(SecureRequestCustomizer.class::cast)
|
||||
.forEach(src -> src.setSniRequired(true));
|
||||
});
|
||||
|
||||
// Wrong SNI host.
|
||||
HttpTester.Response response = HttpTester.parseResponse(getResponse("wrong.com", "wrong.com", null));
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(400));
|
||||
|
||||
// No SNI host.
|
||||
response = HttpTester.parseResponse(getResponse(null, "wrong.com", null));
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(400));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testWrongSNIRejectedFunction() throws Exception
|
||||
{
|
||||
start(ssl ->
|
||||
{
|
||||
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
|
||||
// Do not allow unmatched SNI.
|
||||
ssl.setSniRequired(true);
|
||||
ssl.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
|
||||
{
|
||||
if (sniHost == null)
|
||||
return SniX509ExtendedKeyManager.SniSelector.DELEGATE;
|
||||
return ssl.sniSelect(keyType, issuers, session, sniHost, certificates);
|
||||
});
|
||||
_httpsConfiguration.getCustomizers().stream()
|
||||
.filter(SecureRequestCustomizer.class::isInstance)
|
||||
.map(SecureRequestCustomizer.class::cast)
|
||||
.forEach(src -> src.setSniRequired(true));
|
||||
});
|
||||
|
||||
// Wrong SNI host.
|
||||
assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null));
|
||||
|
||||
// No SNI host.
|
||||
HttpTester.Response response = HttpTester.parseResponse(getResponse(null, "wrong.com", null));
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(400));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSameConnectionRequestsForManyDomains() throws Exception
|
||||
{
|
||||
|
@ -247,6 +331,7 @@ public class SniSslConnectionFactoryTest
|
|||
|
||||
InputStream input = sslSocket.getInputStream();
|
||||
HttpTester.Response response = HttpTester.parseResponse(input);
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(200));
|
||||
|
||||
// Same socket, send a request for a different domain but same alias.
|
||||
|
@ -257,6 +342,7 @@ public class SniSslConnectionFactoryTest
|
|||
output.write(request.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
response = HttpTester.parseResponse(input);
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(200));
|
||||
|
||||
// Same socket, send a request for a different domain but different alias.
|
||||
|
@ -268,6 +354,7 @@ public class SniSslConnectionFactoryTest
|
|||
output.flush();
|
||||
|
||||
response = HttpTester.parseResponse(input);
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(400));
|
||||
assertThat(response.getContent(), containsString("Host does not match SNI"));
|
||||
}
|
||||
|
@ -303,6 +390,7 @@ public class SniSslConnectionFactoryTest
|
|||
|
||||
InputStream input = sslSocket.getInputStream();
|
||||
HttpTester.Response response = HttpTester.parseResponse(input);
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(200));
|
||||
|
||||
// Now, on the same socket, send a request for a different valid domain.
|
||||
|
@ -314,6 +402,7 @@ public class SniSslConnectionFactoryTest
|
|||
output.flush();
|
||||
|
||||
response = HttpTester.parseResponse(input);
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(200));
|
||||
|
||||
// Now make a request for an invalid domain for this connection.
|
||||
|
@ -325,6 +414,7 @@ public class SniSslConnectionFactoryTest
|
|||
output.flush();
|
||||
|
||||
response = HttpTester.parseResponse(input);
|
||||
assertNotNull(response);
|
||||
assertThat(response.getStatus(), is(400));
|
||||
assertThat(response.getContent(), containsString("Host does not match SNI"));
|
||||
}
|
||||
|
@ -334,49 +424,6 @@ public class SniSslConnectionFactoryTest
|
|||
}
|
||||
}
|
||||
|
||||
private String getResponse(String host, String cn) throws Exception
|
||||
{
|
||||
String response = getResponse(host, host, cn);
|
||||
assertThat(response, Matchers.startsWith("HTTP/1.1 200 "));
|
||||
assertThat(response, Matchers.containsString("X-URL: /ctx/path"));
|
||||
return response;
|
||||
}
|
||||
|
||||
private String getResponse(String sniHost, String reqHost, String cn) throws Exception
|
||||
{
|
||||
SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
|
||||
clientContextFactory.start();
|
||||
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
|
||||
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
|
||||
{
|
||||
if (sniHost != null)
|
||||
{
|
||||
SNIHostName serverName = new SNIHostName(sniHost);
|
||||
List<SNIServerName> serverNames = new ArrayList<>();
|
||||
serverNames.add(serverName);
|
||||
|
||||
SSLParameters params = sslSocket.getSSLParameters();
|
||||
params.setServerNames(serverNames);
|
||||
sslSocket.setSSLParameters(params);
|
||||
}
|
||||
sslSocket.startHandshake();
|
||||
|
||||
if (cn != null)
|
||||
{
|
||||
X509Certificate cert = ((X509Certificate)sslSocket.getSession().getPeerCertificates()[0]);
|
||||
assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn));
|
||||
}
|
||||
|
||||
String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n";
|
||||
sslSocket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1));
|
||||
return IO.toString(sslSocket.getInputStream());
|
||||
}
|
||||
finally
|
||||
{
|
||||
clientContextFactory.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSocketCustomization() throws Exception
|
||||
{
|
||||
|
@ -420,4 +467,47 @@ public class SniSslConnectionFactoryTest
|
|||
assertEquals("customize http class org.eclipse.jetty.server.HttpConnection,true", history.poll());
|
||||
assertEquals(0, history.size());
|
||||
}
|
||||
|
||||
private String getResponse(String host, String cn) throws Exception
|
||||
{
|
||||
String response = getResponse(host, host, cn);
|
||||
assertThat(response, Matchers.startsWith("HTTP/1.1 200 "));
|
||||
assertThat(response, Matchers.containsString("X-URL: /ctx/path"));
|
||||
return response;
|
||||
}
|
||||
|
||||
private String getResponse(String sniHost, String reqHost, String cn) throws Exception
|
||||
{
|
||||
SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
|
||||
clientContextFactory.start();
|
||||
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
|
||||
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
|
||||
{
|
||||
if (sniHost != null)
|
||||
{
|
||||
SNIHostName serverName = new SNIHostName(sniHost);
|
||||
List<SNIServerName> serverNames = new ArrayList<>();
|
||||
serverNames.add(serverName);
|
||||
|
||||
SSLParameters params = sslSocket.getSSLParameters();
|
||||
params.setServerNames(serverNames);
|
||||
sslSocket.setSSLParameters(params);
|
||||
}
|
||||
sslSocket.startHandshake();
|
||||
|
||||
if (cn != null)
|
||||
{
|
||||
X509Certificate cert = ((X509Certificate)sslSocket.getSession().getPeerCertificates()[0]);
|
||||
assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn));
|
||||
}
|
||||
|
||||
String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n";
|
||||
sslSocket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1));
|
||||
return IO.toString(sslSocket.getInputStream());
|
||||
}
|
||||
finally
|
||||
{
|
||||
clientContextFactory.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||
org.eclipse.jetty.LEVEL=INFO
|
||||
#org.eclipse.jetty.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.server.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.server.ConnectionLimit.LEVEL=DEBUG
|
||||
|
|
|
@ -24,8 +24,12 @@ import java.security.PrivateKey;
|
|||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.net.ssl.SNIMatcher;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
|
@ -41,14 +45,20 @@ import org.eclipse.jetty.util.log.Logger;
|
|||
public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
|
||||
{
|
||||
public static final String SNI_X509 = "org.eclipse.jetty.util.ssl.snix509";
|
||||
private static final String NO_MATCHERS = "no_matchers";
|
||||
private static final Logger LOG = Log.getLogger(SniX509ExtendedKeyManager.class);
|
||||
|
||||
private final X509ExtendedKeyManager _delegate;
|
||||
private final SslContextFactory.Server _sslContextFactory;
|
||||
|
||||
public SniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
|
||||
{
|
||||
this(keyManager, null);
|
||||
}
|
||||
|
||||
public SniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager, SslContextFactory.Server sslContextFactory)
|
||||
{
|
||||
_delegate = keyManager;
|
||||
_sslContextFactory = sslContextFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -65,53 +75,69 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
|
|||
|
||||
protected String chooseServerAlias(String keyType, Principal[] issuers, Collection<SNIMatcher> matchers, SSLSession session)
|
||||
{
|
||||
// Look for the aliases that are suitable for the keytype and issuers
|
||||
// Look for the aliases that are suitable for the keyType and issuers.
|
||||
String[] aliases = _delegate.getServerAliases(keyType, issuers);
|
||||
if (aliases == null || aliases.length == 0)
|
||||
return null;
|
||||
|
||||
// Look for the SNI information.
|
||||
String host = null;
|
||||
X509 x509 = null;
|
||||
if (matchers != null)
|
||||
// Find our SNIMatcher. There should only be one and it always matches (always returns true
|
||||
// from AliasSNIMatcher.matches), but it will capture the SNI Host if one was presented.
|
||||
String host = matchers == null ? null : matchers.stream()
|
||||
.filter(SslContextFactory.AliasSNIMatcher.class::isInstance)
|
||||
.map(SslContextFactory.AliasSNIMatcher.class::cast)
|
||||
.findFirst()
|
||||
.map(SslContextFactory.AliasSNIMatcher::getHost)
|
||||
.orElse(null);
|
||||
|
||||
try
|
||||
{
|
||||
for (SNIMatcher m : matchers)
|
||||
// Filter the certificates by alias.
|
||||
Collection<X509> certificates = Arrays.stream(aliases)
|
||||
.map(_sslContextFactory::getX509)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// delegate the decision to accept to the sniSelector
|
||||
SniSelector sniSelector = _sslContextFactory.getSNISelector();
|
||||
if (sniSelector == null)
|
||||
sniSelector = _sslContextFactory;
|
||||
String alias = sniSelector.sniSelect(keyType, issuers, session, host, certificates);
|
||||
|
||||
// Check selected alias
|
||||
if (alias != null && alias != SniSelector.DELEGATE)
|
||||
{
|
||||
if (m instanceof SslContextFactory.AliasSNIMatcher)
|
||||
// Make sure we got back an alias from the acceptable aliases.
|
||||
X509 x509 = _sslContextFactory.getX509(alias);
|
||||
if (!Arrays.asList(aliases).contains(alias) || x509 == null)
|
||||
{
|
||||
SslContextFactory.AliasSNIMatcher matcher = (SslContextFactory.AliasSNIMatcher)m;
|
||||
host = matcher.getHost();
|
||||
x509 = matcher.getX509();
|
||||
break;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Invalid X509 match for SNI {}: {}", host, alias);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Matched {} with {} from {}", host, x509, Arrays.asList(aliases));
|
||||
|
||||
// Check if the SNI selected alias is allowable
|
||||
if (x509 != null)
|
||||
{
|
||||
for (String a : aliases)
|
||||
{
|
||||
if (a.equals(x509.getAlias()))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Matched SNI {} with X509 {} from {}", host, x509, Arrays.asList(aliases));
|
||||
if (session != null)
|
||||
session.putValue(SNI_X509, x509);
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return alias;
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Failure matching X509 for SNI " + host, x);
|
||||
return null;
|
||||
}
|
||||
return NO_MATCHERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket)
|
||||
{
|
||||
SSLSocket sslSocket = (SSLSocket)socket;
|
||||
String alias = socket == null ? NO_MATCHERS : chooseServerAlias(keyType, issuers, sslSocket.getSSLParameters().getSNIMatchers(), sslSocket.getHandshakeSession());
|
||||
if (alias == NO_MATCHERS)
|
||||
String alias = (socket == null)
|
||||
? chooseServerAlias(keyType, issuers, Collections.emptyList(), null)
|
||||
: chooseServerAlias(keyType, issuers, sslSocket.getSSLParameters().getSNIMatchers(), sslSocket.getHandshakeSession());
|
||||
if (alias == SniSelector.DELEGATE)
|
||||
alias = _delegate.chooseServerAlias(keyType, issuers, socket);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Chose alias {}/{} on {}", alias, keyType, socket);
|
||||
|
@ -121,8 +147,10 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
|
|||
@Override
|
||||
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)
|
||||
{
|
||||
String alias = engine == null ? NO_MATCHERS : chooseServerAlias(keyType, issuers, engine.getSSLParameters().getSNIMatchers(), engine.getHandshakeSession());
|
||||
if (alias == NO_MATCHERS)
|
||||
String alias = (engine == null)
|
||||
? chooseServerAlias(keyType, issuers, Collections.emptyList(), null)
|
||||
: chooseServerAlias(keyType, issuers, engine.getSSLParameters().getSNIMatchers(), engine.getHandshakeSession());
|
||||
if (alias == SniSelector.DELEGATE)
|
||||
alias = _delegate.chooseEngineServerAlias(keyType, issuers, engine);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Chose alias {}/{} on {}", alias, keyType, engine);
|
||||
|
@ -152,4 +180,31 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
|
|||
{
|
||||
return _delegate.getServerAliases(keyType, issuers);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Selects a certificate based on SNI information.</p>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SniSelector
|
||||
{
|
||||
String DELEGATE = "delegate_no_sni_match";
|
||||
|
||||
/**
|
||||
* <p>Selects a certificate based on SNI information.</p>
|
||||
* <p>This method may be invoked multiple times during the TLS handshake, with different parameters.
|
||||
* For example, the {@code keyType} could be different, and subsequently the collection of certificates
|
||||
* (because they need to match the {@code keyType}.</p>
|
||||
*
|
||||
* @param keyType the key algorithm type name
|
||||
* @param issuers the list of acceptable CA issuer subject names or null if it does not matter which issuers are used
|
||||
* @param session the TLS handshake session or null if not known.
|
||||
* @param sniHost the server name indication sent by the client, or null if the client did not send the server name indication
|
||||
* @param certificates the list of certificates matching {@code keyType} and {@code issuers} known to this SslContextFactory
|
||||
* @return the alias of the certificate to return to the client, from the {@code certificates} list,
|
||||
* or {@link SniSelector#DELEGATE} if the certificate choice should be delegated to the
|
||||
* nested key manager or null for no match.
|
||||
* @throws SSLHandshakeException if the TLS handshake should be aborted
|
||||
*/
|
||||
public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates) throws SSLHandshakeException;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ import javax.net.ssl.SNIMatcher;
|
|||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
|
@ -1251,7 +1252,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
for (int idx = 0; idx < managers.length; idx++)
|
||||
{
|
||||
if (managers[idx] instanceof X509ExtendedKeyManager)
|
||||
managers[idx] = new SniX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx]);
|
||||
managers[idx] = newSniX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1263,6 +1264,11 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
return managers;
|
||||
}
|
||||
|
||||
protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
|
||||
{
|
||||
return new SniX509ExtendedKeyManager(keyManager);
|
||||
}
|
||||
|
||||
protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception
|
||||
{
|
||||
TrustManager[] managers = null;
|
||||
|
@ -2113,7 +2119,6 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
class AliasSNIMatcher extends SNIMatcher
|
||||
{
|
||||
private String _host;
|
||||
private X509 _x509;
|
||||
|
||||
AliasSNIMatcher()
|
||||
{
|
||||
|
@ -2128,36 +2133,14 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
|
||||
if (serverName instanceof SNIHostName)
|
||||
{
|
||||
String host = _host = ((SNIHostName)serverName).getAsciiName();
|
||||
host = StringUtil.asciiToLowerCase(host);
|
||||
|
||||
// Try an exact match
|
||||
_x509 = _certHosts.get(host);
|
||||
|
||||
// Else try an exact wild match
|
||||
if (_x509 == null)
|
||||
{
|
||||
_x509 = _certWilds.get(host);
|
||||
|
||||
// Else try an 1 deep wild match
|
||||
if (_x509 == null)
|
||||
{
|
||||
int dot = host.indexOf('.');
|
||||
if (dot >= 0)
|
||||
{
|
||||
String domain = host.substring(dot + 1);
|
||||
_x509 = _certWilds.get(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_host = StringUtil.asciiToLowerCase(((SNIHostName)serverName).getAsciiName());
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("SNI matched {}->{}", host, _x509);
|
||||
LOG.debug("SNI host name {}", _host);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("SNI no match for {}", serverName);
|
||||
LOG.debug("No SNI host name for {}", serverName);
|
||||
}
|
||||
|
||||
// Return true and allow the KeyManager to accept or reject when choosing a certificate.
|
||||
|
@ -2170,11 +2153,6 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
{
|
||||
return _host;
|
||||
}
|
||||
|
||||
public X509 getX509()
|
||||
{
|
||||
return _x509;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Client extends SslContextFactory
|
||||
|
@ -2198,8 +2176,12 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
}
|
||||
}
|
||||
|
||||
public static class Server extends SslContextFactory
|
||||
@ManagedObject
|
||||
public static class Server extends SslContextFactory implements SniX509ExtendedKeyManager.SniSelector
|
||||
{
|
||||
private boolean _sniRequired;
|
||||
private SniX509ExtendedKeyManager.SniSelector _sniSelector;
|
||||
|
||||
public Server()
|
||||
{
|
||||
setEndpointIdentificationAlgorithm(null);
|
||||
|
@ -2227,6 +2209,88 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable
|
|||
{
|
||||
super.setNeedClientAuth(needClientAuth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the default {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} implementation
|
||||
* require an SNI match? Note that if a non SNI handshake is accepted, requests may still be rejected
|
||||
* at the HTTP level for incorrect SNI (see SecureRequestCustomizer).
|
||||
* @return true if no SNI match is handled as no certificate match, false if no SNI match is handled by
|
||||
* delegation to the non SNI matching methods.
|
||||
*/
|
||||
@ManagedAttribute("Whether the TLS handshake is rejected if there is no SNI host match")
|
||||
public boolean isSniRequired()
|
||||
{
|
||||
return _sniRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the default {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} implementation
|
||||
* require an SNI match? Note that if a non SNI handshake is accepted, requests may still be rejected
|
||||
* at the HTTP level for incorrect SNI (see SecureRequestCustomizer).
|
||||
* This setting may have no effect if {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} is
|
||||
* overridden or a non null function is passed to {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.
|
||||
* @param sniRequired true if no SNI match is handled as no certificate match, false if no SNI match is handled by
|
||||
* delegation to the non SNI matching methods.
|
||||
*/
|
||||
public void setSniRequired(boolean sniRequired)
|
||||
{
|
||||
_sniRequired = sniRequired;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception
|
||||
{
|
||||
KeyManager[] managers = super.getKeyManagers(keyStore);
|
||||
if (isSniRequired())
|
||||
{
|
||||
if (managers == null || Arrays.stream(managers).noneMatch(SniX509ExtendedKeyManager.class::isInstance))
|
||||
throw new IllegalStateException("No SNI Key managers when SNI is required");
|
||||
}
|
||||
return managers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the custom function to select certificates based on SNI information
|
||||
*/
|
||||
public SniX509ExtendedKeyManager.SniSelector getSNISelector()
|
||||
{
|
||||
return _sniSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets a custom function to select certificates based on SNI information.</p>
|
||||
*
|
||||
* @param sniSelector the selection function
|
||||
*/
|
||||
public void setSNISelector(SniX509ExtendedKeyManager.SniSelector sniSelector)
|
||||
{
|
||||
_sniSelector = sniSelector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates) throws SSLHandshakeException
|
||||
{
|
||||
if (sniHost == null)
|
||||
{
|
||||
// No SNI, so reject or delegate.
|
||||
return _sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Match the SNI host, or let the JDK decide unless unmatched SNIs are rejected.
|
||||
return certificates.stream()
|
||||
.filter(x509 -> x509.matches(sniHost))
|
||||
.findFirst()
|
||||
.map(X509::getAlias)
|
||||
.orElse(_sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyManager keyManager)
|
||||
{
|
||||
return new SniX509ExtendedKeyManager(keyManager, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue