This is an automated email from the ASF dual-hosted git repository.
steveloughran pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/hadoop.git
The following commit(s) were added to refs/heads/branch-3.5 by this push:
new b78e7f6b394 HADOOP-19931. Summary: SSLHostnameVerifier subject CN
parsing is too brittle (#8584)
b78e7f6b394 is described below
commit b78e7f6b394432d48c5edf331c820a7a18ed0a26
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 "花子.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]