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

dhavalshah9131 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 7c2dec7aa RANGER-5318: Hostname verifier, check the hostname across 
chain (#665)
7c2dec7aa is described below

commit 7c2dec7aaabdcda81f108ad3a79377974c5b8c1b
Author: Vyom Mani Tiwari <[email protected]>
AuthorDate: Wed Sep 17 21:51:52 2025 +0530

    RANGER-5318: Hostname verifier, check the hostname across chain (#665)
---
 .../nifi/registry/client/NiFiRegistryClient.java   |  17 +-
 .../registry/client/TestNiFiRegistryClient.java    | 186 +++++++++++++++++++--
 2 files changed, 183 insertions(+), 20 deletions(-)

diff --git 
a/plugin-nifi-registry/src/main/java/org/apache/ranger/services/nifi/registry/client/NiFiRegistryClient.java
 
b/plugin-nifi-registry/src/main/java/org/apache/ranger/services/nifi/registry/client/NiFiRegistryClient.java
index acfd451d0..ed126ab36 100644
--- 
a/plugin-nifi-registry/src/main/java/org/apache/ranger/services/nifi/registry/client/NiFiRegistryClient.java
+++ 
b/plugin-nifi-registry/src/main/java/org/apache/ranger/services/nifi/registry/client/NiFiRegistryClient.java
@@ -168,14 +168,15 @@ private static class NiFiRegistryHostnameVerifier 
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-registry/src/test/java/org/apache/ranger/services/nifi/registry/client/TestNiFiRegistryClient.java
 
b/plugin-nifi-registry/src/test/java/org/apache/ranger/services/nifi/registry/client/TestNiFiRegistryClient.java
index 4775a33a5..583c9d0ec 100644
--- 
a/plugin-nifi-registry/src/test/java/org/apache/ranger/services/nifi/registry/client/TestNiFiRegistryClient.java
+++ 
b/plugin-nifi-registry/src/test/java/org/apache/ranger/services/nifi/registry/client/TestNiFiRegistryClient.java
@@ -27,26 +27,50 @@
 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.io.IOException;
 import java.net.URL;
 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 TestNiFiRegistryClient {
     private NiFiRegistryClient registryClient;
+    private static final String HOSTNAME = "example.com";
+    private static final String POLICIES_RESOURCES = 
"http://localhost:18080/nifi-registry-api/policiesresources";;
 
     @Before
-    public void setup() throws IOException {
-        final URL    responseFile      = 
TestNiFiRegistryClient.class.getResource("/resources-response.json");
+    public void setup() throws IOException, NoSuchAlgorithmException, 
KeyManagementException {
+        final URL responseFile = 
TestNiFiRegistryClient.class.getResource("/resources-response.json");
+        if (responseFile == null) {
+            throw new AssertionError();
+        }
         final String resourcesResponse = Resources.toString(responseFile, 
StandardCharsets.UTF_8);
-        registryClient = new MockNiFiRegistryClient(resourcesResponse, 200);
+        registryClient = new MockNiFiRegistryClient(resourcesResponse, 200, 
false, null);
     }
 
     @Test
@@ -103,9 +127,9 @@ public void testGetResourcesWithUserInputAnywhere() throws 
Exception {
     }
 
     @Test
-    public void testGetResourcesErrorResponse() {
+    public void testGetResourcesErrorResponse() throws 
NoSuchAlgorithmException, KeyManagementException {
         final String errorMsg = "unknown error";
-        registryClient = new MockNiFiRegistryClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode());
+        registryClient = new MockNiFiRegistryClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode(), false, null);
 
         ResourceLookupContext resourceLookupContext = 
Mockito.mock(ResourceLookupContext.class);
         when(resourceLookupContext.getUserInput()).thenReturn("");
@@ -126,26 +150,161 @@ public void testConnectionTestSuccess() {
     }
 
     @Test
-    public void testConnectionTestFailure() {
+    public void testConnectionTestFailure() throws NoSuchAlgorithmException, 
KeyManagementException {
         final String errorMsg = "unknown error";
-        registryClient = new MockNiFiRegistryClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode());
+        registryClient = new MockNiFiRegistryClient(errorMsg, 
Response.Status.BAD_REQUEST.getStatusCode(), false, null);
 
         HashMap<String, Object> ret = registryClient.connectionTest();
         Assert.assertNotNull(ret);
         Assert.assertEquals(NiFiRegistryClient.FAILURE_MSG, 
ret.get("message"));
     }
 
+    @Test
+    public void testHostnameVerifierMatch() throws NoSuchAlgorithmException, 
KeyManagementException, CertificateParsingException, SSLPeerUnverifiedException 
{
+        MockNiFiRegistryClient sslClient = new MockNiFiRegistryClient("", 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 
{
+        MockNiFiRegistryClient sslClient = new MockNiFiRegistryClient("", 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 {
+        MockNiFiRegistryClient sslClient = new MockNiFiRegistryClient("", 200, 
true, HOSTNAME);
+        sslClient.setupSSLMockWithNoCerts();
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        Assert.assertFalse(sslClient.lastVerifyResult);
+    }
+
+    @Test
+    public void testHostnameVerifierEmptyCerts() throws 
NoSuchAlgorithmException, KeyManagementException, SSLPeerUnverifiedException {
+        MockNiFiRegistryClient sslClient = new MockNiFiRegistryClient("", 200, 
true, HOSTNAME);
+        sslClient.setupSSLMockWithEmptyCerts();
+        sslClient.getResponse(sslClient.getWebResource(), "application/json");
+        Assert.assertFalse(sslClient.lastVerifyResult);
+    }
+
+    @Test
+    public void testHostnameVerifierSanInIntermediateCertsFails() throws 
NoSuchAlgorithmException, KeyManagementException, CertificateParsingException, 
SSLPeerUnverifiedException {
+        MockNiFiRegistryClient sslClient = new MockNiFiRegistryClient("", 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 NiFiRegistryClient to return mock responses.
      */
     private static final class MockNiFiRegistryClient extends 
NiFiRegistryClient {
-        private int    statusCode;
-        private String responseEntity;
+        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;
 
-        private MockNiFiRegistryClient(String responseEntity, int statusCode) {
-            
super("http://localhost:18080/nifi-registry-api/policiesresources";, null);
-            this.statusCode     = statusCode;
+        private MockNiFiRegistryClient(String responseEntity, int statusCode, 
boolean useSSL, String hostname) throws NoSuchAlgorithmException, 
KeyManagementException {
+            super(useSSL ? ("https://"; + (hostname != null ? hostname : 
"localhost") + ":443") : POLICIES_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
@@ -155,6 +314,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