NIFI-11195 Refactored Identity Mapping to nifi-security-identity

- Moved StringUtils from nifi-properties to nifi-property-utils
- Moved Peer Identity methods from CertificateUtils to specific Site-to-Site classes

Signed-off-by: Joe Gresock <jgresock@gmail.com>
This closes #6977.
This commit is contained in:
exceptionfactory 2023-02-16 20:51:25 -06:00 committed by Matt Burgess
parent 49bc62c928
commit fec34036a5
34 changed files with 388 additions and 366 deletions

View File

@ -15,6 +15,12 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>1.24.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-properties</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
@ -23,10 +29,4 @@
<scope>compile</scope>
</dependency>
</dependencies>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>1.24.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-properties</artifactId>
</project>

View File

@ -0,0 +1,35 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-security-identity</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-properties</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -18,12 +18,10 @@ package org.apache.nifi.security.util;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
@ -42,8 +40,6 @@ import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@ -81,14 +77,8 @@ import org.slf4j.LoggerFactory;
public final class CertificateUtils {
private static final Logger logger = LoggerFactory.getLogger(CertificateUtils.class);
private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not authenticated";
private static final Map<ASN1ObjectIdentifier, Integer> dnOrderMap = createDnOrderMap();
public static final String JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION = "TLSv1.2";
public static final String JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION = "TLSv1.3";
public static final String[] JAVA_8_SUPPORTED_TLS_PROTOCOL_VERSIONS = new String[]{JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION};
public static final String[] JAVA_11_SUPPORTED_TLS_PROTOCOL_VERSIONS = new String[]{JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION, JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION};
static {
Security.addProvider(new BouncyCastleProvider());
}
@ -206,136 +196,6 @@ public final class CertificateUtils {
return result;
}
/**
* Returns the DN extracted from the peer certificate (the server DN if run on the client; the client DN (if available) if run on the server).
* <p>
* If the client auth setting is WANT or NONE and a client certificate is not present, this method will return {@code null}.
* If the client auth is NEED, it will throw a {@link CertificateException}.
*
* @param socket the SSL Socket
* @return the extracted DN
* @throws CertificateException if there is a problem parsing the certificate
*/
public static String extractPeerDNFromSSLSocket(Socket socket) throws CertificateException {
String dn = null;
if (socket instanceof SSLSocket) {
final SSLSocket sslSocket = (SSLSocket) socket;
boolean clientMode = sslSocket.getUseClientMode();
logger.debug("SSL Socket in {} mode", clientMode ? "client" : "server");
ClientAuth clientAuth = getClientAuthStatus(sslSocket);
logger.debug("SSL Socket client auth status: {}", clientAuth);
if (clientMode) {
logger.debug("This socket is in client mode, so attempting to extract certificate from remote 'server' socket");
dn = extractPeerDNFromServerSSLSocket(sslSocket);
} else {
logger.debug("This socket is in server mode, so attempting to extract certificate from remote 'client' socket");
dn = extractPeerDNFromClientSSLSocket(sslSocket);
}
}
return dn;
}
/**
* Returns the DN extracted from the client certificate.
* <p>
* If the client auth setting is WANT or NONE and a certificate is not present (and {@code respectClientAuth} is {@code true}), this method will return {@code null}.
* If the client auth is NEED, it will throw a {@link CertificateException}.
*
* @param sslSocket the SSL Socket
* @return the extracted DN
* @throws CertificateException if there is a problem parsing the certificate
*/
private static String extractPeerDNFromClientSSLSocket(SSLSocket sslSocket) throws CertificateException {
String dn = null;
/** The clientAuth value can be "need", "want", or "none"
* A client must send client certificates for need, should for want, and will not for none.
* This method should throw an exception if none are provided for need, return null if none are provided for want, and return null (without checking) for none.
*/
ClientAuth clientAuth = getClientAuthStatus(sslSocket);
logger.debug("SSL Socket client auth status: {}", clientAuth);
if (clientAuth != ClientAuth.NONE) {
try {
final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
if (certChains != null && certChains.length > 0) {
X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
dn = x509Certificate.getSubjectDN().getName().trim();
logger.debug("Extracted DN={} from client certificate", dn);
}
} catch (SSLPeerUnverifiedException e) {
if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
logger.error("The incoming request did not contain client certificates and thus the DN cannot" +
" be extracted. Check that the other endpoint is providing a complete client certificate chain");
}
if (clientAuth == ClientAuth.WANT) {
logger.warn("Suppressing missing client certificate exception because client auth is set to 'want'");
return null;
}
throw new CertificateException(e);
}
}
return dn;
}
/**
* Returns the DN extracted from the server certificate.
*
* @param socket the SSL Socket
* @return the extracted DN
* @throws CertificateException if there is a problem parsing the certificate
*/
private static String extractPeerDNFromServerSSLSocket(Socket socket) throws CertificateException {
String dn = null;
if (socket instanceof SSLSocket) {
final SSLSocket sslSocket = (SSLSocket) socket;
try {
final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
if (certChains != null && certChains.length > 0) {
X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
dn = x509Certificate.getSubjectDN().getName().trim();
logger.debug("Extracted DN={} from server certificate", dn);
}
} catch (SSLPeerUnverifiedException e) {
if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
logger.error("The server did not present a certificate and thus the DN cannot" +
" be extracted. Check that the other endpoint is providing a complete certificate chain");
}
throw new CertificateException(e);
}
}
return dn;
}
private static ClientAuth getClientAuthStatus(SSLSocket sslSocket) {
return sslSocket.getNeedClientAuth() ? ClientAuth.REQUIRED : sslSocket.getWantClientAuth() ? ClientAuth.WANT : ClientAuth.NONE;
}
/**
* Accepts a legacy {@link javax.security.cert.X509Certificate} and returns an {@link X509Certificate}. The {@code javax.*} package certificate classes are for legacy compatibility and should
* not be used for new development.
*
* @param legacyCertificate the {@code javax.security.cert.X509Certificate}
* @return a new {@code java.security.cert.X509Certificate}
* @throws CertificateException if there is an error generating the new certificate
*/
@SuppressWarnings("deprecation")
public static X509Certificate convertLegacyX509Certificate(javax.security.cert.X509Certificate legacyCertificate) throws CertificateException {
if (legacyCertificate == null) {
throw new IllegalArgumentException("The X.509 certificate cannot be null");
}
try {
return formX509Certificate(legacyCertificate.getEncoded());
} catch (javax.security.cert.CertificateEncodingException e) {
throw new CertificateException(e);
}
}
/**
* Accepts an abstract {@link java.security.cert.Certificate} and returns an {@link X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} returns an array of the
* abstract certificates, they must be translated to X.509 to replace the functionality of {@code sslSocket.getSession().getPeerCertificateChain()}.

View File

@ -46,16 +46,6 @@
<artifactId>nifi-utils</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-ssl</artifactId>
@ -136,6 +126,14 @@
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>

View File

@ -26,12 +26,12 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
@ -64,7 +64,6 @@ import org.apache.nifi.remote.io.socket.SocketCommunicationsSession;
import org.apache.nifi.remote.protocol.CommunicationsSession;
import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
import org.apache.nifi.remote.protocol.socket.SocketClientProtocol;
import org.apache.nifi.security.util.CertificateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -72,6 +71,8 @@ public class EndpointConnectionPool implements PeerStatusProvider {
private static final Logger logger = LoggerFactory.getLogger(EndpointConnectionPool.class);
private static final SocketPeerIdentityProvider socketPeerIdentityProvider = new StandardSocketPeerIdentityProvider();
private final ConcurrentMap<PeerDescription, BlockingQueue<EndpointConnection>> connectionQueueMap = new ConcurrentHashMap<>();
private final Set<EndpointConnection> activeConnections = Collections.synchronizedSet(new HashSet<>());
@ -449,11 +450,12 @@ public class EndpointConnectionPool implements PeerStatusProvider {
socket.setSoTimeout(commsTimeout);
commsSession = new SocketCommunicationsSession(socket);
try {
final String dn = CertificateUtils.extractPeerDNFromSSLSocket(socket);
commsSession.setUserDn(dn);
} catch (final CertificateException ex) {
throw new IOException(ex);
final Optional<String> peerIdentity = socketPeerIdentityProvider.getPeerIdentity(socket);
if (peerIdentity.isPresent()) {
final String userDn = peerIdentity.get();
commsSession.setUserDn(userDn);
} else {
throw new IOException(String.format("Site-to-Site Peer [%s] Identity not found", socket.getRemoteSocketAddress()));
}
} else {

View File

@ -0,0 +1,33 @@
/*
* 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.nifi.remote.client.socket;
import java.net.Socket;
import java.util.Optional;
/**
* Abstraction for reading identity information from socket connections
*/
public interface SocketPeerIdentityProvider {
/**
* Get Peer Identity from Socket
*
* @param socket Socket
* @return Peer Identity or empty when not found
*/
Optional<String> getPeerIdentity(Socket socket);
}

View File

@ -0,0 +1,74 @@
/*
* 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.nifi.remote.client.socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.net.Socket;
import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Optional;
/**
* Standard implementation attempts to read X.509 certificates from an SSLSocket
*/
public class StandardSocketPeerIdentityProvider implements SocketPeerIdentityProvider {
private static final Logger logger = LoggerFactory.getLogger(StandardSocketPeerIdentityProvider.class);
@Override
public Optional<String> getPeerIdentity(final Socket socket) {
final Optional<String> peerIdentity;
if (socket instanceof SSLSocket) {
final SSLSocket sslSocket = (SSLSocket) socket;
final SSLSession sslSession = sslSocket.getSession();
peerIdentity = getPeerIdentity(sslSession);
} else {
peerIdentity = Optional.empty();
}
return peerIdentity;
}
private Optional<String> getPeerIdentity(final SSLSession sslSession) {
String peerIdentity = null;
final String peerHost = sslSession.getPeerHost();
final int peerPort = sslSession.getPeerPort();
try {
final Certificate[] peerCertificates = sslSession.getPeerCertificates();
if (peerCertificates == null || peerCertificates.length == 0) {
logger.warn("Peer Identity not found: Peer Certificates not provided [{}:{}]", peerHost, peerPort);
} else {
final X509Certificate peerCertificate = (X509Certificate) peerCertificates[0];
final Principal subjectDistinguishedName = peerCertificate.getSubjectDN();
peerIdentity = subjectDistinguishedName.getName();
}
} catch (final SSLPeerUnverifiedException e) {
logger.warn("Peer Identity not found: Peer Unverified [{}:{}]", peerHost, peerPort);
logger.debug("TLS Protocol [{}] Peer Unverified [{}:{}]", sslSession.getProtocol(), peerHost, peerPort, e);
}
return Optional.ofNullable(peerIdentity);
}
}

View File

@ -47,7 +47,6 @@ import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
@ -128,7 +127,6 @@ import org.apache.nifi.remote.protocol.ResponseCode;
import org.apache.nifi.remote.protocol.http.HttpHeaders;
import org.apache.nifi.remote.protocol.http.HttpProxy;
import org.apache.nifi.reporting.Severity;
import org.apache.nifi.security.util.CertificateUtils;
import org.apache.nifi.stream.io.StreamUtils;
import org.apache.nifi.web.api.dto.ControllerDTO;
import org.apache.nifi.web.api.dto.remote.PeerDTO;
@ -319,9 +317,9 @@ public class SiteToSiteRestApiClient implements Closeable {
}
try {
final X509Certificate cert = CertificateUtils.convertAbstractX509Certificate(certChain[0]);
final X509Certificate cert = (X509Certificate) certChain[0];
trustedPeerDn = cert.getSubjectDN().getName().trim();
} catch (final CertificateException e) {
} catch (final RuntimeException e) {
final String msg = "Could not extract subject DN from SSL session peer certificate";
logger.warn(msg);
eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, msg);

View File

@ -16,21 +16,16 @@
*/
package org.apache.nifi.remote.client
import org.apache.nifi.remote.PeerDescription
import org.apache.nifi.remote.PeerStatus
import org.apache.nifi.remote.TransferDirection
import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol
import org.apache.nifi.remote.util.PeerStatusCache
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.Security
import java.util.concurrent.ArrayBlockingQueue
import static org.junit.jupiter.api.Assertions.assertEquals
@ -52,16 +47,6 @@ class PeerSelectorTest {
private static mockPSP
private static mockPP
@BeforeAll
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@BeforeEach
void setUp() {
// Mock collaborators
@ -69,11 +54,6 @@ class PeerSelectorTest {
mockPP = mockPeerPersistence()
}
@AfterEach
void tearDown() {
}
private static String buildRemoteInstanceUris(List<String> nodes = DEFAULT_NODES) {
String remoteInstanceUris = "http://" + nodes.join(":8443/nifi-api,http://") + ":8443/nifi-api";
remoteInstanceUris
@ -206,7 +186,6 @@ class PeerSelectorTest {
new PeerStatusCache(peerStatuses, System.currentTimeMillis(), remoteInstanceUris, SiteToSiteTransportProtocol.HTTP)
},
save : { PeerStatusCache psc ->
logger.mock("Persisting PeerStatusCache: ${psc}")
}] as PeerPersistence
}
@ -985,8 +964,6 @@ class PeerSelectorTest {
bootstrapDescription
},
fetchRemotePeerStatuses : { PeerDescription pd ->
// Depending on the scenario, return given peer statuses
logger.mock("Scenario ${currentAttempt} fetchRemotePeerStatus for ${pd}")
switch (currentAttempt) {
case 1:
return [bootstrapStatus, node2Status] as Set<PeerStatus>

View File

@ -22,10 +22,8 @@ import org.apache.nifi.events.EventReporter;
import org.apache.nifi.remote.Peer;
import org.apache.nifi.remote.Transaction;
import org.apache.nifi.remote.TransferDirection;
import org.apache.nifi.remote.client.KeystoreType;
import org.apache.nifi.remote.client.SiteToSiteClient;
import org.apache.nifi.remote.codec.StandardFlowFileCodec;
import org.apache.nifi.remote.exception.HandshakeException;
import org.apache.nifi.remote.io.CompressionInputStream;
import org.apache.nifi.remote.io.CompressionOutputStream;
import org.apache.nifi.remote.protocol.DataPacket;
@ -34,8 +32,6 @@ import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
import org.apache.nifi.remote.protocol.http.HttpHeaders;
import org.apache.nifi.remote.protocol.http.HttpProxy;
import org.apache.nifi.remote.util.StandardDataPacket;
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.stream.io.StreamUtils;
import org.apache.nifi.web.api.dto.ControllerDTO;
import org.apache.nifi.web.api.dto.PortDTO;
@ -45,16 +41,11 @@ import org.apache.nifi.web.api.entity.PeersEntity;
import org.apache.nifi.web.api.entity.TransactionResultEntity;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
@ -117,7 +108,6 @@ public class TestHttpClient {
private static Server server;
private static ServerConnector httpConnector;
private static ServerConnector sslConnector;
private static CountDownLatch testCaseFinished;
private static HttpProxyServer proxyServer;
@ -126,11 +116,8 @@ public class TestHttpClient {
private static Set<PortDTO> inputPorts;
private static Set<PortDTO> outputPorts;
private static Set<PeerDTO> peers;
private static Set<PeerDTO> peersSecure;
private static String serverChecksum;
private static TlsConfiguration tlsConfiguration;
private static final int INITIAL_TRANSACTIONS = 0;
private static final AtomicInteger outputExtendTransactions = new AtomicInteger(INITIAL_TRANSACTIONS);
@ -141,16 +128,13 @@ public class TestHttpClient {
public static class SiteInfoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
final ControllerDTO controller = new ControllerDTO();
if (req.getLocalPort() == httpConnector.getLocalPort()) {
controller.setRemoteSiteHttpListeningPort(httpConnector.getLocalPort());
controller.setSiteToSiteSecure(false);
} else {
controller.setRemoteSiteHttpListeningPort(sslConnector.getLocalPort());
controller.setSiteToSiteSecure(true);
}
controller.setId("remote-controller-id");
@ -175,7 +159,7 @@ public class TestHttpClient {
public static class WrongSiteInfoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// This response simulates when a Site-to-Site is given a URL which has wrong path.
respondWithText(resp, "<p class=\"message-pane-content\">You may have mistyped...</p>", 200);
}
@ -184,16 +168,13 @@ public class TestHttpClient {
public static class PeersServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
final PeersEntity peersEntity = new PeersEntity();
if (req.getLocalPort() == httpConnector.getLocalPort()) {
assertNotNull(peers, "Test case should set <peers> depending on the test scenario.");
peersEntity.setPeers(peers);
} else {
assertNotNull(peersSecure, "Test case should set <peersSecure> depending on the test scenario.");
peersEntity.setPeers(peersSecure);
}
respondWithJson(resp, peersEntity);
@ -368,7 +349,7 @@ public class TestHttpClient {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
final int reqProtocolVersion = getReqProtocolVersion(req);
@ -477,24 +458,10 @@ public class TestHttpClient {
final ServletHandler wrongPathServletHandler = new ServletHandler();
wrongPathContextHandler.insertHandler(wrongPathServletHandler);
final SslContextFactory sslContextFactory = new SslContextFactory.Server();
setTlsConfiguration();
sslContextFactory.setKeyStorePath(tlsConfiguration.getKeystorePath());
sslContextFactory.setKeyStorePassword(tlsConfiguration.getKeystorePassword());
sslContextFactory.setKeyStoreType(tlsConfiguration.getKeystoreType().getType());
sslContextFactory.setProtocol(TlsConfiguration.getHighestCurrentSupportedTlsProtocolVersion());
httpConnector = new ServerConnector(server);
final HttpConfiguration https = new HttpConfiguration();
https.addCustomizer(new SecureRequestCustomizer());
sslConnector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, "http/1.1"),
new HttpConnectionFactory(https));
logger.info("SSL Connector: " + sslConnector.dump());
server.setConnectors(new Connector[] { httpConnector, sslConnector });
server.setConnectors(new Connector[] { httpConnector });
wrongPathServletHandler.addServletWithMapping(WrongSiteInfoServlet.class, "/site-to-site");
@ -528,8 +495,6 @@ public class TestHttpClient {
server.start();
logger.info("Starting server on port {} for HTTP, and {} for HTTPS", httpConnector.getLocalPort(), sslConnector.getLocalPort());
startProxyServer();
startProxyServerWithAuth();
}
@ -634,15 +599,6 @@ public class TestHttpClient {
peers = new HashSet<>();
peers.add(peer);
final PeerDTO peerSecure = new PeerDTO();
peerSecure.setHostname("localhost");
peerSecure.setPort(sslConnector.getLocalPort());
peerSecure.setFlowFileCount(10);
peerSecure.setSecure(true);
peersSecure = new HashSet<>();
peersSecure.add(peerSecure);
inputPorts = new HashSet<>();
final PortDTO runningInputPort = new PortDTO();
@ -711,18 +667,6 @@ public class TestHttpClient {
;
}
private SiteToSiteClient.Builder getDefaultBuilderHTTPS() {
return new SiteToSiteClient.Builder().transportProtocol(SiteToSiteTransportProtocol.HTTP)
.url("https://localhost:" + sslConnector.getLocalPort() + "/nifi")
.timeout(3, TimeUnit.MINUTES)
.keystoreFilename(tlsConfiguration.getKeystorePath())
.keystorePass(tlsConfiguration.getKeystorePassword())
.keystoreType(KeystoreType.valueOf(tlsConfiguration.getKeystoreType().getType()))
.truststoreFilename(tlsConfiguration.getTruststorePath())
.truststorePass(tlsConfiguration.getTruststorePassword())
.truststoreType(KeystoreType.valueOf(tlsConfiguration.getTruststoreType().getType()));
}
private static void consumeDataPacket(DataPacket packet) throws IOException {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
StreamUtils.copy(packet.getData(), bos);
@ -893,31 +837,6 @@ public class TestHttpClient {
}
@Test
public void testSendAccessDeniedHTTPS() throws Exception {
try (
final SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("input-access-denied")
.build()
) {
assertThrows(HandshakeException.class, () -> client.createTransaction(TransferDirection.SEND));
}
}
@Test
public void testSendSuccessHTTPS() throws Exception {
try (
final SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("input-running")
.build()
) {
testSend(client);
}
}
private interface SendData {
void apply(final Transaction transaction) throws IOException;
}
@ -1013,47 +932,6 @@ public class TestHttpClient {
}
@Test
public void testSendLargeFileHTTPS() throws Exception {
try (
SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("input-running")
.build()
) {
testSendLargeFile(client);
}
}
@Test
public void testSendLargeFileHTTPSWithProxy() throws Exception {
try (
SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("input-running")
.httpProxy(new HttpProxy("localhost", proxyServer.getListenAddress().getPort(), null, null))
.build()
) {
testSendLargeFile(client);
}
}
@Test
public void testSendLargeFileHTTPSWithProxyAuth() throws Exception {
try (
SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("input-running")
.httpProxy(new HttpProxy("localhost", proxyServerWithAuth.getListenAddress().getPort(), PROXY_USER, PROXY_PASSWORD))
.build()
) {
testSendLargeFile(client);
}
}
@Test
public void testSendSuccessCompressed() throws Exception {
@ -1264,44 +1142,6 @@ public class TestHttpClient {
}
}
@Test
public void testReceiveSuccessHTTPS() throws Exception {
try (
SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("output-running")
.build()
) {
testReceive(client);
}
}
@Test
public void testReceiveSuccessHTTPSWithProxy() throws Exception {
try (
SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("output-running")
.httpProxy(new HttpProxy("localhost", proxyServer.getListenAddress().getPort(), null, null))
.build()
) {
testReceive(client);
}
}
@Test
public void testReceiveSuccessHTTPSWithProxyAuth() throws Exception {
try (
SiteToSiteClient client = getDefaultBuilderHTTPS()
.portName("output-running")
.httpProxy(new HttpProxy("localhost", proxyServerWithAuth.getListenAddress().getPort(), PROXY_USER, PROXY_PASSWORD))
.build()
) {
testReceive(client);
}
}
@Test
public void testReceiveSuccessCompressed() throws Exception {
@ -1375,8 +1215,4 @@ public class TestHttpClient {
assertNotSame(INITIAL_TRANSACTIONS, outputExtendTransactions.get());
}
}
private static void setTlsConfiguration() {
tlsConfiguration = new TemporaryKeyStoreBuilder().trustStoreType(KeystoreType.JKS.name()).build();
}
}

View File

@ -0,0 +1,103 @@
/*
* 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.nifi.remote.client.socket;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.net.Socket;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Optional;
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 static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class StandardSocketPeerIdentityProviderTest {
private static final String DISTINGUISHED_NAME = "CN=Common Name,OU=Organizational Unit,O=Organization";
@Mock
SSLSocket sslSocket;
@Mock
SSLSession sslSession;
@Mock
X509Certificate peerCertificate;
StandardSocketPeerIdentityProvider provider;
@BeforeEach
void setProvider() {
provider = new StandardSocketPeerIdentityProvider();
}
@Test
void testGetPeerIdentityStandardSocket() throws IOException {
try (Socket socket = new Socket()) {
final Optional<String> peerIdentity = provider.getPeerIdentity(socket);
assertFalse(peerIdentity.isPresent());
}
}
@Test
void testGetPeerIdentitySSLSocketPeerUnverifiedException() throws SSLPeerUnverifiedException {
when(sslSocket.getSession()).thenReturn(sslSession);
when(sslSession.getPeerCertificates()).thenThrow(new SSLPeerUnverifiedException(SSLPeerUnverifiedException.class.getSimpleName()));
final Optional<String> peerIdentity = provider.getPeerIdentity(sslSocket);
assertFalse(peerIdentity.isPresent());
}
@Test
void testGetPeerIdentitySSLSocketPeerCertificatesNotFound() throws SSLPeerUnverifiedException {
when(sslSocket.getSession()).thenReturn(sslSession);
when(sslSession.getPeerCertificates()).thenReturn(new Certificate[]{});
final Optional<String> peerIdentity = provider.getPeerIdentity(sslSocket);
assertFalse(peerIdentity.isPresent());
}
@Test
void testGetPeerIdentityFound() throws SSLPeerUnverifiedException {
when(sslSocket.getSession()).thenReturn(sslSession);
when(sslSession.getPeerCertificates()).thenReturn(new X509Certificate[]{peerCertificate});
final X500Principal subjectDistinguishedName = new X500Principal(DISTINGUISHED_NAME);
when(peerCertificate.getSubjectDN()).thenReturn(subjectDistinguishedName);
final Optional<String> peerIdentity = provider.getPeerIdentity(sslSocket);
assertTrue(peerIdentity.isPresent());
final String identity = peerIdentity.get();
assertEquals(DISTINGUISHED_NAME, identity);
}
}

View File

@ -39,6 +39,12 @@
<artifactId>nifi-api</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<!-- Included for StringUtils -->
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-property-utils</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>

View File

@ -55,6 +55,7 @@
<module>nifi-repository-encryption</module>
<module>nifi-schema-utils</module>
<module>nifi-security-crypto-key</module>
<module>nifi-security-identity</module>
<module>nifi-security-kerberos-api</module>
<module>nifi-security-kerberos</module>
<module>nifi-security-kms</module>

View File

@ -136,6 +136,12 @@
<version>1.24.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>

View File

@ -32,6 +32,16 @@
<artifactId>nifi-api</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>

View File

@ -38,6 +38,10 @@
<artifactId>nifi-security-socket-ssl</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
</dependency>
<!-- Other modules using nifi-standard-record-utils are expected to have these APIs available, typically through a NAR dependency -->
<dependency>
<groupId>org.apache.nifi</groupId>

View File

@ -34,6 +34,10 @@
<artifactId>nifi-utils</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>

View File

@ -45,7 +45,6 @@
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>

View File

@ -202,7 +202,8 @@
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<artifactId>nifi-security-identity</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>

View File

@ -77,6 +77,11 @@
<artifactId>nifi-security-kms</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-identity</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-repository-encryption</artifactId>

View File

@ -38,6 +38,15 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-core-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-identity</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>

View File

@ -26,6 +26,10 @@ import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -33,7 +37,12 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.remote.cluster.ClusterNodeInformation;
import org.apache.nifi.remote.cluster.NodeInformant;
@ -46,7 +55,6 @@ import org.apache.nifi.remote.io.socket.SocketCommunicationsSession;
import org.apache.nifi.remote.protocol.CommunicationsSession;
import org.apache.nifi.remote.protocol.RequestType;
import org.apache.nifi.remote.protocol.ServerProtocol;
import org.apache.nifi.security.util.CertificateUtils;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
@ -160,7 +168,8 @@ public class SocketRemoteSiteListener implements RemoteSiteListener {
try {
if (secure) {
LOG.trace("{} Connection is secure", this);
dn = CertificateUtils.extractPeerDNFromSSLSocket(socket);
final SSLSocket sslSocket = (SSLSocket) socket;
dn = getPeerIdentity(sslSocket);
commsSession = new SocketCommunicationsSession(socket);
commsSession.setUserDn(dn);
@ -174,7 +183,7 @@ public class SocketRemoteSiteListener implements RemoteSiteListener {
// TODO: Add SocketProtocolListener#handleTlsError logic here
String msg = String.format("RemoteSiteListener Unable to accept connection from %s due to %s", socket, e.getLocalizedMessage());
// Suppress repeated TLS errors
if (CertificateUtils.isTlsError(e)) {
if (isTlsError(e)) {
boolean printedAsWarning = handleTlsError(msg);
// TODO: Move into handleTlsError and refactor shared behavior
@ -320,6 +329,32 @@ public class SocketRemoteSiteListener implements RemoteSiteListener {
listenerThread.start();
}
private boolean isTlsError(final Throwable e) {
final boolean tlsError;
if (e instanceof SSLException || e instanceof GeneralSecurityException) {
tlsError = true;
} else if (e.getCause() == null) {
tlsError = false;
} else {
tlsError = isTlsError(e.getCause());
}
return tlsError;
}
private String getPeerIdentity(final SSLSocket sslSocket) throws SSLPeerUnverifiedException {
final SSLSession sslSession = sslSocket.getSession();
final Certificate[] peerCertificates = sslSession.getPeerCertificates();
if (peerCertificates == null || peerCertificates.length == 0) {
throw new SSLPeerUnverifiedException(String.format("Peer [%s] certificates not found", sslSocket.getRemoteSocketAddress()));
}
final X509Certificate peerCertificate = (X509Certificate) peerCertificates[0];
final Principal subjectDistinguishedName = peerCertificate.getSubjectDN();
return subjectDistinguishedName.getName();
}
private boolean handleTlsError(String msg) {
if (tlsErrorRecentlySeen()) {
LOG.debug(msg);
@ -331,7 +366,7 @@ public class SocketRemoteSiteListener implements RemoteSiteListener {
}
/**
* Returns {@code true} if any related exception (determined by {@link CertificateUtils#isTlsError(Throwable)}) has occurred within the last
* Returns {@code true} if any related exception has occurred within the last
* {@link #EXCEPTION_THRESHOLD_MILLIS} milliseconds. Does not evaluate the error locally,
* simply checks the last time the timestamp was updated.
*

View File

@ -198,6 +198,12 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-xml-processing</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-identity</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-web-security</artifactId>

View File

@ -152,6 +152,11 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-identity</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-core</artifactId>

View File

@ -32,7 +32,6 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<version>1.24.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>

View File

@ -41,6 +41,11 @@
<artifactId>nifi-security-utils</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-identity</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-properties</artifactId>

View File

@ -59,6 +59,11 @@
<artifactId>nifi-event-transport</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>

View File

@ -57,6 +57,12 @@
<artifactId>nifi-utils</artifactId>
<version>1.24.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>