Fixes #6099 - Cipher preference may break SNI if certificates have different key types.

Updated the logic in SslContextFactory.Server.sniSelect(...) to check if there is
any certificate that matches, and if so return a null alias in the hope to be called
again and pick the right alias for the SNI.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
(cherry picked from commit 68296911b7)
This commit is contained in:
Simone Bordet 2021-05-06 22:08:11 +02:00
parent e9f260f4c3
commit b5b3874275
4 changed files with 40 additions and 15 deletions

View File

@ -40,6 +40,7 @@ import javax.net.ssl.SSLSocketFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.Connection;
@ -489,6 +490,21 @@ public class SniSslConnectionFactoryTest
assertEquals(0, history.size());
}
@Test
public void testSNIWithDifferentKeyTypes() throws Exception
{
// This KeyStore contains 2 certificates, one with keyAlg=EC, one with keyAlg=RSA.
start(ssl -> ssl.setKeyStorePath("src/test/resources/keystore_sni_key_types.p12"));
// Make a request with SNI = rsa.domain.com, the RSA certificate should be chosen.
HttpTester.Response response1 = HttpTester.parseResponse(getResponse("rsa.domain.com", "rsa.domain.com"));
assertEquals(HttpStatus.OK_200, response1.getStatus());
// Make a request with SNI = ec.domain.com, the EC certificate should be chosen.
HttpTester.Response response2 = HttpTester.parseResponse(getResponse("ec.domain.com", "ec.domain.com"));
assertEquals(HttpStatus.OK_200, response2.getStatus());
}
private String getResponse(String host, String cn) throws Exception
{
String response = getResponse(host, host, cn);

View File

@ -250,7 +250,7 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
* <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>
* (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

View File

@ -2155,12 +2155,17 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
}
/**
* 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).
* <p>Returns whether an SNI match is required when choosing the alias that
* identifies the certificate to send to the client.</p>
* <p>The exact logic to choose an alias given the SNI is configurable via
* {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.</p>
* <p>The default implementation is {@link #sniSelect(String, Principal[], SSLSession, String, Collection)}
* and if SNI is not required it will delegate the TLS implementation to
* choose an alias (typically the first alias in the KeyStore).</p>
* <p>Note that if a non SNI handshake is accepted, requests may still be rejected
* at the HTTP level for incorrect SNI (see SecureRequestCustomizer).</p>
*
* @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.
* @return whether an SNI match is required when choosing the alias that identifies the certificate
*/
@ManagedAttribute("Whether the TLS handshake is rejected if there is no SNI host match")
public boolean isSniRequired()
@ -2169,14 +2174,12 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
}
/**
* 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)}.
* <p>Sets whether an SNI match is required when choosing the alias that
* identifies the certificate to send to the client.</p>
* <p>This setting may have no effect if {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} is
* overridden or a custom function is passed to {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.</p>
*
* @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.
* @param sniRequired whether an SNI match is required when choosing the alias that identifies the certificate
*/
public void setSniRequired(boolean sniRequired)
{
@ -2244,9 +2247,15 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
.filter(x509 -> x509.matches(sniHost))
.collect(Collectors.toList());
// No match, let the JDK decide unless unmatched SNIs are rejected.
if (matching.isEmpty())
return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
{
// There is no match for this SNI among the certificates valid for
// this keyType; check if there is any certificate that matches this
// 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;
}
String alias = matching.get(0).getAlias();
if (matching.size() == 1)