Repository: nifi
Updated Branches:
  refs/heads/0.x 59d77671e -> 06d306f0b


NIFI-1981 Resolved issue where cluster communication without client 
certificates would fail even if needClientAuth set to false.

Fixed IDE setting for import wildcarding on Groovy files. (+4 squashed commits)
Squashed commits:
[4c3b174] NIFI-1981 Lowered logging level of client auth setting on cluster 
connection receive.
[b50f473] NIFI-1981 Finished logic to suppress exception on missing client 
certificates when clientAuth is set to WANT.
Added unit tests for CertificateUtil methods.
[ace35a2] NIFI-1981 Added test scope dependency on BouncyCastle and BC PKIX 
modules for CertificateUtils tests.
[2c463d1] NIFI-1981 Added ClientAuth enum and CertificateUtil methods to 
extract this setting from an SSLSocket.
Added logic to compare X509Certificate DNs regardless of RDN element order.
Added logic to suppress peer certificate exceptions when client authentication 
is not required.
Removed duplicate dependency in pom.xml.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/06d306f0
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/06d306f0
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/06d306f0

Branch: refs/heads/0.x
Commit: 06d306f0be7690c8b00c1e52aba8ecade01f111a
Parents: 59d7767
Author: Andy LoPresto <[email protected]>
Authored: Tue Jun 7 16:19:02 2016 -0700
Committer: Matt Burgess <[email protected]>
Committed: Tue Jun 14 11:19:09 2016 -0400

----------------------------------------------------------------------
 nifi-commons/nifi-security-utils/pom.xml        |  10 +
 .../nifi/security/util/CertificateUtils.java    |  98 +++-
 .../security/util/CertificateUtilsTest.groovy   | 453 +++++++++++++++++++
 .../security/util/CertificateUtilsTest.groovy   | 275 -----------
 4 files changed, 554 insertions(+), 282 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/06d306f0/nifi-commons/nifi-security-utils/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/pom.xml 
b/nifi-commons/nifi-security-utils/pom.xml
index 030cda0..0bf7a4d 100644
--- a/nifi-commons/nifi-security-utils/pom.xml
+++ b/nifi-commons/nifi-security-utils/pom.xml
@@ -30,6 +30,16 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk15on</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/06d306f0/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
 
b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
index cf9a538..b3321f7 100644
--- 
a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
+++ 
b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
@@ -30,6 +30,9 @@ import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSocket;
 import org.apache.commons.lang3.StringUtils;
@@ -39,6 +42,25 @@ 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";
+
+    public enum ClientAuth {
+        NONE(0, "none"),
+        WANT(1, "want"),
+        NEED(2, "need");
+
+        private int value;
+        private String description;
+
+        ClientAuth(int value, String description) {
+            this.value = value;
+            this.description = description;
+        }
+
+        public String toString() {
+            return "Client Auth: " + this.description + " (" + this.value + 
")";
+        }
+    }
 
     /**
      * Returns true if the given keystore can be loaded using the given 
keystore type and password. Returns false otherwise.
@@ -148,20 +170,43 @@ public final class CertificateUtils {
         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();
+
+            /** 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();
+                    }
+                } 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 dn;
+                    }
+                    throw new CertificateException(e);
                 }
-            } catch (SSLPeerUnverifiedException e) {
-                throw new CertificateException(e);
             }
         }
 
         return dn;
     }
 
+    private static ClientAuth getClientAuthStatus(SSLSocket sslSocket) {
+        return sslSocket.getNeedClientAuth() ? ClientAuth.NEED : 
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.
@@ -213,6 +258,45 @@ public final class CertificateUtils {
         }
     }
 
+    /**
+     * Returns true if the two provided DNs are equivalent, regardless of the 
order of the elements. Returns false if one or both are invalid DNs.
+     *
+     * Example:
+     *
+     * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true
+     * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true
+     * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false
+     * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false
+     * CN=test1, O=testOrg, C=US compared to                           -> false
+     *                           compared to                           -> true
+     *
+     * @param dn1 the first DN to compare
+     * @param dn2 the second DN to compare
+     * @return true if the DNs are equivalent, false otherwise
+     */
+    public static boolean compareDNs(String dn1, String dn2) {
+        if (dn1 == null) {
+            dn1 = "";
+        }
+
+        if (dn2 == null) {
+            dn2 = "";
+        }
+
+        if (StringUtils.isEmpty(dn1) || StringUtils.isEmpty(dn2)) {
+            return dn1.equals(dn2);
+        }
+        try {
+            List<Rdn> rdn1 = new LdapName(dn1).getRdns();
+            List<Rdn> rdn2 = new LdapName(dn2).getRdns();
+
+            return rdn1.size() == rdn2.size() && rdn1.containsAll(rdn2);
+        } catch (InvalidNameException e) {
+            logger.warn("Cannot compare DNs: {} and {} because one or both is 
not a valid DN", dn1, dn2);
+            return false;
+        }
+    }
+
     private CertificateUtils() {
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/06d306f0/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
 
b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
new file mode 100644
index 0000000..2d00a25
--- /dev/null
+++ 
b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
@@ -0,0 +1,453 @@
+/*
+ * 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.security.util
+
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.KeyUsage
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+import org.bouncycastle.asn1.x509.X509Extension
+import org.bouncycastle.cert.X509CertificateHolder
+import org.bouncycastle.cert.X509v3CertificateBuilder
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.operator.ContentSigner
+import org.bouncycastle.operator.OperatorCreationException
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+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.security.InvalidKeyException
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.NoSuchAlgorithmException
+import java.security.NoSuchProviderException
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.security.Security
+import java.security.SignatureException
+import java.security.cert.Certificate
+import java.security.cert.CertificateException
+import java.security.cert.X509Certificate
+
+@RunWith(JUnit4.class)
+class CertificateUtilsTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(CertificateUtilsTest.class);
+
+    private static final int KEY_SIZE = 2048;
+
+    private static final long YESTERDAY = System.currentTimeMillis() - 24 * 60 
* 60 * 1000;
+    private static final long ONE_YEAR_FROM_NOW = System.currentTimeMillis() + 
365 * 24 * 60 * 60 * 1000;
+    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+    private static final String PROVIDER = "BC";
+
+    private static final String SUBJECT_DN = "CN=NiFi Test 
Server,OU=Security,O=Apache,ST=CA,C=US";
+    private static final String ISSUER_DN = "CN=NiFi Test 
CA,OU=Security,O=Apache,ST=CA,C=US";
+
+    @BeforeClass
+    static void setUpOnce() {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() {
+        super.setUp()
+
+    }
+
+    @After
+    void tearDown() {
+
+    }
+
+    /**
+     * Generates a public/private RSA keypair using the default key size.
+     *
+     * @return the keypair
+     * @throws java.security.NoSuchAlgorithmException if the RSA algorithm is 
not available
+     */
+    private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
+        KeyPairGenerator keyPairGenerator = 
KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(KEY_SIZE);
+        return keyPairGenerator.generateKeyPair();
+    }
+
+    /**
+     * Generates a signed certificate using an on-demand keypair.
+     *
+     * @param dn the DN
+     * @return the certificate
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     * @throws java.security.cert.CertificateException
+     * @throws java.security.NoSuchProviderException
+     * @throws java.security.SignatureException
+     * @throws java.security.InvalidKeyException
+     * @throws OperatorCreationException
+     */
+    private
+    static X509Certificate generateCertificate(String dn) throws IOException, 
NoSuchAlgorithmException, CertificateException, NoSuchProviderException, 
SignatureException, InvalidKeyException, OperatorCreationException {
+        KeyPair keyPair = generateKeyPair();
+        return generateCertificate(dn, keyPair);
+    }
+
+    /**
+     * Generates a signed certificate with a specific keypair.
+     *
+     * @param dn the DN
+     * @param keyPair the public key will be included in the certificate and 
the the private key is used to sign the certificate
+     * @return the certificate
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     * @throws CertificateException
+     * @throws NoSuchProviderException
+     * @throws SignatureException
+     * @throws InvalidKeyException
+     * @throws OperatorCreationException
+     */
+    private
+    static X509Certificate generateCertificate(String dn, KeyPair keyPair) 
throws IOException, NoSuchAlgorithmException, CertificateException, 
NoSuchProviderException, SignatureException, InvalidKeyException, 
OperatorCreationException {
+        PrivateKey privateKey = keyPair.getPrivate();
+        ContentSigner sigGen = new 
JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER).build(privateKey);
+        SubjectPublicKeyInfo subPubKeyInfo = 
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
+        Date startDate = new Date(YESTERDAY);
+        Date endDate = new Date(ONE_YEAR_FROM_NOW);
+
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                new X500Name(dn),
+                BigInteger.valueOf(System.currentTimeMillis()),
+                startDate, endDate,
+                new X500Name(dn),
+                subPubKeyInfo);
+
+        // Set certificate extensions
+        // (1) digitalSignature extension
+        certBuilder.addExtension(X509Extension.keyUsage, true,
+                new KeyUsage(KeyUsage.digitalSignature | 
KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement));
+
+        // (2) extendedKeyUsage extension
+        Vector<KeyPurposeId> ekUsages = new Vector<>();
+        ekUsages.add(KeyPurposeId.id_kp_clientAuth);
+        ekUsages.add(KeyPurposeId.id_kp_serverAuth);
+        certBuilder.addExtension(X509Extension.extendedKeyUsage, false, new 
ExtendedKeyUsage(ekUsages));
+
+        // Sign the certificate
+        X509CertificateHolder certificateHolder = certBuilder.build(sigGen);
+        return new JcaX509CertificateConverter().setProvider(PROVIDER)
+                .getCertificate(certificateHolder);
+    }
+
+    /**
+     * Generates a certificate signed by the issuer key.
+     *
+     * @param dn the subject DN
+     * @param issuerDn the issuer DN
+     * @param issuerKey the issuer private key
+     * @return the certificate
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     * @throws CertificateException
+     * @throws NoSuchProviderException
+     * @throws SignatureException
+     * @throws InvalidKeyException
+     * @throws OperatorCreationException
+     */
+    private
+    static X509Certificate generateIssuedCertificate(String dn, String 
issuerDn, PrivateKey issuerKey) throws IOException, NoSuchAlgorithmException, 
CertificateException, NoSuchProviderException, SignatureException, 
InvalidKeyException, OperatorCreationException {
+        KeyPair keyPair = generateKeyPair();
+        return generateIssuedCertificate(dn, keyPair.getPublic(), issuerDn, 
issuerKey);
+    }
+
+    /**
+     * Generates a certificate with a specific public key signed by the issuer 
key.
+     *
+     * @param dn the subject DN
+     * @param publicKey the subject public key
+     * @param issuerDn the issuer DN
+     * @param issuerKey the issuer private key
+     * @return the certificate
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     * @throws CertificateException
+     * @throws NoSuchProviderException
+     * @throws SignatureException
+     * @throws InvalidKeyException
+     * @throws OperatorCreationException
+     */
+    private
+    static X509Certificate generateIssuedCertificate(String dn, PublicKey 
publicKey, String issuerDn, PrivateKey issuerKey) throws IOException, 
NoSuchAlgorithmException, CertificateException, NoSuchProviderException, 
SignatureException, InvalidKeyException, OperatorCreationException {
+        ContentSigner sigGen = new 
JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER).build(issuerKey);
+        SubjectPublicKeyInfo subPubKeyInfo = 
SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
+        Date startDate = new Date(YESTERDAY);
+        Date endDate = new Date(ONE_YEAR_FROM_NOW);
+
+        X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(
+                new X500Name(issuerDn),
+                BigInteger.valueOf(System.currentTimeMillis()),
+                startDate, endDate,
+                new X500Name(dn),
+                subPubKeyInfo);
+
+        X509CertificateHolder certificateHolder = v3CertGen.build(sigGen);
+        return new JcaX509CertificateConverter().setProvider(PROVIDER)
+                .getCertificate(certificateHolder);
+    }
+
+    private static X509Certificate[] generateCertificateChain(String dn = 
SUBJECT_DN, String issuerDn = ISSUER_DN) {
+        final KeyPair issuerKeyPair = generateKeyPair();
+        final PrivateKey issuerPrivateKey = issuerKeyPair.getPrivate();
+
+        final X509Certificate issuerCertificate = 
generateCertificate(issuerDn, issuerKeyPair);
+        final X509Certificate certificate = generateIssuedCertificate(dn, 
issuerDn, issuerPrivateKey);
+        [certificate, issuerCertificate] as X509Certificate[]
+    }
+
+    private static javax.security.cert.X509Certificate 
generateLegacyCertificate(X509Certificate x509Certificate) {
+        return 
javax.security.cert.X509Certificate.getInstance(x509Certificate.getEncoded())
+    }
+
+    private static Certificate generateAbstractCertificate(X509Certificate 
x509Certificate) {
+        return x509Certificate as Certificate
+    }
+
+    @Test
+    void testShouldConvertLegacyX509Certificate() {
+        // Arrange
+        final X509Certificate EXPECTED_NEW_CERTIFICATE = 
generateCertificate(SUBJECT_DN)
+        logger.info("Expected certificate: 
${EXPECTED_NEW_CERTIFICATE.class.canonicalName} 
${EXPECTED_NEW_CERTIFICATE.subjectDN.toString()} 
(${EXPECTED_NEW_CERTIFICATE.getSerialNumber()})")
+
+        // Form the legacy certificate
+        final javax.security.cert.X509Certificate LEGACY_CERTIFICATE = 
generateLegacyCertificate(EXPECTED_NEW_CERTIFICATE)
+        logger.info("Legacy certificate: 
${LEGACY_CERTIFICATE.class.canonicalName} 
${LEGACY_CERTIFICATE.subjectDN.toString()} 
(${LEGACY_CERTIFICATE.getSerialNumber()})")
+
+        // Act
+        X509Certificate convertedCertificate = 
CertificateUtils.convertLegacyX509Certificate(LEGACY_CERTIFICATE)
+        logger.info("Converted certificate: 
${convertedCertificate.class.canonicalName} 
${convertedCertificate.subjectDN.toString()} 
(${convertedCertificate.getSerialNumber()})")
+
+        // Assert
+        assert convertedCertificate instanceof X509Certificate
+        assert convertedCertificate == EXPECTED_NEW_CERTIFICATE
+    }
+
+    @Test
+    void testShouldConvertAbstractX509Certificate() {
+        // Arrange
+        final X509Certificate EXPECTED_NEW_CERTIFICATE = 
generateCertificate(SUBJECT_DN)
+        logger.info("Expected certificate: 
${EXPECTED_NEW_CERTIFICATE.class.canonicalName} 
${EXPECTED_NEW_CERTIFICATE.subjectDN.toString()} 
(${EXPECTED_NEW_CERTIFICATE.getSerialNumber()})")
+
+        // Form the abstract certificate
+        final Certificate ABSTRACT_CERTIFICATE = 
generateAbstractCertificate(EXPECTED_NEW_CERTIFICATE)
+        logger.info("Abstract certificate: 
${ABSTRACT_CERTIFICATE.class.canonicalName} (?)")
+
+        // Act
+        X509Certificate convertedCertificate = 
CertificateUtils.convertAbstractX509Certificate(ABSTRACT_CERTIFICATE)
+        logger.info("Converted certificate: 
${convertedCertificate.class.canonicalName} 
${convertedCertificate.subjectDN.toString()} 
(${convertedCertificate.getSerialNumber()})")
+
+        // Assert
+        assert convertedCertificate instanceof X509Certificate
+        assert convertedCertificate == EXPECTED_NEW_CERTIFICATE
+    }
+
+    @Test
+    void testShouldDetermineClientAuthStatusFromSocket() {
+        // Arrange
+        SSLSocket needSocket = [getNeedClientAuth: { -> true }] as SSLSocket
+        SSLSocket wantSocket = [getNeedClientAuth: { -> false }, 
getWantClientAuth: { -> true }] as SSLSocket
+        SSLSocket noneSocket = [getNeedClientAuth: { -> false }, 
getWantClientAuth: { -> false }] as SSLSocket
+
+        // Act
+        CertificateUtils.ClientAuth needClientAuthStatus = 
CertificateUtils.getClientAuthStatus(needSocket)
+        logger.info("Client auth (needSocket): ${needClientAuthStatus}")
+        CertificateUtils.ClientAuth wantClientAuthStatus = 
CertificateUtils.getClientAuthStatus(wantSocket)
+        logger.info("Client auth (wantSocket): ${wantClientAuthStatus}")
+        CertificateUtils.ClientAuth noneClientAuthStatus = 
CertificateUtils.getClientAuthStatus(noneSocket)
+        logger.info("Client auth (noneSocket): ${noneClientAuthStatus}")
+
+        // Assert
+        assert needClientAuthStatus == CertificateUtils.ClientAuth.NEED
+        assert wantClientAuthStatus == CertificateUtils.ClientAuth.WANT
+        assert noneClientAuthStatus == CertificateUtils.ClientAuth.NONE
+    }
+
+    @Test
+    void 
testShouldNotExtractClientCertificatesFromSSLSocketWithClientAuthNone() {
+        // Arrange
+        SSLSocket mockSocket = [
+                getNeedClientAuth: { -> false },
+                getWantClientAuth: { -> false }
+        ] as SSLSocket
+
+        // Act
+        String clientDN = 
CertificateUtils.extractClientDNFromSSLSocket(mockSocket)
+        logger.info("Extracted client DN: ${clientDN}")
+
+        // Assert
+        assert !clientDN
+    }
+
+    @Test
+    void testShouldExtractClientCertificatesFromSSLSocketWithClientAuthWant() {
+        // Arrange
+        final String EXPECTED_DN = 
"CN=client.nifi.apache.org,OU=Security,O=Apache,ST=CA,C=US"
+        Certificate[] certificateChain = generateCertificateChain(EXPECTED_DN)
+        logger.info("Expected DN: ${EXPECTED_DN}")
+        logger.info("Expected certificate chain: ${certificateChain.collect { 
(it as X509Certificate).getSubjectDN().name }.join(" issued by ")}")
+
+        SSLSession mockSession = [getPeerCertificates: { -> certificateChain 
}] as SSLSession
+
+        SSLSocket mockSocket = [
+                getNeedClientAuth: { -> false },
+                getWantClientAuth: { -> true },
+                getSession       : { -> mockSession }
+        ] as SSLSocket
+
+        // Act
+        String clientDN = 
CertificateUtils.extractClientDNFromSSLSocket(mockSocket)
+        logger.info("Extracted client DN: ${clientDN}")
+
+        // Assert
+        assert CertificateUtils.compareDNs(clientDN, EXPECTED_DN)
+    }
+
+    @Test
+    void 
testShouldHandleFailureToExtractClientCertificatesFromSSLSocketWithClientAuthWant()
 {
+        // Arrange
+        SSLSession mockSession = [getPeerCertificates: { -> throw new 
SSLPeerUnverifiedException("peer not authenticated") }] as SSLSession
+
+        SSLSocket mockSocket = [
+                getNeedClientAuth: { -> false },
+                getWantClientAuth: { -> true },
+                getSession       : { -> mockSession }
+        ] as SSLSocket
+
+        // Act
+        String clientDN = 
CertificateUtils.extractClientDNFromSSLSocket(mockSocket)
+        logger.info("Extracted client DN: ${clientDN}")
+
+        // Assert
+        assert CertificateUtils.compareDNs(clientDN, null)
+    }
+
+
+    @Test
+    void testShouldExtractClientCertificatesFromSSLSocketWithClientAuthNeed() {
+        // Arrange
+        final String EXPECTED_DN = 
"CN=client.nifi.apache.org,OU=Security,O=Apache,ST=CA,C=US"
+        Certificate[] certificateChain = generateCertificateChain(EXPECTED_DN)
+        logger.info("Expected DN: ${EXPECTED_DN}")
+        logger.info("Expected certificate chain: ${certificateChain.collect { 
(it as X509Certificate).getSubjectDN().name }.join(" issued by ")}")
+
+        SSLSession mockSession = [getPeerCertificates: { -> certificateChain 
}] as SSLSession
+
+        SSLSocket mockSocket = [
+                getNeedClientAuth: { -> true },
+                getWantClientAuth: { -> false },
+                getSession       : { -> mockSession }
+        ] as SSLSocket
+
+        // Act
+        String clientDN = 
CertificateUtils.extractClientDNFromSSLSocket(mockSocket)
+        logger.info("Extracted client DN: ${clientDN}")
+
+        // Assert
+        assert CertificateUtils.compareDNs(clientDN, EXPECTED_DN)
+    }
+
+    @Test
+    void 
testShouldHandleFailureToExtractClientCertificatesFromSSLSocketWithClientAuthNeed()
 {
+        // Arrange
+        SSLSession mockSession = [getPeerCertificates: { -> throw new 
SSLPeerUnverifiedException("peer not authenticated") }] as SSLSession
+
+        SSLSocket mockSocket = [
+                getNeedClientAuth: { -> true },
+                getWantClientAuth: { -> false },
+                getSession       : { -> mockSession }
+        ] as SSLSocket
+
+        // Act
+        def msg = shouldFail(CertificateException) {
+            String clientDN = 
CertificateUtils.extractClientDNFromSSLSocket(mockSocket)
+            logger.info("Extracted client DN: ${clientDN}")
+        }
+
+        // Assert
+        assert msg =~ "peer not authenticated"
+    }
+
+    @Test
+    void testShouldCompareDNs() {
+        // Arrange
+        final String DN_1_ORDERED = "CN=test1.nifi.apache.org, OU=Apache NiFi, 
O=Apache, ST=California, C=US"
+        logger.info("DN 1 Ordered : ${DN_1_ORDERED}")
+        final String DN_1_REVERSED = DN_1_ORDERED.split(", 
").reverse().join(", ")
+        logger.info("DN 1 Reversed: ${DN_1_REVERSED}")
+
+        final String DN_2_ORDERED = "CN=test2.nifi.apache.org, OU=Apache NiFi, 
O=Apache, ST=California, C=US"
+        logger.info("DN 2 Ordered : ${DN_2_ORDERED}")
+        final String DN_2_REVERSED = DN_2_ORDERED.split(", 
").reverse().join(", ")
+        logger.info("DN 2 Reversed: ${DN_2_REVERSED}")
+
+        // Act
+
+        // True
+        boolean dn1MatchesSelf = CertificateUtils.compareDNs(DN_1_ORDERED, 
DN_1_ORDERED)
+        logger.matches("DN 1, DN 1: ${dn1MatchesSelf}")
+
+        boolean dn1MatchesReversed = CertificateUtils.compareDNs(DN_1_ORDERED, 
DN_1_REVERSED)
+        logger.matches("DN 1, DN 1 (R): ${dn1MatchesReversed}")
+
+        boolean emptyMatchesEmpty = CertificateUtils.compareDNs("", "")
+        logger.matches("empty, empty: ${emptyMatchesEmpty}")
+
+        boolean nullMatchesNull = CertificateUtils.compareDNs(null, null)
+        logger.matches("null, null: ${nullMatchesNull}")
+
+        // False
+        boolean dn1MatchesDn2 = CertificateUtils.compareDNs(DN_1_ORDERED, 
DN_2_ORDERED)
+        logger.matches("DN 1, DN 2: ${dn1MatchesDn2}")
+
+        boolean dn1MatchesDn2Reversed = 
CertificateUtils.compareDNs(DN_1_ORDERED, DN_2_REVERSED)
+        logger.matches("DN 1, DN 2 (R): ${dn1MatchesDn2Reversed}")
+
+        boolean dn1MatchesEmpty = CertificateUtils.compareDNs(DN_1_ORDERED, "")
+        logger.matches("DN 1, empty: ${dn1MatchesEmpty}")
+
+        // Assert
+        assert dn1MatchesSelf
+        assert dn1MatchesReversed
+        assert emptyMatchesEmpty
+        assert nullMatchesNull
+
+        assert !dn1MatchesDn2
+        assert !dn1MatchesDn2Reversed
+        assert !dn1MatchesEmpty
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/06d306f0/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
deleted file mode 100644
index 2be2e16..0000000
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/CertificateUtilsTest.groovy
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * 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.security.util
-
-import org.bouncycastle.asn1.x500.X500Name
-import org.bouncycastle.asn1.x509.ExtendedKeyUsage
-import org.bouncycastle.asn1.x509.KeyPurposeId
-import org.bouncycastle.asn1.x509.KeyUsage
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
-import org.bouncycastle.asn1.x509.X509Extension
-import org.bouncycastle.cert.X509CertificateHolder
-import org.bouncycastle.cert.X509v3CertificateBuilder
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.bouncycastle.operator.ContentSigner
-import org.bouncycastle.operator.OperatorCreationException
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
-import org.junit.After
-import org.junit.Before
-import org.junit.BeforeClass
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-
-import java.security.InvalidKeyException
-import java.security.KeyPair
-import java.security.KeyPairGenerator
-import java.security.NoSuchAlgorithmException
-import java.security.NoSuchProviderException
-import java.security.PrivateKey
-import java.security.PublicKey
-import java.security.Security
-import java.security.SignatureException
-import java.security.cert.Certificate
-import java.security.cert.CertificateException
-import java.security.cert.X509Certificate
-
-@RunWith(JUnit4.class)
-class CertificateUtilsTest extends GroovyTestCase {
-    private static final Logger logger = 
LoggerFactory.getLogger(CertificateUtilsTest.class);
-
-    private static final int KEY_SIZE = 2048;
-
-    private static final long YESTERDAY = System.currentTimeMillis() - 24 * 60 
* 60 * 1000;
-    private static final long ONE_YEAR_FROM_NOW = System.currentTimeMillis() + 
365 * 24 * 60 * 60 * 1000;
-    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
-    private static final String PROVIDER = "BC";
-
-    private static final String SUBJECT_DN = "CN=NiFi Test 
Server,OU=Security,O=Apache,ST=CA,C=US";
-    private static final String ISSUER_DN = "CN=NiFi Test 
CA,OU=Security,O=Apache,ST=CA,C=US";
-
-    @BeforeClass
-    static void setUpOnce() {
-        Security.addProvider(new BouncyCastleProvider())
-
-        logger.metaClass.methodMissing = { String name, args ->
-            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
-        }
-    }
-
-    @Before
-    void setUp() {
-        super.setUp()
-
-    }
-
-    @After
-    void tearDown() {
-
-    }
-
-    /**
-     * Generates a public/private RSA keypair using the default key size.
-     *
-     * @return the keypair
-     * @throws java.security.NoSuchAlgorithmException if the RSA algorithm is 
not available
-     */
-    private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
-        KeyPairGenerator keyPairGenerator = 
KeyPairGenerator.getInstance("RSA");
-        keyPairGenerator.initialize(KEY_SIZE);
-        return keyPairGenerator.generateKeyPair();
-    }
-
-    /**
-     * Generates a signed certificate using an on-demand keypair.
-     *
-     * @param dn the DN
-     * @return the certificate
-     * @throws IOException
-     * @throws NoSuchAlgorithmException
-     * @throws java.security.cert.CertificateException
-     * @throws java.security.NoSuchProviderException
-     * @throws java.security.SignatureException
-     * @throws java.security.InvalidKeyException
-     * @throws OperatorCreationException
-     */
-    private
-    static X509Certificate generateCertificate(String dn) throws IOException, 
NoSuchAlgorithmException, CertificateException, NoSuchProviderException, 
SignatureException, InvalidKeyException, OperatorCreationException {
-        KeyPair keyPair = generateKeyPair();
-        return generateCertificate(dn, keyPair);
-    }
-
-    /**
-     * Generates a signed certificate with a specific keypair.
-     *
-     * @param dn the DN
-     * @param keyPair the public key will be included in the certificate and 
the the private key is used to sign the certificate
-     * @return the certificate
-     * @throws IOException
-     * @throws NoSuchAlgorithmException
-     * @throws CertificateException
-     * @throws NoSuchProviderException
-     * @throws SignatureException
-     * @throws InvalidKeyException
-     * @throws OperatorCreationException
-     */
-    private
-    static X509Certificate generateCertificate(String dn, KeyPair keyPair) 
throws IOException, NoSuchAlgorithmException, CertificateException, 
NoSuchProviderException, SignatureException, InvalidKeyException, 
OperatorCreationException {
-        PrivateKey privateKey = keyPair.getPrivate();
-        ContentSigner sigGen = new 
JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER).build(privateKey);
-        SubjectPublicKeyInfo subPubKeyInfo = 
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
-        Date startDate = new Date(YESTERDAY);
-        Date endDate = new Date(ONE_YEAR_FROM_NOW);
-
-        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
-                new X500Name(dn),
-                BigInteger.valueOf(System.currentTimeMillis()),
-                startDate, endDate,
-                new X500Name(dn),
-                subPubKeyInfo);
-
-        // Set certificate extensions
-        // (1) digitalSignature extension
-        certBuilder.addExtension(X509Extension.keyUsage, true,
-                new KeyUsage(KeyUsage.digitalSignature | 
KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement));
-
-        // (2) extendedKeyUsage extension
-        Vector<KeyPurposeId> ekUsages = new Vector<>();
-        ekUsages.add(KeyPurposeId.id_kp_clientAuth);
-        ekUsages.add(KeyPurposeId.id_kp_serverAuth);
-        certBuilder.addExtension(X509Extension.extendedKeyUsage, false, new 
ExtendedKeyUsage(ekUsages));
-
-        // Sign the certificate
-        X509CertificateHolder certificateHolder = certBuilder.build(sigGen);
-        return new JcaX509CertificateConverter().setProvider(PROVIDER)
-                .getCertificate(certificateHolder);
-    }
-
-    /**
-     * Generates a certificate signed by the issuer key.
-     *
-     * @param dn the subject DN
-     * @param issuerDn the issuer DN
-     * @param issuerKey the issuer private key
-     * @return the certificate
-     * @throws IOException
-     * @throws NoSuchAlgorithmException
-     * @throws CertificateException
-     * @throws NoSuchProviderException
-     * @throws SignatureException
-     * @throws InvalidKeyException
-     * @throws OperatorCreationException
-     */
-    private
-    static X509Certificate generateIssuedCertificate(String dn, String 
issuerDn, PrivateKey issuerKey) throws IOException, NoSuchAlgorithmException, 
CertificateException, NoSuchProviderException, SignatureException, 
InvalidKeyException, OperatorCreationException {
-        KeyPair keyPair = generateKeyPair();
-        return generateIssuedCertificate(dn, keyPair.getPublic(), issuerDn, 
issuerKey);
-    }
-
-    /**
-     * Generates a certificate with a specific public key signed by the issuer 
key.
-     *
-     * @param dn the subject DN
-     * @param publicKey the subject public key
-     * @param issuerDn the issuer DN
-     * @param issuerKey the issuer private key
-     * @return the certificate
-     * @throws IOException
-     * @throws NoSuchAlgorithmException
-     * @throws CertificateException
-     * @throws NoSuchProviderException
-     * @throws SignatureException
-     * @throws InvalidKeyException
-     * @throws OperatorCreationException
-     */
-    private
-    static X509Certificate generateIssuedCertificate(String dn, PublicKey 
publicKey, String issuerDn, PrivateKey issuerKey) throws IOException, 
NoSuchAlgorithmException, CertificateException, NoSuchProviderException, 
SignatureException, InvalidKeyException, OperatorCreationException {
-        ContentSigner sigGen = new 
JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER).build(issuerKey);
-        SubjectPublicKeyInfo subPubKeyInfo = 
SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
-        Date startDate = new Date(YESTERDAY);
-        Date endDate = new Date(ONE_YEAR_FROM_NOW);
-
-        X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(
-                new X500Name(issuerDn),
-                BigInteger.valueOf(System.currentTimeMillis()),
-                startDate, endDate,
-                new X500Name(dn),
-                subPubKeyInfo);
-
-        X509CertificateHolder certificateHolder = v3CertGen.build(sigGen);
-        return new JcaX509CertificateConverter().setProvider(PROVIDER)
-                .getCertificate(certificateHolder);
-    }
-
-    private static X509Certificate[] generateCertificateChain(String dn = 
SUBJECT_DN, String issuerDn = ISSUER_DN) {
-        final KeyPair issuerKeyPair = generateKeyPair();
-        final PrivateKey issuerPrivateKey = issuerKeyPair.getPrivate();
-
-        final X509Certificate issuerCertificate = 
generateCertificate(issuerDn, issuerKeyPair);
-        final X509Certificate certificate = generateIssuedCertificate(dn, 
issuerDn, issuerPrivateKey);
-        [certificate, issuerCertificate] as X509Certificate[]
-    }
-
-    private static javax.security.cert.X509Certificate 
generateLegacyCertificate(X509Certificate x509Certificate) {
-        return 
javax.security.cert.X509Certificate.getInstance(x509Certificate.getEncoded())
-    }
-
-    private static Certificate generateAbstractCertificate(X509Certificate 
x509Certificate) {
-        return x509Certificate as Certificate
-    }
-
-    @Test
-    void testShouldConvertLegacyX509Certificate() {
-        // Arrange
-        final X509Certificate EXPECTED_NEW_CERTIFICATE = 
generateCertificate(SUBJECT_DN)
-        logger.info("Expected certificate: 
${EXPECTED_NEW_CERTIFICATE.class.canonicalName} 
${EXPECTED_NEW_CERTIFICATE.subjectDN.toString()} 
(${EXPECTED_NEW_CERTIFICATE.getSerialNumber()})")
-
-        // Form the legacy certificate
-        final javax.security.cert.X509Certificate LEGACY_CERTIFICATE = 
generateLegacyCertificate(EXPECTED_NEW_CERTIFICATE)
-        logger.info("Legacy certificate: 
${LEGACY_CERTIFICATE.class.canonicalName} 
${LEGACY_CERTIFICATE.subjectDN.toString()} 
(${LEGACY_CERTIFICATE.getSerialNumber()})")
-
-        // Act
-        X509Certificate convertedCertificate = 
CertificateUtils.convertLegacyX509Certificate(LEGACY_CERTIFICATE)
-        logger.info("Converted certificate: 
${convertedCertificate.class.canonicalName} 
${convertedCertificate.subjectDN.toString()} 
(${convertedCertificate.getSerialNumber()})")
-
-        // Assert
-        assert convertedCertificate instanceof X509Certificate
-        assert convertedCertificate == EXPECTED_NEW_CERTIFICATE
-    }
-
-    @Test
-    void testShouldConvertAbstractX509Certificate() {
-        // Arrange
-        final X509Certificate EXPECTED_NEW_CERTIFICATE = 
generateCertificate(SUBJECT_DN)
-        logger.info("Expected certificate: 
${EXPECTED_NEW_CERTIFICATE.class.canonicalName} 
${EXPECTED_NEW_CERTIFICATE.subjectDN.toString()} 
(${EXPECTED_NEW_CERTIFICATE.getSerialNumber()})")
-
-        // Form the abstract certificate
-        final Certificate ABSTRACT_CERTIFICATE = 
generateAbstractCertificate(EXPECTED_NEW_CERTIFICATE)
-        logger.info("Abstract certificate: 
${ABSTRACT_CERTIFICATE.class.canonicalName} (?)")
-
-        // Act
-        X509Certificate convertedCertificate = 
CertificateUtils.convertAbstractX509Certificate(ABSTRACT_CERTIFICATE)
-        logger.info("Converted certificate: 
${convertedCertificate.class.canonicalName} 
${convertedCertificate.subjectDN.toString()} 
(${convertedCertificate.getSerialNumber()})")
-
-        // Assert
-        assert convertedCertificate instanceof X509Certificate
-        assert convertedCertificate == EXPECTED_NEW_CERTIFICATE
-    }
-}

Reply via email to