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

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


The following commit(s) were added to refs/heads/master by this push:
     new a16a2b13e RANGER-5318: Hostname verifier, check the hostname across 
chain (#682)
a16a2b13e is described below

commit a16a2b13e8e81a57be95d6dae80bd492ce4c4377
Author: Vyom Mani Tiwari <[email protected]>
AuthorDate: Wed Sep 24 10:50:59 2025 +0530

    RANGER-5318: Hostname verifier, check the hostname across chain (#682)
---
 .../ranger/services/nifi/client/NiFiClient.java    |  17 +-
 .../services/nifi/client/TestNiFiClient.java       | 180 +++++++++++++++++++--
 2 files changed, 179 insertions(+), 18 deletions(-)

diff --git 
a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java
 
b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java
index e97375d40..6a64a8e64 100644
--- 
a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java
+++ 
b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java
@@ -168,14 +168,15 @@ private static class NiFiHostnameVerifier implements 
HostnameVerifier {
         @Override
         public boolean verify(final String hostname, final SSLSession ssls) {
             try {
-                for (final Certificate peerCertificate : 
ssls.getPeerCertificates()) {
-                    if (peerCertificate instanceof X509Certificate) {
-                        final X509Certificate x509Cert        = 
(X509Certificate) peerCertificate;
-                        final List<String>    subjectAltNames = 
getSubjectAlternativeNames(x509Cert);
-                        if (subjectAltNames.contains(hostname.toLowerCase())) {
-                            return true;
-                        }
-                    }
+                Certificate[] certificates = ssls.getPeerCertificates();
+                if (certificates == null || certificates.length == 0) {
+                    return false;
+                }
+                // verify hostname against server certificate[0]
+                if (certificates[0] instanceof X509Certificate) {
+                    final X509Certificate x509Cert = (X509Certificate) 
certificates[0];
+                    final List<String> subjectAltNames = 
getSubjectAlternativeNames(x509Cert);
+                    return subjectAltNames.contains(hostname.toLowerCase());
                 }
             } catch (final SSLPeerUnverifiedException | 
CertificateParsingException ex) {
                 LOG.warn("Hostname Verification encountered exception 
verifying hostname due to: {}", ex, ex);
diff --git 
a/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java
 
b/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java
index 684c9ab8c..afe884c4d 100644
--- 
a/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java
+++ 
b/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java
@@ -26,14 +26,33 @@
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSessionContext;
 import javax.ws.rs.core.Response;
 
 import java.io.ByteArrayInputStream;
 import java.nio.charset.StandardCharsets;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 public class TestNiFiClient {
@@ -70,10 +89,13 @@ public class TestNiFiClient {
             "}";
 
     private NiFiClient niFiClient;
+    private static final String HOSTNAME = "example.com";
+    private static final String HTTP_RESOURCES = 
"http://localhost:8080/nifi-api/resources";;
+    private static final String RESPONSE_ENTITY = "{\"status\": \"success\"}";
 
     @Before
-    public void setup() {
-        niFiClient = new MockNiFiClient(RESOURCES_RESPONSE, 200);
+    public void setup() throws NoSuchAlgorithmException, 
KeyManagementException {
+        niFiClient = new MockNiFiClient(RESOURCES_RESPONSE, 200, false, 
HOSTNAME);
     }
 
     @Test
@@ -131,9 +153,9 @@ public void testGetResourcesWithUserInputAnywhere() throws 
Exception {
     }
 
     @Test
-    public void testGetResourcesErrorResponse() {
+    public void testGetResourcesErrorResponse() throws 
NoSuchAlgorithmException, KeyManagementException {
         final String errorMsg = "unknown error";
-        niFiClient = new MockNiFiClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode());
+        niFiClient = new MockNiFiClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode(), false, HOSTNAME);
 
         ResourceLookupContext resourceLookupContext = 
Mockito.mock(ResourceLookupContext.class);
         when(resourceLookupContext.getUserInput()).thenReturn("");
@@ -154,26 +176,161 @@ public void testConnectionTestSuccess() {
     }
 
     @Test
-    public void testConnectionTestFailure() {
+    public void testConnectionTestFailure() throws NoSuchAlgorithmException, 
KeyManagementException {
         final String errorMsg = "unknown error";
-        niFiClient = new MockNiFiClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode());
+        niFiClient = new MockNiFiClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode(), false, HOSTNAME);
 
         HashMap<String, Object> ret = niFiClient.connectionTest();
         Assert.assertNotNull(ret);
         Assert.assertEquals(NiFiClient.FAILURE_MSG, ret.get("message"));
     }
 
+    @Test
+    public void testHostnameVerifierMatch() throws NoSuchAlgorithmException, 
KeyManagementException, CertificateParsingException, SSLPeerUnverifiedException 
{
+        MockNiFiClient sslClient = new MockNiFiClient(RESPONSE_ENTITY, 200, 
true, HOSTNAME);
+        sslClient.setupSSLMock(HOSTNAME);
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        verify(sslClient.hostnameVerifierSpy).verify(eq(HOSTNAME), 
any(SSLSession.class));
+        Assert.assertTrue(sslClient.lastVerifyResult);
+    }
+
+    @Test
+    public void testHostnameVerifierNoMatch() throws NoSuchAlgorithmException, 
KeyManagementException, CertificateParsingException, SSLPeerUnverifiedException 
{
+        MockNiFiClient sslClient = new MockNiFiClient(RESPONSE_ENTITY, 200, 
true, HOSTNAME);
+        sslClient.setupSSLMock("other.com");
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        verify(sslClient.hostnameVerifierSpy).verify(eq(HOSTNAME), 
any(SSLSession.class));
+        Assert.assertFalse(sslClient.lastVerifyResult);
+    }
+
+    @Test
+    public void testHostnameVerifierNoCerts() throws NoSuchAlgorithmException, 
KeyManagementException, SSLPeerUnverifiedException {
+        MockNiFiClient sslClient = new MockNiFiClient(RESPONSE_ENTITY, 200, 
true, HOSTNAME);
+        sslClient.setupSSLMockWithNoCerts();
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        Assert.assertFalse(sslClient.lastVerifyResult);
+    }
+
+    @Test
+    public void testHostnameVerifierEmptyCerts() throws 
NoSuchAlgorithmException, KeyManagementException, SSLPeerUnverifiedException {
+        MockNiFiClient sslClient = new MockNiFiClient(RESPONSE_ENTITY, 200, 
true, HOSTNAME);
+        sslClient.setupSSLMockWithEmptyCerts();
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        Assert.assertFalse(sslClient.lastVerifyResult);
+    }
+
+    @Test
+    public void testHostnameVerifierSanInIntermediateCertsFails() throws 
NoSuchAlgorithmException, KeyManagementException, CertificateParsingException, 
SSLPeerUnverifiedException {
+        MockNiFiClient sslClient = new MockNiFiClient(RESPONSE_ENTITY, 200, 
true, HOSTNAME);
+        sslClient.setupSSLMockWithSanInIntermediate();
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        verify(sslClient.hostnameVerifierSpy).verify(eq(HOSTNAME), 
any(SSLSession.class));
+        Assert.assertFalse(sslClient.lastVerifyResult);
+    }
+
     /**
      * Extend NiFiClient to return mock responses.
      */
     private static final class MockNiFiClient extends NiFiClient {
-        private final int    statusCode;
+        private final int statusCode;
         private final String responseEntity;
+        private final boolean useSSL;
+        private final String hostname;
+        private HostnameVerifier hostnameVerifierSpy;
+        private boolean lastVerifyResult;
+        private SSLSession mockSession;
 
-        public MockNiFiClient(String responseEntity, int statusCode) {
-            super("http://localhost:8080/nifi-api/resources";, null);
-            this.statusCode     = statusCode;
+        private MockNiFiClient(String responseEntity, int statusCode, boolean 
useSSL, String hostname) throws NoSuchAlgorithmException, 
KeyManagementException {
+            super(useSSL ? ("https://"; + (hostname != null ? hostname : 
"localhost") + ":443") : HTTP_RESOURCES,
+                    useSSL ? createInitializedSSLContext() : null);
+            this.statusCode = statusCode;
             this.responseEntity = responseEntity;
+            this.useSSL = useSSL;
+            this.hostname = hostname;
+        }
+
+        private static SSLContext createInitializedSSLContext() throws 
NoSuchAlgorithmException, KeyManagementException {
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, null, null);
+            return sslContext;
+        }
+
+        void setupSSLMock(String sanHostname) throws 
CertificateParsingException, SSLPeerUnverifiedException {
+            if (!useSSL) {
+                throw new IllegalStateException("SSL setup not supported for 
non-SSL mock");
+            }
+            hostnameVerifierSpy = spy(getHostnameVerifier());
+            mockSession = Mockito.mock(SSLSession.class);
+            SSLSessionContext mockContext = 
Mockito.mock(SSLSessionContext.class);
+            doReturn(mockContext).when(mockSession).getSessionContext();
+
+            X509Certificate mockCert = Mockito.mock(X509Certificate.class);
+            Certificate[] certs = {mockCert};
+            doReturn(certs).when(mockSession).getPeerCertificates();
+
+            Collection<List<?>> altNames = Collections.singletonList(
+                    Arrays.asList(2, sanHostname.toLowerCase()));
+            doReturn(altNames).when(mockCert).getSubjectAlternativeNames();
+            doAnswer(invocation -> {
+                Boolean result = (Boolean) invocation.callRealMethod();
+                lastVerifyResult = result;
+                return result;
+            }).when(hostnameVerifierSpy).verify(any(String.class), 
any(SSLSession.class));
+        }
+
+        void setupSSLMockWithNoCerts() throws SSLPeerUnverifiedException {
+            if (!useSSL) {
+                throw new IllegalStateException("SSL setup not supported for 
non-SSL mock");
+            }
+            hostnameVerifierSpy = spy(getHostnameVerifier());
+            mockSession = Mockito.mock(SSLSession.class);
+            doReturn(null).when(mockSession).getPeerCertificates();
+            
doReturn(false).when(hostnameVerifierSpy).verify(any(String.class), 
any(SSLSession.class));
+            lastVerifyResult = false;
+        }
+
+        void setupSSLMockWithEmptyCerts() throws SSLPeerUnverifiedException {
+            if (!useSSL) {
+                throw new IllegalStateException("SSL setup not supported for 
non-SSL mock");
+            }
+            hostnameVerifierSpy = spy(getHostnameVerifier());
+            mockSession = Mockito.mock(SSLSession.class);
+            doReturn(new 
Certificate[0]).when(mockSession).getPeerCertificates();
+            
doReturn(false).when(hostnameVerifierSpy).verify(any(String.class), 
any(SSLSession.class));
+            lastVerifyResult = false;
+        }
+
+        void setupSSLMockWithSanInIntermediate() throws 
CertificateParsingException, SSLPeerUnverifiedException {
+            if (!useSSL) {
+                throw new IllegalStateException("SSL setup not supported for 
non-SSL mock");
+            }
+            hostnameVerifierSpy = spy(getHostnameVerifier());
+            mockSession = Mockito.mock(SSLSession.class);
+            SSLSessionContext mockContext = 
Mockito.mock(SSLSessionContext.class);
+            doReturn(mockContext).when(mockSession).getSessionContext();
+
+            // Server cert (index 0): No SANs
+            X509Certificate serverCert = Mockito.mock(X509Certificate.class);
+            doReturn(null).when(serverCert).getSubjectAlternativeNames();
+
+            // Intermediate cert (index 1): Has SAN with hostname
+            X509Certificate intermediateCert = 
Mockito.mock(X509Certificate.class);
+            Collection<List<?>> intermediateAltNames = 
Collections.singletonList(
+                    Arrays.asList(2, HOSTNAME.toLowerCase()));
+            
doReturn(intermediateAltNames).when(intermediateCert).getSubjectAlternativeNames();
+
+            // Root cert (index 2): No SANs
+            X509Certificate rootCert = Mockito.mock(X509Certificate.class);
+            doReturn(null).when(rootCert).getSubjectAlternativeNames();
+
+            Certificate[] certs = {serverCert, intermediateCert, rootCert};
+            doReturn(certs).when(mockSession).getPeerCertificates();
+
+            doAnswer(invocation -> {
+                Boolean result = (Boolean) invocation.callRealMethod();
+                lastVerifyResult = result;
+                return result;
+            }).when(hostnameVerifierSpy).verify(any(String.class), 
any(SSLSession.class));
         }
 
         @Override
@@ -183,6 +340,9 @@ protected WebResource getWebResource() {
 
         @Override
         protected ClientResponse getResponse(WebResource resource, String 
accept) {
+            if (useSSL) {
+                hostnameVerifierSpy.verify(hostname, mockSession);
+            }
             ClientResponse response = Mockito.mock(ClientResponse.class);
             when(response.getStatus()).thenReturn(statusCode);
             when(response.getEntityInputStream()).thenReturn(new 
ByteArrayInputStream(responseEntity.getBytes(StandardCharsets.UTF_8)));

Reply via email to