Fixes #6034 - SslContextFactory may select a wildcard certificate during SNI selection when a more specific SSL certificate is present.

Now matching certificates are sorted, non-wildcard first, so that a more specific alias is returned.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2021-03-04 13:09:24 +01:00
parent 217a97b952
commit 8de7b83966
3 changed files with 44 additions and 7 deletions

View File

@ -28,6 +28,7 @@ import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName; import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngine;
@ -58,6 +59,7 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager; import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.ssl.X509;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -163,7 +165,27 @@ public class SniSslConnectionFactoryTest
@Test @Test
public void testSNIConnect() throws Exception public void testSNIConnect() throws Exception
{ {
start("src/test/resources/keystore_sni.p12"); start(ssl ->
{
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
ssl.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
{
// Make sure the *.domain.com comes before sub.domain.com
// to test that we prefer more specific domains.
List<X509> sortedCertificates = certificates.stream()
// As sorted() sorts ascending, make *.domain.com the smallest.
.sorted((x509a, x509b) ->
{
if (x509a.matches("domain.com"))
return -1;
if (x509b.matches("domain.com"))
return 1;
return 0;
})
.collect(Collectors.toList());
return ssl.sniSelect(keyType, issuers, session, sniHost, sortedCertificates);
});
});
String response = getResponse("jetty.eclipse.org", "jetty.eclipse.org"); String response = getResponse("jetty.eclipse.org", "jetty.eclipse.org");
assertThat(response, Matchers.containsString("X-HOST: jetty.eclipse.org")); assertThat(response, Matchers.containsString("X-HOST: jetty.eclipse.org"));
@ -174,6 +196,9 @@ public class SniSslConnectionFactoryTest
response = getResponse("foo.domain.com", "*.domain.com"); response = getResponse("foo.domain.com", "*.domain.com");
assertThat(response, Matchers.containsString("X-HOST: foo.domain.com")); assertThat(response, Matchers.containsString("X-HOST: foo.domain.com"));
response = getResponse("sub.domain.com", "sub.domain.com");
assertThat(response, Matchers.containsString("X-HOST: sub.domain.com"));
response = getResponse("m.san.com", "san example"); response = getResponse("m.san.com", "san example");
assertThat(response, Matchers.containsString("X-HOST: m.san.com")); assertThat(response, Matchers.containsString("X-HOST: m.san.com"));

View File

@ -49,6 +49,7 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.CertPathTrustManagerParameters;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;
@ -58,7 +59,6 @@ import javax.net.ssl.SNIMatcher;
import javax.net.ssl.SNIServerName; import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocket;
@ -2230,7 +2230,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
} }
@Override @Override
public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates) throws SSLHandshakeException public String sniSelect(String keyType, Principal[] issuers, SSLSession session, String sniHost, Collection<X509> certificates)
{ {
if (sniHost == null) if (sniHost == null)
{ {
@ -2239,12 +2239,24 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
} }
else else
{ {
// Match the SNI host, or let the JDK decide unless unmatched SNIs are rejected. // Match the SNI host.
return certificates.stream() List<X509> matching = certificates.stream()
.filter(x509 -> x509.matches(sniHost)) .filter(x509 -> x509.matches(sniHost))
.findFirst() .collect(Collectors.toList());
// No match, let the JDK decide unless unmatched SNIs are rejected.
if (matching.isEmpty())
return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
String alias = matching.get(0).getAlias();
if (matching.size() == 1)
return alias;
// Prefer strict matches over wildcard matches.
return matching.stream()
.min(Comparator.comparingInt(cert -> cert.getWilds().size()))
.map(X509::getAlias) .map(X509::getAlias)
.orElse(_sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE); .orElse(alias);
} }
} }