Fixes #5379 - Better handling for wrong SNI. (#5398)

* Fixes #5379 - Better handling for wrong SNI.

Reworked the SNI logic.
Added support for IP addresses in the SAN extension of certificates in the X509 class.
Fixed keystores to have CN=localhost and SAN with ip=127.0.0.1 and ip=[::1].
Fixed tests that were not using the correct Host header.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2020-10-12 15:48:53 +02:00 committed by GitHub
parent dcf4a835d7
commit 1cd15e8d85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 346 additions and 239 deletions

View File

@ -27,10 +27,14 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@ -49,7 +53,7 @@ import static org.junit.jupiter.api.Assertions.fail;
*/
public class HostnameVerificationTest
{
private SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client();
private final SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client();
private Server server;
private HttpClient client;
private NetworkConnector connector;
@ -64,7 +68,13 @@ public class HostnameVerificationTest
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
serverSslContextFactory.setKeyStorePassword("storepwd");
connector = new ServerConnector(server, serverSslContextFactory);
HttpConfiguration httpConfig = new HttpConfiguration();
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
customizer.setSniHostCheck(false);
httpConfig.addCustomizer(customizer);
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
SslConnectionFactory ssl = new SslConnectionFactory(serverSslContextFactory, http.getProtocol());
connector = new ServerConnector(server, 1, 1, ssl, http);
server.addConnector(connector);
server.setHandler(new DefaultHandler()
{
@ -102,14 +112,17 @@ public class HostnameVerificationTest
/**
* This test is supposed to verify that hostname verification works as described in:
* http://www.ietf.org/rfc/rfc2818.txt section 3.1. It uses a certificate with a common name different to localhost
* and sends a request to localhost. This should fail with an SSLHandshakeException.
* http://www.ietf.org/rfc/rfc2818.txt section 3.1.
* It uses a certificate with a common name "localhost" and SAN=127.0.0.1,
* and sends a request to 127.0.0.2.
* This should fail with on the client an SSLHandshakeException, because SNI
* host checking on the server side is disabled.
*/
@Test
public void simpleGetWithHostnameVerificationEnabledTest()
{
clientSslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
String uri = "https://localhost:" + connector.getLocalPort() + "/";
String uri = "https://127.0.0.2:" + connector.getLocalPort() + "/";
ExecutionException x = assertThrows(ExecutionException.class, () -> client.GET(uri));
Throwable cause = x.getCause();

View File

@ -1,9 +1,9 @@
Since OpenJDK 13.0.2/11.0.6 it is required that CA certificates have the extension CA=true.
Since OpenJDK 13.0.2/11.0.6 it is required that CA certificates have the extension "bc=ca:true".
The keystores are generated in the following way:
# Generates the server keystore. Note the BasicConstraint=CA:true extension.
$ keytool -v -genkeypair -validity 36500 -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype pkcs12 -dname "CN=server, OU=Jetty, O=Webtide, L=Omaha, S=NE, C=US" -ext BC=CA:true
$ keytool -v -genkeypair -validity 36500 -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype pkcs12 -dname "CN=localhost, OU=Jetty, O=Webtide, L=Omaha, S=NE, C=US" -ext bc=ca:true -ext san=ip:127.0.0.1,ip:[::1]
# Export the server certificate.
$ keytool -v -export -keystore keystore.p12 -rfc -file server.crt

View File

@ -57,4 +57,7 @@ For example, if you have bought domains `domain.com` and `domain.org`, you want
Furthermore, to specify additional domains or subdomains within the same certificate, you must specify the SAN extension.
In the example above, `san=dns:www.domain.com,dns:domain.org` specifies `www.domain.com` and `domain.org` as alternative names for your web applications (that you can configure using xref:og-deploy-virtual-hosts[virtual hosts]).
In rare cases, you may want to specify IP addresses, rather than domains, in the SAN extension.
The syntax in such case is `san=ip:127.0.0.1,ip:[::1]`, which specifies as subject alternative names IPv4 `127.0.0.1` and IPv6 `[::1]`.
====

View File

@ -195,8 +195,10 @@ The KeyStore contains two certificates, one for `one.com` and one for `two.net`.
There are three `ssl` module properties that control the SNI behavior on the server: one that works at the TLS level, and two that works at the HTTP level.
The property that works at the TLS level is:
`jetty.sslContext.sniRequired`::
This is the TLS level property, defaults to `false`.
Whether SNI is required at the TLS level, defaults to `false`.
Its behavior is explained by the following table:
@ -222,54 +224,77 @@ Its behavior is explained by the following table:
|===
WARNING: The _default certificate_ is the certificate returned by the TLS implementation in case there is no SNI match, and you should not rely on this certificate to be the same across Java vendors and versions, or Jetty versions, or TLS provider vendors and versions.
[WARNING]
====
The _default certificate_ is the certificate returned by the TLS implementation in case there is no SNI match, and you should not rely on this certificate to be the same across Java vendors and versions, or Jetty versions, or TLS provider vendors and versions.
In the example above it could be either the `one.com` certificate or the `two.net` certificate.
====
When `jetty.sslContext.sniRequired=true`, clients that don't send a valid SNI receive a TLS failure, and their attempt to connect to the server fails.
The details of this failure may not be reported and could be difficult to figure out that the failure is related to an invalid SNI.
For this reason, other two properties are defined at the HTTP level, so that clients can received an HTTP 400 response with more details about what went wrong while trying to connect to the server:
`jetty.ssl.sniRequired`::
Whether SNI is required at the HTTP level, defaults to `false`.
`jetty.ssl.sniHostCheck`::
Whether the SNI domain must be verified at the HTTP level against the `Host` header, defaults to `true`.
Their combined behavior is explained by the following table.
Its behavior is similar to the `jetty.sslContext.sniRequired` property above, and is explained by the following table:
.Behavior of the `jetty.ssl.[sniRequired|sniHostCheck]` properties
[cols="5*a"]
.Behavior of the `jetty.ssl.sniRequired` property
[cols=3*a]
|===
|
| `sniRequired=false` +
`sniHostCheck=false`
| `sniRequired=false` +
`sniHostCheck=true`
| `sniRequired=true` +
`sniHostCheck=false`
| `sniRequired=true` +
`sniHostCheck=true`
| `sniRequired=false`
| `sniRequired=true`
| SNI = `null`
| 200 OK
| 200 OK
| 400 Bad Request
| 400 Bad Request
| Accept
| Reject: 400 Bad Request
| SNI = `wrong.org`
| 200 OK
| 400 Bad Request
| 400 Bad Request
| 400 Bad Request
| Accept
| Reject: 400 Bad Request
| SNI = `one.com`
| 200 OK
| SNI matches `Host` header ? +
200 OK : 400 Bad Request
| 200 OK
| SNI matches `Host` header ? +
200 OK : 400 Bad Request
| Accept
| Accept
|===
In the normal case, with modern TLS clients and HTTP requests with a correct `Host` header, Jetty will pick the correct certificate from the KeyStore based on the SNI and respond with an HTTP 200 OK.
When `jetty.ssl.sniRequired=true`, the SNI is matched against the certificate sent to the client, and only if there is a match the request is accepted.
When the request is accepted, there could be an additional check controlled by the following property:
`jetty.ssl.sniHostCheck`::
Whether the certificate sent to the client matches the `Host` header, defaults to `true`.
Its behavior is explained by the following table:
.Behavior of the `jetty.ssl.sniHostCheck` property
[cols="3*a"]
|===
|
| `sniHostCheck=false`
| `sniHostCheck=true`
| certificate = `one.com` +
`Host: wrong.org`
| Accept
| Reject: 400 Bad Request
| certificate = `one.com` +
`Host: one.com`
| Accept
| Accept
|===
In the normal case with the default server configuration, for a TLS clients that sends SNI, and then sends an HTTP request with the correct `Host` header, Jetty will pick the correct certificate from the KeyStore based on the SNI received from the client, and accept the request.
Accepting the request does not mean that the request is responded with an HTTP 200 OK, but just that the request passed successfully the SNI checks and will be processed by the server.
If the request URI is for a resource that does not exist, the response will likely be a 404 Not Found.
You may modify the default values of the SNI properties if you want stricter control over old/broken TLS clients or bad HTTP requests.

View File

@ -646,35 +646,21 @@ public class ForwardProxyTLSServerTest
@Test
public void testBothProxyAndServerNeedClientAuth() throws Exception
{
// Keystore server_keystore.p12 contains:
// - alias "mykey": self-signed certificate with private key.
// - alias "client_root": certificate from client_keystore.p12 under the "server" alias.
// Keystore proxy_keystore.p12 contains:
// - alias "mykey": self-signed certificate with private key.
// - alias "client_root": certificate from client_keystore.p12 under the "proxy" alias.
// Keystore client_keystore.p12 contains:
// - alias "proxy": self-signed certificate with private key to send to the proxy.
// - alias "server": self-signed certificate with private key to send to the server.
// - alias "proxy_root": certificate from proxy_keystore under the "mykey" alias.
// - alias "server_root": certificate from server_keystore under the "mykey" alias.
// We want setEndpointIdentificationAlgorithm(null) for all 3 SslContextFactory
// because the certificate common names do not match the host names.
// See src/test/resources/readme_keystores.txt.
SslContextFactory.Server serverTLS = newServerSslContextFactory();
serverTLS.setEndpointIdentificationAlgorithm(null);
serverTLS.setNeedClientAuth(true);
startTLSServer(serverTLS, new ServerHandler());
int serverPort = serverConnector.getLocalPort();
String serverAlias = "server";
SslContextFactory.Server proxyTLS = newProxySslContextFactory();
proxyTLS.setEndpointIdentificationAlgorithm(null);
proxyTLS.setNeedClientAuth(true);
startProxy(proxyTLS);
int proxyPort = proxyConnector.getLocalPort();
String proxyAlias = "proxy";
String proxyAlias = "client_to_proxy";
String serverAlias = "client_to_server";
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client()
{
@Override

View File

@ -0,0 +1,22 @@
The keystores are generated differently from jetty-client's readme_keystores.txt.
Since SslContextFactory also loads the KeyStore as a TrustStore, rather than doing
CSR for the client certificates and sign them with the server|proxy certificate,
we just load the client certificates in the server|proxy KeyStores so that they
are trusted.
Structure is the following:
server_keystore.p12:
mykey: self-signed certificate with private key
client: certificate from client_keystore.p12@client_to_server
proxy_keystore.p12:
mykey: self-signed certificate with private key
client: certificate from client_keystore.p12@client_to_proxy
client_keystore.p12
client_to_proxy: self-signed certificate with private key (client certificate to send to proxy)
client_to_server: self-signed certificate with private key (client certificate to send to server)
proxy: certificate from proxy_keystore.p12@mykey (to trust proxy certificate)
server: certificate from server_keystore.p12@mykey (to trust server certificate)

View File

@ -40,7 +40,6 @@ import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.ssl.X509;
import org.slf4j.Logger;
@ -63,7 +62,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
private boolean _sniRequired;
private boolean _sniHostCheck;
private long _stsMaxAge = -1;
private long _stsMaxAge;
private boolean _stsIncludeSubDomains;
private HttpField _stsField;
@ -247,26 +246,32 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
{
SSLSession sslSession = sslEngine.getSession();
if (_sniHostCheck || _sniRequired)
if (isSniRequired() || isSniHostCheck())
{
X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509);
String sniHost = (String)sslSession.getValue(SslContextFactory.Server.SNI_HOST);
X509 cert = new X509(null, (X509Certificate)sslSession.getLocalCertificates()[0]);
String serverName = request.getServerName();
if (LOG.isDebugEnabled())
LOG.debug("Host {} with SNI {}", request.getServerName(), x509);
LOG.debug("Host={}, SNI={}, SNI Certificate={}", serverName, sniHost, cert);
if (x509 == null)
if (isSniRequired())
{
if (_sniRequired)
throw new BadMessageException(400, "SNI required");
if (sniHost == null)
throw new BadMessageException(400, "Invalid SNI");
if (!cert.matches(sniHost))
throw new BadMessageException(400, "Invalid SNI");
}
else if (_sniHostCheck && !x509.matches(request.getServerName()))
if (isSniHostCheck())
{
throw new BadMessageException(400, "Host does not match SNI");
if (!cert.matches(serverName))
throw new BadMessageException(400, "Invalid SNI");
}
}
request.setAttributes(new SslAttributes(request, sslSession, request.getAttributes()));
request.setAttributes(new SslAttributes(request, sslSession));
}
/**
* Customizes the request attributes for general secure settings.
* The default impl calls {@link Request#setSecure(boolean)} with true
@ -325,9 +330,9 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
private String _sessionId;
private String _sessionAttribute;
public SslAttributes(Request request, SSLSession sslSession, Attributes attributes)
private SslAttributes(Request request, SSLSession sslSession)
{
super(attributes);
super(request.getAttributes());
this._request = request;
this._session = sslSession;

View File

@ -106,7 +106,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
{
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"\r\n").getBytes("utf-8"));
os.flush();
@ -141,7 +141,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
byte[] contentB = content.getBytes("utf-8");
os.write((
"POST /echo HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: " + contentB.length + "\r\n" +
"\r\n").getBytes("utf-8"));
@ -189,7 +189,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"\r\n").getBytes("utf-8"));
os.flush();
@ -250,7 +250,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
byte[] contentB = content.getBytes("utf-8");
os.write((
"POST /echo HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: " + contentB.length + "\r\n" +
"connection: close\r\n" +
@ -295,7 +295,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
OutputStream os = client.getOutputStream();
os.write(("GET / HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Content-Type: text/plain\r\n" +
"Connection: close\r\n" +
@ -356,7 +356,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
os.write(("GET / HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Content-Type: text/plain\r\n" +
"Connection: close\r\n" +
@ -413,7 +413,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes("utf-8"));
@ -456,7 +456,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes("utf-8"));
@ -561,7 +561,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
os.write((
"GET / HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Content-Length: 20\r\n" +
"Content-Type: text/plain\r\n" +
@ -600,7 +600,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
long start = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
os.write((
"GET / HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Content-Length: 20\r\n" +
"Content-Type: text/plain\r\n" +
@ -643,7 +643,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
byte[] contentB = content.getBytes("utf-8");
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Content-Length: " + (contentB.length * 20) + "\r\n" +
"Content-Type: text/plain\r\n" +
@ -684,7 +684,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes("utf-8"));
@ -716,7 +716,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture
os.write((
"GET / HTTP/1.0\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: keep-alive\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes("utf-8"));

View File

@ -138,7 +138,11 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
{
OutputStream os = client.getOutputStream();
os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
String request = "GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n";
os.write(request.getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// Read the response.
@ -159,7 +163,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
OutputStream os = client.getOutputStream();
os.write(("OPTIONS * HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + "\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush();
@ -666,7 +670,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
String test = encoding[e] + "x" + b + "x" + w + "x" + c;
try
{
URL url = new URL(_scheme + "://" + _serverURI.getHost() + ":" + _serverURI.getPort() + "/?writes=" + w + "&block=" + b + (e == 0 ? "" : ("&encoding=" + encoding[e])) + (c == 0 ? "&chars=true" : ""));
URL url = new URL(_scheme + "://localhost:" + _serverURI.getPort() + "/?writes=" + w + "&block=" + b + (e == 0 ? "" : ("&encoding=" + encoding[e])) + (c == 0 ? "&chars=true" : ""));
InputStream in = (InputStream)url.getContent();
String response = IO.toString(in, e == 0 ? null : encoding[e]);
@ -698,7 +702,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"GET /data?writes=1024&block=256 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"content-type: unknown\r\n" +
"content-length: 30\r\n" +
@ -758,7 +762,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"GET /data HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: unknown\r\n" +
"transfer-encoding: chunked\r\n" +
"\r\n"
@ -807,7 +811,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"GET /data?writes=256&block=1024 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"content-type: unknown\r\n" +
"\r\n"
@ -848,7 +852,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"GET /data?encoding=iso-8859-1&writes=100&block=100000 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"content-type: unknown\r\n" +
"\r\n"
@ -875,7 +879,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
InputStream is = client.getInputStream();
os.write(("GET /data?writes=1&block=1024 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"content-type: unknown\r\n" +
"\r\n"
@ -901,10 +905,10 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"GET /r1 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"\r\n" +
"GET /r2 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"\r\n"
).getBytes());
@ -1049,7 +1053,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
{
request +=
"GET /data?writes=1&block=16&id=" + i + " HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"user-agent: testharness/1.0 (blah foo/bar)\r\n" +
"accept-encoding: nothing\r\n" +
"cookie: aaa=1234567890\r\n" +
@ -1058,7 +1062,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
request +=
"GET /data?writes=1&block=16 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"user-agent: testharness/1.0 (blah foo/bar)\r\n" +
"accept-encoding: nothing\r\n" +
"cookie: aaa=bbbbbb\r\n" +
@ -1095,7 +1099,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"POST /echo?charset=utf-8 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
@ -1106,7 +1110,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"POST /echo?charset=utf-8 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\r\n"
@ -1120,7 +1124,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
byte[] contentB = content.getBytes("utf-8");
os.write((
"POST /echo?charset=utf-16 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: " + contentB.length + "\r\n" +
"connection: close\r\n" +
@ -1182,21 +1186,21 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
//@checkstyle-disable-check : IllegalTokenText
os.write((
"POST /R1 HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"Host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\r\n" +
"123456789\n" +
"HEAD /R2 HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"Host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\r\n" +
"ABCDEFGHI\n" +
"POST /R3 HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"Host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"Connection: close\r\n" +
@ -1224,7 +1228,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"POST /echo/0?charset=utf-8 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
@ -1235,7 +1239,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"POST /echo/1?charset=utf-8 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\r\n"
@ -1249,7 +1253,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
byte[] contentB = content.getBytes(StandardCharsets.UTF_16);
os.write((
"POST /echo/2?charset=utf-8 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"content-type: text/plain; charset=utf-16\r\n" +
"content-length: " + contentB.length + "\r\n" +
"connection: close\r\n" +
@ -1279,7 +1283,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
// Send a request with chunked input and expect 100
os.write((
"GET / HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"Host: localhost:" + _serverURI.getPort() + "\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Expect: 100-continue\r\n" +
"Connection: Keep-Alive\r\n" +
@ -1316,7 +1320,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
// Send a request
os.write(("GET / HTTP/1.1\r\n" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"Host: localhost:" + _serverURI.getPort() + "\r\n" +
"\r\n"
).getBytes());
os.flush();
@ -1452,7 +1456,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
os.write((
"GET /data?writes=1024&block=256 HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"content-type: unknown\r\n" +
"content-length: 30\r\n" +
@ -1608,7 +1612,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
{
out.write(bytes, 0, bytes.length);
}
out.write("GET / HTTP/1.1\r\nHost: last\r\nConnection: close\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
out.write("GET /last HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
out.flush();
}
catch (Exception e)
@ -1636,8 +1640,8 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort());
final OutputStream out = client.getOutputStream();
out.write("GET / HTTP/1.1\r\nHost: test\r\n\r\n".getBytes());
out.write("GET / HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n".getBytes());
out.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n".getBytes());
out.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n".getBytes());
out.flush();
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
@ -1679,7 +1683,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
while (line != null);
}
private class WriteBodyAfterNoBodyResponseHandler extends AbstractHandler
private static class WriteBodyAfterNoBodyResponseHandler extends AbstractHandler
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
@ -1722,7 +1726,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
// write an initial request
os.write((
"GET / HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"\r\n"
).getBytes());
os.flush();
@ -1732,7 +1736,7 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
// write an pipelined request
os.write((
"GET / HTTP/1.1\r\n" +
"host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\r\n" +
"host: localhost:" + _serverURI.getPort() + "\r\n" +
"connection: close\r\n" +
"\r\n"
).getBytes());

View File

@ -253,7 +253,11 @@ public class SelectChannelServerSslTest extends HttpServerTestBase
{
OutputStream os = client.getOutputStream();
os.write("GET / HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
String request = "GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n";
os.write(request.getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// Read the response.

View File

@ -31,6 +31,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
@ -59,11 +60,11 @@ 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.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -77,55 +78,39 @@ public class SniSslConnectionFactoryTest
{
private Server _server;
private ServerConnector _connector;
private HttpConfiguration _httpsConfiguration;
private int _port;
@BeforeEach
public void before()
{
_server = new Server();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(8443);
httpConfig.setOutputBufferSize(32768);
_httpsConfiguration = new HttpConfiguration(httpConfig);
SecureRequestCustomizer src = new SecureRequestCustomizer();
src.setSniHostCheck(true);
_httpsConfiguration.addCustomizer(src);
_httpsConfiguration.addCustomizer((connector, hc, request) ->
{
EndPoint endp = request.getHttpChannel().getEndPoint();
if (endp instanceof SslConnection.DecryptedEndPoint)
{
try
{
SslConnection.DecryptedEndPoint sslEndp = (SslConnection.DecryptedEndPoint)endp;
SslConnection sslConnection = sslEndp.getSslConnection();
SSLEngine sslEngine = sslConnection.getSSLEngine();
SSLSession session = sslEngine.getSession();
for (Certificate c : session.getLocalCertificates())
{
request.getResponse().getHttpFields().add("X-Cert", ((X509Certificate)c).getSubjectDN().toString());
}
}
catch (Throwable th)
{
th.printStackTrace();
}
}
});
}
protected void start(String keystorePath) throws Exception
private void start(String keystorePath) throws Exception
{
start(ssl -> ssl.setKeyStorePath(keystorePath));
}
protected void start(Consumer<SslContextFactory.Server> sslConfig) throws Exception
private void start(Consumer<SslContextFactory.Server> sslConfig) throws Exception
{
start((ssl, customizer) -> sslConfig.accept(ssl));
}
private void start(BiConsumer<SslContextFactory.Server, SecureRequestCustomizer> config) throws Exception
{
_server = new Server();
HttpConfiguration httpConfiguration = new HttpConfiguration();
SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer();
httpConfiguration.addCustomizer(secureRequestCustomizer);
httpConfiguration.addCustomizer((connector, httpConfig, request) ->
{
EndPoint endPoint = request.getHttpChannel().getEndPoint();
SslConnection.DecryptedEndPoint sslEndPoint = (SslConnection.DecryptedEndPoint)endPoint;
SslConnection sslConnection = sslEndPoint.getSslConnection();
SSLEngine sslEngine = sslConnection.getSSLEngine();
SSLSession session = sslEngine.getSession();
for (Certificate c : session.getLocalCertificates())
{
request.getResponse().getHttpFields().add("X-CERT", ((X509Certificate)c).getSubjectDN().toString());
}
});
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslConfig.accept(sslContextFactory);
config.accept(sslContextFactory, secureRequestCustomizer);
File keystoreFile = sslContextFactory.getKeyStoreResource().getFile();
if (!keystoreFile.exists())
@ -133,10 +118,10 @@ public class SniSslConnectionFactoryTest
sslContextFactory.setKeyStorePassword("storepwd");
ServerConnector https = _connector = new ServerConnector(_server,
_connector = new ServerConnector(_server,
new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory(_httpsConfiguration));
_server.addConnector(https);
new HttpConnectionFactory(httpConfiguration));
_server.addConnector(_connector);
_server.setHandler(new AbstractHandler()
{
@ -151,36 +136,31 @@ public class SniSslConnectionFactoryTest
});
_server.start();
_port = https.getLocalPort();
}
@AfterEach
public void after() throws Exception
public void after()
{
if (_server != null)
_server.stop();
}
@Test
public void testConnect() throws Exception
{
start("src/test/resources/keystore_sni.p12");
String response = getResponse("127.0.0.1", null);
assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1"));
LifeCycle.stop(_server);
}
@Test
public void testSNIConnectNoWild() throws Exception
{
start("src/test/resources/keystore_sni_nowild.p12");
start((ssl, customizer) ->
{
// Disable the host check because this keystore has no CN and no SAN.
ssl.setKeyStorePath("src/test/resources/keystore_sni_nowild.p12");
customizer.setSniHostCheck(false);
});
String response = getResponse("www.acme.org", null);
assertThat(response, Matchers.containsString("X-HOST: www.acme.org"));
assertThat(response, Matchers.containsString("X-Cert: OU=default"));
assertThat(response, Matchers.containsString("X-CERT: OU=default"));
response = getResponse("www.example.com", null);
assertThat(response, Matchers.containsString("X-HOST: www.example.com"));
assertThat(response, Matchers.containsString("X-Cert: OU=example"));
assertThat(response, Matchers.containsString("X-CERT: OU=example"));
}
@Test
@ -202,6 +182,9 @@ public class SniSslConnectionFactoryTest
response = getResponse("www.san.com", "san example");
assertThat(response, Matchers.containsString("X-HOST: www.san.com"));
response = getResponse("wrongHost", "wrongHost", null);
assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
}
@Test
@ -226,7 +209,7 @@ public class SniSslConnectionFactoryTest
String response = getResponse("www.example.com", "some.other.com", "www.example.com");
assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
assertThat(response, Matchers.containsString("Host does not match SNI"));
assertThat(response, Matchers.containsString("Invalid SNI"));
}
@Test
@ -249,15 +232,12 @@ public class SniSslConnectionFactoryTest
@Test
public void testWrongSNIRejectedBadRequest() throws Exception
{
start(ssl ->
start((ssl, customizer) ->
{
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));
customizer.setSniRequired(true);
});
// Wrong SNI host.
@ -274,7 +254,7 @@ public class SniSslConnectionFactoryTest
@Test
public void testWrongSNIRejectedFunction() throws Exception
{
start(ssl ->
start((ssl, customizer) ->
{
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
// Do not allow unmatched SNI.
@ -285,10 +265,7 @@ public class SniSslConnectionFactoryTest
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));
customizer.setSniRequired(true);
});
// Wrong SNI host.
@ -318,7 +295,6 @@ public class SniSslConnectionFactoryTest
// Good SNI host.
HttpTester.Response response = HttpTester.parseResponse(getResponse("localhost", "localhost", null));
assertNotNull(response);
assertThat(response.getStatus(), is(200));
}
@ -332,7 +308,7 @@ public class SniSslConnectionFactoryTest
SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
clientContextFactory.start();
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _connector.getLocalPort()))
{
SNIHostName serverName = new SNIHostName("m.san.com");
SSLParameters params = sslSocket.getSSLParameters();
@ -376,7 +352,7 @@ public class SniSslConnectionFactoryTest
response = HttpTester.parseResponse(input);
assertNotNull(response);
assertThat(response.getStatus(), is(400));
assertThat(response.getContent(), containsString("Host does not match SNI"));
assertThat(response.getContent(), containsString("Invalid SNI"));
}
finally
{
@ -392,7 +368,7 @@ public class SniSslConnectionFactoryTest
SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
clientContextFactory.start();
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _connector.getLocalPort()))
{
SNIHostName serverName = new SNIHostName("www.domain.com");
SSLParameters params = sslSocket.getSSLParameters();
@ -436,7 +412,7 @@ public class SniSslConnectionFactoryTest
response = HttpTester.parseResponse(input);
assertNotNull(response);
assertThat(response.getStatus(), is(400));
assertThat(response.getContent(), containsString("Host does not match SNI"));
assertThat(response.getContent(), containsString("Invalid SNI"));
}
finally
{
@ -449,7 +425,7 @@ public class SniSslConnectionFactoryTest
{
start("src/test/resources/keystore_sni.p12");
final Queue<String> history = new LinkedBlockingQueue<>();
Queue<String> history = new LinkedBlockingQueue<>();
_connector.addBean(new SocketCustomizationListener()
{
@ -478,8 +454,8 @@ public class SniSslConnectionFactoryTest
}
});
String response = getResponse("127.0.0.1", null);
assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1"));
String response = getResponse("www.example.com", null);
assertThat(response, Matchers.containsString("X-HOST: www.example.com"));
assertEquals("customize connector class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
assertEquals("customize ssl class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
@ -501,7 +477,7 @@ public class SniSslConnectionFactoryTest
SslContextFactory clientContextFactory = new SslContextFactory.Client(true);
clientContextFactory.start();
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _connector.getLocalPort()))
{
if (sniHost != null)
{
@ -521,7 +497,7 @@ public class SniSslConnectionFactoryTest
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";
String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _connector.getLocalPort() + "\r\n\r\n";
sslSocket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1));
return IO.toString(sslSocket.getInputStream());
}

View File

@ -75,7 +75,9 @@ public class SslContextFactoryReloadTest
sslContextFactory.setKeyStorePassword("storepwd");
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.addCustomizer(new SecureRequestCustomizer());
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
customizer.setSniHostCheck(false);
httpsConfig.addCustomizer(customizer);
connector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory(httpsConfig));

View File

@ -30,6 +30,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIMatcher;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
@ -47,7 +49,6 @@ import org.slf4j.LoggerFactory;
*/
public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
{
public static final String SNI_X509 = "org.eclipse.jetty.util.ssl.snix509";
private static final Logger LOG = LoggerFactory.getLogger(SniX509ExtendedKeyManager.class);
private final X509ExtendedKeyManager _delegate;
@ -116,14 +117,29 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
Arrays.stream(mangledAliases)
.forEach(alias -> aliasMap.put(getAliasMapper().apply(alias), alias));
// 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);
String host = null;
if (session instanceof ExtendedSSLSession)
{
host = ((ExtendedSSLSession)session).getRequestedServerNames().stream()
.findAny()
.filter(SNIHostName.class::isInstance)
.map(SNIHostName.class::cast)
.map(SNIHostName::getAsciiName)
.orElse(null);
}
if (host == 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.
host = matchers == null ? null : matchers.stream()
.filter(SslContextFactory.AliasSNIMatcher.class::isInstance)
.map(SslContextFactory.AliasSNIMatcher.class::cast)
.findFirst()
.map(SslContextFactory.AliasSNIMatcher::getHost)
.orElse(null);
}
if (session != null && host != null)
session.putValue(SslContextFactory.Server.SNI_HOST, host);
try
{
@ -152,9 +168,6 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
return null;
}
if (session != null)
session.putValue(SNI_X509, x509);
// Convert the selected alias back to the original
// value before the alias mapping performed above.
String mangledAlias = aliasMap.get(alias);

View File

@ -1864,7 +1864,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
sslParams.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm());
sslParams.setUseCipherSuitesOrder(isUseCipherSuitesOrder());
if (!_certHosts.isEmpty() || !_certWilds.isEmpty())
sslParams.setSNIMatchers(Collections.singletonList(new AliasSNIMatcher()));
sslParams.setSNIMatchers(List.of(new AliasSNIMatcher()));
if (_selectedCipherSuites != null)
sslParams.setCipherSuites(_selectedCipherSuites);
if (_selectedProtocols != null)
@ -2032,7 +2032,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
}
}
class AliasSNIMatcher extends SNIMatcher
static class AliasSNIMatcher extends SNIMatcher
{
private String _host;
@ -2095,6 +2095,8 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
@ManagedObject
public static class Server extends SslContextFactory implements SniX509ExtendedKeyManager.SniSelector
{
public static final String SNI_HOST = "org.eclipse.jetty.util.ssl.sniHost";
private boolean _needClientAuth;
private boolean _wantClientAuth;
private boolean _sniRequired;

View File

@ -18,14 +18,13 @@
package org.eclipse.jetty.util.ssl;
import java.security.cert.CertificateParsingException;
import java.net.InetAddress;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.x500.X500Principal;
@ -37,17 +36,15 @@ import org.slf4j.LoggerFactory;
public class X509
{
private static final Logger LOG = LoggerFactory.getLogger(X509.class);
/*
* @see {@link X509Certificate#getKeyUsage()}
*/
private static final int KEY_USAGE__KEY_CERT_SIGN = 5;
/*
*
* @see {@link X509Certificate#getSubjectAlternativeNames()}
*/
private static final int SUBJECT_ALTERNATIVE_NAMES__DNS_NAME = 2;
private static final int SUBJECT_ALTERNATIVE_NAMES__IP_ADDRESS = 7;
public static boolean isCertSign(X509Certificate x509)
{
@ -63,39 +60,62 @@ public class X509
private final String _alias;
private final Set<String> _hosts = new LinkedHashSet<>();
private final Set<String> _wilds = new LinkedHashSet<>();
private final Set<InetAddress> _addresses = new LinkedHashSet<>();
public X509(String alias, X509Certificate x509) throws CertificateParsingException, InvalidNameException
public X509(String alias, X509Certificate x509)
{
_alias = alias;
_x509 = x509;
// Look for alternative name extensions
Collection<List<?>> altNames = x509.getSubjectAlternativeNames();
if (altNames != null)
try
{
for (List<?> list : altNames)
// Look for alternative name extensions
Collection<List<?>> altNames = x509.getSubjectAlternativeNames();
if (altNames != null)
{
if (((Number)list.get(0)).intValue() == SUBJECT_ALTERNATIVE_NAMES__DNS_NAME)
for (List<?> list : altNames)
{
String cn = list.get(1).toString();
int nameType = ((Number)list.get(0)).intValue();
switch (nameType)
{
case SUBJECT_ALTERNATIVE_NAMES__DNS_NAME:
{
String name = list.get(1).toString();
if (LOG.isDebugEnabled())
LOG.debug("Certificate alias={} SAN dns={} in {}", alias, name, this);
addName(name);
break;
}
case SUBJECT_ALTERNATIVE_NAMES__IP_ADDRESS:
{
String address = list.get(1).toString();
if (LOG.isDebugEnabled())
LOG.debug("Certificate alias={} SAN ip={} in {}", alias, address, this);
addAddress(address);
break;
}
default:
break;
}
}
}
// If no names found, look up the CN from the subject
LdapName name = new LdapName(x509.getSubjectX500Principal().getName(X500Principal.RFC2253));
for (Rdn rdn : name.getRdns())
{
if (rdn.getType().equalsIgnoreCase("CN"))
{
String cn = rdn.getValue().toString();
if (LOG.isDebugEnabled())
LOG.debug("Certificate SAN alias={} CN={} in {}", alias, cn, this);
LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this);
addName(cn);
}
}
}
// If no names found, look up the CN from the subject
LdapName name = new LdapName(x509.getSubjectX500Principal().getName(X500Principal.RFC2253));
for (Rdn rdn : name.getRdns())
catch (Exception x)
{
if (rdn.getType().equalsIgnoreCase("CN"))
{
String cn = rdn.getValue().toString();
if (LOG.isDebugEnabled())
LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this);
addName(cn);
}
throw new IllegalArgumentException(x);
}
}
@ -111,6 +131,28 @@ public class X509
}
}
private void addAddress(String host)
{
// Class InetAddress handles IPV6 brackets and IPv6 short forms, so that [::1]
// would match 0:0:0:0:0:0:0:1 as well as 0000:0000:0000:0000:0000:0000:0000:0001.
InetAddress address = toInetAddress(host);
if (address != null)
_addresses.add(address);
}
private InetAddress toInetAddress(String address)
{
try
{
return InetAddress.getByName(address);
}
catch (Throwable x)
{
LOG.trace("IGNORED", x);
return null;
}
}
public String getAlias()
{
return _alias;
@ -144,6 +186,11 @@ public class X509
if (_wilds.contains(domain))
return true;
}
InetAddress address = toInetAddress(host);
if (address != null)
return _addresses.contains(address);
return false;
}

View File

@ -89,7 +89,7 @@ public class WebSocketTester
protected static HttpFields.Mutable newUpgradeRequest(String extensions)
{
HttpFields.Mutable fields = HttpFields.build()
.add(HttpHeader.HOST, "127.0.0.1")
.add(HttpHeader.HOST, "localhost")
.add(HttpHeader.UPGRADE, "websocket")
.add(HttpHeader.CONNECTION, "Upgrade")
.add(HttpHeader.SEC_WEBSOCKET_KEY, NON_RANDOM_KEY)

View File

@ -47,6 +47,7 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.Net;
@ -349,6 +350,10 @@ public class HttpClientTest extends AbstractTest<TransportScenario>
Assumptions.assumeTrue(scenario.transport.isTlsBased());
scenario.startServer(new EmptyServerHandler());
// Disable validations on the server to be sure
// that the test failure happens during the
// validation of the certificate on the client.
scenario.httpConfig.getCustomizer(SecureRequestCustomizer.class).setSniHostCheck(false);
// Use a default SslContextFactory, requests should fail because the server certificate is unknown.
SslContextFactory.Client clientTLS = scenario.newClientSslContextFactory();
@ -361,9 +366,9 @@ public class HttpClientTest extends AbstractTest<TransportScenario>
assertThrows(ExecutionException.class, () ->
{
// Use IP address since the certificate contains a host name.
// Use an IP address not present in the certificate.
int serverPort = ((ServerConnector)scenario.connector).getLocalPort();
scenario.client.newRequest("https://127.0.0.1:" + serverPort)
scenario.client.newRequest("https://127.0.0.2:" + serverPort)
.timeout(5, TimeUnit.SECONDS)
.send();
});