/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.common.ssl; import org.opensearch.common.Nullable; import javax.net.ssl.SSLSession; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class SslDiagnostics { public static List describeValidHostnames(X509Certificate certificate) { try { final Collection> names = certificate.getSubjectAlternativeNames(); if (names == null || names.isEmpty()) { return Collections.emptyList(); } final List description = new ArrayList<>(names.size()); for (List pair : names) { if (pair == null || pair.size() != 2) { continue; } if ((pair.get(0) instanceof Integer) == false || (pair.get(1) instanceof String) == false) { continue; } final int type = ((Integer) pair.get(0)).intValue(); final String name = (String) pair.get(1); if (type == 2) { description.add("DNS:" + name); } else if (type == 7) { description.add("IP:" + name); } } return description; } catch (CertificateParsingException e) { return Collections.emptyList(); } } public enum PeerType { CLIENT, SERVER } private static class IssuerTrust { private final List issuerCerts; private final boolean verified; private IssuerTrust(List issuerCerts, boolean verified) { this.issuerCerts = issuerCerts; this.verified = verified; } private static IssuerTrust noMatchingCertificate() { return new IssuerTrust(null, false); } private static IssuerTrust verifiedCertificates(List issuerCert) { return new IssuerTrust(issuerCert, true); } private static IssuerTrust unverifiedCertificates(List issuerCert) { return new IssuerTrust(issuerCert, false); } boolean isVerified() { return issuerCerts != null && verified; } boolean foundCertificateForDn() { return issuerCerts != null; } } private static class CertificateTrust { /** * These certificates are trusted in the relevant context. * They might not match with the requested certificate (see {@link #match}) but will be for the requested DN. */ private final List trustedCertificates; private final boolean match; private final boolean identicalCertificate; private CertificateTrust(List certificates, boolean match, boolean identicalCertificate) { this.trustedCertificates = certificates; this.match = match; this.identicalCertificate = identicalCertificate; } private static CertificateTrust noMatchingIssuer() { return new CertificateTrust(null, false, false); } /** * We trust the provided certificates. */ private static CertificateTrust sameCertificate(X509Certificate issuerCert) { return new CertificateTrust(Collections.singletonList(issuerCert), true, true); } /** * Found trusted certificates with the same DN + same public keys, but different certificates */ private static CertificateTrust samePublicKey(List issuerCerts) { return new CertificateTrust(issuerCerts, true, false); } /** * Found certificates for the requested DN, but they have different public keys */ private static CertificateTrust nonMatchingCertificates(List certificates) { return new CertificateTrust(certificates, false, false); } boolean hasCertificates() { return trustedCertificates != null && trustedCertificates.isEmpty() == false; } boolean isTrusted() { return hasCertificates() && match; } boolean isSameCertificate() { return isTrusted() && identicalCertificate; } } /** * @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 * (see {@link javax.net.ssl.X509TrustManager#getAcceptedIssuers()}) */ public static String getTrustDiagnosticFailure( X509Certificate[] chain, PeerType peerType, SSLSession session, String contextName, @Nullable Map> trustedIssuers ) { final String peerAddress = Optional.ofNullable(session).map(SSLSession::getPeerHost).orElse(""); final StringBuilder message = new StringBuilder("failed to establish trust with ").append(peerType.name().toLowerCase(Locale.ROOT)) .append(" at [") .append(peerAddress) .append("]; "); if (chain == null || chain.length == 0) { message.append("the ").append(peerType.name().toLowerCase(Locale.ROOT)).append(" did not provide a certificate"); return message.toString(); } final X509Certificate peerCert = chain[0]; message.append("the ") .append(peerType.name().toLowerCase(Locale.ROOT)) .append(" provided a certificate with subject name [") .append(peerCert.getSubjectX500Principal().getName()) .append("] and ") .append(fingerprintDescription(peerCert)); if (peerType == PeerType.SERVER) { try { final Collection> alternativeNames = peerCert.getSubjectAlternativeNames(); if (alternativeNames == null || alternativeNames.isEmpty()) { message.append("; the certificate does not have any subject alternative names"); } else { final List hostnames = describeValidHostnames(peerCert); if (hostnames.isEmpty()) { message.append("; the certificate does not have any DNS/IP subject alternative names"); } else { message.append("; the certificate has subject alternative names [") .append(hostnames.stream().collect(Collectors.joining(","))) .append("]"); } } } catch (CertificateParsingException e) { message.append("; the certificate's subject alternative names cannot be parsed"); } } if (isSelfIssued(peerCert)) { message.append("; the certificate is ").append(describeSelfIssuedCertificate(peerCert, contextName, trustedIssuers)); } else { final String issuerName = peerCert.getIssuerX500Principal().getName(); message.append("; the certificate is issued by [").append(issuerName).append("]"); if (chain.length == 1) { message.append(" but the ") .append(peerType.name().toLowerCase(Locale.ROOT)) .append(" did not provide a copy of the issuing certificate in the certificate chain") .append(describeIssuerTrust(contextName, trustedIssuers, peerCert, issuerName)); } } if (chain.length > 1) { message.append("; the certificate is "); // skip index-0, that's the peer cert. for (int i = 1; i < chain.length; i++) { message.append("signed by (subject [") .append(chain[i].getSubjectX500Principal().getName()) .append("] ") .append(fingerprintDescription(chain[i])); if (trustedIssuers != null) { if (resolveCertificateTrust(trustedIssuers, chain[i]).isTrusted()) { message.append(" {trusted issuer}"); } } message.append(") "); } final X509Certificate root = chain[chain.length - 1]; if (isSelfIssued(root)) { message.append("which is ").append(describeSelfIssuedCertificate(root, contextName, trustedIssuers)); } else { final String rootIssuer = root.getIssuerX500Principal().getName(); message.append("which is issued by [") .append(rootIssuer) .append("] (but that issuer certificate was not provided in the chain)") .append(describeIssuerTrust(contextName, trustedIssuers, root, rootIssuer)); } } return message.toString(); } private static CharSequence describeIssuerTrust( String contextName, @Nullable Map> trustedIssuers, X509Certificate certificate, String issuerName ) { if (trustedIssuers == null) { return ""; } StringBuilder message = new StringBuilder(); final IssuerTrust trust = checkIssuerTrust(trustedIssuers, certificate); if (trust.isVerified()) { message.append("; the issuing ") .append(trust.issuerCerts.size() == 1 ? "certificate" : "certificates") .append(" with ") .append(fingerprintDescription(trust.issuerCerts)) .append(" ") .append(trust.issuerCerts.size() == 1 ? "is" : "are") .append(" trusted in this ssl context ([") .append(contextName) .append("])"); } else if (trust.foundCertificateForDn()) { message.append("; this ssl context ([") .append(contextName) .append("]) trusts [") .append(trust.issuerCerts.size()) .append("] ") .append(trust.issuerCerts.size() == 1 ? "certificate" : "certificates") .append(" with subject name [") .append(issuerName) .append("] and ") .append(fingerprintDescription(trust.issuerCerts)) .append(" but the signatures do not match"); } else { message.append("; this ssl context ([").append(contextName).append("]) is not configured to trust that issuer"); } return message; } private static CharSequence describeSelfIssuedCertificate( X509Certificate certificate, String contextName, @Nullable Map> trustedIssuers ) { final StringBuilder message = new StringBuilder(); final CertificateTrust trust = resolveCertificateTrust(trustedIssuers, certificate); message.append("self-issued; the [") .append(certificate.getIssuerX500Principal().getName()) .append("] certificate ") .append(trust.isTrusted() ? "is" : "is not") .append(" trusted in this ssl context ([") .append(contextName) .append("])"); if (trust.isTrusted()) { if (trust.isSameCertificate() == false) { if (trust.trustedCertificates.size() == 1) { message.append(" because we trust a certificate with ") .append(fingerprintDescription(trust.trustedCertificates.get(0))) .append(" for the same public key"); } else { message.append(" because we trust [") .append(trust.trustedCertificates.size()) .append("] certificates with ") .append(fingerprintDescription(trust.trustedCertificates)) .append(" for the same public key"); } } } else { if (trust.hasCertificates()) { if (trust.trustedCertificates.size() == 1) { final X509Certificate match = trust.trustedCertificates.get(0); message.append("; this ssl context does trust a certificate with subject [") .append(match.getSubjectX500Principal().getName()) .append("] but the trusted certificate has ") .append(fingerprintDescription(match)); } else { message.append("; this ssl context does trust [") .append(trust.trustedCertificates.size()) .append("] certificates with subject [") .append(certificate.getSubjectX500Principal().getName()) .append("] but those certificates have ") .append(fingerprintDescription(trust.trustedCertificates)); } } } return message; } private static CertificateTrust resolveCertificateTrust(Map> trustedIssuers, X509Certificate cert) { final List trustedCerts = trustedIssuers.get(cert.getSubjectX500Principal().getName()); if (trustedCerts == null || trustedCerts.isEmpty()) { return CertificateTrust.noMatchingIssuer(); } final int index = trustedCerts.indexOf(cert); if (index != -1) { return CertificateTrust.sameCertificate(trustedCerts.get(index)); } final List sameKey = trustedCerts.stream() .filter(c -> c.getPublicKey().equals(cert.getPublicKey())) .collect(Collectors.toList()); if (sameKey.isEmpty() == false) { return CertificateTrust.samePublicKey(sameKey); } else { return CertificateTrust.nonMatchingCertificates(trustedCerts); } } public static IssuerTrust checkIssuerTrust(Map> trustedIssuers, X509Certificate peerCert) { final List knownIssuers = trustedIssuers.get(peerCert.getIssuerX500Principal().getName()); if (knownIssuers == null || knownIssuers.isEmpty()) { return IssuerTrust.noMatchingCertificate(); } final List matchIssuers = knownIssuers.stream().filter(i -> checkIssuer(peerCert, i)).collect(Collectors.toList()); if (matchIssuers.isEmpty() == false) { return IssuerTrust.verifiedCertificates(matchIssuers); } else { return IssuerTrust.unverifiedCertificates(knownIssuers); } } private static String fingerprintDescription(List certificates) { return certificates.stream().map(SslDiagnostics::fingerprintDescription).collect(Collectors.joining(", ")); } private static String fingerprintDescription(X509Certificate certificate) { try { final String fingerprint = SslUtil.calculateFingerprint(certificate); return "fingerprint [" + fingerprint + "]"; } catch (CertificateEncodingException e) { return "invalid encoding [" + e.toString() + "]"; } } private static boolean checkIssuer(X509Certificate certificate, X509Certificate possibleIssuer) { try { certificate.verify(possibleIssuer.getPublicKey()); return true; } catch (Exception e) { return false; } } private static boolean isSelfIssued(X509Certificate certificate) { return certificate.getIssuerX500Principal().equals(certificate.getSubjectX500Principal()); } }