This is an automated email from the ASF dual-hosted git repository.

steveloughran pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/hadoop.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 8a979cbf0fd HADOOP-19931. Summary: SSLHostnameVerifier subject CN 
parsing is too brittle (#8584)
8a979cbf0fd is described below

commit 8a979cbf0fd05108bf3326f95c5044899bf34783
Author: Steve Loughran <[email protected]>
AuthorDate: Tue Jul 7 13:03:20 2026 +0100

    HADOOP-19931. Summary: SSLHostnameVerifier subject CN parsing is too 
brittle (#8584)
    
    
    
    Co-authored by Claude Code
    
    Contributed by Steve Loughran
---
 .../hadoop/security/ssl/SSLHostnameVerifier.java   |  65 ++++++-------
 .../security/ssl/TestCertificatesGetCNs.java       | 101 +++++++++++++++++++++
 2 files changed, 135 insertions(+), 31 deletions(-)

diff --git 
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLHostnameVerifier.java
 
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLHostnameVerifier.java
index 86c92ab1147..34787732188 100644
--- 
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLHostnameVerifier.java
+++ 
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/SSLHostnameVerifier.java
@@ -31,9 +31,12 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
-import java.util.StringTokenizer;
 import java.util.TreeSet;
 
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
@@ -518,38 +521,38 @@ public static int countDots(final String s) {
     }
 
     static class Certificates {
+      /**
+       * Extracts the Common Name (CN) values from the subject of an
+       * X509Certificate, in most-significant-first order.  Returns null if
+       * there aren't any.
+
+       *
+       * @param cert X509Certificate
+       * @return Array of CN values stored in the subject, null if none.
+       */
       public static String[] getCNs(X509Certificate cert) {
-        final List<String> cnList = new LinkedList<String>();
-        /*
-          Sebastian Hauer's original StrictSSLProtocolSocketFactory used
-          getName() and had the following comment:
-
-             Parses a X.500 distinguished name for the value of the
-             "Common Name" field.  This is done a bit sloppy right
-             now and should probably be done a bit more according to
-             <code>RFC 2253</code>.
-
-           I've noticed that toString() seems to do a better job than
-           getName() on these X500Principal objects, so I'm hoping that
-           addresses Sebastian's concern.
-
-           For example, getName() gives me this:
-           
1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
-
-           whereas toString() gives me this:
-           [email protected]
-
-           Looks like toString() even works with non-ascii domain names!
-           I tested it with "&#x82b1;&#x5b50;.co.jp" and it worked fine.
-          */
-        String subjectPrincipal = cert.getSubjectX500Principal().toString();
-        StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
-        while (st.hasMoreTokens()) {
-            String tok = st.nextToken();
-            int x = tok.indexOf("CN=");
-            if (x >= 0) {
-                cnList.add(tok.substring(x + 3));
+        final List<String> cnList = new LinkedList<>();
+        try {
+          LdapName dn =
+              new LdapName(cert.getSubjectX500Principal().getName());
+          // getRdns() returns least-significant-first; walk it in reverse so
+          // the first CN in the subject stays first in the result.
+          final List<Rdn> rdns = dn.getRdns();
+          for (int i = rdns.size() - 1; i >= 0; i--) {
+            // "cn" lookup is case-insensitive and covers multi-valued RDNs.
+            Attribute cn = rdns.get(i).toAttributes().get("cn");
+            if (cn != null) {
+              for (int k = 0; k < cn.size(); k++) {
+                Object value = cn.get(k);
+                if (value != null) {
+                  cnList.add(value.toString());
+                }
+              }
             }
+          }
+        } catch (NamingException e) {
+          AbstractVerifier.LOG.warn("Unable to read subject from certificate 
{}",
+              cert.getSubjectX500Principal(), e);
         }
         if (!cnList.isEmpty()) {
             String[] cns = new String[cnList.size()];
diff --git 
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestCertificatesGetCNs.java
 
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestCertificatesGetCNs.java
new file mode 100644
index 00000000000..e2126cd7dbf
--- /dev/null
+++ 
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/ssl/TestCertificatesGetCNs.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+package org.apache.hadoop.security.ssl;
+
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link SSLHostnameVerifier.Certificates#getCNs(X509Certificate)},
+ * checking that CN values are read from the subject's CN relative
+ * distinguished names and nothing else.
+ */
+public class TestCertificatesGetCNs {
+
+  private static final int DAYS = 30;
+
+  private static final String ALG = "SHA256withRSA";
+
+  /**
+   * Get the Common Names of an X509 Distinguished Name as round tripped
+   * through the {@link SSLHostnameVerifier}.
+   * @param dn Distinguished Name.
+   * @return extracted CNs.
+   */
+  private String[] commonNames(String dn) throws Exception {
+    KeyPair pair = KeyStoreTestUtil.generateKeyPair("RSA");
+    X509Certificate cert =
+        KeyStoreTestUtil.generateCertificate(dn, pair, DAYS, ALG);
+    return SSLHostnameVerifier.Certificates.getCNs(cert);
+  }
+
+  @Test
+  public void testSingleCN() throws Exception {
+    assertHasCommonName("CN=good.example.com, O=Example, C=US", 
"good.example.com");
+  }
+
+  /**
+   * Assert Common Name is as expected.
+   * @param dn Distinguished Name.
+   * @param expected expected common name
+   */
+  private void assertHasCommonName(final String dn,
+      final String expected) throws Exception {
+    assertThat(commonNames(dn))
+        .describedAs("Common Name of %s", dn)
+        .containsExactly(expected);
+  }
+
+  /**
+   * Assert Common Name array is null.
+   * @param dn Distinguished Name.
+   */
+  private void assertNullCommonNames(final String dn) throws Exception {
+    assertThat(commonNames(dn))
+        .describedAs("Common Name of %s", dn)
+        .isNull();
+  }
+
+  @Test
+  public void testMostSignificantFirst() throws Exception {
+    assertThat(commonNames("CN=first.example.com, OU=unit, 
CN=second.example.com"))
+        .containsExactly("first.example.com", "second.example.com");
+  }
+
+  @Test
+  public void testNoCN() throws Exception {
+    assertNullCommonNames("OU=unit, O=Example, C=US");
+  }
+
+  @Test
+  public void testCnTextInOtherAttributeIsIgnored() throws Exception {
+    assertHasCommonName("CN=real.example.com, OU=CN=other.example.com\\,",
+        "real.example.com");
+  }
+
+  @Test
+  public void testCnTextInLeadingAttributeIsIgnored() throws Exception {
+    assertNullCommonNames("OU=CN=other.example.com\\,, O=Example");
+  }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to