From 3a9c34ede47a2316ff9d10bc86f5a80ef076b7fa Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 18 May 2021 15:27:52 +0200 Subject: [PATCH] Fixes #6276 - Support non-standard domains in SNI and X509. Backported support for IP addresses in X509 (from #5379). Introduced SslContextFactory.Client.SniProvider to allow applications to specify the SNI names to send to the server. Improved logging of SNI processing. Signed-off-by: Simone Bordet --- .../jetty/client/HttpClientTLSTest.java | 79 +++++++++++ .../test/resources/jetty-logging.properties | 1 + .../resources/keystore_sni_non_domain.p12 | Bin 0 -> 4967 bytes .../util/ssl/SniX509ExtendedKeyManager.java | 4 +- .../jetty/util/ssl/SslContextFactory.java | 124 ++++++++++++++++-- .../java/org/eclipse/jetty/util/ssl/X509.java | 116 +++++++++++----- .../jetty/util/ssl/SslContextFactoryTest.java | 2 +- 7 files changed, 279 insertions(+), 47 deletions(-) create mode 100644 jetty-client/src/test/resources/keystore_sni_non_domain.p12 diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 363bb90cc867..c941f4b43f55 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -28,12 +28,14 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SNIHostName; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; @@ -75,6 +77,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; @@ -924,4 +927,80 @@ 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(); + // Force to send a non-domain SNI. + clientTLS.setSNIProvider((sslEngine, serverNames) -> Collections.singletonList(new SNIHostName("127.0.0.1"))); + // 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); + + // 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()); + + // 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()); + } } diff --git a/jetty-client/src/test/resources/jetty-logging.properties b/jetty-client/src/test/resources/jetty-logging.properties index f74a4da98d1f..fba2f6655488 100644 --- a/jetty-client/src/test/resources/jetty-logging.properties +++ b/jetty-client/src/test/resources/jetty-logging.properties @@ -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 diff --git a/jetty-client/src/test/resources/keystore_sni_non_domain.p12 b/jetty-client/src/test/resources/keystore_sni_non_domain.p12 new file mode 100644 index 0000000000000000000000000000000000000000..0ded97ee3f3f464f1d6d254c2e1e26d3d8e522ca GIT binary patch literal 4967 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 serverNames; + public Client() { this(false); @@ -2203,6 +2206,92 @@ protected X509ExtendedKeyManager newSniX509ExtendedKeyManager(X509ExtendedKeyMan // Client has no SNI functionality. return keyManager; } + + @Override + public void customize(SSLEngine sslEngine) + { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + List serverNames = sslParameters.getServerNames(); + if (serverNames == null) + serverNames = Collections.emptyList(); + List 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); + } + + /** + *

A provider for SNI names to send to the server during the TLS handshake.

+ *

By default, the OpenJDK TLS implementation does not send SNI names when + * they are IP addresses, following what currently specified in + * TLS 1.3, + * or when they are non-domain strings such as {@code "localhost"}.

+ *

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 #IP_ADDRESS_SNI_PROVIDER}.

+ */ + @FunctionalInterface + public interface SniProvider + { + /** + *

An SNI provider that sends, if the given {@code serverNames} list is empty, + * the host address retrieved via + * {@link InetAddress#getHostAddress() InetAddress.getLocalHost().getHostAddress()}.

+ */ + public static final SniProvider IP_ADDRESS_SNI_PROVIDER = Client::getSniServerNames; + + /** + *

Provides the SNI names to send to the server.

+ *

Currently, RFC 6066 allows for different types of server names, + * but defines only one of type "host_name".

+ *

As such, the input {@code serverNames} list and the list to be returned + * contain at most one element.

+ * + * @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 apply(SSLEngine sslEngine, List serverNames); + } + + private static List getSniServerNames(SSLEngine sslEngine, List serverNames) + { + if (serverNames.isEmpty()) + { + try + { + String address = InetAddress.getLocalHost().getHostAddress(); + // 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(address.getBytes(StandardCharsets.US_ASCII))); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("Could not retrieve localhost address", x); + } + } + return serverNames; + } } @ManagedObject @@ -2304,10 +2393,16 @@ public void setSNISelector(SniX509ExtendedKeyManager.SniSelector sniSelector) @Override public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection 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 { @@ -2323,19 +2418,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 diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java index 1c2139f37072..b27961982d73 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java @@ -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 @@ public class X509 { private static final Logger LOG = Log.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,51 +60,97 @@ public static boolean isCertSign(X509Certificate x509) private final String _alias; private final Set _hosts = new LinkedHashSet<>(); private final Set _wilds = new LinkedHashSet<>(); + private final Set _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> altNames = x509.getSubjectAlternativeNames(); - if (altNames != null) + try { - for (List list : altNames) + // Look for alternative name extensions + Collection> 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(); - if (LOG.isDebugEnabled()) - LOG.debug("Certificate SAN alias={} CN={} in {}", alias, cn, this); - if (cn != null) - addName(cn); + 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")) + // 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()) { - String cn = rdn.getValue().toString(); - if (LOG.isDebugEnabled()) - LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this); - if (cn != null && cn.contains(".") && !cn.contains(" ")) + 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); + } } } + catch (Exception x) + { + throw new IllegalArgumentException(x); + } } protected void addName(String cn) { - cn = StringUtil.asciiToLowerCase(cn); - if (cn.startsWith("*.")) - _wilds.add(cn.substring(2)); - else - _hosts.add(cn); + if (cn != null) + { + cn = StringUtil.asciiToLowerCase(cn); + if (cn.startsWith("*.")) + _wilds.add(cn.substring(2)); + else + _hosts.add(cn); + } + } + + 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.ignore(x); + return null; + } } public String getAlias() @@ -140,19 +183,26 @@ public boolean matches(String host) if (dot >= 0) { String domain = host.substring(dot + 1); - return _wilds.contains(domain); + if (_wilds.contains(domain)) + return true; } + + InetAddress address = toInetAddress(host); + if (address != null) + return _addresses.contains(address); + return false; } @Override public String toString() { - return String.format("%s@%x(%s,h=%s,w=%s)", + return String.format("%s@%x(%s,h=%s,a=%s,w=%s)", getClass().getSimpleName(), hashCode(), _alias, _hosts, + _addresses, _wilds); } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java index 751b124e606c..011d12e16099 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java @@ -361,7 +361,7 @@ public void testSNICertificates() throws Exception assertTrue(cf.getX509("other").matches("www.example.com")); assertFalse(cf.getX509("other").matches("eclipse.org")); - assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("www.san.com", "m.san.com")); + assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("san example", "www.san.com", "m.san.com")); assertTrue(cf.getX509("san").getWilds().isEmpty()); assertTrue(cf.getX509("san").matches("www.san.com")); assertTrue(cf.getX509("san").matches("m.san.com"));