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

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new a2065ce9994 HDDS-16034. Certificates omit DNS SANs on clusters with 
non-public hostname suffixes (#10908)
a2065ce9994 is described below

commit a2065ce999480589dde5f0035ee58f4c0d59cb21
Author: Sergey Soldatov <[email protected]>
AuthorDate: Wed Aug 5 05:00:56 2026 -0700

    HDDS-16034. Certificates omit DNS SANs on clusters with non-public hostname 
suffixes (#10908)
---
 .../authority/profile/DefaultProfile.java          |   4 +-
 .../certificate/utils/CertificateSignRequest.java  |  46 ++++++--
 .../security/x509/certificate/utils/DnsNames.java  | 107 ++++++++++++++++++
 .../certificate/utils/SelfSignedCertificate.java   |  59 ++++++++--
 .../certificate/authority/TestDefaultCAServer.java |  45 ++++++++
 .../certificate/authority/TestDefaultProfile.java  |  42 +++++++
 .../utils/TestCertificateSignRequest.java          | 125 +++++++++++++++++++++
 .../x509/certificate/utils/TestDnsNames.java       |  93 +++++++++++++++
 .../certificate/utils/TestRootCertificate.java     |  47 ++++++++
 hadoop-ozone/integration-test/pom.xml              |   5 -
 .../hadoop/ozone/TestSecureOzoneCluster.java       |   4 +-
 11 files changed, 551 insertions(+), 26 deletions(-)

diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/profile/DefaultProfile.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/profile/DefaultProfile.java
index b4c7ad5d4e1..7749ac99dd8 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/profile/DefaultProfile.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/profile/DefaultProfile.java
@@ -34,7 +34,7 @@
 import java.util.stream.Stream;
 import org.apache.commons.codec.DecoderException;
 import org.apache.commons.codec.binary.Hex;
-import org.apache.commons.validator.routines.DomainValidator;
+import org.apache.hadoop.hdds.security.x509.certificate.utils.DnsNames;
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
 import org.bouncycastle.asn1.x500.RDN;
 import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
@@ -233,7 +233,7 @@ public boolean validateGeneralName(int type, String value) {
         return false;
       }
     case GeneralName.dNSName:
-      return DomainValidator.getInstance().isValid(value);
+      return DnsNames.isValidDnsName(value);
     case GeneralName.otherName:
       // for other name it's a general string, nothing to validate
       return true;
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateSignRequest.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateSignRequest.java
index a3933e22df4..74206f71600 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateSignRequest.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/CertificateSignRequest.java
@@ -26,13 +26,13 @@
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.security.KeyPair;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.validator.routines.DomainValidator;
 import org.apache.hadoop.hdds.security.SecurityConfig;
 import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
 import org.apache.hadoop.hdds.security.x509.exception.CertificateException;
@@ -282,6 +282,19 @@ public boolean hasDnsName() {
       return false;
     }
 
+    private boolean hasDnsName(String candidate) {
+      if (altNames == null) {
+        return false;
+      }
+      for (GeneralName name : altNames) {
+        if (name.getTagNo() == GeneralName.dNSName
+            && name.getName().toString().equalsIgnoreCase(candidate)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
     // IP address is subject to change which is optional for now.
     public CertificateSignRequest.Builder addIpAddress(String ip) {
       Objects.requireNonNull(ip, "Ip address cannot be null");
@@ -292,10 +305,9 @@ public CertificateSignRequest.Builder addIpAddress(String 
ip) {
     public CertificateSignRequest.Builder addInetAddresses()
         throws CertificateException {
       try {
-        DomainValidator validator = DomainValidator.getInstance();
         // Add all valid ips.
         List<InetAddress> inetAddresses = getValidInetsForCurrentHost();
-        this.addInetAddresses(inetAddresses, validator);
+        this.addInetAddresses(inetAddresses);
       } catch (IOException e) {
         throw new CertificateException("Error while getting Inet addresses " +
             "for the CSR builder", e, CSR_ERROR);
@@ -304,18 +316,36 @@ public CertificateSignRequest.Builder addInetAddresses()
     }
 
     public CertificateSignRequest.Builder addInetAddresses(
-        List<InetAddress> addresses,
-        DomainValidator validator) {
+        List<InetAddress> addresses) {
       // Add all valid ips.
       addresses.forEach(
           ip -> {
             this.addIpAddress(ip.getHostAddress());
-            if (validator.isValid(ip.getCanonicalHostName())) {
-              this.addDnsName(ip.getCanonicalHostName());
+            Optional<String> dnsName = 
DnsNames.toDnsSanValue(ip.getCanonicalHostName());
+            if (dnsName.isPresent()) {
+              if (!hasDnsName(dnsName.get())) {
+                this.addDnsName(dnsName.get());
+              }
             } else {
-              LOG.error("Invalid domain {}", ip.getCanonicalHostName());
+              LOG.warn("Rejected DNS SAN candidate '{}': not a valid RFC 1123 
DNS name",
+                  ip.getCanonicalHostName());
             }
           });
+
+      if (!hasDnsName()) {
+        Optional<String> dnsName;
+        try {
+          dnsName = 
DnsNames.toDnsSanValue(InetAddress.getLocalHost().getCanonicalHostName());
+        } catch (UnknownHostException e) {
+          dnsName = Optional.empty();
+        }
+        if (dnsName.isPresent()) {
+          this.addDnsName(dnsName.get());
+        } else {
+          LOG.warn("Certificate will have no DNS SAN; by-name TLS connections 
" +
+              "to this node will fail");
+        }
+      }
       return this;
     }
 
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/DnsNames.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/DnsNames.java
new file mode 100644
index 00000000000..f216af27c5a
--- /dev/null
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/DnsNames.java
@@ -0,0 +1,107 @@
+/*
+ * 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.hdds.security.x509.certificate.utils;
+
+import java.net.IDN;
+import java.util.Optional;
+import org.apache.commons.validator.routines.InetAddressValidator;
+
+/**
+ * Shared helper for validating and normalizing RFC 1123 DNS names used as
+ * certificate Subject Alternative Names.
+ */
+public final class DnsNames {
+
+  private static final int MAX_NAME_LENGTH = 253;
+  private static final int MAX_LABEL_LENGTH = 63;
+
+  private DnsNames() {
+  }
+
+  /**
+   * Normalizes a candidate DNS name for use as a certificate SAN value.
+   * Strips at most one trailing '.', converts it to its ASCII/A-label form
+   * via IDN, and validates the result with {@link #isValidDnsName(String)}.
+   *
+   * @param candidate the raw candidate DNS name
+   * @return the normalized DNS name, or {@link Optional#empty()} if the
+   *     candidate is null, empty, or not a valid DNS name
+   */
+  public static Optional<String> toDnsSanValue(String candidate) {
+    if (candidate == null || candidate.isEmpty()) {
+      return Optional.empty();
+    }
+
+    String stripped = candidate.endsWith(".")
+        ? candidate.substring(0, candidate.length() - 1)
+        : candidate;
+
+    String ascii;
+    try {
+      ascii = IDN.toASCII(stripped, IDN.ALLOW_UNASSIGNED);
+    } catch (IllegalArgumentException e) {
+      return Optional.empty();
+    }
+
+    return isValidDnsName(ascii) ? Optional.of(ascii) : Optional.empty();
+  }
+
+  /**
+   * Validates that the given value is a syntactically valid RFC 1123 DNS
+   * name for use as a certificate Subject Alternative Name. Does not perform
+   * IDN conversion or trailing-dot stripping.
+   *
+   * @param value the DNS name to validate
+   * @return true iff the value is a valid RFC 1123 DNS name
+   */
+  public static boolean isValidDnsName(String value) {
+    if (value == null || value.isEmpty() || value.length() > MAX_NAME_LENGTH) {
+      return false;
+    }
+
+    if (InetAddressValidator.getInstance().isValid(value)) {
+      return false;
+    }
+
+    String[] labels = value.split("\\.", -1);
+    for (String label : labels) {
+      if (!isValidLabel(label)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static boolean isValidLabel(String label) {
+    int length = label.length();
+    if (length < 1 || length > MAX_LABEL_LENGTH) {
+      return false;
+    }
+    if (label.charAt(0) == '-' || label.charAt(length - 1) == '-') {
+      return false;
+    }
+    for (int i = 0; i < length; i++) {
+      char c = label.charAt(i);
+      boolean isAlphaNumeric = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 
'z') || (c >= '0' && c <= '9');
+      if (!isAlphaNumeric && c != '-') {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/SelfSignedCertificate.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/SelfSignedCertificate.java
index 1d9cd7d5847..e56b4466e9e 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/SelfSignedCertificate.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/utils/SelfSignedCertificate.java
@@ -26,6 +26,7 @@
 import java.io.IOException;
 import java.math.BigInteger;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.security.KeyPair;
 import java.security.cert.X509Certificate;
 import java.time.Duration;
@@ -34,8 +35,8 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.validator.routines.DomainValidator;
 import org.apache.hadoop.hdds.security.SecurityConfig;
 import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
 import org.apache.hadoop.hdds.security.x509.exception.CertificateException;
@@ -219,10 +220,9 @@ public Builder makeCA(BigInteger serialId) {
 
     public Builder addInetAddresses() throws CertificateException {
       try {
-        DomainValidator validator = DomainValidator.getInstance();
         // Add all valid ips.
         List<InetAddress> inetAddresses = getValidInetsForCurrentHost();
-        this.addInetAddresses(inetAddresses, validator);
+        this.addInetAddresses(inetAddresses);
       } catch (IOException e) {
         throw new CertificateException("Error while getting Inet addresses " +
             "for the CSR builder", e, CSR_ERROR);
@@ -230,20 +230,63 @@ public Builder addInetAddresses() throws 
CertificateException {
       return this;
     }
 
-    public Builder addInetAddresses(List<InetAddress> addresses,
-        DomainValidator validator) {
+    public Builder addInetAddresses(List<InetAddress> addresses) {
       addresses.forEach(
           ip -> {
             this.addIpAddress(ip.getHostAddress());
-            if (validator.isValid(ip.getCanonicalHostName())) {
-              this.addDnsName(ip.getCanonicalHostName());
+            Optional<String> dnsName = 
DnsNames.toDnsSanValue(ip.getCanonicalHostName());
+            if (dnsName.isPresent()) {
+              if (!hasDnsName(dnsName.get())) {
+                this.addDnsName(dnsName.get());
+              }
             } else {
-              LOG.error("Invalid domain {}", ip.getCanonicalHostName());
+              LOG.warn("Rejected DNS SAN candidate '{}': not a valid RFC 1123 
DNS name",
+                  ip.getCanonicalHostName());
             }
           });
+
+      if (!hasDnsName()) {
+        Optional<String> dnsName;
+        try {
+          dnsName = 
DnsNames.toDnsSanValue(InetAddress.getLocalHost().getCanonicalHostName());
+        } catch (UnknownHostException e) {
+          dnsName = Optional.empty();
+        }
+        if (dnsName.isPresent()) {
+          this.addDnsName(dnsName.get());
+        } else {
+          LOG.warn("Certificate will have no DNS SAN; by-name TLS connections 
" +
+              "to this node will fail");
+        }
+      }
       return this;
     }
 
+    private boolean hasDnsName() {
+      if (altNames == null) {
+        return false;
+      }
+      for (GeneralName name : altNames) {
+        if (name.getTagNo() == GeneralName.dNSName) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    private boolean hasDnsName(String candidate) {
+      if (altNames == null) {
+        return false;
+      }
+      for (GeneralName name : altNames) {
+        if (name.getTagNo() == GeneralName.dNSName
+            && name.getName().toString().equalsIgnoreCase(candidate)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
     // Support SAN extension with DNS and RFC822 Name
     // other name type will be added as needed.
     public Builder addDnsName(String dnsName) {
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
index 5f8b72dc3a1..7e435bf9f60 100644
--- 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
@@ -46,6 +46,7 @@
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
@@ -68,6 +69,7 @@
 import org.apache.hadoop.hdds.security.x509.keys.HDDSKeyGenerator;
 import org.apache.hadoop.hdds.security.x509.keys.KeyStorage;
 import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
+import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.pkcs.PKCS10CertificationRequest;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -196,6 +198,49 @@ public void testRequestCertificate() throws Exception {
 
   }
 
+  /**
+   * Tests that an internal-suffix DNS name in the CSR is retained as a
+   * dNSName Subject Alternative Name in the issued certificate.
+   * @throws Exception - on ERROR.
+   */
+  @Test
+  public void testRequestCertificateRetainsInternalDnsName() throws Exception {
+    String scmId = RandomStringUtils.secure().nextAlphabetic(4);
+    String clusterId = RandomStringUtils.secure().nextAlphabetic(4);
+    KeyPair keyPair =
+        new HDDSKeyGenerator(securityConfig).generateKey();
+    PKCS10CertificationRequest csr = new CertificateSignRequest.Builder()
+        .addDnsName("scm1.lxd")
+        .setCA(false)
+        .setClusterID(clusterId)
+        .setScmID(scmId)
+        .setSubject("Ozone Cluster")
+        .setConfiguration(securityConfig)
+        .setKey(keyPair)
+        .build()
+        .generateCSR();
+
+    CertificateServer testCA = new DefaultCAServer("testCA",
+        clusterId, scmId, caStore,
+        new DefaultProfile(),
+        Paths.get(SCM_CA_CERT_STORAGE_DIR, SCM_CA_PATH).toString());
+    testCA.init(securityConfig, CAType.ROOT);
+
+    Future<CertPath> holder = testCA.requestCertificate(
+        csr, CertificateApprover.ApprovalType.TESTING_AUTOMATIC, SCM,
+        String.valueOf(System.nanoTime()));
+    assertTrue(holder.isDone());
+    X509Certificate signedCert =
+        CertificateCodec.firstCertificateFrom(holder.get());
+
+    Collection<List<?>> subjectAlternativeNames =
+        signedCert.getSubjectAlternativeNames();
+    assertNotNull(subjectAlternativeNames);
+    assertTrue(subjectAlternativeNames.stream().anyMatch(
+        san -> ((Integer) san.get(0)) == GeneralName.dNSName
+            && "scm1.lxd".equals(san.get(1))));
+  }
+
   /**
    * Tests that we are able
    * to create a Test CA, creates it own self-Signed CA and then issue a
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultProfile.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultProfile.java
index 5f09b347b93..e8a5ebfdf23 100644
--- 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultProfile.java
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultProfile.java
@@ -155,6 +155,48 @@ public void testExtensions() throws Exception {
     assertTrue(approver.verfiyExtensions(csr));
   }
 
+  /**
+   * Tests that internal-suffix and single-label DNS names, which are not
+   * accepted by a public-suffix based validator, are accepted by
+   * the RFC 1123 based DnsNames validation.
+   */
+  @Test
+  public void testExtensionsWithInternalDnsNames() throws Exception {
+    PKCS10CertificationRequest csr = new CertificateSignRequest.Builder()
+        .addDnsName("scm1.lxd")
+        .addDnsName("datanode1")
+        .setCA(false)
+        .setClusterID("ClusterID")
+        .setScmID("SCMID")
+        .setSubject("Ozone Cluster")
+        .setConfiguration(securityConfig)
+        .setKey(keyPair)
+        .build()
+        .generateCSR();
+    assertTrue(approver.verfiyExtensions(csr));
+  }
+
+  /**
+   * Tests that a wildcard, an IP literal, or an empty dNSName are still
+   * rejected by the DnsNames validation.
+   */
+  @Test
+  public void testInvalidExtensionsWithDnsName() throws IOException,
+      OperatorCreationException {
+    Extensions dnsExtension = getSANExtension(GeneralName.dNSName,
+        "*.example.com", false);
+    PKCS10CertificationRequest csr = getInvalidCSR(keyPair, dnsExtension);
+    assertFalse(approver.verfiyExtensions(csr));
+
+    dnsExtension = getSANExtension(GeneralName.dNSName, "10.0.0.5", false);
+    csr = getInvalidCSR(keyPair, dnsExtension);
+    assertFalse(approver.verfiyExtensions(csr));
+
+    dnsExtension = getSANExtension(GeneralName.dNSName, "", false);
+    csr = getInvalidCSR(keyPair, dnsExtension);
+    assertFalse(approver.verfiyExtensions(csr));
+  }
+
   /**
    * Tests that  invalid extensions cause a failure in validation. We will fail
    * if CA extension is enabled.
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestCertificateSignRequest.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestCertificateSignRequest.java
index 051d28593c9..a73b2173bbf 100644
--- 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestCertificateSignRequest.java
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestCertificateSignRequest.java
@@ -25,10 +25,20 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.nio.file.Path;
 import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.UUID;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.security.SecurityConfig;
@@ -49,6 +59,7 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedStatic;
 
 /**
  * Certificate Signing Request.
@@ -259,6 +270,120 @@ public void testCsrSerialization() throws Exception {
     assertEquals(csr, dsCsr);
   }
 
+  @Test
+  public void testAddInetAddressesAddsDnsNameFromCanonicalHostName() throws 
Exception {
+    InetAddress address = mock(InetAddress.class);
+    when(address.getHostAddress()).thenReturn("192.0.2.10");
+    when(address.getCanonicalHostName()).thenReturn("scm1.lxd");
+
+    CertificateSignRequest.Builder builder = newBuilder();
+    builder.addInetAddresses(Collections.singletonList(address));
+
+    PKCS10CertificationRequest csr = builder.build().generateCSR();
+    List<GeneralName> sanNames = getSanNames(csr);
+    assertEquals(1, countByTag(sanNames, GeneralName.iPAddress));
+    assertEquals(1, countByTag(sanNames, GeneralName.dNSName));
+    assertTrue(dnsNameValues(sanNames).contains("scm1.lxd"));
+  }
+
+  @Test
+  public void testAddInetAddressesSkipsIpLiteralAndFallbackFailure() throws 
Exception {
+    InetAddress address = mock(InetAddress.class);
+    when(address.getHostAddress()).thenReturn("192.0.2.11");
+    when(address.getCanonicalHostName()).thenReturn("10.0.0.5");
+
+    CertificateSignRequest.Builder builder = newBuilder();
+    try (MockedStatic<InetAddress> mockedInetAddress = 
mockStatic(InetAddress.class, CALLS_REAL_METHODS)) {
+      mockedInetAddress.when(InetAddress::getLocalHost).thenThrow(new 
UnknownHostException("no localhost"));
+      builder.addInetAddresses(Collections.singletonList(address));
+    }
+
+    PKCS10CertificationRequest csr = builder.build().generateCSR();
+    List<GeneralName> sanNames = getSanNames(csr);
+    assertEquals(1, countByTag(sanNames, GeneralName.iPAddress));
+    assertEquals(0, countByTag(sanNames, GeneralName.dNSName));
+  }
+
+  @Test
+  public void testAddInetAddressesFallsBackToLocalHostCanonicalName() throws 
Exception {
+    InetAddress address = mock(InetAddress.class);
+    when(address.getHostAddress()).thenReturn("192.0.2.12");
+    when(address.getCanonicalHostName()).thenReturn("10.0.0.5");
+
+    InetAddress localHost = mock(InetAddress.class);
+    when(localHost.getCanonicalHostName()).thenReturn("fallback1.lxd");
+
+    CertificateSignRequest.Builder builder = newBuilder();
+    try (MockedStatic<InetAddress> mockedInetAddress = 
mockStatic(InetAddress.class, CALLS_REAL_METHODS)) {
+      mockedInetAddress.when(InetAddress::getLocalHost).thenReturn(localHost);
+      builder.addInetAddresses(Collections.singletonList(address));
+    }
+
+    PKCS10CertificationRequest csr = builder.build().generateCSR();
+    List<GeneralName> sanNames = getSanNames(csr);
+    assertEquals(1, countByTag(sanNames, GeneralName.dNSName));
+    assertEquals(Collections.singletonList("fallback1.lxd"), 
dnsNameValues(sanNames));
+  }
+
+  @Test
+  public void testAddInetAddressesDeduplicatesDnsNamesCaseInsensitively() 
throws Exception {
+    InetAddress address1 = mock(InetAddress.class);
+    when(address1.getHostAddress()).thenReturn("192.0.2.13");
+    when(address1.getCanonicalHostName()).thenReturn("SCM1.LXD");
+
+    InetAddress address2 = mock(InetAddress.class);
+    when(address2.getHostAddress()).thenReturn("192.0.2.14");
+    when(address2.getCanonicalHostName()).thenReturn("scm1.lxd");
+
+    CertificateSignRequest.Builder builder = newBuilder();
+    builder.addInetAddresses(Arrays.asList(address1, address2));
+
+    PKCS10CertificationRequest csr = builder.build().generateCSR();
+    List<GeneralName> sanNames = getSanNames(csr);
+    assertEquals(2, countByTag(sanNames, GeneralName.iPAddress));
+    assertEquals(1, countByTag(sanNames, GeneralName.dNSName));
+  }
+
+  private CertificateSignRequest.Builder newBuilder() throws Exception {
+    String clusterID = UUID.randomUUID().toString();
+    String scmID = UUID.randomUUID().toString();
+    String subject = "DN001";
+    HDDSKeyGenerator keyGen = new HDDSKeyGenerator(securityConfig);
+    KeyPair keyPair = keyGen.generateKey();
+    return new CertificateSignRequest.Builder()
+        .setSubject(subject)
+        .setScmID(scmID)
+        .setClusterID(clusterID)
+        .setKey(keyPair)
+        .setConfiguration(securityConfig);
+  }
+
+  private List<GeneralName> getSanNames(PKCS10CertificationRequest csr) throws 
Exception {
+    Extensions extensions = getPkcs9Extensions(csr);
+    Extension ext = extensions.getExtension(Extension.subjectAlternativeName);
+    return 
Arrays.asList(GeneralNames.getInstance(ext.getParsedValue()).getNames());
+  }
+
+  private long countByTag(List<GeneralName> names, int tag) {
+    long count = 0;
+    for (GeneralName name : names) {
+      if (name.getTagNo() == tag) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  private List<String> dnsNameValues(List<GeneralName> names) {
+    List<String> values = new ArrayList<>();
+    for (GeneralName name : names) {
+      if (name.getTagNo() == GeneralName.dNSName) {
+        values.add(name.getName().toString());
+      }
+    }
+    return values;
+  }
+
   private void verifyServiceId(Extensions extensions) {
     GeneralNames gns =
         GeneralNames.fromExtensions(
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestDnsNames.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestDnsNames.java
new file mode 100644
index 00000000000..d65c92b6f03
--- /dev/null
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestDnsNames.java
@@ -0,0 +1,93 @@
+/*
+ * 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.hdds.security.x509.certificate.utils;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link DnsNames}.
+ */
+public class TestDnsNames {
+
+  @Test
+  public void acceptsValidDnsNames() {
+    String[] valid = {
+        "scm1.lxd",
+        "datanode1",
+        "om.internal",
+        "dn3.local",
+        "a-b.c-d.example.com",
+        "hadoop.apache.org",
+        StringUtils.repeat('a', 63), // 63-character label
+    };
+    for (String name : valid) {
+      assertTrue(DnsNames.isValidDnsName(name), name);
+      assertTrue(DnsNames.toDnsSanValue(name).isPresent(), name);
+    }
+  }
+
+  @Test
+  public void rejectsInvalidDnsNames() {
+    String longLabel = StringUtils.repeat('a', 64); // 64-character label
+    String longName = StringUtils.repeat("a234567890.", 24) + "example.com"; 
// 275 chars, > 253
+    String[] invalid = {
+        "",
+        " ",
+        "*.example.com",
+        "10.0.0.5",
+        "2001:db8::1",
+        "-lead.example.com",
+        "trail-.example.com",
+        "host_name.lxd",
+        longLabel,
+        longName,
+    };
+    for (String name : invalid) {
+      assertFalse(DnsNames.isValidDnsName(name), name);
+      assertFalse(DnsNames.toDnsSanValue(name).isPresent(), name);
+    }
+  }
+
+  @Test
+  public void doesNotThrowForNull() {
+    assertDoesNotThrow(() -> DnsNames.isValidDnsName(null));
+    assertDoesNotThrow(() -> DnsNames.toDnsSanValue(null));
+    assertFalse(DnsNames.isValidDnsName(null));
+    assertFalse(DnsNames.toDnsSanValue(null).isPresent());
+  }
+
+  @Test
+  public void stripsAtMostOneTrailingDot() {
+    assertEquals(Optional.of("scm1.lxd"), DnsNames.toDnsSanValue("scm1.lxd."));
+    assertFalse(DnsNames.isValidDnsName("scm1.lxd."));
+    assertFalse(DnsNames.toDnsSanValue("scm1.lxd..").isPresent());
+  }
+
+  @Test
+  public void handlesIdnConversion() {
+    assertEquals(Optional.of("xn--bcher-kva.lxd"), 
DnsNames.toDnsSanValue("bücher.lxd"));
+    assertFalse(DnsNames.isValidDnsName("bücher.lxd"));
+  }
+}
diff --git 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestRootCertificate.java
 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestRootCertificate.java
index 7f71b6515c8..549f2c8aaa8 100644
--- 
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestRootCertificate.java
+++ 
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/utils/TestRootCertificate.java
@@ -25,8 +25,11 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.math.BigInteger;
+import java.net.InetAddress;
 import java.nio.file.Path;
 import java.security.InvalidKeyException;
 import java.security.KeyPair;
@@ -34,7 +37,11 @@
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 import java.util.UUID;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.security.SecurityConfig;
@@ -151,6 +158,46 @@ public void testCACert(@TempDir Path basePath) throws 
Exception {
         loadedCert.getSerialNumber());
   }
 
+  @Test
+  public void testCACertWithMockedInetAddressAddsDnsName() throws Exception {
+    ZonedDateTime notBefore = ZonedDateTime.now();
+    ZonedDateTime notAfter = notBefore.plusYears(1);
+    String clusterID = UUID.randomUUID().toString();
+    String scmID = UUID.randomUUID().toString();
+    String subject = "testRootCert";
+    HDDSKeyGenerator keyGen =
+        new HDDSKeyGenerator(securityConfig);
+    KeyPair keyPair = keyGen.generateKey();
+
+    InetAddress address = mock(InetAddress.class);
+    when(address.getHostAddress()).thenReturn("192.0.2.20");
+    when(address.getCanonicalHostName()).thenReturn("scm1.lxd");
+
+    X509Certificate certificate =
+        SelfSignedCertificate.newBuilder()
+            .setBeginDate(notBefore)
+            .setEndDate(notAfter)
+            .setClusterID(clusterID)
+            .setScmID(scmID)
+            .setSubject(subject)
+            .setKey(keyPair)
+            .setConfiguration(securityConfig)
+            .makeCA()
+            .addInetAddresses(Collections.singletonList(address))
+            .build();
+
+    Collection<List<?>> subjectAlternativeNames = 
certificate.getSubjectAlternativeNames();
+    assertNotNull(subjectAlternativeNames);
+    List<String> dnsNames = new ArrayList<>();
+    for (List<?> san : subjectAlternativeNames) {
+      // GeneralName type 2 is dNSName, see RFC 5280 4.2.1.6.
+      if (((Number) san.get(0)).intValue() == 2) {
+        dnsNames.add((String) san.get(1));
+      }
+    }
+    assertTrue(dnsNames.contains("scm1.lxd"));
+  }
+
   @Test
   public void testInvalidParamFails() throws Exception {
     ZonedDateTime notBefore = ZonedDateTime.now();
diff --git a/hadoop-ozone/integration-test/pom.xml 
b/hadoop-ozone/integration-test/pom.xml
index 6c56728b938..0e67eeb811b 100644
--- a/hadoop-ozone/integration-test/pom.xml
+++ b/hadoop-ozone/integration-test/pom.xml
@@ -71,11 +71,6 @@
       <artifactId>commons-io</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>commons-validator</groupId>
-      <artifactId>commons-validator</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>info.picocli</groupId>
       <artifactId>picocli</artifactId>
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
index bb9fb9f6180..e6178ee380c 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
@@ -92,7 +92,6 @@
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.validator.routines.DomainValidator;
 import org.apache.hadoop.hdds.HddsConfigKeys;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
@@ -1268,9 +1267,8 @@ private static X509Certificate signX509Cert(
 
   private static void addIpAndDnsDataToBuilder(
       CertificateSignRequest.Builder csrBuilder) throws IOException {
-    DomainValidator validator = DomainValidator.getInstance();
     // Add all valid ips.
     List<InetAddress> inetAddresses = getValidInetsForCurrentHost();
-    csrBuilder.addInetAddresses(inetAddresses, validator);
+    csrBuilder.addInetAddresses(inetAddresses);
   }
 }


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

Reply via email to