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)));