From e83ab0bee49b6536e7de7ced6e59eb0dc6393e61 Mon Sep 17 00:00:00 2001 From: Martin D'Aloia Date: Sun, 7 May 2023 16:21:45 -0300 Subject: [PATCH] Add an experimental Certificate Chain sorter Adds a simple experimental (disable by default) implementation to sort a list of certificates and get a certificate chain. Some servers send the certificates unordered. Until TLSv1.2 this was not allowed. TLSv1.3 allows it (see RFC 8446 section 4.4.2). This is not at all fully complaint with the RFC 8446 as it does not (currently) handles some cases like multiple chain paths or certificates with the same CN (Common Name) that are in a specific order. --- .../internal/ssl/CertificateChainSorter.java | 101 ++++++++++++++++++ .../ecf/internal/ssl/ECFTrustManager.java | 7 ++ 2 files changed, 108 insertions(+) create mode 100644 framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/CertificateChainSorter.java diff --git a/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/CertificateChainSorter.java b/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/CertificateChainSorter.java new file mode 100644 index 0000000000..4b4fd1fa77 --- /dev/null +++ b/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/CertificateChainSorter.java @@ -0,0 +1,101 @@ +package org.eclipse.ecf.internal.ssl; + +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * Simple implementation to sort a list of certificates and get a certificate + * chain. Some servers send the certificates unordered. Until TLSv1.2 this was + * not allowed. TLSv1.3 allows it (see RFC 8446 section 4.4.2). + * + * This is not at all fully complaint with the RFC 8446 as it does not + * (currently) handles some cases like multiple chain paths or certificates with + * the same CN (Common Name) that are in a specific order (see + * {@link org.eclipse.ecf.tests.ssl.ECFTrustManagerTest#secondCertSharingCNWithIntermediateCA_AfterProperOne()} + */ +public class CertificateChainSorter { + + /** + * Sorts the certificates to obtain the certificate chain in the proper + * order. Certificates that are not part of the chain are not considered + * (i.e. not included in the result). + * + * @param certificates The original list of certificates in the order + * provided by the server. + * + * @return a new X509Certificate array with the certificate chain in proper + * order. If no certificates are provided or just one, then it returns the + * same array provided. + */ + public static X509Certificate[] sortCertificates(X509Certificate[] certificates) { + if (certificates == null || certificates.length <= 1) { + return certificates; + } + + Map subjects = createIndexBySubject(certificates); + + List sortedChain = buildSortedChain(certificates[0], subjects); + + return sortedChain.toArray(new X509Certificate[] {}); + } + + /** + * Builds an index from the provided certificates using the Subject DN as + * the key. + * + * @param certificates The original certificate list provided by the server. + * + * @return a new map where the Subject DN is the key and the certificate the + * value. + */ + private static Map createIndexBySubject(X509Certificate[] certificates) { + Map subjects = new HashMap(); + + for (int i = 0; i < certificates.length; i++) { + X509Certificate currentCert = certificates[i]; + + subjects.put(currentCert.getSubjectDN(), currentCert); + } + + return subjects; + } + + /** + * Builds a certificate chain from the ones provided by the server starting + * with the first certificate from the original list (which should be the + * end-entity certificate per RFC 8446 section 4.4.2). + * + * @param endEntityCertificate The end-entity certificate which MUST be the + * first one of the provided by the server. + * @param subjects An SubjectDN based index of the certificates provided by + * the server. + * + * @return A new list with the certificate chain calculated. + */ + private static List buildSortedChain(X509Certificate endEntityCertificate, + Map subjects) { + List sortedChain = new ArrayList(); + + X509Certificate currentCert = endEntityCertificate; + X509Certificate issuer; + + sortedChain.add(currentCert); + + do { + issuer = subjects.get(currentCert.getIssuerDN()); + + if (issuer != null) { + sortedChain.add(issuer); + currentCert = issuer; + } + } while (issuer != null && !isRootCertificate(issuer)); + + return sortedChain; + } + + private static boolean isRootCertificate(X509Certificate certificate) { + return certificate != null && certificate.getSubjectDN().equals(certificate.getIssuerDN()); + } + +} \ No newline at end of file diff --git a/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/ECFTrustManager.java b/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/ECFTrustManager.java index 2ff79e50d7..0bff7b819f 100644 --- a/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/ECFTrustManager.java +++ b/framework/bundles/org.eclipse.ecf.ssl/src/org/eclipse/ecf/internal/ssl/ECFTrustManager.java @@ -21,6 +21,8 @@ public class ECFTrustManager implements X509TrustManager, BundleActivator { + public static final String SORT_CERTS_EXPERIMENTAL_FLAG = "org.eclipse.ecf.internal.ssl.ECFTrustManager.experimental.sortCerts"; + private static volatile BundleContext context; private volatile ServiceTracker trustEngineTracker = null; private ServiceRegistration socketFactoryRegistration; @@ -28,6 +30,11 @@ public class ECFTrustManager implements X509TrustManager, BundleActivator { public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException { + + if (Boolean.getBoolean(SORT_CERTS_EXPERIMENTAL_FLAG)) { + certs = CertificateChainSorter.sortCertificates(certs); + } + // verify the cert chain verify(certs, authType);