This is an automated email from the ASF dual-hosted git repository. madhan pushed a commit to branch ranger-2.8 in repository https://gitbox.apache.org/repos/asf/ranger.git
commit 4fe7eae8ef41a3327721c4b3b1eb27a8f2954426 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) (cherry picked from commit 7c2dec7aaabdcda81f108ad3a79377974c5b8c1b) --- .../nifi/registry/client/NiFiRegistryClient.java | 17 +- .../registry/client/TestNiFiRegistryClient.java | 183 +++++++++++++++++++-- 2 files changed, 181 insertions(+), 19 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 f1cab4a88..445b8bb77 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 @@ -173,14 +173,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 7db646f4d..e61bb2a81 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 { + 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,28 +150,162 @@ 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 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 int statusCode; - private String responseEntity; - - private MockNiFiRegistryClient(String responseEntity, int statusCode) { - super("http://localhost:18080/nifi-registry-api/policiesresources", null); + 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 @@ -157,6 +315,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(
