diff --git a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslDiagnostics.java b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslDiagnostics.java index 719d2bf1744ab..a3d4ad5086781 100644 --- a/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslDiagnostics.java +++ b/libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslDiagnostics.java @@ -20,11 +20,14 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.IntStream; public class SslDiagnostics { + public static List describeValidHostnames(X509Certificate certificate) { try { final Collection> names = certificate.getSubjectAlternativeNames(); @@ -140,6 +143,44 @@ boolean isSameCertificate() { } } + /** + * These names align with the values (and indices) defined by {@link X509Certificate#getKeyUsage()} + */ + private static final String[] KEY_USAGE_NAMES = new String[] { + "digitalSignature", + "nonRepudiation", + "keyEncipherment", + "dataEncipherment", + "keyAgreement", + "keyCertSign", + "cRLSign", + "encipherOnly", + "decipherOnly" }; + + private enum ExtendedKeyUsage { + serverAuth ("1.3.6.1.5.5.7.3.1"), + clientAuth ("1.3.6.1.5.5.7.3.2"), + codeSigning ("1.3.6.1.5.5.7.3.3"), + emailProtection ("1.3.6.1.5.5.7.3.4"), + timeStamping ("1.3.6.1.5.5.7.3.8"), + ocspSigning ("1.3.6.1.5.5.7.3.9"); + + private String oid; + + ExtendedKeyUsage(String oid) { + this.oid = Objects.requireNonNull(oid); + } + + public static String decodeOid(String oid) { + for (ExtendedKeyUsage e : values()) { + if (e.oid.equals(oid)) { + return e.name(); + } + } + return oid; + } + } + /** * @param contextName The descriptive name of this SSL context (e.g. "xpack.security.transport.ssl") * @param trustedIssuers A Map of DN to Certificate, for the issuers that were trusted in the context in which this failure occurred @@ -166,8 +207,14 @@ public static String getTrustDiagnosticFailure(X509Certificate[] chain, PeerType .append(peerType.name().toLowerCase(Locale.ROOT)) .append(" provided a certificate with subject name [") .append(peerCert.getSubjectX500Principal().getName()) - .append("] and ") - .append(fingerprintDescription(peerCert)); + .append("], ") + .append(fingerprintDescription(peerCert)) + .append(", ") + .append(keyUsageDescription(peerCert)) + .append(" and ") + .append(extendedKeyUsageDescription(peerCert)); + + addSessionDescription(session, message); if (peerType == PeerType.SERVER) { try { @@ -395,4 +442,47 @@ private static boolean checkIssuer(X509Certificate certificate, X509Certificate private static boolean isSelfIssued(X509Certificate certificate) { return certificate.getIssuerX500Principal().equals(certificate.getSubjectX500Principal()); } + + private static String keyUsageDescription(X509Certificate certificate) { + boolean[] keyUsage = certificate.getKeyUsage(); + if (keyUsage == null || keyUsage.length == 0) { + return "no keyUsage"; + } + final String keyUsageDescription = IntStream.range(0, keyUsage.length) + .filter(i -> keyUsage[i]) + .mapToObj(i -> (i < KEY_USAGE_NAMES.length) ? KEY_USAGE_NAMES[i] : ("#" + i)) + .collect(Collectors.joining(", ")); + return keyUsageDescription.isEmpty() ? "no keyUsage" : ("keyUsage [" + keyUsageDescription + "]"); + } + + private static String extendedKeyUsageDescription(X509Certificate certificate) { + try { + return Optional.ofNullable(certificate.getExtendedKeyUsage()) + .flatMap(keyUsage -> generateExtendedKeyUsageDescription(keyUsage)) + .orElse("no extendedKeyUsage"); + } catch (CertificateParsingException e) { + return "invalid extendedKeyUsage [" + e + "]"; + } + } + + private static Optional generateExtendedKeyUsageDescription(List oids) { + return oids.stream() + .map(ExtendedKeyUsage::decodeOid) + .reduce((x, y) -> x + ", " + y) + .map(str -> "extendedKeyUsage [" + str + "]"); + } + + private static void addSessionDescription(SSLSession session, StringBuilder message) { + String cipherSuite = Optional.ofNullable(session) + .map(SSLSession::getCipherSuite) + .orElse(""); + String protocol = Optional.ofNullable(session) + .map(SSLSession::getProtocol) + .orElse(""); + message.append("; the session uses cipher suite [") + .append(cipherSuite) + .append("] and protocol [") + .append(protocol) + .append("]"); + } } diff --git a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslDiagnosticsTests.java b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslDiagnosticsTests.java index a1dfc1018a055..9ff1e50485efa 100644 --- a/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslDiagnosticsTests.java +++ b/libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslDiagnosticsTests.java @@ -13,8 +13,6 @@ import org.hamcrest.Matchers; import org.mockito.Mockito; -import javax.net.ssl.SSLSession; -import javax.security.auth.x500.X500Principal; import java.io.IOException; import java.nio.file.Path; import java.security.PublicKey; @@ -32,6 +30,8 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.net.ssl.SSLSession; +import javax.security.auth.x500.X500Principal; public class SslDiagnosticsTests extends ESTestCase { @@ -52,7 +52,9 @@ public void testDiagnosticMessageWhenServerProvidesAFullCertChainThatIsTrusted() final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1];" + " the certificate is signed by" + @@ -67,7 +69,9 @@ public void testDiagnosticMessageWhenServerProvidesAFullCertChainThatIsntTrusted final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1];" + " the certificate is signed by (subject [CN=Test CA 1] fingerprint [2b7b0416391bdf86502505c23149022d2213dadc])" + @@ -81,7 +85,9 @@ public void testDiagnosticMessageWithPartialChainAndUnknownTrustedIssuers() thro final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1]" + " but the server did not provide a copy of the issuing certificate in the certificate chain")); @@ -95,7 +101,9 @@ public void testDiagnosticMessageWithFullChainAndUnknownTrustedIssuers() throws final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1];" + " the certificate is signed by (subject [CN=Test CA 1] fingerprint [2b7b0416391bdf86502505c23149022d2213dadc])" + @@ -109,7 +117,9 @@ public void testDiagnosticMessageWhenServerFullCertChainIsntTrustedButMimicIssue final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1];" + " the certificate is signed by (subject [CN=Test CA 1] fingerprint [2b7b0416391bdf86502505c23149022d2213dadc])" + @@ -125,7 +135,9 @@ public void testDiagnosticMessageWhenServerProvidesEndCertificateOnlyAndTheCertA final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1]" + " but the server did not provide a copy of the issuing certificate in the certificate chain;" + @@ -140,7 +152,9 @@ public void testDiagnosticMessageWhenServerProvidesEndCertificateOnlyButTheCertA final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1]" + " but the server did not provide a copy of the issuing certificate in the certificate chain;" + @@ -160,7 +174,9 @@ public void testDiagnosticMessageWhenServerTrustsManyCAs() throws Exception { final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.CLIENT, session, "xpack.security.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with client at [192.168.1.2];" + - " the client provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the client provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate is issued by [CN=Test CA 1]" + " but the client did not provide a copy of the issuing certificate in the certificate chain;" + " this ssl context ([xpack.security.http.ssl]) is not configured to trust that issuer" + @@ -174,7 +190,9 @@ public void testDiagnosticMessageWhenServerProvidesEndCertificateOnlyWithMimicIs final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1]" + " but the server did not provide a copy of the issuing certificate in the certificate chain;" + @@ -190,7 +208,9 @@ public void testDiagnosticMessageWhenServerProvidesEndCertificateWithMultipleMim final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.9];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1]" + " but the server did not provide a copy of the issuing certificate in the certificate chain;" + @@ -217,7 +237,9 @@ public void testDiagnosticMessageWhenServerProvidePartialChainFromTrustedCA() th "xpack.security.authc.realms.ldap.ldap1.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.5];" + " the server provided a certificate with subject name [CN=elastic1,OU=windows,DC=example,DC=com]" + - " and fingerprint [" + MOCK_FINGERPRINT_4 + "];" + + ", fingerprint [" + MOCK_FINGERPRINT_4 + "]," + + " keyUsage [digitalSignature, nonRepudiation] and extendedKeyUsage [serverAuth, codeSigning];" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate does not have any subject alternative names;" + " the certificate is issued by [CN=ca,OU=windows,DC=example,DC=com];" + " the certificate is" + @@ -246,7 +268,9 @@ public void testDiagnosticMessageWhenServerProvidePartialChainFromUntrustedCA() "xpack.security.authc.realms.ldap.ldap1.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.6];" + " the server provided a certificate with subject name [CN=elastic1,OU=windows,DC=example,DC=com]" + - " and fingerprint [" + MOCK_FINGERPRINT_4 + "];" + + ", fingerprint [" + MOCK_FINGERPRINT_4 + "]," + + " keyUsage [digitalSignature, nonRepudiation] and extendedKeyUsage [serverAuth, codeSigning];" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate does not have any subject alternative names;" + " the certificate is issued by [CN=ca,OU=windows,DC=example,DC=com];" + " the certificate is" + @@ -264,7 +288,8 @@ public void testDiagnosticMessageWhenServerProvidesASelfSignedCertThatIsDirectly "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + " the server provided a certificate with subject name [CN=Test CA 1]" + - " and fingerprint [2b7b0416391bdf86502505c23149022d2213dadc];" + + ", fingerprint [2b7b0416391bdf86502505c23149022d2213dadc], no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate does not have any subject alternative names;" + " the certificate is self-issued; the [CN=Test CA 1] certificate is trusted in this ssl context ([xpack.http.ssl])")); } @@ -277,7 +302,8 @@ public void testDiagnosticMessageWhenServerProvidesASelfSignedCertThatIsNotTrust "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.10.10];" + " the server provided a certificate with subject name [CN=Test CA 1]" + - " and fingerprint [2b7b0416391bdf86502505c23149022d2213dadc];" + + ", fingerprint [2b7b0416391bdf86502505c23149022d2213dadc], no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate does not have any subject alternative names;" + " the certificate is self-issued; the [CN=Test CA 1] certificate is not trusted in this ssl context ([xpack.http.ssl])")); } @@ -290,7 +316,8 @@ public void testDiagnosticMessageWhenServerProvidesASelfSignedCertWithMimicName( "xpack.http.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.1];" + " the server provided a certificate with subject name [CN=Test CA 1]" + - " and fingerprint [2b7b0416391bdf86502505c23149022d2213dadc];" + + ", fingerprint [2b7b0416391bdf86502505c23149022d2213dadc], no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate does not have any subject alternative names;" + " the certificate is self-issued; the [CN=Test CA 1] certificate is not trusted in this ssl context ([xpack.http.ssl]);" + " this ssl context does trust a certificate with subject [CN=Test CA 1]" + @@ -319,7 +346,136 @@ public void testDiagnosticMessageWhenServerProvidesAnEmailSubjAltName() throws E "xpack.monitoring.exporters.elastic-cloud.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.3];" + " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]" + - " and fingerprint [" + MOCK_FINGERPRINT_1 + "];" + + ", fingerprint [" + MOCK_FINGERPRINT_1 + "]," + + " keyUsage [digitalSignature, nonRepudiation] and extendedKeyUsage [serverAuth, codeSigning];" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + + " the certificate does not have any DNS/IP subject alternative names;" + + " the certificate is self-issued;" + + " the [CN=foo,DC=example,DC=com] certificate is trusted in this ssl context ([xpack.monitoring.exporters.elastic-cloud.ssl])")); + } + + public void testDiagnosticMessageWhenServerCertificateHasNoKeyUsage() throws Exception { + final String subjectName = "CN=foo,DC=example,DC=com"; + + final X509Certificate certificate = mockCertificateWithIssuer( + subjectName, + MOCK_ENCODING_1, + Collections.singletonList(List.of(1, "foo@example.com")), + null, + null, + null + ); + X509Certificate[] chain = new X509Certificate[] { certificate }; + + final String peerHost = "192.168.1." + randomIntBetween(1, 128); + final String cipherSuite = randomFrom(SslConfigurationLoader.DEFAULT_CIPHERS); + final String protocol = randomFrom(SslConfigurationLoader.DEFAULT_PROTOCOLS); + final SSLSession session = session(peerHost, cipherSuite, protocol); + final Map> trustIssuers = trust(certificate); + final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, + "xpack.monitoring.exporters.elastic-cloud.ssl", trustIssuers); + + assertThat(message, Matchers.equalTo("failed to establish trust with server at [" + peerHost + "];" + + " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]" + + ", fingerprint [" + MOCK_FINGERPRINT_1 + "], no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [" + cipherSuite + "] and protocol [" + protocol + "];" + + " the certificate does not have any DNS/IP subject alternative names;" + + " the certificate is self-issued;" + + " the [CN=foo,DC=example,DC=com] certificate is trusted in this ssl context ([xpack.monitoring.exporters.elastic-cloud.ssl])")); + } + + public void testDiagnosticMessageWhenServerCertificateHasKeyUsageAndNoExtendedKeyUsage() throws Exception { + final String subjectName = "CN=foo,DC=example,DC=com"; + + final boolean[] keyUsage = {true, false, true, true, true, false, false, false, false, false}; + final X509Certificate certificate = mockCertificateWithIssuer( + subjectName, + MOCK_ENCODING_1, + Collections.singletonList(List.of(1, "foo@example.com")), + null, + keyUsage, + null + ); + X509Certificate[] chain = new X509Certificate[] { certificate }; + + final String peerHost = "192.168.1." + randomIntBetween(1, 128); + final String cipherSuite = randomFrom(SslConfigurationLoader.DEFAULT_CIPHERS); + final String protocol = randomFrom(SslConfigurationLoader.DEFAULT_PROTOCOLS); + final SSLSession session = session(peerHost, cipherSuite, protocol); + final Map> trustIssuers = trust(certificate); + final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, + "xpack.monitoring.exporters.elastic-cloud.ssl", trustIssuers); + + assertThat(message, Matchers.equalTo("failed to establish trust with server at [" + peerHost + "];" + + " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]" + + ", fingerprint [" + MOCK_FINGERPRINT_1 + "]," + + " keyUsage [digitalSignature, keyEncipherment, dataEncipherment, keyAgreement]" + + " and no extendedKeyUsage;" + + " the session uses cipher suite [" + cipherSuite + "] and protocol [" + protocol + "];" + + " the certificate does not have any DNS/IP subject alternative names;" + + " the certificate is self-issued;" + + " the [CN=foo,DC=example,DC=com] certificate is trusted in this ssl context ([xpack.monitoring.exporters.elastic-cloud.ssl])")); + } + + public void testDiagnosticMessageWhenServerCertificateHasKeyUsageAndExtendedKeyUsage() throws Exception { + final String subjectName = "CN=foo,DC=example,DC=com"; + + final boolean[] keyUsage = {false, false, false, false, false, false, false, true, false}; + final X509Certificate certificate = mockCertificateWithIssuer( + subjectName, + MOCK_ENCODING_1, + Collections.singletonList(List.of(1, "foo@example.com")), + null, + keyUsage, + List.of("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2") + ); + X509Certificate[] chain = new X509Certificate[] { certificate }; + + final String peerHost = "192.168.1." + randomIntBetween(1, 128); + final String cipherSuite = randomFrom(SslConfigurationLoader.DEFAULT_CIPHERS); + final String protocol = randomFrom(SslConfigurationLoader.DEFAULT_PROTOCOLS); + final SSLSession session = session(peerHost, cipherSuite, protocol); + final Map> trustIssuers = trust(certificate); + final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, + "xpack.monitoring.exporters.elastic-cloud.ssl", trustIssuers); + + assertThat(message, Matchers.equalTo("failed to establish trust with server at [" + peerHost + "];" + + " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]" + + ", fingerprint [" + MOCK_FINGERPRINT_1 + "]," + + " keyUsage [encipherOnly] and extendedKeyUsage [serverAuth, clientAuth];" + + " the session uses cipher suite [" + cipherSuite + "] and protocol [" + protocol + "];" + + " the certificate does not have any DNS/IP subject alternative names;" + + " the certificate is self-issued;" + + " the [CN=foo,DC=example,DC=com] certificate is trusted in this ssl context ([xpack.monitoring.exporters.elastic-cloud.ssl])")); + } + + public void testDiagnosticMessageWhenServerCertificateHasOversizedKeyUsageAndUnrecognisedExtendedKeyUsage() throws Exception { + final String subjectName = "CN=foo,DC=example,DC=com"; + + final boolean[] keyUsage = { false, false, false, false, false, true, false, false, false, /* extra --> */ true, false, true }; + final X509Certificate certificate = mockCertificateWithIssuer( + subjectName, + MOCK_ENCODING_1, + Collections.singletonList(List.of(1, "foo@example.com")), + null, + keyUsage, + List.of("1.3.6.1.5.5.7.3.8", "1.3.6.1.5.5.7.3.12") + ); + X509Certificate[] chain = new X509Certificate[] { certificate }; + + final String peerHost = "192.168.1." + randomIntBetween(1, 128); + final String cipherSuite = randomFrom(SslConfigurationLoader.DEFAULT_CIPHERS); + final String protocol = randomFrom(SslConfigurationLoader.DEFAULT_PROTOCOLS); + final SSLSession session = session(peerHost, cipherSuite, protocol); + final Map> trustIssuers = trust(certificate); + final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, + "xpack.monitoring.exporters.elastic-cloud.ssl", trustIssuers); + + assertThat(message, Matchers.equalTo("failed to establish trust with server at [" + peerHost + "];" + + " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]" + + ", fingerprint [" + MOCK_FINGERPRINT_1 + "]," + + " keyUsage [keyCertSign, #9, #11] and extendedKeyUsage [timeStamping, 1.3.6.1.5.5.7.3.12];" + + " the session uses cipher suite [" + cipherSuite + "] and protocol [" + protocol + "];" + " the certificate does not have any DNS/IP subject alternative names;" + " the certificate is self-issued;" + " the [CN=foo,DC=example,DC=com] certificate is trusted in this ssl context ([xpack.monitoring.exporters.elastic-cloud.ssl])")); @@ -336,8 +492,10 @@ public void testDiagnosticMessageWhenACertificateHasAnInvalidEncoding() throws E final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.security.transport.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.6];" + - " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]" + - " and invalid encoding [java.security.cert.CertificateEncodingException: MOCK INVALID ENCODING];" + + " the server provided a certificate with subject name [CN=foo,DC=example,DC=com]," + + " invalid encoding [java.security.cert.CertificateEncodingException: MOCK INVALID ENCODING]," + + " keyUsage [digitalSignature, nonRepudiation] and extendedKeyUsage [serverAuth, codeSigning];" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate does not have any subject alternative names;" + " the certificate is self-issued;" + " the [CN=foo,DC=example,DC=com] certificate is not trusted in this ssl context ([xpack.security.transport.ssl])")); @@ -352,7 +510,8 @@ public void testDiagnosticMessageForClientCertificate() throws Exception { "xpack.security.transport.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with client at [192.168.1.7];" + " the client provided a certificate with subject name [CN=cert1]" + - " and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + ", fingerprint [3bebe388a66362784afd6c51a9000961a4e10050], no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate is issued by [CN=Test CA 1]" + " but the client did not provide a copy of the issuing certificate in the certificate chain;" + " the issuing certificate with fingerprint [2b7b0416391bdf86502505c23149022d2213dadc]" + @@ -375,7 +534,9 @@ public void testDiagnosticMessageWhenCaHasNewIssuingCertificate() throws Excepti final String message = SslDiagnostics.getTrustDiagnosticFailure(chain, SslDiagnostics.PeerType.SERVER, session, "xpack.security.authc.realms.saml.saml1.ssl", trustIssuers); assertThat(message, Matchers.equalTo("failed to establish trust with server at [192.168.1.4];" + - " the server provided a certificate with subject name [CN=cert1] and fingerprint [3bebe388a66362784afd6c51a9000961a4e10050];" + + " the server provided a certificate with subject name [CN=cert1], fingerprint [3bebe388a66362784afd6c51a9000961a4e10050]," + + " no keyUsage and no extendedKeyUsage;" + + " the session uses cipher suite [TLS_ECDHE_RSA_WITH_RC4_128_SHA] and protocol [SSLv3];" + " the certificate has subject alternative names [DNS:localhost,IP:127.0.0.1];" + " the certificate is issued by [CN=Test CA 1];" + " the certificate is signed by (subject [CN=Test CA 1]" + @@ -398,8 +559,21 @@ public X509Certificate cloneCertificateAsMock(X509Certificate clone) throws Cert } public X509Certificate mockCertificateWithIssuer(String principal, byte[] encoding, List> subjAltNames, - @Nullable X509Certificate issuer) throws CertificateException { + @Nullable X509Certificate issuer) throws CertificateException { + + final List extendedKeyUsage = List.of("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.3"); + final boolean[] keyUsage = {true, true, false, false, false, false, false, false, false}; + + return mockCertificateWithIssuer(principal, encoding, subjAltNames, issuer, keyUsage, extendedKeyUsage); + } + private X509Certificate mockCertificateWithIssuer( + String principal, + byte[] encoding, + List> subjAltNames, + X509Certificate issuer, + boolean[] keyUsage, List extendedKeyUsage + ) throws CertificateParsingException, CertificateEncodingException { final X509Certificate cert = Mockito.mock(X509Certificate.class); final X500Principal x500Principal = new X500Principal(principal); final PublicKey key = Mockito.mock(PublicKey.class); @@ -410,7 +584,8 @@ public X509Certificate mockCertificateWithIssuer(String principal, byte[] encodi Mockito.when(cert.getIssuerX500Principal()).thenReturn(issuerPrincipal); Mockito.when(cert.getPublicKey()).thenReturn(key); Mockito.when(cert.getEncoded()).thenReturn(encoding); - + Mockito.when(cert.getExtendedKeyUsage()).thenReturn(extendedKeyUsage); + Mockito.when(cert.getKeyUsage()).thenReturn(keyUsage); return cert; } @@ -452,8 +627,14 @@ private Map> trust(Collection