瀏覽代碼

Include trusted issuer details in SSL diagnostics (#61702)

This commit changes the SSL Diagnostic warning to include additional
details about trusted certificate issuers when the provide certificate
chain does not match any trust anchors.

- If there are no trusted issuers, this is explicitly called out
- If there is one trusted issuer, it is listed by name (DN) and fingerprint
- If there are between 2 and 10 trusted issuers, then they are listed
  by name (DN)
- If there are more than 10 trusted issuers, the number of issuers is
  included in the message (but no other details).
Tim Vernum 5 年之前
父節點
當前提交
235521ccaf

+ 28 - 2
libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/SslDiagnostics.java

@@ -254,11 +254,11 @@ public class SslDiagnostics {
         final IssuerTrust trust = checkIssuerTrust(trustedIssuers, certificate);
         if (trust.isVerified()) {
             message.append("; the issuing ")
-                .append(trust.issuerCerts.size() == 1 ? "certificate": "certificates")
+                .append(trust.issuerCerts.size() == 1 ? "certificate" : "certificates")
                 .append(" with ")
                 .append(fingerprintDescription(trust.issuerCerts))
                 .append(" ")
-                .append(trust.issuerCerts.size() == 1 ? "is": "are")
+                .append(trust.issuerCerts.size() == 1 ? "is" : "are")
                 .append(" trusted in this ssl context ([")
                 .append(contextName)
                 .append("])");
@@ -277,12 +277,37 @@ public class SslDiagnostics {
             message.append("; this ssl context ([")
                 .append(contextName)
                 .append("]) is not configured to trust that issuer");
+
+            if (trustedIssuers.isEmpty()) {
+                message.append(" or any other issuer");
+            } else {
+                if (trustedIssuers.size() == 1) {
+                    String trustedIssuer = trustedIssuers.keySet().iterator().next();
+                    message.append(", it only trusts the issuer [")
+                        .append(trustedIssuer)
+                        .append("] with ")
+                        .append(fingerprintDescription(trustedIssuers.get(trustedIssuer)));
+                } else {
+                    message.append(" but trusts [")
+                        .append(trustedIssuers.size())
+                        .append("] other issuers");
+                    if (trustedIssuers.size() < 10) {
+                        // 10 is an arbitrary number, but printing out hundreds of trusted issuers isn't helpful
+                        message.append(" ([")
+                            .append(trustedIssuers.keySet().stream().sorted().collect(Collectors.joining(", ")))
+                            .append("])");
+                    }
+                }
+            }
         }
         return message;
     }
 
     private static CharSequence describeSelfIssuedCertificate(X509Certificate certificate, String contextName,
                                                               @Nullable Map<String, List<X509Certificate>> trustedIssuers) {
+        if (trustedIssuers == null) {
+            return "self-issued";
+        }
         final StringBuilder message = new StringBuilder();
         final CertificateTrust trust = resolveCertificateTrust(trustedIssuers, certificate);
         message.append("self-issued; the [").append(certificate.getIssuerX500Principal().getName()).append("] certificate ")
@@ -324,6 +349,7 @@ public class SslDiagnostics {
     }
 
     private static CertificateTrust resolveCertificateTrust(Map<String, List<X509Certificate>> trustedIssuers, X509Certificate cert) {
+        assert trustedIssuers != null : "Do not call `resolveCertificateTrust` with null issuers";
         final List<X509Certificate> trustedCerts = trustedIssuers.get(cert.getSubjectX500Principal().getName());
         if (trustedCerts == null || trustedCerts.isEmpty()) {
             return CertificateTrust.noMatchingIssuer();

+ 51 - 2
libs/ssl-config/src/test/java/org/elasticsearch/common/ssl/SslDiagnosticsTests.java

@@ -38,6 +38,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -84,6 +85,34 @@ public class SslDiagnosticsTests extends ESTestCase {
             " which is self-issued; the [CN=Test CA 1] certificate is not trusted in this ssl context ([xpack.http.ssl])"));
     }
 
+    public void testDiagnosticMessageWithPartialChainAndUnknownTrustedIssuers() throws Exception {
+        X509Certificate[] chain = loadCertChain("cert1/cert1.crt");
+        final SSLSession session = session("192.168.1.1");
+        final Map<String, List<X509Certificate>> trustIssuers = null;
+        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 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"));
+    }
+
+
+    public void testDiagnosticMessageWithFullChainAndUnknownTrustedIssuers() throws Exception {
+        X509Certificate[] chain = loadCertChain("cert1/cert1.crt", "ca1/ca.crt");
+        final SSLSession session = session("192.168.1.1");
+        final Map<String, List<X509Certificate>> trustIssuers = null;
+        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 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])" +
+            " which is self-issued"));
+    }
+
     public void testDiagnosticMessageWhenServerFullCertChainIsntTrustedButMimicIssuerExists() throws Exception {
         X509Certificate[] chain = loadCertChain("cert1/cert1.crt", "ca1/ca.crt");
         final SSLSession session = session("192.168.1.1");
@@ -126,7 +155,27 @@ public class SslDiagnosticsTests extends ESTestCase {
             " 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;" +
-            " this ssl context ([xpack.http.ssl]) is not configured to trust that issuer"));
+            " this ssl context ([xpack.http.ssl]) is not configured to trust that issuer" +
+            " but trusts [2] other issuers ([CN=Test CA 2, CN=Test CA 3])"));
+    }
+
+    public void testDiagnosticMessageWhenServerTrustsManyCAs() throws Exception {
+        final X509Certificate[] chain = loadCertChain("cert1/cert1.crt");
+        final SSLSession session = session("192.168.1.2");
+        final Map<String, List<X509Certificate>> trustIssuers = new HashMap<>();
+        final X509Certificate dummyCa = loadCertificate("ca2/ca.crt");
+        final int numberOfCAs = randomIntBetween(30, 50);
+        for (int i = 0; i < numberOfCAs; i++) {
+            trustIssuers.put("CN=Authority-" + i + ",OU=security,DC=example,DC=net", randomList(1, 3, () -> dummyCa));
+        }
+        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 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" +
+            " but trusts [" + numberOfCAs + "] other issuers"));
     }
 
     public void testDiagnosticMessageWhenServerProvidesEndCertificateOnlyWithMimicIssuer() throws Exception {
@@ -215,7 +264,7 @@ public class SslDiagnosticsTests extends ESTestCase {
             " signed by (subject [CN=ca,OU=windows,DC=example,DC=com] fingerprint [" + MOCK_FINGERPRINT_3 + "])" +
             " signed by (subject [CN=issuing-ca,DC=example,DC=com] fingerprint [" + MOCK_FINGERPRINT_2 + "])" +
             " which is issued by [CN=root-ca,DC=example,DC=com] (but that issuer certificate was not provided in the chain);" +
-            " this ssl context ([xpack.security.authc.realms.ldap.ldap1.ssl]) is not configured to trust that issuer"));
+            " this ssl context ([xpack.security.authc.realms.ldap.ldap1.ssl]) is not configured to trust that issuer or any other issuer"));
     }
 
     public void testDiagnosticMessageWhenServerProvidesASelfSignedCertThatIsDirectlyTrusted() throws Exception {