Skip to content

Commit 6829691

Browse files
committed
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>
1 parent e5f28db commit 6829691

File tree

4 files changed

+61
-22
lines changed

4 files changed

+61
-22
lines changed

jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import javax.servlet.http.HttpServletRequest;
4545
import javax.servlet.http.HttpServletResponse;
4646

47+
import org.eclipse.jetty.http.HttpStatus;
4748
import org.eclipse.jetty.http.HttpTester;
4849
import org.eclipse.jetty.http.HttpVersion;
4950
import org.eclipse.jetty.io.Connection;
@@ -123,21 +124,23 @@ public void before()
123124

124125
protected void start(String keystorePath) throws Exception
125126
{
126-
start(ssl -> ssl.setKeyStorePath(keystorePath));
127+
start(ssl ->
128+
{
129+
ssl.setKeyStorePath(keystorePath);
130+
ssl.setKeyManagerPassword("keypwd");
131+
});
127132
}
128133

129134
protected void start(Consumer<SslContextFactory.Server> sslConfig) throws Exception
130135
{
131136
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
137+
sslContextFactory.setKeyStorePassword("storepwd");
132138
sslConfig.accept(sslContextFactory);
133139

134140
File keystoreFile = sslContextFactory.getKeyStoreResource().getFile();
135141
if (!keystoreFile.exists())
136142
throw new FileNotFoundException(keystoreFile.getAbsolutePath());
137143

138-
sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
139-
sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
140-
141144
ServerConnector https = _connector = new ServerConnector(_server,
142145
new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
143146
new HttpConnectionFactory(_httpsConfiguration));
@@ -194,6 +197,7 @@ public void testSNIConnect() throws Exception
194197
start(ssl ->
195198
{
196199
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
200+
ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
197201
ssl.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
198202
{
199203
// Make sure the *.domain.com comes before sub.domain.com
@@ -264,6 +268,7 @@ public void testWrongSNIRejectedConnection() throws Exception
264268
start(ssl ->
265269
{
266270
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
271+
ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
267272
// Do not allow unmatched SNI.
268273
ssl.setSniRequired(true);
269274
});
@@ -281,6 +286,7 @@ public void testWrongSNIRejectedBadRequest() throws Exception
281286
start(ssl ->
282287
{
283288
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
289+
ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
284290
// Do not allow unmatched SNI.
285291
ssl.setSniRequired(false);
286292
_httpsConfiguration.getCustomizers().stream()
@@ -307,6 +313,7 @@ public void testWrongSNIRejectedFunction() throws Exception
307313
start(ssl ->
308314
{
309315
ssl.setKeyStorePath("src/test/resources/keystore_sni.p12");
316+
ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
310317
// Do not allow unmatched SNI.
311318
ssl.setSniRequired(true);
312319
ssl.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
@@ -338,6 +345,7 @@ public void testWrongSNIRejectedConnectionWithNonSNIKeystore() throws Exception
338345
{
339346
// Keystore has only one certificate, but we want to enforce SNI.
340347
ssl.setKeyStorePath("src/test/resources/keystore");
348+
ssl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
341349
ssl.setSniRequired(true);
342350
});
343351

@@ -519,6 +527,21 @@ protected void customize(Socket socket, Class<? extends Connection> connection,
519527
assertEquals(0, history.size());
520528
}
521529

530+
@Test
531+
public void testSNIWithDifferentKeyTypes() throws Exception
532+
{
533+
// This KeyStore contains 2 certificates, one with keyAlg=EC, one with keyAlg=RSA.
534+
start(ssl -> ssl.setKeyStorePath("src/test/resources/keystore_sni_key_types.p12"));
535+
536+
// Make a request with SNI = rsa.domain.com, the RSA certificate should be chosen.
537+
HttpTester.Response response1 = HttpTester.parseResponse(getResponse("rsa.domain.com", "rsa.domain.com"));
538+
assertEquals(HttpStatus.OK_200, response1.getStatus());
539+
540+
// Make a request with SNI = ec.domain.com, the EC certificate should be chosen.
541+
HttpTester.Response response2 = HttpTester.parseResponse(getResponse("ec.domain.com", "ec.domain.com"));
542+
assertEquals(HttpStatus.OK_200, response2.getStatus());
543+
}
544+
522545
private String getResponse(String host, String cn) throws Exception
523546
{
524547
String response = getResponse(host, host, cn);
Binary file not shown.

jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SniX509ExtendedKeyManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public interface SniSelector
245245
* <p>Selects a certificate based on SNI information.</p>
246246
* <p>This method may be invoked multiple times during the TLS handshake, with different parameters.
247247
* For example, the {@code keyType} could be different, and subsequently the collection of certificates
248-
* (because they need to match the {@code keyType}.</p>
248+
* (because they need to match the {@code keyType}).</p>
249249
*
250250
* @param keyType the key algorithm type name
251251
* @param issuers the list of acceptable CA issuer subject names or null if it does not matter which issuers are used

jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,11 @@ private void unload()
479479
_certWilds.clear();
480480
}
481481

482+
Map<String, X509> aliasCerts()
483+
{
484+
return _aliasX509;
485+
}
486+
482487
@ManagedAttribute(value = "The selected TLS protocol versions", readonly = true)
483488
public String[] getSelectedProtocols()
484489
{
@@ -2118,7 +2123,7 @@ public String toString()
21182123
_trustStoreResource);
21192124
}
21202125

2121-
class Factory
2126+
private static class Factory
21222127
{
21232128
private final KeyStore _keyStore;
21242129
private final KeyStore _trustStore;
@@ -2133,7 +2138,7 @@ class Factory
21332138
}
21342139
}
21352140

2136-
class AliasSNIMatcher extends SNIMatcher
2141+
static class AliasSNIMatcher extends SNIMatcher
21372142
{
21382143
private String _host;
21392144

@@ -2235,11 +2240,17 @@ public void setNeedClientAuth(boolean needClientAuth)
22352240
}
22362241

22372242
/**
2238-
* Does the default {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} implementation
2239-
* require an SNI match? Note that if a non SNI handshake is accepted, requests may still be rejected
2240-
* at the HTTP level for incorrect SNI (see SecureRequestCustomizer).
2241-
* @return true if no SNI match is handled as no certificate match, false if no SNI match is handled by
2242-
* delegation to the non SNI matching methods.
2243+
* <p>Returns whether an SNI match is required when choosing the alias that
2244+
* identifies the certificate to send to the client.</p>
2245+
* <p>The exact logic to choose an alias given the SNI is configurable via
2246+
* {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.</p>
2247+
* <p>The default implementation is {@link #sniSelect(String, Principal[], SSLSession, String, Collection)}
2248+
* and if SNI is not required it will delegate the TLS implementation to
2249+
* choose an alias (typically the first alias in the KeyStore).</p>
2250+
* <p>Note that if a non SNI handshake is accepted, requests may still be rejected
2251+
* at the HTTP level for incorrect SNI (see SecureRequestCustomizer).</p>
2252+
*
2253+
* @return whether an SNI match is required when choosing the alias that identifies the certificate
22432254
*/
22442255
@ManagedAttribute("Whether the TLS handshake is rejected if there is no SNI host match")
22452256
public boolean isSniRequired()
@@ -2248,13 +2259,12 @@ public boolean isSniRequired()
22482259
}
22492260

22502261
/**
2251-
* Set if the default {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} implementation
2252-
* require an SNI match? Note that if a non SNI handshake is accepted, requests may still be rejected
2253-
* at the HTTP level for incorrect SNI (see SecureRequestCustomizer).
2254-
* This setting may have no effect if {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} is
2255-
* overridden or a non null function is passed to {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.
2256-
* @param sniRequired true if no SNI match is handled as no certificate match, false if no SNI match is handled by
2257-
* delegation to the non SNI matching methods.
2262+
* <p>Sets whether an SNI match is required when choosing the alias that
2263+
* identifies the certificate to send to the client.</p>
2264+
* <p>This setting may have no effect if {@link #sniSelect(String, Principal[], SSLSession, String, Collection)} is
2265+
* overridden or a custom function is passed to {@link #setSNISelector(SniX509ExtendedKeyManager.SniSelector)}.</p>
2266+
*
2267+
* @param sniRequired whether an SNI match is required when choosing the alias that identifies the certificate
22582268
*/
22592269
public void setSniRequired(boolean sniRequired)
22602270
{
@@ -2297,7 +2307,7 @@ public String sniSelect(String keyType, Principal[] issuers, SSLSession session,
22972307
if (sniHost == null)
22982308
{
22992309
// No SNI, so reject or delegate.
2300-
return _sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
2310+
return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
23012311
}
23022312
else
23032313
{
@@ -2306,9 +2316,15 @@ public String sniSelect(String keyType, Principal[] issuers, SSLSession session,
23062316
.filter(x509 -> x509.matches(sniHost))
23072317
.collect(Collectors.toList());
23082318

2309-
// No match, let the JDK decide unless unmatched SNIs are rejected.
23102319
if (matching.isEmpty())
2311-
return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
2320+
{
2321+
// There is no match for this SNI among the certificates valid for
2322+
// this keyType; check if there is any certificate that matches this
2323+
// SNI, as we will likely be called again with a different keyType.
2324+
boolean anyMatching = aliasCerts().values().stream()
2325+
.anyMatch(x509 -> x509.matches(sniHost));
2326+
return isSniRequired() || anyMatching ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE;
2327+
}
23122328

23132329
String alias = matching.get(0).getAlias();
23142330
if (matching.size() == 1)

0 commit comments

Comments
 (0)