Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fixes #6276 - Support non-standard domains in SNI and X509. #6296

Merged
merged 4 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.toolchain.test.Net;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
Expand All @@ -75,6 +76,7 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -924,4 +926,81 @@ protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuff
// The HTTP request will resume and be forced to handle the TLS buffer expansion.
assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
}

@Test
public void testDefaultNonDomainSNI() throws Exception
{
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
serverTLS.setKeyStorePath("src/test/resources/keystore_sni_non_domain.p12");
serverTLS.setKeyStorePassword("storepwd");
serverTLS.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
{
// Java clients don't send SNI by default if it's not a domain.
assertNull(sniHost);
return serverTLS.sniSelect(keyType, issuers, session, sniHost, certificates);
});
startServer(serverTLS, new EmptyServerHandler());

SslContextFactory.Client clientTLS = new SslContextFactory.Client();
// Trust any certificate received by the server.
clientTLS.setTrustStorePath("src/test/resources/keystore_sni_non_domain.p12");
clientTLS.setTrustStorePassword("storepwd");
// Disable TLS-level hostName verification, as we may receive a random certificate.
clientTLS.setEndpointIdentificationAlgorithm(null);
startClient(clientTLS);

// Host is "localhost" which is not a domain, so the JDK won't send SNI.
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.send();

assertEquals(HttpStatus.OK_200, response.getStatus());
}

@Test
public void testForcedNonDomainSNI() throws Exception
{
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
serverTLS.setKeyStorePath("src/test/resources/keystore_sni_non_domain.p12");
serverTLS.setKeyStorePassword("storepwd");
serverTLS.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
{
// We have forced the client to send the non-domain SNI.
assertNotNull(sniHost);
return serverTLS.sniSelect(keyType, issuers, session, sniHost, certificates);
});
startServer(serverTLS, new EmptyServerHandler());

SslContextFactory.Client clientTLS = new SslContextFactory.Client();
// Trust any certificate received by the server.
clientTLS.setTrustStorePath("src/test/resources/keystore_sni_non_domain.p12");
clientTLS.setTrustStorePassword("storepwd");
// Force TLS-level hostName verification, as we want to receive the correspondent certificate.
clientTLS.setEndpointIdentificationAlgorithm("HTTPS");
startClient(clientTLS);

clientTLS.setSNIProvider(SslContextFactory.Client.SniProvider.NON_DOMAIN_SNI_PROVIDER);

// Send a request with SNI "localhost", we should get the certificate at alias=localhost.
ContentResponse response1 = client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.send();
assertEquals(HttpStatus.OK_200, response1.getStatus());

// Send a request with SNI "127.0.0.1", we should get the certificate at alias=ip.
ContentResponse response2 = client.newRequest("127.0.0.1", connector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.send();
assertEquals(HttpStatus.OK_200, response2.getStatus());

if (Net.isIpv6InterfaceAvailable())
{
// Send a request with SNI "[::1]", we should get the certificate at alias=ip.
ContentResponse response3 = client.newRequest("[::1]", connector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.send();

assertEquals(HttpStatus.OK_200, response3.getStatus());
}
}
}
1 change: 1 addition & 0 deletions jetty-client/src/test/resources/jetty-logging.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG
#org.eclipse.jetty.io.ssl.LEVEL=DEBUG
#org.eclipse.jetty.http.LEVEL=DEBUG
#org.eclipse.jetty.util.ssl.LEVEL=DEBUG
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public String chooseServerAlias(String keyType, Principal[] issuers, Socket sock
if (delegate)
alias = _delegate.chooseServerAlias(keyType, issuers, socket);
if (LOG.isDebugEnabled())
LOG.debug("Chose {} alias {}/{} on {}", delegate ? "delegate" : "explicit", alias, keyType, socket);
LOG.debug("Chose {} alias={} keyType={} on {}", delegate ? "delegate" : "explicit", String.valueOf(alias), keyType, socket);
return alias;
}

Expand All @@ -205,7 +205,7 @@ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEn
if (delegate)
alias = _delegate.chooseEngineServerAlias(keyType, issuers, engine);
if (LOG.isDebugEnabled())
LOG.debug("Chose {} alias {}/{} on {}", delegate ? "delegate" : "explicit", alias, keyType, engine);
LOG.debug("Chose {} alias={} keyType={} on {}", delegate ? "delegate" : "explicit", String.valueOf(alias), keyType, engine);
return alias;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -2179,6 +2180,8 @@ public String getHost()

public static class Client extends SslContextFactory
{
private SniProvider sniProvider = (sslEngine, serverNames) -> serverNames;

public Client()
{
this(false);
Expand All @@ -2203,6 +2206,89 @@ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyMan
// Client has no SNI functionality.
return keyManager;
}

@Override
public void customize(SSLEngine sslEngine)
{
SSLParameters sslParameters = sslEngine.getSSLParameters();
List<SNIServerName> serverNames = sslParameters.getServerNames();
if (serverNames == null)
serverNames = Collections.emptyList();
List<SNIServerName> newServerNames = getSNIProvider().apply(sslEngine, serverNames);
if (newServerNames != null && newServerNames != serverNames)
{
sslParameters.setServerNames(newServerNames);
sslEngine.setSSLParameters(sslParameters);
}
super.customize(sslEngine);
}

/**
* @return the SNI provider used to customize the SNI
*/
public SniProvider getSNIProvider()
{
return sniProvider;
}

/**
* @param sniProvider the SNI provider used to customize the SNI
*/
public void setSNIProvider(SniProvider sniProvider)
{
this.sniProvider = Objects.requireNonNull(sniProvider);
}

/**
* <p>A provider for SNI names to send to the server during the TLS handshake.</p>
* <p>By default, the OpenJDK TLS implementation does not send SNI names when
* they are IP addresses, following what currently specified in
* <a href="https://datatracker.ietf.org/doc/html/rfc6066#section-3">TLS 1.3</a>,
* or when they are non-domain strings such as {@code "localhost"}.</p>
* <p>If you need to send custom SNI, such as a non-domain SNI or an IP address SNI,
* you can set your own SNI provider or use {@link #NON_DOMAIN_SNI_PROVIDER}.</p>
*/
@FunctionalInterface
public interface SniProvider
{
/**
* <p>An SNI provider that, if the given {@code serverNames} list is empty,
* retrieves the host via {@link SSLEngine#getPeerHost()}, converts it to
* ASCII bytes, and sends it as SNI.</p>
* <p>This allows to send non-domain SNI such as {@code "localhost"} or
* IP addresses.</p>
*/
public static final SniProvider NON_DOMAIN_SNI_PROVIDER = Client::getSniServerNames;

/**
* <p>Provides the SNI names to send to the server.</p>
* <p>Currently, RFC 6066 allows for different types of server names,
* but defines only one of type "host_name".</p>
* <p>As such, the input {@code serverNames} list and the list to be returned
* contain at most one element.</p>
*
* @param sslEngine the SSLEngine that processes the TLS handshake
* @param serverNames the non-null immutable list of server names computed by implementation
* @return either the same {@code serverNames} list passed as parameter, or a new list
* containing the server names to send to the server
*/
public List<SNIServerName> apply(SSLEngine sslEngine, List<SNIServerName> serverNames);
}

private static List<SNIServerName> getSniServerNames(SSLEngine sslEngine, List<SNIServerName> serverNames)
{
if (serverNames.isEmpty())
{
String host = sslEngine.getPeerHost();
if (host != null)
{
// Must use the byte[] constructor, because the character ':' is forbidden when
// using the String constructor (but typically present in IPv6 addresses).
return Collections.singletonList(new SNIHostName(host.getBytes(StandardCharsets.US_ASCII)));
}
}
return serverNames;
}
}

@ManagedObject
Expand Down Expand Up @@ -2304,10 +2390,16 @@ public void setSNISelector(SniX509ExtendedKeyManager.SniSelector sniSelector)
@Override
public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates)
{
boolean sniRequired = isSniRequired();

if (LOG.isDebugEnabled())
LOG.debug("Selecting alias: keyType={}, sni={}, sniRequired={}, certs={}", keyType, String.valueOf(sniHost), sniRequired, certificates);

String alias;
if (sniHost == null)
{
// No SNI, so reject or delegate.
return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
alias = sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
}
else
{
Expand All @@ -2323,19 +2415,26 @@ public String sniSelect(String keyType, Principal[] issuers, SSLSession session,
// SNI, as we will likely be called again with a different keyType.
boolean anyMatching = aliasCerts().values().stream()
.anyMatch(x509 -> x509.matches(sniHost));
return isSniRequired() || anyMatching ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
alias = sniRequired || anyMatching ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
}
else
{
alias = matching.get(0).getAlias();
if (matching.size() > 1)
{
// Prefer strict matches over wildcard matches.
alias = matching.stream()
.min(Comparator.comparingInt(cert -> cert.getWilds().size()))
.map(X509::getAlias)
.orElse(alias);
}
}
}

String alias = matching.get(0).getAlias();
if (matching.size() == 1)
return alias;
if (LOG.isDebugEnabled())
LOG.debug("Selected alias={}", String.valueOf(alias));

// Prefer strict matches over wildcard matches.
return matching.stream()
.min(Comparator.comparingInt(cert -> cert.getWilds().size()))
.map(X509::getAlias)
.orElse(alias);
}
return alias;
}

@Override
Expand Down
Loading