diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml index 4dca67b92d..9e2ea1daeb 100644 --- a/nifi-assembly/pom.xml +++ b/nifi-assembly/pom.xml @@ -368,147 +368,6 @@ language governing permissions and limitations under the License. --> - - - 512 - 128 - - - ${project.version} - true - 10 sec - 500 ms - 30 sec - 10 millis - - ./conf/flow.xml.gz - true - ./conf/archive/ - 30 days - 500 MB - ./conf/login-identity-providers.xml - ./conf/authorizers.xml - ./conf/templates - ./database_repository - - ./conf/state-management.xml - false - ./conf/zookeeper.properties - local-provider - zk-provider - - org.apache.nifi.controller.repository.WriteAheadFlowFileRepository - ./flowfile_repository - 256 - 2 mins - false - org.apache.nifi.controller.FileSystemSwapManager - 20000 - 5 sec - 1 - 5 sec - 4 - - org.apache.nifi.controller.repository.FileSystemRepository - 10 MB - 100 - ./content_repository - 12 hours - 50% - true - false - /nifi-content-viewer/ - - - - 30 sec - ./lib - ./work/nar/ - ./work/docs/components - - PBEWITHMD5AND256BITAES-CBC-OPENSSL - BC - ;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE - - 9990 - - - org.apache.nifi.provenance.PersistentProvenanceRepository - ./provenance_repository - 24 hours - 1 GB - 30 secs - 100 MB - 2 - 1 - true - EventType, FlowFileUUID, Filename, ProcessorID, Relationship - - 500 MB - false - 16 - 65536 - - - 100000 - - - org.apache.nifi.controller.status.history.VolatileComponentStatusRepository - 1440 - 1 min - - - ./lib - - 8080 - - - ./work/jetty - 200 - - - - - - - - - - - file-provider - - - - - - - 5 sec - false - - - false - - - 10 - 25 - 5 sec - 5 sec - - - 15 secs - - - - 3 secs - 3 secs - /nifi - - - - - - 12 hours - rpm diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml index 1381e82d9d..fddeeca17a 100644 --- a/nifi-commons/nifi-security-utils/pom.xml +++ b/nifi-commons/nifi-security-utils/pom.xml @@ -33,12 +33,10 @@ org.bouncycastle bcprov-jdk15on - test org.bouncycastle bcpkix-jdk15on - test 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 3579493dcf..20497180d1 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 @@ -16,12 +16,41 @@ */ package org.apache.nifi.security.util; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.math.BigInteger; import java.net.Socket; import java.net.URL; +import java.security.KeyPair; import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -29,15 +58,9 @@ import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.TimeUnit; public final class CertificateUtils { @@ -326,6 +349,100 @@ public final class CertificateUtils { } } + /** + * Generates a self-signed {@link X509Certificate} suitable for use as a Certificate Authority. + * + * @param keyPair the {@link KeyPair} to generate the {@link X509Certificate} for + * @param dn the distinguished name to user for the {@link X509Certificate} + * @param signingAlgorithm the signing algorithm to use for the {@link X509Certificate} + * @param certificateDurationDays the duration in days for which the {@link X509Certificate} should be valid + * @return a self-signed {@link X509Certificate} suitable for use as a Certificate Authority + * @throws CertificateException if there is an generating the new certificate + */ + public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays) + throws CertificateException { + try { + ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(certificateDurationDays)); + + 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(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment + | KeyUsage.keyAgreement | KeyUsage.nonRepudiation | KeyUsage.cRLSign | KeyUsage.keyCertSign)); + + certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic())); + + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic())); + + // (2) extendedKeyUsage extension + certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + + // Sign the certificate + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + /** + * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * + * @param dn the distinguished name to use + * @param publicKey the public key to issue the certificate to + * @param issuer the issuer's certificate + * @param issuerKeyPair the issuer's keypair + * @param signingAlgorithm the signing algorithm to use + * @param days the number of days it should be valid for + * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * @throws CertificateException if there is an error issueing the certificate + */ + public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) + throws CertificateException { + try { + ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(days)); + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + new X500Name(issuer.getSubjectDN().getName()), + BigInteger.valueOf(System.currentTimeMillis()), + startDate, endDate, + new X500Name(dn), + subPubKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey)); + + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerKeyPair.getPublic())); + // Set certificate extensions + // (1) digitalSignature extension + certBuilder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation)); + + certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); + + // (2) extendedKeyUsage extension + certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + /** * 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. * 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 index 22fc62847b..7f93f51c94 100644 --- 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 @@ -16,19 +16,8 @@ */ 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 @@ -46,13 +35,14 @@ 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 +import java.util.concurrent.TimeUnit + +import static org.junit.Assert.assertEquals @RunWith(JUnit4.class) class CertificateUtilsTest extends GroovyTestCase { @@ -116,53 +106,7 @@ class CertificateUtilsTest extends GroovyTestCase { 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 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); + return CertificateUtils.generateSelfSignedX509Certificate(keyPair, dn, SIGNATURE_ALGORITHM, 365); } /** @@ -181,52 +125,16 @@ class CertificateUtilsTest extends GroovyTestCase { * @throws OperatorCreationException */ private - static X509Certificate generateIssuedCertificate(String dn, String issuerDn, PrivateKey issuerKey) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException { + static X509Certificate generateIssuedCertificate(String dn, X509Certificate issuer, KeyPair 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); + return CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKey, SIGNATURE_ALGORITHM, 365); } 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); + final X509Certificate issuerCertificate = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, issuerDn, SIGNATURE_ALGORITHM, 365); + final X509Certificate certificate = generateIssuedCertificate(dn, issuerCertificate, issuerKeyPair); [certificate, issuerCertificate] as X509Certificate[] } @@ -238,6 +146,10 @@ class CertificateUtilsTest extends GroovyTestCase { return x509Certificate as Certificate } + private static Date inFuture(int days) { + return new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(days)); + } + @Test void testShouldConvertLegacyX509Certificate() { // Arrange @@ -513,4 +425,58 @@ class CertificateUtilsTest extends GroovyTestCase { assert !dn1MatchesDn2Reversed assert !dn1MatchesEmpty } + + + + @Test + public void testShouldGenerateSelfSignedCert() throws Exception { + String dn = "CN=testDN,O=testOrg"; + + int days = 365; + X509Certificate x509Certificate = CertificateUtils.generateSelfSignedX509Certificate(generateKeyPair(), dn, SIGNATURE_ALGORITHM, days); + + Date notAfter = x509Certificate.getNotAfter(); + assertTrue(notAfter.after(inFuture(days - 1))); + assertTrue(notAfter.before(inFuture(days + 1))); + + Date notBefore = x509Certificate.getNotBefore(); + assertTrue(notBefore.after(inFuture(-1))); + assertTrue(notBefore.before(inFuture(1))); + + assertEquals(dn, x509Certificate.getIssuerDN().getName()); + assertEquals(SIGNATURE_ALGORITHM.toUpperCase(), x509Certificate.getSigAlgName().toUpperCase()); + assertEquals("RSA", x509Certificate.getPublicKey().getAlgorithm()); + + x509Certificate.checkValidity(); + } + + + + @Test + public void testIssueCert() throws Exception { + int days = 365; + KeyPair issuerKeyPair = generateKeyPair(); + X509Certificate issuer = CertificateUtils.generateSelfSignedX509Certificate(issuerKeyPair, "CN=testCa,O=testOrg", SIGNATURE_ALGORITHM, days); + + String dn = "CN=testIssued,O=testOrg"; + + KeyPair keyPair = generateKeyPair(); + X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKeyPair, SIGNATURE_ALGORITHM, days); + assertEquals(dn, x509Certificate.getSubjectDN().toString()); + assertEquals(issuer.getSubjectDN().toString(), x509Certificate.getIssuerDN().toString()); + assertEquals(keyPair.getPublic(), x509Certificate.getPublicKey()); + + Date notAfter = x509Certificate.getNotAfter(); + assertTrue(notAfter.after(inFuture(days - 1))); + assertTrue(notAfter.before(inFuture(days + 1))); + + Date notBefore = x509Certificate.getNotBefore(); + assertTrue(notBefore.after(inFuture(-1))); + assertTrue(notBefore.before(inFuture(1))); + + assertEquals(SIGNATURE_ALGORITHM.toUpperCase(), x509Certificate.getSigAlgName().toUpperCase()); + assertEquals("RSA", x509Certificate.getPublicKey().getAlgorithm()); + + x509Certificate.verify(issuerKeyPair.getPublic()); + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml index a738069714..fff546b87e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml @@ -23,6 +23,147 @@ nifi-resources pom holds common resources used to build installers + + + 512 + 128 + + + ${project.version} + true + 10 sec + 500 ms + 30 sec + 10 millis + + ./conf/flow.xml.gz + true + ./conf/archive/ + 30 days + 500 MB + ./conf/login-identity-providers.xml + ./conf/authorizers.xml + ./conf/templates + ./database_repository + + ./conf/state-management.xml + false + ./conf/zookeeper.properties + local-provider + zk-provider + + org.apache.nifi.controller.repository.WriteAheadFlowFileRepository + ./flowfile_repository + 256 + 2 mins + false + org.apache.nifi.controller.FileSystemSwapManager + 20000 + 5 sec + 1 + 5 sec + 4 + + org.apache.nifi.controller.repository.FileSystemRepository + 10 MB + 100 + ./content_repository + 12 hours + 50% + true + false + /nifi-content-viewer/ + + + + 30 sec + ./lib + ./work/nar/ + ./work/docs/components + + PBEWITHMD5AND256BITAES-CBC-OPENSSL + BC + ;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + + 9990 + + + org.apache.nifi.provenance.PersistentProvenanceRepository + ./provenance_repository + 24 hours + 1 GB + 30 secs + 100 MB + 2 + 1 + true + EventType, FlowFileUUID, Filename, ProcessorID, Relationship + + 500 MB + false + 16 + 65536 + + + 100000 + + + org.apache.nifi.controller.status.history.VolatileComponentStatusRepository + 1440 + 1 min + + + ./lib + + 8080 + + + ./work/jetty + 200 + + + + + + + + + + + file-provider + + + + + + + 5 sec + false + + + false + + + 10 + 25 + 5 sec + 5 sec + + + 15 secs + + + + 3 secs + 3 secs + /nifi + + + + + + 12 hours + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/assembly/dependencies.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/assembly/dependencies.xml index 2cc37cb2f4..9e7750a57e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/assembly/dependencies.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/assembly/dependencies.xml @@ -23,6 +23,7 @@ src/main/resources / + true src/main/resources/bin diff --git a/nifi-toolkit/nifi-toolkit-assembly/LICENSE b/nifi-toolkit/nifi-toolkit-assembly/LICENSE new file mode 100644 index 0000000000..412bea37c6 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/LICENSE @@ -0,0 +1,256 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +APACHE NIFI TOOLKIT SUBCOMPONENTS: + +The Apache NiFi Toolkit project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + + The binary distribution of this product bundles 'Bouncy Castle JDK 1.5 Provider' + under an MIT style license. + + Copyright (c) 2000 - 2016 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + The binary distribution of this product bundles 'Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs' + under an MIT style license. + + Copyright (c) 2000 - 2016 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + diff --git a/nifi-toolkit/nifi-toolkit-assembly/NOTICE b/nifi-toolkit/nifi-toolkit-assembly/NOTICE new file mode 100644 index 0000000000..7cd5ffc072 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/NOTICE @@ -0,0 +1,94 @@ +Apache NiFi Toolkit +Copyright 2014-2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +=========================================== +Apache Software License v2 +=========================================== + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Apache NiFi + The following NOTICE information applies: + Apache NiFi + Copyright 2014-2016 The Apache Software Foundation + + This product includes software developed at + The Apache Software Foundation (http://www.apache.org/). + + This product includes the following work from the Apache Hadoop project: + + BoundedByteArrayOutputStream.java adapted to SoftLimitBoundedByteArrayOutputStream.java + + (ASLv2) Apache Commons CLI + The following NOTICE information applies: + Apache Commons IO + Copyright 2001-2015 The Apache Software Foundation + + (ASLv2) Apache Commons Codec + The following NOTICE information applies: + Apache Commons Codec + Copyright 2002-2014 The Apache Software Foundation + + src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java + contains test data from http://aspell.net/test/orig/batch0.tab. + Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org) + + =============================================================================== + + The content of package org.apache.commons.codec.language.bm has been translated + from the original php source code available at http://stevemorse.org/phoneticinfo.htm + with permission from the original authors. + Original source copyright: + Copyright (c) 2008 Alexander Beider & Stephen P. Morse. + + (ASLv2) Apache Commons IO + The following NOTICE information applies: + Apache Commons IO + Copyright 2002-2012 The Apache Software Foundation + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2015 The Apache Software Foundation + + (ASLv2) Apache HttpComponents + The following NOTICE information applies: + Apache HttpClient + Copyright 1999-2015 The Apache Software Foundation + + Apache HttpCore + Copyright 2005-2015 The Apache Software Foundation + + This project contains annotations derived from JCIP-ANNOTATIONS + Copyright (c) 2005 Brian Goetz and Tim Peierls. See http://www.jcip.net + + (ASLv2) Jackson JSON processor + The following NOTICE information applies: + # Jackson JSON processor + + Jackson is a high-performance, Free/Open Source JSON processing library. + It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has + been in development since 2007. + It is currently developed by a community of developers, as well as supported + commercially by FasterXML.com. + + ## Licensing + + Jackson core and extension components may licensed under different licenses. + To find the details that apply to this artifact see the accompanying LICENSE file. + For more information, including possible other licensing options, contact + FasterXML.com (http://fasterxml.com). + + ## Credits + + A list of contributors may be found from CREDITS file, which is included + in some artifacts (usually source distributions); but is always available + from the source code management (SCM) system project uses. + + (ASLv2) Jetty + The following NOTICE information applies: + Jetty Web Container + Copyright 1995-2015 Mort Bay Consulting Pty Ltd. \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-assembly/pom.xml b/nifi-toolkit/nifi-toolkit-assembly/pom.xml new file mode 100644 index 0000000000..edd5aa1d71 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-toolkit + 1.0.0-SNAPSHOT + + nifi-toolkit-assembly + pom + This is the assembly Apache NiFi Toolkit + + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/conf/config-client.json + src/main/resources/conf/config-server.json + + + + + maven-assembly-plugin + + nifi-toolkit-${project.version} + + + + make shared resource + + single + + package + + + 0755 + 0755 + 0644 + + + src/main/assembly/dependencies.xml + + posix + + + + + + + + + org.apache.nifi + nifi-toolkit-tls + + + org.slf4j + slf4j-api + compile + 1.7.12 + + + org.eclipse.jetty + jetty-server + compile + + + javax.servlet + javax.servlet-api + compile + + + commons-io + commons-io + 2.5 + + + diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml b/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml new file mode 100644 index 0000000000..626287a122 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/assembly/dependencies.xml @@ -0,0 +1,75 @@ + + + + bin + + dir + zip + tar.gz + + true + nifi-toolkit-${project.version} + + + + + lib + false + 0770 + 0660 + + + + + ${project.basedir}/src/main/resources/bin + bin/ + 0700 + + + ${project.basedir}/src/main/resources/conf + conf/ + 0600 + + + ${project.basedir}/src/main/resources/classpath + classpath/ + 0600 + + + ${project.build.directory}/nifi-resources/conf + lib/ + 0600 + + + + + ./LICENSE + ./ + LICENSE + 0644 + true + + + ./NOTICE + ./ + NOTICE + 0644 + true + + + diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat new file mode 100644 index 0000000000..b875e1ab47 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.bat @@ -0,0 +1,40 @@ +@echo off +rem +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. +rem + +rem Use JAVA_HOME if it's set; otherwise, just use java + +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +set JAVA_EXE=%JAVA_HOME%\bin\java.exe +goto startConfig + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo Instead the PATH will be used to find the java executable. +echo. +set JAVA_EXE=java +goto startConfig + +:startConfig +set LIB_DIR=%~dp0..\classpath;%~dp0..\lib + +SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.toolkit.tls.service.server.TlsCertificateAuthorityService + +cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %* + +popd diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.sh b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.sh new file mode 100644 index 0000000000..c34c4201f0 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/tls-toolkit.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# +# 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. +# +# + +# Script structure inspired from Apache Karaf and other Apache projects with similar startup approaches + +SCRIPT_DIR=$(dirname "$0") +SCRIPT_NAME=$(basename "$0") +NIFI_TOOLKIT_HOME=$(cd "${SCRIPT_DIR}" && cd .. && pwd) +PROGNAME=$(basename "$0") + + +warn() { + echo "${PROGNAME}: $*" +} + +die() { + warn "$*" + exit 1 +} + +detectOS() { + # OS specific support (must be 'true' or 'false'). + cygwin=false; + aix=false; + os400=false; + darwin=false; + case "$(uname)" in + CYGWIN*) + cygwin=true + ;; + AIX*) + aix=true + ;; + OS400*) + os400=true + ;; + Darwin) + darwin=true + ;; + esac + # For AIX, set an environment variable + if ${aix}; then + export LDR_CNTRL=MAXDATA=0xB0000000@DSA + echo ${LDR_CNTRL} + fi +} + +locateJava() { + # Setup the Java Virtual Machine + if $cygwin ; then + [ -n "${JAVA}" ] && JAVA=$(cygpath --unix "${JAVA}") + [ -n "${JAVA_HOME}" ] && JAVA_HOME=$(cygpath --unix "${JAVA_HOME}") + fi + + if [ "x${JAVA}" = "x" ] && [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi + if [ "x${JAVA}" = "x" ]; then + if [ "x${JAVA_HOME}" != "x" ]; then + if [ ! -d "${JAVA_HOME}" ]; then + die "JAVA_HOME is not valid: ${JAVA_HOME}" + fi + JAVA="${JAVA_HOME}/bin/java" + else + warn "JAVA_HOME not set; results may vary" + JAVA=$(type java) + JAVA=$(expr "${JAVA}" : '.* \(/.*\)$') + if [ "x${JAVA}" = "x" ]; then + die "java command not found" + fi + fi + fi +} + +init() { + # Determine if there is special OS handling we must perform + detectOS + + # Locate the Java VM to execute + locateJava "$1" +} + +run() { + LIBS="${NIFI_TOOLKIT_HOME}/lib/*" + + sudo_cmd_prefix="" + if $cygwin; then + NIFI_TOOLKIT_HOME=$(cygpath --path --windows "${NIFI_TOOLKIT_HOME}") + CLASSPATH="$NIFI_TOOLKIT_HOME/classpath";$(cygpath --path --windows "${LIBS}") + else + CLASSPATH="$NIFI_TOOLKIT_HOME/classpath:${LIBS}" + fi + + export JAVA_HOME="$JAVA_HOME" + export NIFI_TOOLKIT_HOME="$NIFI_TOOLKIT_HOME" + + umask 0077 + "${JAVA}" -cp "${CLASSPATH}" -Xms12m -Xmx24m org.apache.nifi.toolkit.tls.TlsToolkitMain "$@" + return $? +} + + +init "$1" +run "$@" \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/log4j.properties b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/log4j.properties new file mode 100644 index 0000000000..fc2aaf12c4 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/log4j.properties @@ -0,0 +1,22 @@ +# +# 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. +# + +log4j.rootLogger=INFO,console + +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/conf/config-client.json b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/conf/config-client.json new file mode 100644 index 0000000000..e572bc1407 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/conf/config-client.json @@ -0,0 +1,14 @@ +{ + "keyStore" : "clientKeyStore", + "keyStoreType" : "jks", + "token" : "myTestTokenUseSomethingStronger", + "dn" : "CN=otherHostname,OU=NIFI", + "port" : 8443, + "caHostname" : "localhost", + "trustStore" : "clientTrustStore", + "trustStoreType" : "jks", + "days" : 1095, + "keySize" : 2048, + "keyPairAlgorithm" : "RSA", + "signingAlgorithm" : "SHA256WITHRSA" +} diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/conf/config-server.json b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/conf/config-server.json new file mode 100644 index 0000000000..fae89ed5ca --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/conf/config-server.json @@ -0,0 +1,11 @@ +{ + "keyStore" : "serverKeyStore", + "keyStoreType" : "jks", + "token" : "myTestTokenUseSomethingStronger", + "caHostname" : "localhost", + "port" : 8443, + "days" : 1095, + "keySize" : 2048, + "keyPairAlgorithm" : "RSA", + "signingAlgorithm" : "SHA256WITHRSA" +} diff --git a/nifi-toolkit/nifi-toolkit-tls/pom.xml b/nifi-toolkit/nifi-toolkit-tls/pom.xml new file mode 100644 index 0000000000..2c05755561 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-toolkit + 1.0.0-SNAPSHOT + + nifi-toolkit-tls + Tooling to make tls configuration easier + + + org.apache.nifi + nifi-properties + + + org.apache.nifi + nifi-security-utils + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + 1.7.12 + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + commons-cli + commons-cli + 1.3.1 + + + commons-io + commons-io + 2.5 + + + org.eclipse.jetty + jetty-server + + + com.fasterxml.jackson.core + jackson-databind + + + org.apache.httpcomponents + httpclient + 4.5.2 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack + process-resources + + unpack + + + + + org.apache.nifi + nifi-resources + zip + resources + true + ${project.build.directory}/classes + **/nifi.properties + + + + + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/rootCert.crt + src/test/resources/rootCert.key + + + + + + diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java new file mode 100644 index 0000000000..7d445a5ee6 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/TlsToolkitMain.java @@ -0,0 +1,97 @@ +/* + * 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.toolkit.tls; + +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.service.client.TlsCertificateAuthorityClientCommandLine; +import org.apache.nifi.toolkit.tls.service.server.TlsCertificateAuthorityServiceCommandLine; +import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Command line entry point + */ +public class TlsToolkitMain { + public static final String DESCRIPTION = "DESCRIPTION"; + private final Map> mainMap; + + public TlsToolkitMain() { + mainMap = new LinkedHashMap<>(); + mainMap.put("standalone", TlsToolkitStandaloneCommandLine.class); + mainMap.put("server", TlsCertificateAuthorityServiceCommandLine.class); + mainMap.put("client", TlsCertificateAuthorityClientCommandLine.class); + } + + public static void main(String[] args) { + new TlsToolkitMain().doMain(args); + } + + private void printUsageAndExit(String message, ExitCode exitCode) { + System.out.println(message); + System.out.println(); + System.out.println("Usage: tls-toolkit service [-h] [args]"); + System.out.println(); + System.out.println("Services:"); + mainMap.forEach((s, aClass) -> System.out.println(" " + s + ": " + getDescription(aClass))); + System.out.println(); + System.exit(exitCode.ordinal()); + } + + private String getDescription(Class clazz) { + try { + Field declaredField = clazz.getDeclaredField(DESCRIPTION); + return String.valueOf(declaredField.get(null)); + } catch (Exception e) { + return "Unable to get description. (" + e.getMessage() + ")"; + } + } + + public void doMain(String[] args) { + if (args.length < 1) { + printUsageAndExit("Expected at least a service argument.", ExitCode.INVALID_ARGS); + } + + String service = args[0].toLowerCase(); + Class mainClass = mainMap.get(service); + if (mainClass == null) { + printUsageAndExit("Unknown service: " + service, ExitCode.INVALID_ARGS); + } + + Method main; + try { + main = mainClass.getDeclaredMethod("main", String[].class); + } catch (NoSuchMethodException e) { + printUsageAndExit("Service " + service + " is missing main method.", ExitCode.SERVICE_ERROR); + return; + } + + try { + main.invoke(null, (Object) args); + } catch (IllegalAccessException e) { + printUsageAndExit("Service " + service + " has invalid main method.", ExitCode.SERVICE_ERROR); + } catch (InvocationTargetException e) { + printUsageAndExit("Service " + service + " error: " + e.getCause().getMessage(), ExitCode.SERVICE_ERROR); + } + return; + } +} \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/BaseCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/BaseCommandLine.java new file mode 100644 index 0000000000..763f71535d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/BaseCommandLine.java @@ -0,0 +1,178 @@ +/* + * 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.toolkit.tls.commandLine; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.nifi.toolkit.tls.TlsToolkitMain; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; + +public abstract class BaseCommandLine { + public static final String HELP_ARG = "help"; + public static final String JAVA_HOME = "JAVA_HOME"; + public static final String NIFI_TOOLKIT_HOME = "NIFI_TOOLKIT_HOME"; + public static final String FOOTER = new StringBuilder(System.lineSeparator()).append("Java home: ") + .append(System.getenv(JAVA_HOME)).append(System.lineSeparator()).append("NiFi Toolkit home: ").append(System.getenv(NIFI_TOOLKIT_HOME)).toString(); + + public static final String KEY_SIZE_ARG = "keySize"; + public static final String KEY_ALGORITHM_ARG = "keyAlgorithm"; + public static final String CERTIFICATE_AUTHORITY_HOSTNAME_ARG = "certificateAuthorityHostname"; + public static final String DAYS_ARG = "days"; + public static final String KEY_STORE_TYPE_ARG = "keyStoreType"; + public static final String SIGNING_ALGORITHM_ARG = "signingAlgorithm"; + public static final String DN_ARG = "dn"; + public static final String DIFFERENT_KEY_AND_KEYSTORE_PASSWORDS_ARG = "differentKeyAndKeystorePasswords"; + + public static final String KEYSTORE = "keystore."; + public static final String TRUSTSTORE = "truststore."; + + private final Options options; + private final String header; + private int keySize; + private String keyAlgorithm; + private String certificateAuthorityHostname; + private String keyStoreType; + private int days; + private String signingAlgorithm; + private boolean differentPasswordForKeyAndKeystore; + + public BaseCommandLine(String header) { + this.header = System.lineSeparator() + header + System.lineSeparator() + System.lineSeparator(); + this.options = new Options(); + if (shouldAddDaysArg()) { + addOptionWithArg("d", DAYS_ARG, "Number of days issued certificate should be valid for.", TlsConfig.DEFAULT_DAYS); + } + addOptionWithArg("T", KEY_STORE_TYPE_ARG, "The type of keyStores to generate.", getKeyStoreTypeDefault()); + options.addOption("h", HELP_ARG, false, "Print help and exit."); + addOptionWithArg("c", CERTIFICATE_AUTHORITY_HOSTNAME_ARG, "Hostname of NiFi Certificate Authority", TlsConfig.DEFAULT_HOSTNAME); + addOptionWithArg("a", KEY_ALGORITHM_ARG, "Algorithm to use for generated keys.", TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + addOptionWithArg("k", KEY_SIZE_ARG, "Number of bits for generated keys.", TlsConfig.DEFAULT_KEY_SIZE); + if (shouldAddSigningAlgorithmArg()) { + addOptionWithArg("s", SIGNING_ALGORITHM_ARG, "Algorithm to use for signing certificates.", TlsConfig.DEFAULT_SIGNING_ALGORITHM); + } + addOptionNoArg("g", DIFFERENT_KEY_AND_KEYSTORE_PASSWORDS_ARG, "Use different generated password for the key and the keyStore."); + } + + protected String getKeyStoreTypeDefault() { + return TlsConfig.DEFAULT_KEY_STORE_TYPE; + } + + protected boolean shouldAddSigningAlgorithmArg() { + return true; + } + + protected boolean shouldAddDaysArg() { + return true; + } + + protected void addOptionWithArg(String arg, String longArg, String description) { + addOptionWithArg(arg, longArg, description, null); + } + + protected void addOptionNoArg(String arg, String longArg, String description) { + options.addOption(arg, longArg, false, description); + } + + protected void addOptionWithArg(String arg, String longArg, String description, Object defaultVal) { + String fullDescription = description; + if (defaultVal != null) { + fullDescription += " (default: " + defaultVal + ")"; + } + options.addOption(arg, longArg, true, fullDescription); + } + + public void printUsage(String errorMessage) { + if (errorMessage != null) { + System.out.println(errorMessage); + System.out.println(); + } + HelpFormatter helpFormatter = new HelpFormatter(); + helpFormatter.setWidth(160); + helpFormatter.printHelp(TlsToolkitMain.class.getCanonicalName(), header, options, FOOTER, true); + } + + protected T printUsageAndThrow(String errorMessage, ExitCode exitCode) throws CommandLineParseException { + printUsage(errorMessage); + throw new CommandLineParseException(errorMessage, exitCode.ordinal()); + } + + protected int getIntValue(CommandLine commandLine, String arg, int defaultVal) throws CommandLineParseException { + try { + return Integer.parseInt(commandLine.getOptionValue(arg, Integer.toString(defaultVal))); + } catch (NumberFormatException e) { + return printUsageAndThrow("Expected integer for " + arg + " argument. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_INT_ARG); + } + } + + public int getKeySize() { + return keySize; + } + + public String getKeyAlgorithm() { + return keyAlgorithm; + } + + public String getCertificateAuthorityHostname() { + return certificateAuthorityHostname; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public int getDays() { + return days; + } + + public String getSigningAlgorithm() { + return signingAlgorithm; + } + + public boolean differentPasswordForKeyAndKeystore() { + return differentPasswordForKeyAndKeystore; + } + + protected CommandLine doParse(String[] args) throws CommandLineParseException { + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine; + try { + commandLine = parser.parse(options, args); + if (commandLine.hasOption(HELP_ARG)) { + return printUsageAndThrow(null, ExitCode.HELP); + } + certificateAuthorityHostname = commandLine.getOptionValue(CERTIFICATE_AUTHORITY_HOSTNAME_ARG, TlsConfig.DEFAULT_HOSTNAME); + days = getIntValue(commandLine, DAYS_ARG, TlsConfig.DEFAULT_DAYS); + keySize = getIntValue(commandLine, KEY_SIZE_ARG, TlsConfig.DEFAULT_KEY_SIZE); + keyAlgorithm = commandLine.getOptionValue(KEY_ALGORITHM_ARG, TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + keyStoreType = commandLine.getOptionValue(KEY_STORE_TYPE_ARG, getKeyStoreTypeDefault()); + signingAlgorithm = commandLine.getOptionValue(SIGNING_ALGORITHM_ARG, TlsConfig.DEFAULT_SIGNING_ALGORITHM); + differentPasswordForKeyAndKeystore = commandLine.hasOption(DIFFERENT_KEY_AND_KEYSTORE_PASSWORDS_ARG); + } catch (ParseException e) { + return printUsageAndThrow("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE); + } + return commandLine; + } + + public void parse(String... args) throws CommandLineParseException { + doParse(args); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/CommandLineParseException.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/CommandLineParseException.java new file mode 100644 index 0000000000..d09cc08d33 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/CommandLineParseException.java @@ -0,0 +1,31 @@ +/* + * 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.toolkit.tls.commandLine; + +public class CommandLineParseException extends Exception { + private final int exitCode; + + public CommandLineParseException(String message, int exitCode) { + super(message); + this.exitCode = exitCode; + } + + public int getExitCode() { + return exitCode; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java new file mode 100644 index 0000000000..b99b1af531 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java @@ -0,0 +1,32 @@ +/* + * 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.toolkit.tls.commandLine; + +public enum ExitCode { + SUCCESS, + HELP, + INVALID_ARGS, + SERVICE_ERROR, + ERROR_PARSING_COMMAND_LINE, + ERROR_GENERATING_CONFIG, + ERROR_SAME_KEY_AND_KEY_PASSWORD, + ERROR_INCORRECT_NUMBER_OF_PASSWORDS, + ERROR_PARSING_INT_ARG, + ERROR_TOKEN_ARG_EMPTY, + ERROR_READING_NIFI_PROPERTIES +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsClientConfig.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsClientConfig.java new file mode 100644 index 0000000000..78ca9d6e0c --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsClientConfig.java @@ -0,0 +1,80 @@ +/* + * 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.toolkit.tls.configuration; + +import org.apache.nifi.toolkit.tls.service.client.TlsCertificateSigningRequestPerformer; +import org.apache.nifi.util.StringUtils; + +import java.security.NoSuchAlgorithmException; + +public class TlsClientConfig extends TlsConfig { + private String trustStore; + private String trustStorePassword; + private String trustStoreType = DEFAULT_KEY_STORE_TYPE; + + public TlsClientConfig() { + } + + public TlsClientConfig(TlsConfig tlsConfig) { + setToken(tlsConfig.getToken()); + setCaHostname(tlsConfig.getCaHostname()); + setPort(tlsConfig.getPort()); + setKeyStoreType(tlsConfig.getKeyStoreType()); + setTrustStoreType(tlsConfig.getKeyStoreType()); + setKeyPairAlgorithm(tlsConfig.getKeyPairAlgorithm()); + setKeySize(tlsConfig.getKeySize()); + setSigningAlgorithm(tlsConfig.getSigningAlgorithm()); + } + + + public String getTrustStoreType() { + return trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public String getTrustStore() { + return trustStore; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public TlsCertificateSigningRequestPerformer createCertificateSigningRequestPerformer() throws NoSuchAlgorithmException { + return new TlsCertificateSigningRequestPerformer(this); + } + + @Override + public void initDefaults() { + super.initDefaults(); + if (StringUtils.isEmpty(trustStoreType)) { + trustStoreType = DEFAULT_KEY_STORE_TYPE; + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java new file mode 100644 index 0000000000..819b41b8d8 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/TlsConfig.java @@ -0,0 +1,171 @@ +/* + * 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.toolkit.tls.configuration; + +import org.apache.nifi.util.StringUtils; + +public class TlsConfig { + public static final String DEFAULT_HOSTNAME = "localhost"; + public static final String DEFAULT_KEY_STORE_TYPE = "jks"; + public static final int DEFAULT_PORT = 8443; + public static final int DEFAULT_DAYS = 3 * 365; + public static final int DEFAULT_KEY_SIZE = 2048; + public static final String DEFAULT_KEY_PAIR_ALGORITHM = "RSA"; + public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"; + + private int days = DEFAULT_DAYS; + private int keySize = DEFAULT_KEY_SIZE; + private String keyPairAlgorithm = DEFAULT_KEY_PAIR_ALGORITHM; + private String signingAlgorithm = DEFAULT_SIGNING_ALGORITHM; + + private String dn; + private String keyStore; + private String keyStoreType = DEFAULT_KEY_STORE_TYPE; + private String keyStorePassword; + private String keyPassword; + private String token; + private String caHostname = DEFAULT_HOSTNAME; + private int port = DEFAULT_PORT; + + public static String calcDefaultDn(String hostname) { + return "CN=" + hostname + ",OU=NIFI"; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getKeyStore() { + return keyStore; + } + + public void setKeyStore(String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyPassword() { + return keyPassword; + } + + public void setKeyPassword(String keyPassword) { + this.keyPassword = keyPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getCaHostname() { + return caHostname; + } + + public void setCaHostname(String caHostname) { + this.caHostname = caHostname; + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public int getDays() { + return days; + } + + public void setDays(int days) { + this.days = days; + } + + public int getKeySize() { + return keySize; + } + + public void setKeySize(int keySize) { + this.keySize = keySize; + } + + public String getKeyPairAlgorithm() { + return keyPairAlgorithm; + } + + public void setKeyPairAlgorithm(String keyPairAlgorithm) { + this.keyPairAlgorithm = keyPairAlgorithm; + } + + public String getSigningAlgorithm() { + return signingAlgorithm; + } + + public void setSigningAlgorithm(String signingAlgorithm) { + this.signingAlgorithm = signingAlgorithm; + } + + public void initDefaults() { + if (days == 0) { + days = DEFAULT_DAYS; + } + if (keySize == 0) { + keySize = DEFAULT_KEY_SIZE; + } + if (StringUtils.isEmpty(keyPairAlgorithm)) { + keyPairAlgorithm = DEFAULT_KEY_PAIR_ALGORITHM; + } + if (StringUtils.isEmpty(signingAlgorithm)) { + signingAlgorithm = DEFAULT_SIGNING_ALGORITHM; + } + if (port == 0) { + port = DEFAULT_PORT; + } + if (StringUtils.isEmpty(keyStoreType)) { + keyStoreType = DEFAULT_KEY_STORE_TYPE; + } + if (StringUtils.isEmpty(caHostname)) { + caHostname = DEFAULT_HOSTNAME; + } + if (StringUtils.isEmpty(dn)) { + dn = calcDefaultDn(caHostname); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/BaseTlsManager.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/BaseTlsManager.java new file mode 100644 index 0000000000..5a5e049556 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/BaseTlsManager.java @@ -0,0 +1,159 @@ +/* + * 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.toolkit.tls.manager; + +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.manager.writer.ConfigurationWriter; +import org.apache.nifi.toolkit.tls.util.InputStreamFactory; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.apache.nifi.toolkit.tls.util.PasswordUtil; +import org.apache.nifi.util.StringUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableEntryException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.List; + +public class BaseTlsManager { + public static final String PKCS_12 = "PKCS12"; + private final TlsConfig tlsConfig; + private final PasswordUtil passwordUtil; + private final InputStreamFactory inputStreamFactory; + private final KeyStore keyStore; + private final List> configurationWriters; + private boolean differentKeyAndKeyStorePassword = false; + + public BaseTlsManager(TlsConfig tlsConfig) throws GeneralSecurityException, IOException { + this(tlsConfig, new PasswordUtil(), FileInputStream::new); + } + + public BaseTlsManager(TlsConfig tlsConfig, PasswordUtil passwordUtil, InputStreamFactory inputStreamFactory) throws GeneralSecurityException, IOException { + this.tlsConfig = tlsConfig; + this.passwordUtil = passwordUtil; + this.inputStreamFactory = inputStreamFactory; + this.keyStore = loadKeystore(tlsConfig.getKeyStore(), tlsConfig.getKeyStoreType(), getKeyStorePassword()); + this.configurationWriters = new ArrayList<>(); + } + + public KeyStore getKeyStore() { + return keyStore; + } + + public KeyStore.Entry getEntry(String alias) throws UnrecoverableEntryException, NoSuchAlgorithmException, KeyStoreException { + String keyPassword = getKeyPassword(); + return keyStore.getEntry(alias, new KeyStore.PasswordProtection(keyPassword == null ? null : keyPassword.toCharArray())); + } + + public KeyStore.Entry addPrivateKeyToKeyStore(KeyPair keyPair, String alias, Certificate... certificates) throws GeneralSecurityException, IOException { + return addPrivateKeyToKeyStore(keyStore, keyPair, alias, getKeyPassword(), certificates); + } + + private KeyStore.Entry addPrivateKeyToKeyStore(KeyStore keyStore, KeyPair keyPair, String alias, String passphrase, Certificate... certificates) throws GeneralSecurityException, IOException { + keyStore.setKeyEntry(alias, keyPair.getPrivate(), passphrase == null ? null : passphrase.toCharArray(), certificates); + return getEntry(alias); + } + + public void setDifferentKeyAndKeyStorePassword(boolean differentKeyAndKeyStorePassword) { + this.differentKeyAndKeyStorePassword = differentKeyAndKeyStorePassword; + } + + private String getKeyPassword() { + if (keyStore.getType().equalsIgnoreCase(PKCS_12)) { + tlsConfig.setKeyPassword(null); + return null; + } else { + String result = tlsConfig.getKeyPassword(); + if (StringUtils.isEmpty(result)) { + if (differentKeyAndKeyStorePassword) { + result = passwordUtil.generatePassword(); + } else { + result = getKeyStorePassword(); + } + tlsConfig.setKeyPassword(result); + } + return result; + } + } + + private String getKeyStorePassword() { + String result = tlsConfig.getKeyStorePassword(); + if (StringUtils.isEmpty(result)) { + result = passwordUtil.generatePassword(); + tlsConfig.setKeyStorePassword(result); + } + return result; + } + + private KeyStore getInstance(String keyStoreType) throws KeyStoreException, NoSuchProviderException { + if (PKCS_12.equals(keyStoreType)) { + return KeyStore.getInstance(keyStoreType, BouncyCastleProvider.PROVIDER_NAME); + } else { + return KeyStore.getInstance(keyStoreType); + } + } + + protected KeyStore loadKeystore(String keyStore, String keyStoreType, String keyStorePassword) throws GeneralSecurityException, IOException { + KeyStore result = getInstance(keyStoreType); + File file = new File(keyStore); + if (file.exists()) { + try (InputStream stream = inputStreamFactory.create(file)) { + result.load(stream, keyStorePassword.toCharArray()); + } + return result; + } + result.load(null, null); + return result; + } + + public void write(OutputStreamFactory outputStreamFactory) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + String keyStorePassword = getKeyStorePassword(); + + try (OutputStream outputStream = outputStreamFactory.create(new File(tlsConfig.getKeyStore()))) { + keyStore.store(outputStream, keyStorePassword.toCharArray()); + } + + for (ConfigurationWriter configurationWriter : configurationWriters) { + configurationWriter.write(tlsConfig); + } + } + + public PasswordUtil getPasswordUtil() { + return passwordUtil; + } + + public void addConfigurationWriter(ConfigurationWriter configurationWriter) { + configurationWriters.add(configurationWriter); + } + + public TlsConfig getTlsConfig() { + return tlsConfig; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/TlsCertificateAuthorityManager.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/TlsCertificateAuthorityManager.java new file mode 100644 index 0000000000..39faf28cd6 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/TlsCertificateAuthorityManager.java @@ -0,0 +1,49 @@ +/* + * 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.toolkit.tls.manager; + +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone; +import org.apache.nifi.toolkit.tls.util.TlsHelper; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +public class TlsCertificateAuthorityManager extends BaseTlsManager { + public TlsCertificateAuthorityManager(TlsConfig tlsConfig) throws GeneralSecurityException, IOException { + super(tlsConfig); + } + + public KeyStore.PrivateKeyEntry getOrGenerateCertificateAuthority() throws GeneralSecurityException, IOException { + KeyStore.Entry entry = getEntry(TlsToolkitStandalone.NIFI_KEY); + if (entry == null) { + TlsConfig tlsConfig = getTlsConfig(); + KeyPair keyPair = TlsHelper.generateKeyPair(tlsConfig.getKeyPairAlgorithm(), tlsConfig.getKeySize()); + X509Certificate caCert = CertificateUtils.generateSelfSignedX509Certificate(keyPair, tlsConfig.getDn(), tlsConfig.getSigningAlgorithm(), tlsConfig.getDays()); + entry = addPrivateKeyToKeyStore(keyPair, TlsToolkitStandalone.NIFI_KEY, caCert); + } else if (!KeyStore.PrivateKeyEntry.class.isInstance(entry)) { + throw new IOException("Expected " + TlsToolkitStandalone.NIFI_KEY + " alias to contain a private key entry"); + } + + return (KeyStore.PrivateKeyEntry) entry; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/TlsClientManager.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/TlsClientManager.java new file mode 100644 index 0000000000..52ea5db80f --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/TlsClientManager.java @@ -0,0 +1,116 @@ +/* + * 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.toolkit.tls.manager; + +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.manager.writer.ConfigurationWriter; +import org.apache.nifi.toolkit.tls.util.InputStreamFactory; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.apache.nifi.toolkit.tls.util.PasswordUtil; +import org.apache.nifi.util.StringUtils; +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.bouncycastle.util.io.pem.PemWriter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class TlsClientManager extends BaseTlsManager { + private final TlsClientConfig tlsClientConfig; + private final KeyStore trustStore; + private final List> configurationWriters; + private final Set certificateAliases; + private File certificateAuthorityDirectory; + + public TlsClientManager(TlsClientConfig tlsClientConfig) throws GeneralSecurityException, IOException { + this(tlsClientConfig, new PasswordUtil(), FileInputStream::new); + } + + public TlsClientManager(TlsClientConfig tlsClientConfig, PasswordUtil passwordUtil, InputStreamFactory inputStreamFactory) throws GeneralSecurityException, IOException { + super(tlsClientConfig, passwordUtil, inputStreamFactory); + this.trustStore = loadKeystore(tlsClientConfig.getTrustStore(), tlsClientConfig.getTrustStoreType(), tlsClientConfig.getTrustStorePassword()); + this.tlsClientConfig = tlsClientConfig; + this.configurationWriters = new ArrayList<>(); + this.certificateAliases = new HashSet<>(); + } + + public void setCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + trustStore.setCertificateEntry(alias, cert); + certificateAliases.add(alias); + } + + public void setCertificateAuthorityDirectory(File certificateAuthorityDirectory) { + this.certificateAuthorityDirectory = certificateAuthorityDirectory; + } + + @Override + public void write(OutputStreamFactory outputStreamFactory) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + super.write(outputStreamFactory); + + String trustStorePassword = tlsClientConfig.getTrustStorePassword(); + if (StringUtils.isEmpty(trustStorePassword)) { + trustStorePassword = getPasswordUtil().generatePassword(); + tlsClientConfig.setTrustStorePassword(trustStorePassword); + } + + try (OutputStream outputStream = outputStreamFactory.create(new File(tlsClientConfig.getTrustStore()))) { + trustStore.store(outputStream, trustStorePassword.toCharArray()); + } + + for (ConfigurationWriter configurationWriter : configurationWriters) { + configurationWriter.write(tlsClientConfig); + } + + if (certificateAuthorityDirectory != null) { + // Write out all trusted certificates from truststore + for (String alias : Collections.list(trustStore.aliases())) { + try { + KeyStore.Entry trustStoreEntry = trustStore.getEntry(alias, null); + if (trustStoreEntry instanceof KeyStore.TrustedCertificateEntry) { + Certificate trustedCertificate = ((KeyStore.TrustedCertificateEntry) trustStoreEntry).getTrustedCertificate(); + try (OutputStream outputStream = outputStreamFactory.create(new File(certificateAuthorityDirectory, alias + ".pem")); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + PemWriter pemWriter = new PemWriter(outputStreamWriter)) { + pemWriter.writeObject(new JcaMiscPEMGenerator(trustedCertificate)); + } + } + } catch (UnrecoverableEntryException e) { + // Ignore, not a trusted cert + } + } + } + } + + public void addClientConfigurationWriter(ConfigurationWriter configurationWriter) { + configurationWriters.add(configurationWriter); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/ConfigurationWriter.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/ConfigurationWriter.java new file mode 100644 index 0000000000..e669d7e087 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/ConfigurationWriter.java @@ -0,0 +1,24 @@ +/* + * 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.toolkit.tls.manager.writer; + +import java.io.IOException; + +public interface ConfigurationWriter { + void write(T t) throws IOException; +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/JsonConfigurationWriter.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/JsonConfigurationWriter.java new file mode 100644 index 0000000000..475553bf3b --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/JsonConfigurationWriter.java @@ -0,0 +1,45 @@ +/* + * 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.toolkit.tls.manager.writer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +public class JsonConfigurationWriter implements ConfigurationWriter { + private final ObjectWriter objectWriter; + private final OutputStreamFactory outputStreamFactory; + private final File file; + + public JsonConfigurationWriter(ObjectMapper objectMapper, OutputStreamFactory outputStreamFactory, File file) { + this.objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); + this.outputStreamFactory = outputStreamFactory; + this.file = file; + } + + @Override + public void write(T tlsConfig) throws IOException { + try (OutputStream stream = outputStreamFactory.create(file)) { + objectWriter.writeValue(stream, tlsConfig); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java new file mode 100644 index 0000000000..5ee08f5b9a --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/manager/writer/NifiPropertiesTlsClientConfigWriter.java @@ -0,0 +1,76 @@ +/* + * 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.toolkit.tls.manager.writer; + +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriter; +import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class NifiPropertiesTlsClientConfigWriter implements ConfigurationWriter { + private final NiFiPropertiesWriterFactory niFiPropertiesWriterFactory; + private final OutputStreamFactory outputStreamFactory; + private final File file; + private final String hostname; + private final String httpsPort; + + public NifiPropertiesTlsClientConfigWriter(NiFiPropertiesWriterFactory niFiPropertiesWriterFactory, OutputStreamFactory outputStreamFactory, File file, String hostname, String httpsPort) { + this.niFiPropertiesWriterFactory = niFiPropertiesWriterFactory; + this.outputStreamFactory = outputStreamFactory; + this.file = file; + this.hostname = hostname; + this.httpsPort = httpsPort; + } + + @Override + public void write(TlsClientConfig tlsClientConfig) throws IOException { + NiFiPropertiesWriter niFiPropertiesWriter = niFiPropertiesWriterFactory.create(); + updateProperties(niFiPropertiesWriter, tlsClientConfig); + try (OutputStream stream = outputStreamFactory.create(file)) { + niFiPropertiesWriter.writeNiFiProperties(stream); + } + } + + protected void updateProperties(NiFiPropertiesWriter niFiPropertiesWriter, TlsClientConfig tlsClientConfig) { + Path parentPath = Paths.get(file.getParentFile().getAbsolutePath()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE, parentPath.relativize(Paths.get(tlsClientConfig.getKeyStore())).toString()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE_TYPE, tlsClientConfig.getKeyStoreType()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEYSTORE_PASSWD, tlsClientConfig.getKeyStorePassword()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_KEY_PASSWD, tlsClientConfig.getKeyPassword()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE, parentPath.relativize(Paths.get(tlsClientConfig.getTrustStore())).toString()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, tlsClientConfig.getTrustStoreType()); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD, tlsClientConfig.getTrustStorePassword()); + if (!StringUtils.isEmpty(httpsPort)) { + if (!StringUtils.isEmpty(hostname)) { + niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTPS_HOST, hostname); + } + niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTPS_PORT, httpsPort); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTP_HOST, ""); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.WEB_HTTP_PORT, ""); + niFiPropertiesWriter.setPropertyValue(NiFiProperties.SITE_TO_SITE_SECURE, "true"); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/properties/NiFiPropertiesWriter.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/properties/NiFiPropertiesWriter.java new file mode 100644 index 0000000000..61cbc675d9 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/properties/NiFiPropertiesWriter.java @@ -0,0 +1,76 @@ +/* + * 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.toolkit.tls.properties; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class NiFiPropertiesWriter { + private final List lines; + private final Map updatedValues; + + public NiFiPropertiesWriter(List lines) { + this.lines = new ArrayList<>(lines); + this.updatedValues = new HashMap<>(); + } + + public void setPropertyValue(String key, String value) { + updatedValues.put(key, value); + } + + public void writeNiFiProperties(OutputStream outputStream) throws IOException { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) { + Map remainingValues = new HashMap<>(updatedValues); + Set keysSeen = new HashSet<>(); + for (String line : lines) { + String key = line.split("=")[0].trim(); + boolean outputLine = true; + if (!key.isEmpty() && !key.startsWith("#")) { + if (!keysSeen.add(key)) { + throw new IOException("Found key more than once in nifi.properties: " + key); + } + String value = remainingValues.remove(key); + if (value != null) { + writer.write(key); + writer.write("="); + writer.write(value); + outputLine = false; + } + } + if (outputLine) { + writer.write(line); + } + writer.newLine(); + } + for (Map.Entry keyValueEntry : remainingValues.entrySet()) { + writer.write(keyValueEntry.getKey()); + writer.write("="); + writer.write(keyValueEntry.getValue()); + writer.newLine(); + } + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/properties/NiFiPropertiesWriterFactory.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/properties/NiFiPropertiesWriterFactory.java new file mode 100644 index 0000000000..cf8212c9c7 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/properties/NiFiPropertiesWriterFactory.java @@ -0,0 +1,51 @@ +/* + * 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.toolkit.tls.properties; + +import org.apache.nifi.toolkit.tls.TlsToolkitMain; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class NiFiPropertiesWriterFactory { + private final List lines; + + public NiFiPropertiesWriterFactory() throws IOException { + this(TlsToolkitMain.class.getClassLoader().getResourceAsStream("conf/nifi.properties")); + } + + public NiFiPropertiesWriterFactory(InputStream inputStream) throws IOException { + List lines = new ArrayList<>(); + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + lines.add(line.trim()); + } + } + this.lines = Collections.unmodifiableList(lines); + } + + public NiFiPropertiesWriter create() { + return new NiFiPropertiesWriter(lines); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/BaseCertificateAuthorityCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/BaseCertificateAuthorityCommandLine.java new file mode 100644 index 0000000000..97fa45b5f2 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/BaseCertificateAuthorityCommandLine.java @@ -0,0 +1,95 @@ +/* + * 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.toolkit.tls.service; + +import org.apache.commons.cli.CommandLine; +import org.apache.nifi.toolkit.tls.commandLine.BaseCommandLine; +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.util.StringUtils; + +import java.io.File; + +public abstract class BaseCertificateAuthorityCommandLine extends BaseCommandLine { + public static final String TOKEN_ARG = "token"; + public static final String CONFIG_JSON_ARG = "configJson"; + public static final String USE_CONFIG_JSON_ARG = "useConfigJson"; + public static final String PORT_ARG = "PORT"; + + public static final String DEFAULT_CONFIG_JSON = new File("config.json").getPath(); + + private String token; + private String configJson; + private boolean onlyUseConfigJson; + private int port; + private String dn; + + public BaseCertificateAuthorityCommandLine(String header) { + super(header); + addOptionWithArg("t", TOKEN_ARG, getTokenDescription()); + addOptionWithArg("f", CONFIG_JSON_ARG, "The place to write configuration info", DEFAULT_CONFIG_JSON); + addOptionNoArg("F", USE_CONFIG_JSON_ARG, "Flag specifying that all configuration is read from " + CONFIG_JSON_ARG + " to facilitate automated use (otherwise " + + CONFIG_JSON_ARG + " will only be written to."); + addOptionWithArg("p", PORT_ARG, getPortDescription(), TlsConfig.DEFAULT_PORT); + addOptionWithArg("D", DN_ARG, getDnDescription(), TlsConfig.calcDefaultDn(getDnHostname())); + } + + protected abstract String getTokenDescription(); + + protected abstract String getDnDescription(); + + protected abstract String getPortDescription(); + + protected abstract String getDnHostname(); + + @Override + protected CommandLine doParse(String[] args) throws CommandLineParseException { + CommandLine commandLine = super.doParse(args); + + token = commandLine.getOptionValue(TOKEN_ARG); + onlyUseConfigJson = commandLine.hasOption(USE_CONFIG_JSON_ARG); + if (StringUtils.isEmpty(token) && !onlyUseConfigJson) { + printUsageAndThrow(TOKEN_ARG + " argument must not be empty unless " + USE_CONFIG_JSON_ARG + " set", ExitCode.ERROR_TOKEN_ARG_EMPTY); + } + configJson = commandLine.getOptionValue(CONFIG_JSON_ARG, DEFAULT_CONFIG_JSON); + port = getIntValue(commandLine, PORT_ARG, TlsConfig.DEFAULT_PORT); + dn = commandLine.getOptionValue(DN_ARG, TlsConfig.calcDefaultDn(getDnHostname())); + return commandLine; + } + + public String getToken() { + return token; + } + + public String getConfigJson() { + return configJson; + } + + public boolean onlyUseConfigJson() { + return onlyUseConfigJson; + } + + public int getPort() { + return port; + } + + public String getDn() { + return dn; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClient.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClient.java new file mode 100644 index 0000000000..c0e2301aa9 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClient.java @@ -0,0 +1,90 @@ +/* + * 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.toolkit.tls.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.manager.TlsClientManager; +import org.apache.nifi.toolkit.tls.manager.writer.JsonConfigurationWriter; +import org.apache.nifi.toolkit.tls.service.BaseCertificateAuthorityCommandLine; +import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.apache.nifi.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +/** + * Client that will generate a CSR and submit to a CA, writing out the results to a keystore and truststore along with a config file if successful + */ +public class TlsCertificateAuthorityClient { + private final Logger logger = LoggerFactory.getLogger(TlsCertificateAuthorityClient.class); + private final OutputStreamFactory outputStreamFactory; + + public TlsCertificateAuthorityClient() { + this(FileOutputStream::new); + } + + public TlsCertificateAuthorityClient(OutputStreamFactory outputStreamFactory) { + this.outputStreamFactory = outputStreamFactory; + } + + public void generateCertificateAndGetItSigned(TlsClientConfig tlsClientConfig, String certificateDirectory, String configJson, boolean differentKeyAndKeyStorePassword) throws Exception { + TlsClientManager tlsClientManager; + try { + tlsClientManager = new TlsClientManager(tlsClientConfig); + } catch (IOException e) { + logger.error("Unable to open existing keystore, it can be reused by specifiying both " + BaseCertificateAuthorityCommandLine.CONFIG_JSON_ARG + " and " + + BaseCertificateAuthorityCommandLine.USE_CONFIG_JSON_ARG); + throw e; + } + tlsClientManager.setDifferentKeyAndKeyStorePassword(differentKeyAndKeyStorePassword); + + if (!StringUtils.isEmpty(certificateDirectory)) { + tlsClientManager.setCertificateAuthorityDirectory(new File(certificateDirectory)); + } + + if (!StringUtils.isEmpty(configJson)) { + tlsClientManager.addClientConfigurationWriter(new JsonConfigurationWriter<>(new ObjectMapper(), outputStreamFactory, new File(configJson))); + } + + if (tlsClientManager.getEntry(TlsToolkitStandalone.NIFI_KEY) == null) { + if (logger.isInfoEnabled()) { + logger.info("Requesting new certificate from " + tlsClientConfig.getCaHostname() + ":" + tlsClientConfig.getPort()); + } + KeyPair keyPair = TlsHelper.generateKeyPair(tlsClientConfig.getKeyPairAlgorithm(), tlsClientConfig.getKeySize()); + + X509Certificate[] certificates = tlsClientConfig.createCertificateSigningRequestPerformer().perform(keyPair); + + tlsClientManager.addPrivateKeyToKeyStore(keyPair, TlsToolkitStandalone.NIFI_KEY, certificates); + tlsClientManager.setCertificateEntry(TlsToolkitStandalone.NIFI_CERT, certificates[certificates.length - 1]); + } else { + if (logger.isInfoEnabled()) { + logger.info("Already had entry for " + TlsToolkitStandalone.NIFI_KEY + " not requesting new certificate."); + } + } + + tlsClientManager.write(outputStreamFactory); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java new file mode 100644 index 0000000000..dd432c363e --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLine.java @@ -0,0 +1,141 @@ +/* + * 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.toolkit.tls.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.cli.CommandLine; +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.service.BaseCertificateAuthorityCommandLine; +import org.apache.nifi.toolkit.tls.util.InputStreamFactory; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class TlsCertificateAuthorityClientCommandLine extends BaseCertificateAuthorityCommandLine { + public static final String DESCRIPTION = "Generates a private key and gets it signed by the certificate authority."; + public static final String CERTIFICATE_DIRECTORY = "certificateDirectory"; + public static final String DEFAULT_CERTIFICATE_DIRECTORY = "."; + + private final Logger logger = LoggerFactory.getLogger(TlsCertificateAuthorityClientCommandLine.class); + private final InputStreamFactory inputStreamFactory; + + private String certificateDirectory; + + public TlsCertificateAuthorityClientCommandLine() { + this(FileInputStream::new); + } + + public TlsCertificateAuthorityClientCommandLine(InputStreamFactory inputStreamFactory) { + super(DESCRIPTION); + this.inputStreamFactory = inputStreamFactory; + addOptionWithArg("C", CERTIFICATE_DIRECTORY, "The file to write the CA certificate to", DEFAULT_CERTIFICATE_DIRECTORY); + } + + public static void main(String[] args) throws Exception { + TlsHelper.addBouncyCastleProvider(); + TlsCertificateAuthorityClientCommandLine tlsCertificateAuthorityClientCommandLine = new TlsCertificateAuthorityClientCommandLine(); + try { + tlsCertificateAuthorityClientCommandLine.parse(args); + } catch (CommandLineParseException e) { + System.exit(e.getExitCode()); + } + new TlsCertificateAuthorityClient().generateCertificateAndGetItSigned(tlsCertificateAuthorityClientCommandLine.createClientConfig(), + tlsCertificateAuthorityClientCommandLine.getCertificateDirectory(), tlsCertificateAuthorityClientCommandLine.getConfigJson(), + tlsCertificateAuthorityClientCommandLine.differentPasswordForKeyAndKeystore()); + System.exit(ExitCode.SUCCESS.ordinal()); + } + + @Override + protected boolean shouldAddDaysArg() { + return false; + } + + @Override + protected boolean shouldAddSigningAlgorithmArg() { + return false; + } + + @Override + protected String getTokenDescription() { + return "The token to use to prevent MITM (required and must be same as one used by CA)"; + } + + @Override + protected String getDnDescription() { + return "The dn to use for the client certificate"; + } + + @Override + protected String getPortDescription() { + return "The port to use to communicate with the Certificate Authority"; + } + + @Override + protected String getDnHostname() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + logger.warn("Unable to determine hostname", e); + return "localhost"; + } + } + + @Override + protected CommandLine doParse(String[] args) throws CommandLineParseException { + CommandLine commandLine = super.doParse(args); + certificateDirectory = commandLine.getOptionValue(CERTIFICATE_DIRECTORY, DEFAULT_CERTIFICATE_DIRECTORY); + return commandLine; + } + + public String getCertificateDirectory() { + return certificateDirectory; + } + + public TlsClientConfig createClientConfig() throws IOException { + if (onlyUseConfigJson()) { + try (InputStream inputStream = inputStreamFactory.create(new File(getConfigJson()))) { + TlsClientConfig tlsClientConfig = new ObjectMapper().readValue(inputStream, TlsClientConfig.class); + tlsClientConfig.initDefaults(); + return tlsClientConfig; + } + } else { + TlsClientConfig tlsClientConfig = new TlsClientConfig(); + tlsClientConfig.setCaHostname(getCertificateAuthorityHostname()); + tlsClientConfig.setDn(getDn()); + tlsClientConfig.setToken(getToken()); + tlsClientConfig.setPort(getPort()); + tlsClientConfig.setKeyStore(KEYSTORE + getKeyStoreType().toLowerCase()); + tlsClientConfig.setKeyStoreType(getKeyStoreType()); + tlsClientConfig.setTrustStore(TRUSTSTORE + getKeyStoreType().toLowerCase()); + tlsClientConfig.setTrustStoreType(getKeyStoreType()); + tlsClientConfig.setKeySize(getKeySize()); + tlsClientConfig.setKeyPairAlgorithm(getKeyAlgorithm()); + tlsClientConfig.setSigningAlgorithm(getSigningAlgorithm()); + return tlsClientConfig; + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientSocketFactory.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientSocketFactory.java new file mode 100644 index 0000000000..e5000c8e40 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientSocketFactory.java @@ -0,0 +1,77 @@ +/* + * 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.toolkit.tls.service.client; + +import org.apache.http.HttpHost; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Socket Factory validates that it is talking to a RootCa claiming to have the given hostname. It adds the certificate + * to a list for later validation against the payload's hmac + */ +public class TlsCertificateAuthorityClientSocketFactory extends SSLConnectionSocketFactory { + private final String caHostname; + private final List certificates; + + public TlsCertificateAuthorityClientSocketFactory(SSLContext sslContext, String caHostname, List certificates) { + super(sslContext); + this.caHostname = caHostname; + this.certificates = certificates; + } + + @Override + public synchronized Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, + InetSocketAddress localAddress, HttpContext context) throws IOException { + Socket result = super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context); + if (!SSLSocket.class.isInstance(result)) { + throw new IOException("Expected tls socket"); + } + SSLSocket sslSocket = (SSLSocket) result; + java.security.cert.Certificate[] peerCertificateChain = sslSocket.getSession().getPeerCertificates(); + if (peerCertificateChain.length != 1) { + throw new IOException("Expected root ca cert"); + } + if (!X509Certificate.class.isInstance(peerCertificateChain[0])) { + throw new IOException("Expected root ca cert in X509 format"); + } + String cn; + try { + X509Certificate certificate = (X509Certificate) peerCertificateChain[0]; + cn = IETFUtils.valueToString(new JcaX509CertificateHolder(certificate).getSubject().getRDNs(BCStyle.CN)[0].getFirst().getValue()); + certificates.add(certificate); + } catch (Exception e) { + throw new IOException(e); + } + if (!caHostname.equals(cn)) { + throw new IOException("Expected cn of " + caHostname + " but got " + cn); + } + return result; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java new file mode 100644 index 0000000000..2c65964519 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformer.java @@ -0,0 +1,156 @@ +/* + * 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.toolkit.tls.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BoundedInputStream; +import org.apache.http.HttpHost; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityRequest; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityResponse; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.eclipse.jetty.server.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class TlsCertificateSigningRequestPerformer { + public static final String RECEIVED_RESPONSE_CODE = "Received response code "; + public static final String EXPECTED_ONE_CERTIFICATE = "Expected one certificate"; + public static final String EXPECTED_RESPONSE_TO_CONTAIN_HMAC = "Expected response to contain hmac"; + public static final String UNEXPECTED_HMAC_RECEIVED_POSSIBLE_MAN_IN_THE_MIDDLE = "Unexpected hmac received, possible man in the middle"; + public static final String EXPECTED_RESPONSE_TO_CONTAIN_CERTIFICATE = "Expected response to contain certificate"; + private final Logger logger = LoggerFactory.getLogger(TlsCertificateSigningRequestPerformer.class); + private final Supplier httpClientBuilderSupplier; + private final String caHostname; + private final String dn; + private final String token; + private final int port; + private final ObjectMapper objectMapper; + private final String signingAlgorithm; + + public TlsCertificateSigningRequestPerformer(TlsClientConfig tlsClientConfig) throws NoSuchAlgorithmException { + this(HttpClientBuilder::create, tlsClientConfig.getCaHostname(), tlsClientConfig.getDn(), tlsClientConfig.getToken(), tlsClientConfig.getPort(), tlsClientConfig.getSigningAlgorithm()); + } + + protected TlsCertificateSigningRequestPerformer(Supplier httpClientBuilderSupplier, TlsClientConfig tlsClientConfig) throws NoSuchAlgorithmException { + this(httpClientBuilderSupplier, tlsClientConfig.getCaHostname(), tlsClientConfig.getDn(), tlsClientConfig.getToken(), tlsClientConfig.getPort(), tlsClientConfig.getSigningAlgorithm()); + } + + private TlsCertificateSigningRequestPerformer(Supplier httpClientBuilderSupplier, String caHostname, String dn, String token, int port, String signingAlgorithm) { + this.httpClientBuilderSupplier = httpClientBuilderSupplier; + this.caHostname = caHostname; + this.dn = dn; + this.token = token; + this.port = port; + this.objectMapper = new ObjectMapper(); + this.signingAlgorithm = signingAlgorithm; + } + + /** + * Submits a CSR to the Certificate authority, checks the resulting hmac, and returns the chain if everything succeeds + * + * @param keyPair the keypair to generate the csr for + * @throws IOException if there is a problem during the process + * @returnd the resulting certificate chain + */ + public X509Certificate[] perform(KeyPair keyPair) throws IOException { + try { + List certificates = new ArrayList<>(); + + HttpClientBuilder httpClientBuilder = httpClientBuilderSupplier.get(); + SSLContextBuilder sslContextBuilder = SSLContextBuilder.create(); + sslContextBuilder.useProtocol("TLSv1.2"); + + // We will be validating that we are talking to the correct host once we get the response's hmac of the token and public key of the ca + sslContextBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + httpClientBuilder.setSSLSocketFactory(new TlsCertificateAuthorityClientSocketFactory(sslContextBuilder.build(), caHostname, certificates)); + + String jsonResponseString; + int responseCode; + try (CloseableHttpClient client = httpClientBuilder.build()) { + JcaPKCS10CertificationRequest request = TlsHelper.generateCertificationRequest(dn, keyPair, signingAlgorithm); + TlsCertificateAuthorityRequest tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest(TlsHelper.calculateHMac(token, request.getPublicKey()), + TlsHelper.pemEncodeJcaObject(request)); + + HttpPost httpPost = new HttpPost(); + httpPost.setEntity(new ByteArrayEntity(objectMapper.writeValueAsBytes(tlsCertificateAuthorityRequest))); + + if (logger.isInfoEnabled()) { + logger.info("Requesting certificate with dn " + dn + " from " + caHostname + ":" + port); + } + try (CloseableHttpResponse response = client.execute(new HttpHost(caHostname, port, "https"), httpPost)) { + jsonResponseString = IOUtils.toString(new BoundedInputStream(response.getEntity().getContent(), 1024 * 1024), StandardCharsets.UTF_8); + responseCode = response.getStatusLine().getStatusCode(); + } + } + + if (responseCode != Response.SC_OK) { + throw new IOException(RECEIVED_RESPONSE_CODE + responseCode + " with payload " + jsonResponseString); + } + + if (certificates.size() != 1) { + throw new IOException(EXPECTED_ONE_CERTIFICATE); + } + + TlsCertificateAuthorityResponse tlsCertificateAuthorityResponse = objectMapper.readValue(jsonResponseString, TlsCertificateAuthorityResponse.class); + if (!tlsCertificateAuthorityResponse.hasHmac()) { + throw new IOException(EXPECTED_RESPONSE_TO_CONTAIN_HMAC); + } + + X509Certificate caCertificate = certificates.get(0); + byte[] expectedHmac = TlsHelper.calculateHMac(token, caCertificate.getPublicKey()); + + if (!MessageDigest.isEqual(expectedHmac, tlsCertificateAuthorityResponse.getHmac())) { + throw new IOException(UNEXPECTED_HMAC_RECEIVED_POSSIBLE_MAN_IN_THE_MIDDLE); + } + + if (!tlsCertificateAuthorityResponse.hasCertificate()) { + throw new IOException(EXPECTED_RESPONSE_TO_CONTAIN_CERTIFICATE); + } + X509Certificate x509Certificate = TlsHelper.parseCertificate(tlsCertificateAuthorityResponse.getPemEncodedCertificate()); + x509Certificate.verify(caCertificate.getPublicKey()); + if (logger.isInfoEnabled()) { + logger.info("Got certificate with dn " + x509Certificate.getSubjectDN()); + } + return new X509Certificate[]{x509Certificate, caCertificate}; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/dto/TlsCertificateAuthorityRequest.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/dto/TlsCertificateAuthorityRequest.java new file mode 100644 index 0000000000..ca6f0ce023 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/dto/TlsCertificateAuthorityRequest.java @@ -0,0 +1,57 @@ +/* + * 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.toolkit.tls.service.dto; + +import org.apache.nifi.util.StringUtils; + +public class TlsCertificateAuthorityRequest { + private byte[] hmac; + private String csr; + + public TlsCertificateAuthorityRequest() { + } + + public TlsCertificateAuthorityRequest(byte[] hmac, String csr) { + this.hmac = hmac; + this.csr = csr; + } + + public byte[] getHmac() { + return hmac; + } + + public void setHmac(byte[] hmac) { + this.hmac = hmac; + } + + public boolean hasHmac() { + return hmac != null && hmac.length > 0; + } + + public String getCsr() { + return csr; + } + + public void setCsr(String csr) { + this.csr = csr; + } + + public boolean hasCsr() { + return !StringUtils.isEmpty(csr); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/dto/TlsCertificateAuthorityResponse.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/dto/TlsCertificateAuthorityResponse.java new file mode 100644 index 0000000000..3a7c238249 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/dto/TlsCertificateAuthorityResponse.java @@ -0,0 +1,70 @@ +/* + * 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.toolkit.tls.service.dto; + +import org.apache.nifi.util.StringUtils; + +public class TlsCertificateAuthorityResponse { + private byte[] hmac; + private String pemEncodedCertificate; + private String error; + + public TlsCertificateAuthorityResponse() { + } + + public TlsCertificateAuthorityResponse(byte[] hmac, String pemEncodedCertificate) { + this.hmac = hmac; + this.pemEncodedCertificate = pemEncodedCertificate; + } + + public TlsCertificateAuthorityResponse(String error) { + this.error = error; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public byte[] getHmac() { + return hmac; + } + + public void setHmac(byte[] hmac) { + this.hmac = hmac; + } + + public String getPemEncodedCertificate() { + return pemEncodedCertificate; + } + + public void setPemEncodedCertificate(String pemEncodedCertificate) { + this.pemEncodedCertificate = pemEncodedCertificate; + } + + public boolean hasCertificate() { + return !StringUtils.isEmpty(pemEncodedCertificate); + } + + public boolean hasHmac() { + return hmac != null && hmac.length > 0; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityService.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityService.java new file mode 100644 index 0000000000..d21ea36f39 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityService.java @@ -0,0 +1,126 @@ +/* + * 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.toolkit.tls.service.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.manager.TlsCertificateAuthorityManager; +import org.apache.nifi.toolkit.tls.manager.writer.JsonConfigurationWriter; +import org.apache.nifi.toolkit.tls.service.BaseCertificateAuthorityCommandLine; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.eclipse.jetty.http.HttpVersion; +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.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +/** + * Starts a Jetty server that will either load an existing CA or create one and use it to sign CSRs + */ +public class TlsCertificateAuthorityService { + private final Logger logger = LoggerFactory.getLogger(TlsCertificateAuthorityService.class); + private final OutputStreamFactory outputStreamFactory; + private Server server; + + public TlsCertificateAuthorityService() { + this(FileOutputStream::new); + } + + public TlsCertificateAuthorityService(OutputStreamFactory outputStreamFactory) { + this.outputStreamFactory = outputStreamFactory; + } + + private static Server createServer(Handler handler, int port, KeyStore keyStore, String keyPassword) throws Exception { + Server server = new Server(); + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setIncludeProtocols("TLSv1.2"); + sslContextFactory.setKeyStore(keyStore); + sslContextFactory.setKeyManagerPassword(keyPassword); + + HttpConfiguration httpsConfig = new HttpConfiguration(); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + + ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig)); + sslConnector.setPort(port); + + server.addConnector(sslConnector); + server.setHandler(handler); + + return server; + } + + public synchronized void start(TlsConfig tlsConfig, String configJson, boolean differentPasswordsForKeyAndKeystore) throws Exception { + if (server != null) { + throw new IllegalStateException("Server already started"); + } + ObjectMapper objectMapper = new ObjectMapper(); + TlsCertificateAuthorityManager tlsManager; + try { + tlsManager = new TlsCertificateAuthorityManager(tlsConfig); + tlsManager.setDifferentKeyAndKeyStorePassword(differentPasswordsForKeyAndKeystore); + } catch (IOException e) { + logger.error("Unable to open existing keystore, it can be reused by specifiying both " + BaseCertificateAuthorityCommandLine.CONFIG_JSON_ARG + " and " + + BaseCertificateAuthorityCommandLine.USE_CONFIG_JSON_ARG); + throw e; + } + tlsManager.addConfigurationWriter(new JsonConfigurationWriter<>(objectMapper, outputStreamFactory, new File(configJson))); + + KeyStore.PrivateKeyEntry privateKeyEntry = tlsManager.getOrGenerateCertificateAuthority(); + KeyPair keyPair = new KeyPair(privateKeyEntry.getCertificate().getPublicKey(), privateKeyEntry.getPrivateKey()); + Certificate[] certificateChain = privateKeyEntry.getCertificateChain(); + if (certificateChain.length != 1) { + throw new IOException("Expected root ca cert to be only certificate in chain"); + } + Certificate certificate = certificateChain[0]; + X509Certificate caCert; + if (certificate instanceof X509Certificate) { + caCert = (X509Certificate) certificate; + } else { + throw new IOException("Expected " + X509Certificate.class + " as root ca cert"); + } + tlsManager.write(outputStreamFactory); + String signingAlgorithm = tlsConfig.getSigningAlgorithm(); + int days = tlsConfig.getDays(); + server = createServer(new TlsCertificateAuthorityServiceHandler(signingAlgorithm, days, tlsConfig.getToken(), caCert, keyPair, objectMapper), tlsConfig.getPort(), tlsManager.getKeyStore(), + tlsConfig.getKeyPassword()); + server.start(); + } + + public synchronized void shutdown() throws Exception { + if (server == null) { + throw new IllegalStateException("Server already shutdown"); + } + server.stop(); + server.join(); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceCommandLine.java new file mode 100644 index 0000000000..2b1db39cd8 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceCommandLine.java @@ -0,0 +1,109 @@ +/* + * 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.toolkit.tls.service.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.service.BaseCertificateAuthorityCommandLine; +import org.apache.nifi.toolkit.tls.util.InputStreamFactory; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.apache.nifi.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class TlsCertificateAuthorityServiceCommandLine extends BaseCertificateAuthorityCommandLine { + public static final String DESCRIPTION = "Acts as a Certificate Authority that can be used by clients to get Certificates"; + public static final String NIFI_CA_KEYSTORE = "nifi-ca-" + KEYSTORE; + + private final InputStreamFactory inputStreamFactory; + + public TlsCertificateAuthorityServiceCommandLine() { + this(FileInputStream::new); + } + + public TlsCertificateAuthorityServiceCommandLine(InputStreamFactory inputStreamFactory) { + super(DESCRIPTION); + this.inputStreamFactory = inputStreamFactory; + } + + public static void main(String[] args) throws Exception { + TlsHelper.addBouncyCastleProvider(); + TlsCertificateAuthorityServiceCommandLine tlsCertificateAuthorityServiceCommandLine = new TlsCertificateAuthorityServiceCommandLine(); + try { + tlsCertificateAuthorityServiceCommandLine.parse(args); + } catch (CommandLineParseException e) { + System.exit(e.getExitCode()); + } + TlsCertificateAuthorityService tlsCertificateAuthorityService = new TlsCertificateAuthorityService(); + tlsCertificateAuthorityService.start(tlsCertificateAuthorityServiceCommandLine.createConfig(), tlsCertificateAuthorityServiceCommandLine.getConfigJson(), + tlsCertificateAuthorityServiceCommandLine.differentPasswordForKeyAndKeystore()); + System.out.println("Server Started"); + System.out.flush(); + } + + public TlsConfig createConfig() throws IOException { + if (onlyUseConfigJson()) { + try (InputStream inputStream = inputStreamFactory.create(new File(getConfigJson()))) { + TlsConfig tlsConfig = new ObjectMapper().readValue(inputStream, TlsConfig.class); + tlsConfig.initDefaults(); + return tlsConfig; + } + } else { + TlsConfig tlsConfig = new TlsConfig(); + tlsConfig.setCaHostname(getCertificateAuthorityHostname()); + tlsConfig.setDn(getDn()); + tlsConfig.setToken(getToken()); + tlsConfig.setPort(getPort()); + tlsConfig.setKeyStore(NIFI_CA_KEYSTORE + getKeyStoreType().toLowerCase()); + tlsConfig.setKeyStoreType(getKeyStoreType()); + tlsConfig.setKeySize(getKeySize()); + tlsConfig.setKeyPairAlgorithm(getKeyAlgorithm()); + tlsConfig.setSigningAlgorithm(getSigningAlgorithm()); + tlsConfig.setDays(getDays()); + return tlsConfig; + } + } + + @Override + protected String getTokenDescription() { + return "The token to use to prevent MITM (required and must be same as one used by clients)"; + } + + @Override + protected String getDnDescription() { + return "The dn to use for the CA certificate"; + } + + @Override + protected String getPortDescription() { + return "The port for the Certificate Authority to listen on"; + } + + @Override + protected String getDnHostname() { + String dnHostname = getCertificateAuthorityHostname(); + if (StringUtils.isEmpty(dnHostname)) { + return "YOUR_CA_HOSTNAME"; + } + return dnHostname; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java new file mode 100644 index 0000000000..fb83498421 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandler.java @@ -0,0 +1,117 @@ +/* + * 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.toolkit.tls.service.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.input.BoundedReader; +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityRequest; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityResponse; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.cert.X509Certificate; + +/** + * Jetty service handler that validates the hmac of a CSR and issues a certificate if it checks out + */ +public class TlsCertificateAuthorityServiceHandler extends AbstractHandler { + public static final String CSR_FIELD_MUST_BE_SET = "csr field must be set"; + public static final String HMAC_FIELD_MUST_BE_SET = "hmac field must be set"; + public static final String FORBIDDEN = "forbidden"; + private final Logger logger = LoggerFactory.getLogger(TlsCertificateAuthorityServiceHandler.class); + private final String signingAlgorithm; + private final int days; + private final String token; + private final X509Certificate caCert; + private final KeyPair keyPair; + private final ObjectMapper objectMapper; + + public TlsCertificateAuthorityServiceHandler(String signingAlgorithm, int days, String token, X509Certificate caCert, KeyPair keyPair, ObjectMapper objectMapper) { + this.signingAlgorithm = signingAlgorithm; + this.days = days; + this.token = token; + this.caCert = caCert; + this.keyPair = keyPair; + this.objectMapper = objectMapper; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + try { + TlsCertificateAuthorityRequest tlsCertificateAuthorityRequest = objectMapper.readValue(new BoundedReader(request.getReader(), 1024 * 1024), TlsCertificateAuthorityRequest.class); + + if (!tlsCertificateAuthorityRequest.hasHmac()) { + writeResponse(objectMapper, request, response, new TlsCertificateAuthorityResponse(HMAC_FIELD_MUST_BE_SET), Response.SC_BAD_REQUEST); + return; + } + + if (!tlsCertificateAuthorityRequest.hasCsr()) { + writeResponse(objectMapper, request, response, new TlsCertificateAuthorityResponse(CSR_FIELD_MUST_BE_SET), Response.SC_BAD_REQUEST); + return; + } + + JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = TlsHelper.parseCsr(tlsCertificateAuthorityRequest.getCsr()); + byte[] expectedHmac = TlsHelper.calculateHMac(token, jcaPKCS10CertificationRequest.getPublicKey()); + + if (MessageDigest.isEqual(expectedHmac, tlsCertificateAuthorityRequest.getHmac())) { + X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(jcaPKCS10CertificationRequest.getSubject().toString(), + jcaPKCS10CertificationRequest.getPublicKey(), caCert, keyPair, signingAlgorithm, days); + writeResponse(objectMapper, request, response, new TlsCertificateAuthorityResponse(TlsHelper.calculateHMac(token, caCert.getPublicKey()), + TlsHelper.pemEncodeJcaObject(x509Certificate)), Response.SC_OK); + return; + } else { + writeResponse(objectMapper, request, response, new TlsCertificateAuthorityResponse(FORBIDDEN), Response.SC_FORBIDDEN); + return; + } + } catch (Exception e) { + throw new ServletException("Server error"); + } finally { + baseRequest.setHandled(true); + } + } + + private void writeResponse(ObjectMapper objectMapper, HttpServletRequest request, HttpServletResponse response, TlsCertificateAuthorityResponse tlsCertificateAuthorityResponse, + int responseCode) throws IOException { + if (logger.isInfoEnabled()) { + logger.info(new StringBuilder("Returning code:").append(responseCode).append(" payload ").append(objectMapper.writeValueAsString(tlsCertificateAuthorityResponse)) + .append(" to ").append(request.getRemoteHost()).toString()); + } + if (responseCode == Response.SC_OK) { + objectMapper.writeValue(response.getWriter(), tlsCertificateAuthorityResponse); + response.setStatus(responseCode); + } else { + response.setStatus(responseCode); + response.setContentType("application/json"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + objectMapper.writeValue(response.getWriter(), tlsCertificateAuthorityResponse); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java new file mode 100644 index 0000000000..cef1bc3962 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandalone.java @@ -0,0 +1,136 @@ +/* + * 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.toolkit.tls.standalone; + +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.manager.TlsCertificateAuthorityManager; +import org.apache.nifi.toolkit.tls.manager.TlsClientManager; +import org.apache.nifi.toolkit.tls.manager.writer.NifiPropertiesTlsClientConfigWriter; +import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.bouncycastle.util.io.pem.PemWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.List; + +public class TlsToolkitStandalone { + public static final String NIFI_KEY = "nifi-key"; + public static final String NIFI_CERT = "nifi-cert"; + public static final String NIFI_PROPERTIES = "nifi.properties"; + + private final Logger logger = LoggerFactory.getLogger(TlsToolkitStandalone.class); + private final OutputStreamFactory outputStreamFactory; + + public TlsToolkitStandalone() { + this(FileOutputStream::new); + } + + public TlsToolkitStandalone(OutputStreamFactory outputStreamFactory) { + this.outputStreamFactory = outputStreamFactory; + } + + public void createNifiKeystoresAndTrustStores(File baseDir, TlsConfig tlsConfig, NiFiPropertiesWriterFactory niFiPropertiesWriterFactory, List hostnames, List keyStorePasswords, + List keyPasswords, List trustStorePasswords, String httpsPort) throws GeneralSecurityException, IOException { + String signingAlgorithm = tlsConfig.getSigningAlgorithm(); + int days = tlsConfig.getDays(); + String keyPairAlgorithm = tlsConfig.getKeyPairAlgorithm(); + int keySize = tlsConfig.getKeySize(); + TlsCertificateAuthorityManager tlsCertificateAuthorityManager = new TlsCertificateAuthorityManager(tlsConfig); + KeyStore.PrivateKeyEntry privateKeyEntry = tlsCertificateAuthorityManager.getOrGenerateCertificateAuthority(); + X509Certificate certificate = (X509Certificate) privateKeyEntry.getCertificateChain()[0]; + KeyPair caKeyPair = new KeyPair(certificate.getPublicKey(), privateKeyEntry.getPrivateKey()); + + if (!baseDir.exists() && !baseDir.mkdirs()) { + throw new IOException(baseDir + " doesn't exist and unable to create it."); + } + + if (!baseDir.isDirectory()) { + throw new IOException("Expected directory to output to"); + } + + if (logger.isInfoEnabled()) { + logger.info("Running standalone certificate generation with output directory " + baseDir + " and hostnames " + hostnames); + } + + File nifiCert = new File(baseDir, NIFI_CERT + ".pem"); + if (nifiCert.exists()) { + throw new IOException(nifiCert.getAbsolutePath() + " exists already."); + } + + File nifiKey = new File(baseDir, NIFI_KEY + ".key"); + if (nifiKey.exists()) { + throw new IOException(nifiKey.getAbsolutePath() + " exists already."); + } + + for (String hostname : hostnames) { + File hostDirectory = new File(baseDir, hostname); + if (hostDirectory.exists()) { + throw new IOException("Output destination for host " + hostname + " (" + hostDirectory.getAbsolutePath() + ") exists already."); + } + } + + try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(outputStreamFactory.create(nifiCert)))) { + pemWriter.writeObject(new JcaMiscPEMGenerator(certificate)); + } + + try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(outputStreamFactory.create(nifiKey)))) { + pemWriter.writeObject(new JcaMiscPEMGenerator(caKeyPair)); + } + + for (int i = 0; i < hostnames.size(); i++) { + String hostname = hostnames.get(i); + File hostDir = new File(baseDir, hostname); + + if (!hostDir.mkdirs()) { + throw new IOException("Unable to make directory: " + hostDir.getAbsolutePath()); + } + + TlsClientConfig tlsClientConfig = new TlsClientConfig(tlsConfig); + tlsClientConfig.setKeyStore(new File(hostDir, "keystore." + tlsClientConfig.getKeyStoreType().toLowerCase()).getAbsolutePath()); + tlsClientConfig.setKeyStorePassword(keyStorePasswords.get(i)); + tlsClientConfig.setKeyPassword(keyPasswords.get(i)); + tlsClientConfig.setTrustStore(new File(hostDir, "truststore." + tlsClientConfig.getTrustStoreType().toLowerCase()).getAbsolutePath()); + tlsClientConfig.setTrustStorePassword(trustStorePasswords.get(i)); + TlsClientManager tlsClientManager = new TlsClientManager(tlsClientConfig); + KeyPair keyPair = TlsHelper.generateKeyPair(keyPairAlgorithm, keySize); + tlsClientManager.addPrivateKeyToKeyStore(keyPair, NIFI_KEY, CertificateUtils.generateIssuedCertificate(TlsConfig.calcDefaultDn(hostname), + keyPair.getPublic(), certificate, caKeyPair, signingAlgorithm, days), certificate); + tlsClientManager.setCertificateEntry(NIFI_CERT, certificate); + tlsClientManager.addClientConfigurationWriter(new NifiPropertiesTlsClientConfigWriter(niFiPropertiesWriterFactory, outputStreamFactory, new File(hostDir, "nifi.properties"), + hostname, httpsPort)); + tlsClientManager.write(outputStreamFactory); + } + + if (logger.isInfoEnabled()) { + logger.info("Successfully generated TLS configuration"); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java new file mode 100644 index 0000000000..7faabde31f --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLine.java @@ -0,0 +1,183 @@ +/* + * 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.toolkit.tls.standalone; + +import org.apache.commons.cli.CommandLine; +import org.apache.nifi.toolkit.tls.commandLine.BaseCommandLine; +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory; +import org.apache.nifi.toolkit.tls.util.PasswordUtil; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.apache.nifi.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class TlsToolkitStandaloneCommandLine extends BaseCommandLine { + public static final String OUTPUT_DIRECTORY_ARG = "outputDirectory"; + public static final String NIFI_PROPERTIES_FILE_ARG = "nifiPropertiesFile"; + public static final String KEY_STORE_PASSWORD_ARG = "keyStorePassword"; + public static final String TRUST_STORE_PASSWORD_ARG = "trustStorePassword"; + public static final String KEY_PASSWORD_ARG = "keyPassword"; + public static final String HOSTNAMES_ARG = "hostnames"; + public static final String HTTPS_PORT_ARG = "httpsPort"; + + public static final String DEFAULT_OUTPUT_DIRECTORY = new File(".").getPath(); + + public static final String DESCRIPTION = "Creates certificates and config files for nifi cluster."; + + private final PasswordUtil passwordUtil; + private File baseDir; + private List hostnames; + private String httpsPort; + private NiFiPropertiesWriterFactory niFiPropertiesWriterFactory; + private List keyStorePasswords; + private List keyPasswords; + private List trustStorePasswords; + + public TlsToolkitStandaloneCommandLine() { + this(new PasswordUtil()); + } + + protected TlsToolkitStandaloneCommandLine(PasswordUtil passwordUtil) { + super(DESCRIPTION); + this.passwordUtil = passwordUtil; + addOptionWithArg("o", OUTPUT_DIRECTORY_ARG, "The directory to output keystores, truststore, config files.", DEFAULT_OUTPUT_DIRECTORY); + addOptionWithArg("n", HOSTNAMES_ARG, "Comma separated list of hostnames.", TlsConfig.DEFAULT_HOSTNAME); + addOptionWithArg("p", HTTPS_PORT_ARG, "Https port to use.", ""); + addOptionWithArg("f", NIFI_PROPERTIES_FILE_ARG, "Base nifi.properties file to update.", ""); + addOptionWithArg("S", KEY_STORE_PASSWORD_ARG, "Keystore password to use. Must either be one value or one for each host. (autogenerate if not specified)"); + addOptionWithArg("K", KEY_PASSWORD_ARG, "Key password to use. Must either be one value or one for each host. (autogenerate if not specified)"); + addOptionWithArg("P", TRUST_STORE_PASSWORD_ARG, "Keystore password to use. Must either be one value or one for each host. (autogenerate if not specified)"); + } + + public static void main(String[] args) { + TlsHelper.addBouncyCastleProvider(); + TlsToolkitStandaloneCommandLine tlsToolkitStandaloneCommandLine = new TlsToolkitStandaloneCommandLine(); + try { + tlsToolkitStandaloneCommandLine.parse(args); + } catch (CommandLineParseException e) { + System.exit(e.getExitCode()); + } + try { + new TlsToolkitStandalone().createNifiKeystoresAndTrustStores(tlsToolkitStandaloneCommandLine.getBaseDir(), tlsToolkitStandaloneCommandLine.createConfig(), + tlsToolkitStandaloneCommandLine.getNiFiPropertiesWriterFactory(), tlsToolkitStandaloneCommandLine.getHostnames(), tlsToolkitStandaloneCommandLine.getKeyStorePasswords(), + tlsToolkitStandaloneCommandLine.getKeyPasswords(), tlsToolkitStandaloneCommandLine.getTrustStorePasswords(), tlsToolkitStandaloneCommandLine.getHttpsPort()); + } catch (Exception e) { + tlsToolkitStandaloneCommandLine.printUsage("Error creating generating tls configuration. (" + e.getMessage() + ")"); + System.exit(ExitCode.ERROR_GENERATING_CONFIG.ordinal()); + } + System.exit(ExitCode.SUCCESS.ordinal()); + } + + @Override + protected CommandLine doParse(String... args) throws CommandLineParseException { + CommandLine commandLine = super.doParse(args); + String outputDirectory = commandLine.getOptionValue(OUTPUT_DIRECTORY_ARG, DEFAULT_OUTPUT_DIRECTORY); + baseDir = new File(outputDirectory); + hostnames = Arrays.stream(commandLine.getOptionValue(HOSTNAMES_ARG, TlsConfig.DEFAULT_HOSTNAME).split(",")).map(String::trim).collect(Collectors.toList()); + httpsPort = commandLine.getOptionValue(HTTPS_PORT_ARG, ""); + + int numHosts = hostnames.size(); + keyStorePasswords = Collections.unmodifiableList(getPasswords(KEY_STORE_PASSWORD_ARG, commandLine, numHosts)); + keyPasswords = Collections.unmodifiableList(getKeyPasswords(commandLine, keyStorePasswords)); + trustStorePasswords = Collections.unmodifiableList(getPasswords(TRUST_STORE_PASSWORD_ARG, commandLine, numHosts)); + + String nifiPropertiesFile = commandLine.getOptionValue(NIFI_PROPERTIES_FILE_ARG, ""); + try { + if (StringUtils.isEmpty(nifiPropertiesFile)) { + niFiPropertiesWriterFactory = new NiFiPropertiesWriterFactory(); + } else { + niFiPropertiesWriterFactory = new NiFiPropertiesWriterFactory(new FileInputStream(nifiPropertiesFile)); + } + } catch (IOException e) { + printUsageAndThrow("Unable to read nifi.properties from " + (StringUtils.isEmpty(nifiPropertiesFile) ? "classpath" : nifiPropertiesFile), ExitCode.ERROR_READING_NIFI_PROPERTIES); + } + return commandLine; + } + + private List getPasswords(String arg, CommandLine commandLine, int numHosts) throws CommandLineParseException { + String[] optionValues = commandLine.getOptionValues(arg); + if (optionValues == null) { + return IntStream.range(0, numHosts).mapToObj(operand -> passwordUtil.generatePassword()).collect(Collectors.toList()); + } + if (optionValues.length == 1) { + return IntStream.range(0, numHosts).mapToObj(value -> optionValues[0]).collect(Collectors.toList()); + } else if (optionValues.length == numHosts) { + return Arrays.stream(optionValues).collect(Collectors.toList()); + } + return printUsageAndThrow("Expected either 1 value or " + numHosts + " (the number of hostnames) values for " + arg, ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS); + } + + private List getKeyPasswords(CommandLine commandLine, List keyStorePasswords) throws CommandLineParseException { + if (differentPasswordForKeyAndKeystore() || commandLine.hasOption(KEY_PASSWORD_ARG)) { + return getPasswords(KEY_PASSWORD_ARG, commandLine, keyStorePasswords.size()); + } + return new ArrayList<>(keyStorePasswords); + } + + public File getBaseDir() { + return baseDir; + } + + public List getHostnames() { + return hostnames; + } + + public String getHttpsPort() { + return httpsPort; + } + + public NiFiPropertiesWriterFactory getNiFiPropertiesWriterFactory() { + return niFiPropertiesWriterFactory; + } + + public List getKeyStorePasswords() { + return keyStorePasswords; + } + + public List getKeyPasswords() { + return keyPasswords; + } + + public List getTrustStorePasswords() { + return trustStorePasswords; + } + + public TlsConfig createConfig() throws IOException { + TlsConfig tlsConfig = new TlsConfig(); + tlsConfig.setCaHostname(getCertificateAuthorityHostname()); + tlsConfig.setKeyStore("nifi-ca-" + KEYSTORE + getKeyStoreType().toLowerCase()); + tlsConfig.setKeyStoreType(getKeyStoreType()); + tlsConfig.setKeySize(getKeySize()); + tlsConfig.setKeyPairAlgorithm(getKeyAlgorithm()); + tlsConfig.setSigningAlgorithm(getSigningAlgorithm()); + tlsConfig.setDays(getDays()); + tlsConfig.initDefaults(); + return tlsConfig; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/InputStreamFactory.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/InputStreamFactory.java new file mode 100644 index 0000000000..50104f46d4 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/InputStreamFactory.java @@ -0,0 +1,26 @@ +/* + * 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.toolkit.tls.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public interface InputStreamFactory { + InputStream create(File file) throws FileNotFoundException; +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/OutputStreamFactory.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/OutputStreamFactory.java new file mode 100644 index 0000000000..759e34ff34 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/OutputStreamFactory.java @@ -0,0 +1,26 @@ +/* + * 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.toolkit.tls.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.OutputStream; + +public interface OutputStreamFactory { + OutputStream create(File file) throws FileNotFoundException; +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java new file mode 100644 index 0000000000..c26fd7f127 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/PasswordUtil.java @@ -0,0 +1,43 @@ +/* + * 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.toolkit.tls.util; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtil { + private final SecureRandom secureRandom; + + public PasswordUtil() { + this(new SecureRandom()); + } + + public PasswordUtil(SecureRandom secureRandom) { + this.secureRandom = secureRandom; + } + + public String generatePassword() { + // [see http://stackoverflow.com/questions/41107/how-to-generate-a-random-alpha-numeric-string#answer-41156] + String string = Base64.getEncoder().encodeToString(new BigInteger(256, secureRandom).toByteArray()); + while (string.endsWith("=")) { + string = string.substring(0, string.length() - 1); + } + return string; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java new file mode 100644 index 0000000000..0622b9e29d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/util/TlsHelper.java @@ -0,0 +1,112 @@ +/* + * 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.toolkit.tls.util; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.bouncycastle.util.io.pem.PemWriter; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class TlsHelper { + private TlsHelper() { + + } + + public static void addBouncyCastleProvider() { + Security.addProvider(new BouncyCastleProvider()); + } + + private static KeyPairGenerator createKeyPairGenerator(String algorithm, int keySize) throws NoSuchAlgorithmException { + KeyPairGenerator instance = KeyPairGenerator.getInstance(algorithm); + instance.initialize(keySize); + return instance; + } + + public static byte[] calculateHMac(String token, PublicKey publicKey) throws GeneralSecurityException { + SecretKeySpec keySpec = new SecretKeySpec(token.getBytes(StandardCharsets.UTF_8), "RAW"); + Mac mac = Mac.getInstance("Hmac-SHA256", BouncyCastleProvider.PROVIDER_NAME); + mac.init(keySpec); + return mac.doFinal(getKeyIdentifier(publicKey)); + } + + public static byte[] getKeyIdentifier(PublicKey publicKey) throws NoSuchAlgorithmException { + return new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey).getKeyIdentifier(); + } + + public static String pemEncodeJcaObject(Object object) throws IOException { + StringWriter writer = new StringWriter(); + try (PemWriter pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new JcaMiscPEMGenerator(object)); + } + return writer.toString(); + } + + public static JcaPKCS10CertificationRequest parseCsr(String pemEncodedCsr) throws IOException { + try (PEMParser pemParser = new PEMParser(new StringReader(pemEncodedCsr))) { + Object o = pemParser.readObject(); + if (!PKCS10CertificationRequest.class.isInstance(o)) { + throw new IOException("Expecting instance of " + PKCS10CertificationRequest.class + " but got " + o); + } + return new JcaPKCS10CertificationRequest((PKCS10CertificationRequest) o); + } + } + + public static X509Certificate parseCertificate(String pemEncodedCertificate) throws IOException, CertificateException { + try (PEMParser pemParser = new PEMParser(new StringReader(pemEncodedCertificate))) { + Object object = pemParser.readObject(); + if (!X509CertificateHolder.class.isInstance(object)) { + throw new IOException("Expected " + X509CertificateHolder.class); + } + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate((X509CertificateHolder) object); + } + } + + public static KeyPair generateKeyPair(String algorithm, int keySize) throws NoSuchAlgorithmException { + return createKeyPairGenerator(algorithm, keySize).generateKeyPair(); + } + + public static JcaPKCS10CertificationRequest generateCertificationRequest(String requestedDn, KeyPair keyPair, String signingAlgorithm) throws OperatorCreationException { + JcaPKCS10CertificationRequestBuilder jcaPKCS10CertificationRequestBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Principal(requestedDn), keyPair.getPublic()); + JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(signingAlgorithm); + return new JcaPKCS10CertificationRequest(jcaPKCS10CertificationRequestBuilder.build(jcaContentSignerBuilder.build(keyPair.getPrivate()))); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/properties/NifiPropertiesWriterTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/properties/NifiPropertiesWriterTest.java new file mode 100644 index 0000000000..c3ef345f41 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/properties/NifiPropertiesWriterTest.java @@ -0,0 +1,77 @@ +/* + * 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.toolkit.tls.properties; + +import org.apache.nifi.util.NiFiProperties; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class NifiPropertiesWriterTest { + private static NiFiPropertiesWriterFactory nifiPropertiesWriterFactory; + private NiFiPropertiesWriter niFiPropertiesWriter; + + @BeforeClass + public static void beforeClass() throws IOException { + nifiPropertiesWriterFactory = new NiFiPropertiesWriterFactory(); + } + + @Before + public void setup() throws IOException { + niFiPropertiesWriter = nifiPropertiesWriterFactory.create(); + } + + @Test + public void testCanUpdateAllKeys() throws IllegalAccessException, IOException { + String testValue = NifiPropertiesWriterTest.class.getCanonicalName(); + + // Get all property keys from NiFiProperties and set them to testValue + Set propertyKeys = new HashSet<>(); + for (Field field : NiFiProperties.class.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) && field.getType() == String.class) { + if (!field.getName().toLowerCase().startsWith("default")) { + String fieldName = (String) field.get(null); + propertyKeys.add(fieldName); + niFiPropertiesWriter.setPropertyValue(fieldName, testValue); + } + } + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + niFiPropertiesWriter.writeNiFiProperties(outputStream); + + // Verify operation worked + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(outputStream.toByteArray())); + for (String propertyKey : propertyKeys) { + assertEquals(testValue, properties.getProperty(propertyKey)); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java new file mode 100644 index 0000000000..78815003af --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/TlsCertificateAuthorityTest.java @@ -0,0 +1,261 @@ +/* + * 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.toolkit.tls.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.service.client.TlsCertificateAuthorityClient; +import org.apache.nifi.toolkit.tls.service.server.TlsCertificateAuthorityService; +import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone; +import org.apache.nifi.toolkit.tls.util.InputStreamFactory; +import org.apache.nifi.toolkit.tls.util.OutputStreamFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.security.SignatureException; +import java.security.UnrecoverableEntryException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.AdditionalMatchers.or; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TlsCertificateAuthorityTest { + private File serverConfigFile; + private File clientConfigFile; + private OutputStreamFactory outputStreamFactory; + private InputStreamFactory inputStreamFactory; + private TlsConfig serverConfig; + private TlsClientConfig clientConfig; + private ObjectMapper objectMapper; + private ByteArrayOutputStream serverKeyStoreOutputStream; + private ByteArrayOutputStream clientKeyStoreOutputStream; + private ByteArrayOutputStream clientTrustStoreOutputStream; + private ByteArrayOutputStream serverConfigFileOutputStream; + private ByteArrayOutputStream clientConfigFileOutputStream; + + @BeforeClass + public static void beforeClass() { + Security.addProvider(new BouncyCastleProvider()); + } + + @Before + public void setup() throws FileNotFoundException { + objectMapper = new ObjectMapper(); + serverConfigFile = new File("fake.server.config"); + clientConfigFile = new File("fake.client.config"); + String serverKeyStore = "serverKeyStore"; + String clientKeyStore = "clientKeyStore"; + String clientTrustStore = "clientTrustStore"; + serverKeyStoreOutputStream = new ByteArrayOutputStream(); + clientKeyStoreOutputStream = new ByteArrayOutputStream(); + clientTrustStoreOutputStream = new ByteArrayOutputStream(); + serverConfigFileOutputStream = new ByteArrayOutputStream(); + clientConfigFileOutputStream = new ByteArrayOutputStream(); + + String myTestTokenUseSomethingStronger = "myTestTokenUseSomethingStronger"; + int port = availablePort(); + + serverConfig = new TlsConfig(); + serverConfig.setCaHostname("localhost"); + serverConfig.setToken(myTestTokenUseSomethingStronger); + serverConfig.setKeyStore(serverKeyStore); + serverConfig.setPort(port); + serverConfig.setDays(5); + serverConfig.setKeySize(2048); + serverConfig.initDefaults(); + + clientConfig = new TlsClientConfig(); + clientConfig.setCaHostname("localhost"); + clientConfig.setDn("CN=otherHostname,OU=NIFI"); + clientConfig.setKeyStore(clientKeyStore); + clientConfig.setTrustStore(clientTrustStore); + clientConfig.setToken(myTestTokenUseSomethingStronger); + clientConfig.setPort(port); + clientConfig.setKeySize(2048); + clientConfig.initDefaults(); + + outputStreamFactory = mock(OutputStreamFactory.class); + mockReturnOutputStream(outputStreamFactory, new File(serverKeyStore), serverKeyStoreOutputStream); + mockReturnOutputStream(outputStreamFactory, new File(clientKeyStore), clientKeyStoreOutputStream); + mockReturnOutputStream(outputStreamFactory, new File(clientTrustStore), clientTrustStoreOutputStream); + mockReturnOutputStream(outputStreamFactory, serverConfigFile, serverConfigFileOutputStream); + mockReturnOutputStream(outputStreamFactory, clientConfigFile, clientConfigFileOutputStream); + + inputStreamFactory = mock(InputStreamFactory.class); + mockReturnProperties(inputStreamFactory, serverConfigFile, serverConfig); + mockReturnProperties(inputStreamFactory, clientConfigFile, clientConfig); + } + + private void mockReturnProperties(InputStreamFactory inputStreamFactory, File file, TlsConfig tlsConfig) throws FileNotFoundException { + when(inputStreamFactory.create(eq(file))).thenAnswer(invocation -> { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + objectMapper.writeValue(byteArrayOutputStream, tlsConfig); + return new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + }); + } + + private void mockReturnOutputStream(OutputStreamFactory outputStreamFactory, File file, OutputStream outputStream) throws FileNotFoundException { + when(outputStreamFactory.create(or(eq(file), eq(new File(file.getAbsolutePath()))))).thenReturn(outputStream).thenReturn(null); + } + + @Test + public void testClientGetCertDifferentPasswordsForKeyAndKeyStore() throws Exception { + TlsCertificateAuthorityService tlsCertificateAuthorityService = null; + try { + tlsCertificateAuthorityService = new TlsCertificateAuthorityService(outputStreamFactory); + tlsCertificateAuthorityService.start(serverConfig, serverConfigFile.getAbsolutePath(), true); + TlsCertificateAuthorityClient tlsCertificateAuthorityClient = new TlsCertificateAuthorityClient(outputStreamFactory); + tlsCertificateAuthorityClient.generateCertificateAndGetItSigned(clientConfig, null, clientConfigFile.getAbsolutePath(), true); + validate(); + } finally { + if (tlsCertificateAuthorityService != null) { + tlsCertificateAuthorityService.shutdown(); + } + } + } + + @Test + public void testClientGetCertSamePasswordsForKeyAndKeyStore() throws Exception { + TlsCertificateAuthorityService tlsCertificateAuthorityService = null; + try { + tlsCertificateAuthorityService = new TlsCertificateAuthorityService(outputStreamFactory); + tlsCertificateAuthorityService.start(serverConfig, serverConfigFile.getAbsolutePath(), false); + TlsCertificateAuthorityClient tlsCertificateAuthorityClient = new TlsCertificateAuthorityClient(outputStreamFactory); + tlsCertificateAuthorityClient.generateCertificateAndGetItSigned(clientConfig, null, clientConfigFile.getAbsolutePath(), false); + validate(); + } finally { + if (tlsCertificateAuthorityService != null) { + tlsCertificateAuthorityService.shutdown(); + } + } + } + + @Test + public void testTokenMismatch() throws Exception { + serverConfig.setToken("a different token..."); + try { + testClientGetCertSamePasswordsForKeyAndKeyStore(); + fail("Expected error with mismatching token"); + } catch (IOException e) { + assertTrue(e.getMessage().contains("forbidden")); + } + } + + private void validate() throws CertificateException, InvalidKeyException, NoSuchAlgorithmException, KeyStoreException, SignatureException, + NoSuchProviderException, UnrecoverableEntryException, IOException { + Certificate caCertificate = validateServerKeyStore(); + validateClient(caCertificate); + } + + private Certificate validateServerKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableEntryException, + InvalidKeyException, NoSuchProviderException, SignatureException { + serverConfig = objectMapper.readValue(new ByteArrayInputStream(serverConfigFileOutputStream.toByteArray()), TlsConfig.class); + + KeyStore serverKeyStore = KeyStore.getInstance(serverConfig.getKeyStoreType()); + serverKeyStore.load(new ByteArrayInputStream(serverKeyStoreOutputStream.toByteArray()), serverConfig.getKeyStorePassword().toCharArray()); + KeyStore.Entry serverKeyEntry = serverKeyStore.getEntry(TlsToolkitStandalone.NIFI_KEY, new KeyStore.PasswordProtection(serverConfig.getKeyPassword().toCharArray())); + + assertTrue(serverKeyEntry instanceof KeyStore.PrivateKeyEntry); + KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) serverKeyEntry; + Certificate[] certificateChain = privateKeyEntry.getCertificateChain(); + assertEquals(1, certificateChain.length); + Certificate caCertificate = certificateChain[0]; + caCertificate.verify(caCertificate.getPublicKey()); + assertPrivateAndPublicKeyMatch(privateKeyEntry.getPrivateKey(), caCertificate.getPublicKey()); + return caCertificate; + } + + private void validateClient(Certificate caCertificate) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, + UnrecoverableEntryException, InvalidKeyException, NoSuchProviderException, SignatureException { + clientConfig = objectMapper.readValue(new ByteArrayInputStream(clientConfigFileOutputStream.toByteArray()), TlsClientConfig.class); + + KeyStore clientKeyStore = KeyStore.getInstance(clientConfig.getKeyStoreType()); + clientKeyStore.load(new ByteArrayInputStream(clientKeyStoreOutputStream.toByteArray()), clientConfig.getKeyStorePassword().toCharArray()); + KeyStore.Entry clientKeyStoreEntry = clientKeyStore.getEntry(TlsToolkitStandalone.NIFI_KEY, new KeyStore.PasswordProtection(clientConfig.getKeyPassword().toCharArray())); + + assertTrue(clientKeyStoreEntry instanceof KeyStore.PrivateKeyEntry); + KeyStore.PrivateKeyEntry clientPrivateKeyEntry = (KeyStore.PrivateKeyEntry) clientKeyStoreEntry; + Certificate[] certificateChain = clientPrivateKeyEntry.getCertificateChain(); + assertEquals(2, certificateChain.length); + assertEquals(caCertificate, certificateChain[1]); + certificateChain[0].verify(caCertificate.getPublicKey()); + assertPrivateAndPublicKeyMatch(clientPrivateKeyEntry.getPrivateKey(), certificateChain[0].getPublicKey()); + + KeyStore clientTrustStore = KeyStore.getInstance(clientConfig.getTrustStoreType()); + clientTrustStore.load(new ByteArrayInputStream(clientTrustStoreOutputStream.toByteArray()), clientConfig.getTrustStorePassword().toCharArray()); + assertEquals(caCertificate, clientTrustStore.getCertificate(TlsToolkitStandalone.NIFI_CERT)); + } + + private void assertPrivateAndPublicKeyMatch(PrivateKey privateKey, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Signature signature = Signature.getInstance(TlsConfig.DEFAULT_SIGNING_ALGORITHM); + signature.initSign(privateKey); + byte[] bytes = "test string".getBytes(StandardCharsets.UTF_8); + signature.update(bytes); + + Signature verify = Signature.getInstance(TlsConfig.DEFAULT_SIGNING_ALGORITHM); + verify.initVerify(publicKey); + verify.update(bytes); + verify.verify(signature.sign()); + } + + /** + * Will determine the available port used by ca server + */ + private int availablePort() { + ServerSocket s = null; + try { + s = new ServerSocket(0); + s.setReuseAddress(true); + return s.getLocalPort(); + } catch (Exception e) { + throw new IllegalStateException("Failed to discover available port.", e); + } finally { + try { + s.close(); + } catch (IOException e) { + // ignore + } + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLineTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLineTest.java new file mode 100644 index 0000000000..40a8268005 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateAuthorityClientCommandLineTest.java @@ -0,0 +1,148 @@ +/* + * 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.toolkit.tls.service.client; + +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.InetAddress; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +public class TlsCertificateAuthorityClientCommandLineTest { + + private TlsCertificateAuthorityClientCommandLine tlsCertificateAuthorityClientCommandLine; + private String testToken; + + @Before + public void setup() { + tlsCertificateAuthorityClientCommandLine = new TlsCertificateAuthorityClientCommandLine(); + testToken = "testToken"; + } + + @Test + public void testNoToken() { + try { + tlsCertificateAuthorityClientCommandLine.parse(new String[0]); + fail("Expected failure with no token argument"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_TOKEN_ARG_EMPTY.ordinal(), e.getExitCode()); + } + } + + @Test + public void testDefaults() throws CommandLineParseException, IOException { + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken); + TlsClientConfig clientConfig = tlsCertificateAuthorityClientCommandLine.createClientConfig(); + + assertEquals(TlsConfig.DEFAULT_HOSTNAME, clientConfig.getCaHostname()); + Assert.assertEquals(TlsConfig.calcDefaultDn(InetAddress.getLocalHost().getHostName()), clientConfig.getDn()); + assertEquals(TlsCertificateAuthorityClientCommandLine.KEYSTORE + TlsConfig.DEFAULT_KEY_STORE_TYPE.toLowerCase(), clientConfig.getKeyStore()); + assertEquals(TlsConfig.DEFAULT_KEY_STORE_TYPE, clientConfig.getKeyStoreType()); + assertNull(clientConfig.getKeyStorePassword()); + assertNull(clientConfig.getKeyPassword()); + assertEquals(TlsCertificateAuthorityClientCommandLine.TRUSTSTORE + TlsConfig.DEFAULT_KEY_STORE_TYPE.toLowerCase(), clientConfig.getTrustStore()); + assertEquals(TlsConfig.DEFAULT_KEY_STORE_TYPE, clientConfig.getTrustStoreType()); + assertNull(clientConfig.getTrustStorePassword()); + assertEquals(TlsConfig.DEFAULT_KEY_SIZE, clientConfig.getKeySize()); + assertEquals(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, clientConfig.getKeyPairAlgorithm()); + assertEquals(testToken, clientConfig.getToken()); + assertEquals(TlsConfig.DEFAULT_PORT, clientConfig.getPort()); + assertEquals(TlsCertificateAuthorityClientCommandLine.DEFAULT_CONFIG_JSON, tlsCertificateAuthorityClientCommandLine.getConfigJson()); + assertEquals(TlsCertificateAuthorityClientCommandLine.DEFAULT_CERTIFICATE_DIRECTORY, tlsCertificateAuthorityClientCommandLine.getCertificateDirectory()); + } + + @Test + public void testKeySize() throws CommandLineParseException { + int keySize = 1234; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-k", Integer.toString(keySize)); + assertEquals(keySize, tlsCertificateAuthorityClientCommandLine.getKeySize()); + } + + @Test + public void testKeyPairAlgorithm() throws CommandLineParseException { + String testAlgorithm = "testAlgorithm"; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-a", testAlgorithm); + assertEquals(testAlgorithm, tlsCertificateAuthorityClientCommandLine.getKeyAlgorithm()); + } + + @Test + public void testHelp() { + try { + tlsCertificateAuthorityClientCommandLine.parse("-h"); + fail("Expected exception"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.HELP.ordinal(), e.getExitCode()); + } + } + + @Test + public void testCaHostname() throws CommandLineParseException, IOException { + String testCaHostname = "testCaHostname"; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-c", testCaHostname); + assertEquals(testCaHostname, tlsCertificateAuthorityClientCommandLine.createClientConfig().getCaHostname()); + } + + @Test + public void testDn() throws CommandLineParseException, IOException { + String testDn = "testDn"; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-D", testDn); + assertEquals(testDn, tlsCertificateAuthorityClientCommandLine.createClientConfig().getDn()); + } + + @Test + public void testPort() throws CommandLineParseException, IOException { + int testPort = 2345; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-p", Integer.toString(testPort)); + assertEquals(testPort, tlsCertificateAuthorityClientCommandLine.createClientConfig().getPort()); + } + + @Test + public void testKeyStoreType() throws CommandLineParseException, IOException { + String testType = "testType"; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-T", testType); + + TlsClientConfig clientConfig = tlsCertificateAuthorityClientCommandLine.createClientConfig(); + assertEquals(testType, clientConfig.getKeyStoreType()); + assertEquals(testType, clientConfig.getTrustStoreType()); + assertEquals(TlsCertificateAuthorityClientCommandLine.KEYSTORE + testType.toLowerCase(), clientConfig.getKeyStore()); + assertEquals(TlsCertificateAuthorityClientCommandLine.TRUSTSTORE + testType.toLowerCase(), clientConfig.getTrustStore()); + } + + @Test + public void testConfigFile() throws CommandLineParseException { + String testPath = "/1/2/3/4"; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-f", testPath); + assertEquals(testPath, tlsCertificateAuthorityClientCommandLine.getConfigJson()); + } + + @Test + public void testCertificateFile() throws CommandLineParseException { + String testCertificateFile = "testCertificateFile"; + tlsCertificateAuthorityClientCommandLine.parse("-t", testToken, "-C", testCertificateFile); + assertEquals(testCertificateFile, tlsCertificateAuthorityClientCommandLine.getCertificateDirectory()); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java new file mode 100644 index 0000000000..bdc01dfb9d --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/client/TlsCertificateSigningRequestPerformerTest.java @@ -0,0 +1,236 @@ +/* + * 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.toolkit.tls.service.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpHost; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityRequest; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityResponse; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.eclipse.jetty.server.Response; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TlsCertificateSigningRequestPerformerTest { + @Mock + Supplier httpClientBuilderSupplier; + + @Mock + HttpClientBuilder httpClientBuilder; + + @Mock + CloseableHttpClient closeableHttpClient; + + @Mock + TlsClientConfig tlsClientConfig; + + X509Certificate caCertificate; + + X509Certificate signedCsr; + + ObjectMapper objectMapper; + KeyPair keyPair; + TlsCertificateSigningRequestPerformer tlsCertificateSigningRequestPerformer; + String testToken; + String testCaHostname; + int testPort; + List certificates; + + TlsCertificateAuthorityResponse tlsCertificateAuthorityResponse; + int statusCode; + private byte[] testHmac; + private String testSignedCsr; + + @BeforeClass + public static void before() { + TlsHelper.addBouncyCastleProvider(); + } + + @Before + public void setup() throws GeneralSecurityException, OperatorCreationException, IOException { + objectMapper = new ObjectMapper(); + keyPair = TlsHelper.generateKeyPair(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, TlsConfig.DEFAULT_KEY_SIZE); + + testToken = "testToken"; + testCaHostname = "testCaHostname"; + testPort = 8993; + certificates = new ArrayList<>(); + + when(tlsClientConfig.getToken()).thenReturn(testToken); + when(tlsClientConfig.getCaHostname()).thenReturn(testCaHostname); + when(tlsClientConfig.getDn()).thenReturn(TlsConfig.calcDefaultDn(testCaHostname)); + when(tlsClientConfig.getPort()).thenReturn(testPort); + when(tlsClientConfig.createCertificateSigningRequestPerformer()).thenReturn(tlsCertificateSigningRequestPerformer); + when(tlsClientConfig.getSigningAlgorithm()).thenReturn(TlsConfig.DEFAULT_SIGNING_ALGORITHM); + JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = TlsHelper.generateCertificationRequest(tlsClientConfig.getDn(), keyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); + String testCsrPem = TlsHelper.pemEncodeJcaObject(jcaPKCS10CertificationRequest); + when(httpClientBuilderSupplier.get()).thenReturn(httpClientBuilder); + when(httpClientBuilder.build()).thenAnswer(invocation -> { + Field sslSocketFactory = HttpClientBuilder.class.getDeclaredField("sslSocketFactory"); + sslSocketFactory.setAccessible(true); + Object o = sslSocketFactory.get(httpClientBuilder); + Field field = TlsCertificateAuthorityClientSocketFactory.class.getDeclaredField("certificates"); + field.setAccessible(true); + ((List) field.get(o)).addAll(certificates); + return closeableHttpClient; + }); + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenAnswer(i -> statusCode); + when(closeableHttpClient.execute(eq(new HttpHost(testCaHostname, testPort, "https")), any(HttpPost.class))).thenAnswer(invocation -> { + HttpPost httpPost = (HttpPost) invocation.getArguments()[1]; + TlsCertificateAuthorityRequest tlsCertificateAuthorityRequest = objectMapper.readValue(httpPost.getEntity().getContent(), TlsCertificateAuthorityRequest.class); + assertEquals(tlsCertificateAuthorityRequest.getCsr(), testCsrPem); + CloseableHttpResponse closeableHttpResponse = mock(CloseableHttpResponse.class); + when(closeableHttpResponse.getEntity()).thenAnswer(i -> { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + objectMapper.writeValue(byteArrayOutputStream, tlsCertificateAuthorityResponse); + return new ByteArrayEntity(byteArrayOutputStream.toByteArray()); + }); + when(closeableHttpResponse.getStatusLine()).thenReturn(statusLine); + return closeableHttpResponse; + }); + KeyPair caKeyPair = TlsHelper.generateKeyPair(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, TlsConfig.DEFAULT_KEY_SIZE); + caCertificate = CertificateUtils.generateSelfSignedX509Certificate(caKeyPair, "CN=fakeCa", TlsConfig.DEFAULT_SIGNING_ALGORITHM, TlsConfig.DEFAULT_DAYS); + testHmac = TlsHelper.calculateHMac(testToken, caCertificate.getPublicKey()); + signedCsr = CertificateUtils.generateIssuedCertificate(jcaPKCS10CertificationRequest.getSubject().toString(), jcaPKCS10CertificationRequest.getPublicKey(), + caCertificate, caKeyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM, TlsConfig.DEFAULT_DAYS); + testSignedCsr = TlsHelper.pemEncodeJcaObject(signedCsr); + + tlsCertificateSigningRequestPerformer = new TlsCertificateSigningRequestPerformer(httpClientBuilderSupplier, tlsClientConfig); + } + + @Test + public void testOk() throws Exception { + certificates.add(caCertificate); + statusCode = Response.SC_OK; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse(testHmac, testSignedCsr); + tlsCertificateSigningRequestPerformer.perform(keyPair); + } + + @Test + public void testBadStatusCode() throws Exception { + statusCode = Response.SC_FORBIDDEN; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse(); + try { + tlsCertificateSigningRequestPerformer.perform(keyPair); + fail("Expected IOE"); + } catch (IOException e) { + assertTrue(e.getMessage().startsWith(TlsCertificateSigningRequestPerformer.RECEIVED_RESPONSE_CODE + statusCode)); + } + } + + @Test + public void test0CertSize() throws Exception { + statusCode = Response.SC_OK; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse(); + try { + tlsCertificateSigningRequestPerformer.perform(keyPair); + fail("Expected IOE"); + } catch (IOException e) { + assertEquals(TlsCertificateSigningRequestPerformer.EXPECTED_ONE_CERTIFICATE, e.getMessage()); + } + } + + @Test + public void test2CertSize() throws Exception { + certificates.add(caCertificate); + certificates.add(caCertificate); + statusCode = Response.SC_OK; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse(); + try { + tlsCertificateSigningRequestPerformer.perform(keyPair); + fail("Expected IOE"); + } catch (IOException e) { + assertEquals(TlsCertificateSigningRequestPerformer.EXPECTED_ONE_CERTIFICATE, e.getMessage()); + } + } + + @Test + public void testNoHmac() throws Exception { + certificates.add(caCertificate); + statusCode = Response.SC_OK; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse(null, testSignedCsr); + try { + tlsCertificateSigningRequestPerformer.perform(keyPair); + fail("Expected IOE"); + } catch (IOException e) { + assertEquals(TlsCertificateSigningRequestPerformer.EXPECTED_RESPONSE_TO_CONTAIN_HMAC, e.getMessage()); + } + } + + @Test + public void testBadHmac() throws Exception { + certificates.add(caCertificate); + statusCode = Response.SC_OK; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse("badHmac".getBytes(StandardCharsets.UTF_8), testSignedCsr); + try { + tlsCertificateSigningRequestPerformer.perform(keyPair); + fail("Expected IOE"); + } catch (IOException e) { + assertEquals(TlsCertificateSigningRequestPerformer.UNEXPECTED_HMAC_RECEIVED_POSSIBLE_MAN_IN_THE_MIDDLE, e.getMessage()); + } + } + + @Test + public void testNoCertificate() throws Exception { + certificates.add(caCertificate); + statusCode = Response.SC_OK; + tlsCertificateAuthorityResponse = new TlsCertificateAuthorityResponse(testHmac, null); + try { + tlsCertificateSigningRequestPerformer.perform(keyPair); + fail("Expected IOE"); + } catch (IOException e) { + assertEquals(TlsCertificateSigningRequestPerformer.EXPECTED_RESPONSE_TO_CONTAIN_CERTIFICATE, e.getMessage()); + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceCommandLineTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceCommandLineTest.java new file mode 100644 index 0000000000..9654186d48 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceCommandLineTest.java @@ -0,0 +1,116 @@ +/* + * 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.toolkit.tls.service.server; + +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.util.InputStreamFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@RunWith(MockitoJUnitRunner.class) +public class TlsCertificateAuthorityServiceCommandLineTest { + @Mock + InputStreamFactory inputStreamFactory; + + TlsCertificateAuthorityServiceCommandLine tlsCertificateAuthorityServiceCommandLine; + + String testToken; + + @Before + public void setup() { + tlsCertificateAuthorityServiceCommandLine = new TlsCertificateAuthorityServiceCommandLine(inputStreamFactory); + testToken = "testToken"; + } + + @Test + public void testDefaults() throws CommandLineParseException, IOException { + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken); + TlsConfig tlsConfig = tlsCertificateAuthorityServiceCommandLine.createConfig(); + assertEquals(TlsConfig.DEFAULT_HOSTNAME, tlsConfig.getCaHostname()); + assertEquals(testToken, tlsConfig.getToken()); + assertEquals(TlsConfig.DEFAULT_PORT, tlsConfig.getPort()); + assertEquals(TlsConfig.DEFAULT_KEY_STORE_TYPE, tlsConfig.getKeyStoreType()); + assertEquals(TlsCertificateAuthorityServiceCommandLine.NIFI_CA_KEYSTORE + tlsConfig.getKeyStoreType().toLowerCase(), tlsConfig.getKeyStore()); + assertNull(tlsConfig.getKeyStorePassword()); + assertNull(tlsConfig.getKeyPassword()); + assertEquals(TlsConfig.DEFAULT_KEY_SIZE, tlsConfig.getKeySize()); + assertEquals(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, tlsConfig.getKeyPairAlgorithm()); + assertEquals(TlsConfig.DEFAULT_SIGNING_ALGORITHM, tlsConfig.getSigningAlgorithm()); + assertEquals(TlsConfig.DEFAULT_DAYS, tlsConfig.getDays()); + } + + @Test + public void testCaHostname() throws CommandLineParseException, IOException { + String testCaHostname = "testCaHostname"; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-c", testCaHostname); + assertEquals(testCaHostname, tlsCertificateAuthorityServiceCommandLine.createConfig().getCaHostname()); + } + + @Test + public void testPort() throws CommandLineParseException, IOException { + int testPort = 4321; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-p", Integer.toString(testPort)); + assertEquals(testPort, tlsCertificateAuthorityServiceCommandLine.createConfig().getPort()); + } + + @Test + public void testKeyStoreType() throws CommandLineParseException, IOException { + String testKeyStoreType = "testKeyStoreType"; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-T", testKeyStoreType); + TlsConfig tlsConfig = tlsCertificateAuthorityServiceCommandLine.createConfig(); + assertEquals(testKeyStoreType, tlsConfig.getKeyStoreType()); + assertEquals(TlsCertificateAuthorityServiceCommandLine.NIFI_CA_KEYSTORE + tlsConfig.getKeyStoreType().toLowerCase(), tlsConfig.getKeyStore()); + } + + @Test + public void testKeySize() throws CommandLineParseException, IOException { + int testKeySize = 8192; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-k", Integer.toString(testKeySize)); + assertEquals(testKeySize, tlsCertificateAuthorityServiceCommandLine.createConfig().getKeySize()); + } + + @Test + public void testKeyPairAlgorithm() throws CommandLineParseException, IOException { + String testAlgorithm = "testAlgorithm"; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-a", testAlgorithm); + assertEquals(testAlgorithm, tlsCertificateAuthorityServiceCommandLine.createConfig().getKeyPairAlgorithm()); + } + + @Test + public void testSigningAlgorithm() throws CommandLineParseException, IOException { + String testSigningAlgorithm = "testSigningAlgorithm"; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-s", testSigningAlgorithm); + assertEquals(testSigningAlgorithm, tlsCertificateAuthorityServiceCommandLine.createConfig().getSigningAlgorithm()); + } + + @Test + public void testDays() throws CommandLineParseException, IOException { + int days = 1234; + tlsCertificateAuthorityServiceCommandLine.parse("-t", testToken, "-d", Integer.toString(days)); + assertEquals(days, tlsCertificateAuthorityServiceCommandLine.createConfig().getDays()); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java new file mode 100644 index 0000000000..bf355fa0ee --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/service/server/TlsCertificateAuthorityServiceHandlerTest.java @@ -0,0 +1,187 @@ +/* + * 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.toolkit.tls.service.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityRequest; +import org.apache.nifi.toolkit.tls.service.dto.TlsCertificateAuthorityResponse; +import org.apache.nifi.toolkit.tls.util.TlsHelper; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.crmf.CRMFException; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.X509Certificate; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TlsCertificateAuthorityServiceHandlerTest { + X509Certificate caCert; + + @Mock + Request baseRequest; + + @Mock + HttpServletRequest httpServletRequest; + + @Mock + HttpServletResponse httpServletResponse; + + JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest; + + KeyPair keyPair; + + String testToken; + + String testPemEncodedCsr; + + String testPemEncodedSignedCertificate; + + ObjectMapper objectMapper; + + TlsCertificateAuthorityServiceHandler tlsCertificateAuthorityServiceHandler; + + TlsCertificateAuthorityRequest tlsCertificateAuthorityRequest; + + int statusCode; + + StringWriter response; + private byte[] testCaHmac; + private byte[] testHmac; + private String requestedDn; + private KeyPair certificateKeyPair; + + @BeforeClass + public static void before() { + TlsHelper.addBouncyCastleProvider(); + } + + @Before + public void setup() throws Exception { + testToken = "testToken"; + testPemEncodedSignedCertificate = "testPemEncodedSignedCertificate"; + keyPair = TlsHelper.generateKeyPair(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, TlsConfig.DEFAULT_KEY_SIZE); + objectMapper = new ObjectMapper(); + when(httpServletRequest.getReader()).thenAnswer(invocation -> { + StringWriter stringWriter = new StringWriter(); + objectMapper.writeValue(stringWriter, tlsCertificateAuthorityRequest); + return new BufferedReader(new StringReader(stringWriter.toString())); + }); + doAnswer(invocation -> statusCode = (int) invocation.getArguments()[0]).when(httpServletResponse).setStatus(anyInt()); + doAnswer(invocation -> { + statusCode = (int) invocation.getArguments()[0]; + StringWriter stringWriter = new StringWriter(); + stringWriter.write((String) invocation.getArguments()[1]); + response = stringWriter; + return null; + }).when(httpServletResponse).sendError(anyInt(), anyString()); + when(httpServletResponse.getWriter()).thenAnswer(invocation -> { + response = new StringWriter(); + return new PrintWriter(response); + }); + caCert = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=fakeCa", TlsConfig.DEFAULT_SIGNING_ALGORITHM, TlsConfig.DEFAULT_DAYS); + requestedDn = TlsConfig.calcDefaultDn(TlsConfig.DEFAULT_HOSTNAME); + certificateKeyPair = TlsHelper.generateKeyPair(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM, TlsConfig.DEFAULT_KEY_SIZE); + jcaPKCS10CertificationRequest = TlsHelper.generateCertificationRequest(requestedDn, certificateKeyPair, TlsConfig.DEFAULT_SIGNING_ALGORITHM); + testPemEncodedCsr = TlsHelper.pemEncodeJcaObject(jcaPKCS10CertificationRequest); + tlsCertificateAuthorityServiceHandler = new TlsCertificateAuthorityServiceHandler(TlsConfig.DEFAULT_SIGNING_ALGORITHM, TlsConfig.DEFAULT_DAYS, testToken, caCert, keyPair, objectMapper); + testHmac = TlsHelper.calculateHMac(testToken, jcaPKCS10CertificationRequest.getPublicKey()); + testCaHmac = TlsHelper.calculateHMac(testToken, caCert.getPublicKey()); + } + + private TlsCertificateAuthorityResponse getResponse() throws IOException { + return objectMapper.readValue(new StringReader(response.toString()), TlsCertificateAuthorityResponse.class); + } + + @Test + public void testSuccess() throws IOException, ServletException, GeneralSecurityException, CRMFException { + tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest(testHmac, testPemEncodedCsr); + tlsCertificateAuthorityServiceHandler.handle(null, baseRequest, httpServletRequest, httpServletResponse); + assertEquals(Response.SC_OK, statusCode); + assertArrayEquals(testCaHmac, getResponse().getHmac()); + X509Certificate certificate = TlsHelper.parseCertificate(getResponse().getPemEncodedCertificate()); + assertEquals(certificateKeyPair.getPublic(), certificate.getPublicKey()); + assertEquals(new X500Name(requestedDn), new X500Name(certificate.getSubjectDN().toString())); + certificate.verify(caCert.getPublicKey()); + } + + @Test + public void testNoCsr() throws IOException, ServletException { + tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest(testHmac, null); + tlsCertificateAuthorityServiceHandler.handle(null, baseRequest, httpServletRequest, httpServletResponse); + assertEquals(Response.SC_BAD_REQUEST, statusCode); + assertEquals(TlsCertificateAuthorityServiceHandler.CSR_FIELD_MUST_BE_SET, getResponse().getError()); + } + + @Test + public void testNoHmac() throws IOException, ServletException { + tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest(null, testPemEncodedCsr); + tlsCertificateAuthorityServiceHandler.handle(null, baseRequest, httpServletRequest, httpServletResponse); + assertEquals(Response.SC_BAD_REQUEST, statusCode); + assertEquals(TlsCertificateAuthorityServiceHandler.HMAC_FIELD_MUST_BE_SET, getResponse().getError()); + } + + @Test + public void testForbidden() throws IOException, ServletException, NoSuchAlgorithmException, CRMFException, NoSuchProviderException, InvalidKeyException { + tlsCertificateAuthorityRequest = new TlsCertificateAuthorityRequest("badHmac".getBytes(StandardCharsets.UTF_8), testPemEncodedCsr); + tlsCertificateAuthorityServiceHandler.handle(null, baseRequest, httpServletRequest, httpServletResponse); + assertEquals(Response.SC_FORBIDDEN, statusCode); + assertEquals(TlsCertificateAuthorityServiceHandler.FORBIDDEN, getResponse().getError()); + } + + @Test(expected = ServletException.class) + public void testServletException() throws IOException, ServletException { + tlsCertificateAuthorityServiceHandler.handle(null, baseRequest, httpServletRequest, httpServletResponse); + } + + @After + public void verifyHandled() { + verify(baseRequest).setHandled(true); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLineTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLineTest.java new file mode 100644 index 0000000000..2152d45929 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneCommandLineTest.java @@ -0,0 +1,325 @@ +/* + * 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.toolkit.tls.standalone; + +import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriter; +import org.apache.nifi.toolkit.tls.util.PasswordUtil; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.List; +import java.util.Properties; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class TlsToolkitStandaloneCommandLineTest { + private SecureRandom secureRandom; + private TlsToolkitStandaloneCommandLine tlsToolkitStandaloneCommandLine; + + @Before + public void setup() { + secureRandom = mock(SecureRandom.class); + doAnswer(new ForwardsInvocations(new Random())).when(secureRandom).nextBytes(any(byte[].class)); + tlsToolkitStandaloneCommandLine = new TlsToolkitStandaloneCommandLine(new PasswordUtil(secureRandom)); + } + + @Test + public void testHelp() { + try { + tlsToolkitStandaloneCommandLine.parse("-h"); + fail("Expected usage and help exit"); + } catch (CommandLineParseException e) { + Assert.assertEquals(ExitCode.HELP.ordinal(), e.getExitCode()); + } + } + + @Test + public void testUnknownArg() { + try { + tlsToolkitStandaloneCommandLine.parse("--unknownArg"); + fail("Expected error parsing command line"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_PARSING_COMMAND_LINE.ordinal(), e.getExitCode()); + } + } + + @Test + public void testKeyAlgorithm() throws CommandLineParseException, IOException { + String testKeyAlgorithm = "testKeyAlgorithm"; + tlsToolkitStandaloneCommandLine.parse("-a", testKeyAlgorithm); + assertEquals(testKeyAlgorithm, tlsToolkitStandaloneCommandLine.createConfig().getKeyPairAlgorithm()); + } + + @Test + public void testKeySizeArgNotInteger() { + try { + tlsToolkitStandaloneCommandLine.parse("-k", "badVal"); + fail("Expected bad keysize exit code"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_PARSING_INT_ARG.ordinal(), e.getExitCode()); + } + } + + @Test + public void testKeySize() throws CommandLineParseException, IOException { + int testKeySize = 4096; + tlsToolkitStandaloneCommandLine.parse("-k", Integer.toString(testKeySize)); + assertEquals(testKeySize, tlsToolkitStandaloneCommandLine.createConfig().getKeySize()); + } + + @Test + public void testSigningAlgorithm() throws CommandLineParseException, IOException { + String testSigningAlgorithm = "testSigningAlgorithm"; + tlsToolkitStandaloneCommandLine.parse("-s", testSigningAlgorithm); + assertEquals(testSigningAlgorithm, tlsToolkitStandaloneCommandLine.createConfig().getSigningAlgorithm()); + } + + @Test + public void testDaysNotInteger() { + try { + tlsToolkitStandaloneCommandLine.parse("-d", "badVal"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_PARSING_INT_ARG.ordinal(), e.getExitCode()); + } + } + + @Test + public void testDays() throws CommandLineParseException, IOException { + int testDays = 29; + tlsToolkitStandaloneCommandLine.parse("-d", Integer.toString(testDays)); + assertEquals(testDays, tlsToolkitStandaloneCommandLine.createConfig().getDays()); + } + + @Test + public void testKeyStoreType() throws CommandLineParseException { + String testKeyStoreType = "testKeyStoreType"; + tlsToolkitStandaloneCommandLine.parse("-T", testKeyStoreType); + assertEquals(testKeyStoreType, tlsToolkitStandaloneCommandLine.getKeyStoreType()); + } + + @Test + public void testOutputDirectory() throws CommandLineParseException { + String testPath = "/fake/path/doesnt/exist"; + tlsToolkitStandaloneCommandLine.parse("-o", testPath); + assertEquals(testPath, tlsToolkitStandaloneCommandLine.getBaseDir().getAbsolutePath()); + } + + @Test + public void testHostnames() throws CommandLineParseException { + String nifi1 = "nifi1"; + String nifi2 = "nifi2"; + + tlsToolkitStandaloneCommandLine.parse("-n", nifi1 + " , " + nifi2); + + List hostnames = tlsToolkitStandaloneCommandLine.getHostnames(); + assertEquals(2, hostnames.size()); + assertEquals(nifi1, hostnames.get(0)); + assertEquals(nifi2, hostnames.get(1)); + } + + @Test + public void testHttpsPort() throws CommandLineParseException { + String testPort = "8998"; + tlsToolkitStandaloneCommandLine.parse("-p", testPort); + assertEquals(testPort, tlsToolkitStandaloneCommandLine.getHttpsPort()); + } + + @Test + public void testNifiPropertiesFile() throws CommandLineParseException, IOException { + tlsToolkitStandaloneCommandLine.parse("-f", TlsToolkitStandaloneTest.TEST_NIFI_PROPERTIES); + assertEquals(TlsToolkitStandaloneTest.FAKE_VALUE, getProperties().get(TlsToolkitStandaloneTest.NIFI_FAKE_PROPERTY)); + } + + @Test + public void testNifiPropertiesFileDefault() throws CommandLineParseException, IOException { + tlsToolkitStandaloneCommandLine.parse(); + assertNull(getProperties().get(TlsToolkitStandaloneTest.NIFI_FAKE_PROPERTY)); + } + + @Test + public void testBadNifiPropertiesFile() { + try { + tlsToolkitStandaloneCommandLine.parse("-f", "/this/file/should/not/exist.txt"); + fail("Expected error when unable to read file"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_READING_NIFI_PROPERTIES.ordinal(), e.getExitCode()); + } + } + + @Test + public void testNotSameKeyAndKeystorePassword() throws CommandLineParseException { + tlsToolkitStandaloneCommandLine.parse("-g"); + List keyStorePasswords = tlsToolkitStandaloneCommandLine.getKeyStorePasswords(); + List keyPasswords = tlsToolkitStandaloneCommandLine.getKeyPasswords(); + assertEquals(1, tlsToolkitStandaloneCommandLine.getHostnames().size()); + assertEquals(1, keyStorePasswords.size()); + assertEquals(1, keyPasswords.size()); + assertNotEquals(keyStorePasswords.get(0), keyPasswords.get(0)); + } + + @Test + public void testSameKeyAndKeystorePassword() throws CommandLineParseException { + tlsToolkitStandaloneCommandLine.parse(); + List keyStorePasswords = tlsToolkitStandaloneCommandLine.getKeyStorePasswords(); + List keyPasswords = tlsToolkitStandaloneCommandLine.getKeyPasswords(); + assertEquals(1, tlsToolkitStandaloneCommandLine.getHostnames().size()); + assertEquals(1, keyStorePasswords.size()); + assertEquals(1, keyPasswords.size()); + assertEquals(keyStorePasswords.get(0), keyPasswords.get(0)); + } + + @Test + public void testSameKeyAndKeystorePasswordWithKeystorePasswordSpecified() throws CommandLineParseException { + String testPassword = "testPassword"; + tlsToolkitStandaloneCommandLine.parse("-S", testPassword); + List keyStorePasswords = tlsToolkitStandaloneCommandLine.getKeyStorePasswords(); + assertEquals(1, keyStorePasswords.size()); + assertEquals(testPassword, keyStorePasswords.get(0)); + assertEquals(keyStorePasswords, tlsToolkitStandaloneCommandLine.getKeyPasswords()); + } + + @Test + public void testSameKeyAndKeystorePasswordWithKeyPasswordSpecified() throws CommandLineParseException { + String testPassword = "testPassword"; + tlsToolkitStandaloneCommandLine.parse("-K", testPassword); + List keyPasswords = tlsToolkitStandaloneCommandLine.getKeyPasswords(); + assertNotEquals(tlsToolkitStandaloneCommandLine.getKeyStorePasswords(), keyPasswords); + assertEquals(1, keyPasswords.size()); + assertEquals(testPassword, keyPasswords.get(0)); + } + + @Test + public void testKeyStorePasswordArg() throws CommandLineParseException { + String testPassword = "testPassword"; + tlsToolkitStandaloneCommandLine.parse("-S", testPassword); + List keyStorePasswords = tlsToolkitStandaloneCommandLine.getKeyStorePasswords(); + assertEquals(1, keyStorePasswords.size()); + assertEquals(testPassword, keyStorePasswords.get(0)); + } + + @Test + public void testMultipleKeystorePasswordArgs() throws CommandLineParseException { + String testPassword1 = "testPassword1"; + String testPassword2 = "testPassword2"; + tlsToolkitStandaloneCommandLine.parse("-n", "nifi1,nifi2", "-S", testPassword1, "-S", testPassword2); + List keyStorePasswords = tlsToolkitStandaloneCommandLine.getKeyStorePasswords(); + assertEquals(2, keyStorePasswords.size()); + assertEquals(testPassword1, keyStorePasswords.get(0)); + assertEquals(testPassword2, keyStorePasswords.get(1)); + } + + @Test + public void testMultipleKeystorePasswordArgSingleHost() { + String testPassword1 = "testPassword1"; + String testPassword2 = "testPassword2"; + try { + tlsToolkitStandaloneCommandLine.parse("-S", testPassword1, "-S", testPassword2); + fail("Expected error with mismatch keystore password number"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS.ordinal(), e.getExitCode()); + } + } + + @Test + public void testKeyPasswordArg() throws CommandLineParseException { + String testPassword = "testPassword"; + tlsToolkitStandaloneCommandLine.parse("-K", testPassword); + List keyPasswords = tlsToolkitStandaloneCommandLine.getKeyPasswords(); + assertEquals(1, keyPasswords.size()); + assertEquals(testPassword, keyPasswords.get(0)); + } + + @Test + public void testMultipleKeyPasswordArgs() throws CommandLineParseException { + String testPassword1 = "testPassword1"; + String testPassword2 = "testPassword2"; + tlsToolkitStandaloneCommandLine.parse("-n", "nifi1,nifi2", "-K", testPassword1, "-K", testPassword2); + List keyPasswords = tlsToolkitStandaloneCommandLine.getKeyPasswords(); + assertEquals(2, keyPasswords.size()); + assertEquals(testPassword1, keyPasswords.get(0)); + assertEquals(testPassword2, keyPasswords.get(1)); + } + + @Test + public void testMultipleKeyPasswordArgSingleHost() { + String testPassword1 = "testPassword1"; + String testPassword2 = "testPassword2"; + try { + tlsToolkitStandaloneCommandLine.parse("-K", testPassword1, "-K", testPassword2); + fail("Expected error with mismatch keystore password number"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS.ordinal(), e.getExitCode()); + } + } + + @Test + public void testTruststorePasswordArg() throws CommandLineParseException { + String testPassword = "testPassword"; + tlsToolkitStandaloneCommandLine.parse("-P", testPassword); + List trustStorePasswords = tlsToolkitStandaloneCommandLine.getTrustStorePasswords(); + assertEquals(1, trustStorePasswords.size()); + assertEquals(testPassword, trustStorePasswords.get(0)); + } + + @Test + public void testMultipleTruststorePasswordArgs() throws CommandLineParseException { + String testPassword1 = "testPassword1"; + String testPassword2 = "testPassword2"; + tlsToolkitStandaloneCommandLine.parse("-n", "nifi1,nifi2", "-P", testPassword1, "-P", testPassword2); + List trustStorePasswords = tlsToolkitStandaloneCommandLine.getTrustStorePasswords(); + assertEquals(2, trustStorePasswords.size()); + assertEquals(testPassword1, trustStorePasswords.get(0)); + assertEquals(testPassword2, trustStorePasswords.get(1)); + } + + @Test + public void testMultipleTruststorePasswordArgSingleHost() { + String testPassword1 = "testPassword1"; + String testPassword2 = "testPassword2"; + try { + tlsToolkitStandaloneCommandLine.parse("-P", testPassword1, "-P", testPassword2); + fail("Expected error with mismatch keystore password number"); + } catch (CommandLineParseException e) { + assertEquals(ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS.ordinal(), e.getExitCode()); + } + } + + private Properties getProperties() throws IOException { + NiFiPropertiesWriter niFiPropertiesWriter = tlsToolkitStandaloneCommandLine.getNiFiPropertiesWriterFactory().create(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + niFiPropertiesWriter.writeNiFiProperties(byteArrayOutputStream); + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); + return properties; + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneTest.java new file mode 100644 index 0000000000..6746c40a36 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneTest.java @@ -0,0 +1,253 @@ +/* + * 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.toolkit.tls.standalone; + +import org.apache.commons.io.FileUtils; +import org.apache.nifi.toolkit.tls.commandLine.BaseCommandLine; +import org.apache.nifi.toolkit.tls.commandLine.ExitCode; +import org.apache.nifi.toolkit.tls.configuration.TlsConfig; +import org.apache.nifi.toolkit.tls.util.TlsHelperTest; +import org.apache.nifi.util.NiFiProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.Permission; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Properties; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +public class TlsToolkitStandaloneTest { + public static final String NIFI_FAKE_PROPERTY = "nifi.fake.property"; + public static final String FAKE_VALUE = "fake value"; + public static final String TEST_NIFI_PROPERTIES = "src/test/resources/localhost/nifi.properties"; + private SecurityManager originalSecurityManager; + + private File tempDir; + + @Before + public void setup() throws IOException { + tempDir = File.createTempFile("tls-test", UUID.randomUUID().toString()); + if (!tempDir.delete()) { + throw new IOException("Couldn't delete " + tempDir); + } + + if (!tempDir.mkdirs()) { + throw new IOException("Couldn't make directory " + tempDir); + } + + originalSecurityManager = System.getSecurityManager(); + // [see http://stackoverflow.com/questions/309396/java-how-to-test-methods-that-call-system-exit#answer-309427] + System.setSecurityManager(new SecurityManager() { + @Override + public void checkPermission(Permission perm) { + // Noop + } + + @Override + public void checkPermission(Permission perm, Object context) { + // Noop + } + + @Override + public void checkExit(int status) { + super.checkExit(status); + throw new ExitException(status); + } + }); + } + + @After + public void teardown() throws IOException { + System.setSecurityManager(originalSecurityManager); + FileUtils.deleteDirectory(tempDir); + } + + @Test + public void testBadParse() { + runAndAssertExitCode(ExitCode.ERROR_PARSING_COMMAND_LINE.ordinal(), "--unknownArgument"); + } + + @Test + public void testHelp() { + runAndAssertExitCode(ExitCode.HELP.ordinal(), "-h"); + runAndAssertExitCode(ExitCode.HELP.ordinal(), "--help"); + } + + @Test + public void testDirOutput() throws Exception { + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath()); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + Properties nifiProperties = checkHostDirAndReturnNifiProperties(TlsConfig.DEFAULT_HOSTNAME, x509Certificate); + assertNull(nifiProperties.get("nifi.fake.property")); + assertEquals(nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD), nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD)); + } + + @Test + public void testDifferentArg() throws Exception { + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath(), "-g"); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + Properties nifiProperties = checkHostDirAndReturnNifiProperties(TlsConfig.DEFAULT_HOSTNAME, x509Certificate); + assertNull(nifiProperties.get("nifi.fake.property")); + assertNotEquals(nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD), nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD)); + } + + @Test + public void testFileArg() throws Exception { + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath(), "-f", TEST_NIFI_PROPERTIES); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + Properties nifiProperties = checkHostDirAndReturnNifiProperties(TlsConfig.DEFAULT_HOSTNAME, x509Certificate); + assertEquals(FAKE_VALUE, nifiProperties.get(NIFI_FAKE_PROPERTY)); + } + + @Test + public void testHostnamesArgument() throws Exception { + String nifi1 = "nifi1"; + String nifi2 = "nifi2"; + String nifi3 = "nifi3"; + + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath(), "-n", nifi1 + "," + nifi2 + "," + nifi3); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + checkHostDirAndReturnNifiProperties(nifi1, x509Certificate); + checkHostDirAndReturnNifiProperties(nifi2, x509Certificate); + checkHostDirAndReturnNifiProperties(nifi3, x509Certificate); + } + + @Test + public void testKeyPasswordArg() throws Exception { + String testKey = "testKey"; + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath(), "-K", testKey); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + Properties nifiProperties = checkHostDirAndReturnNifiProperties(TlsConfig.DEFAULT_HOSTNAME, x509Certificate); + assertEquals(testKey, nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD)); + } + + @Test + public void testKeyStorePasswordArg() throws Exception { + String testKeyStore = "testKeyStore"; + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath(), "-S", testKeyStore); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + Properties nifiProperties = checkHostDirAndReturnNifiProperties(TlsConfig.DEFAULT_HOSTNAME, x509Certificate); + assertEquals(testKeyStore, nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD)); + } + + @Test + public void testTrustStorePasswordArg() throws Exception { + String testTrustStore = "testTrustStore"; + runAndAssertExitCode(0, "-o", tempDir.getAbsolutePath(), "-P", testTrustStore); + X509Certificate x509Certificate = checkLoadCertPrivateKey(TlsConfig.DEFAULT_KEY_PAIR_ALGORITHM); + + Properties nifiProperties = checkHostDirAndReturnNifiProperties(TlsConfig.DEFAULT_HOSTNAME, x509Certificate); + assertEquals(testTrustStore, nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)); + } + + private X509Certificate checkLoadCertPrivateKey(String algorithm) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException { + KeyPair keyPair = TlsHelperTest.loadKeyPair(new File(tempDir, TlsToolkitStandalone.NIFI_KEY + ".key")); + + assertEquals(algorithm, keyPair.getPrivate().getAlgorithm()); + assertEquals(algorithm, keyPair.getPublic().getAlgorithm()); + + X509Certificate x509Certificate = TlsHelperTest.loadCertificate(new File(tempDir, TlsToolkitStandalone.NIFI_CERT + ".pem")); + assertEquals(keyPair.getPublic(), x509Certificate.getPublicKey()); + return x509Certificate; + } + + private Properties checkHostDirAndReturnNifiProperties(String hostname, X509Certificate rootCert) throws Exception { + File hostDir = new File(tempDir, hostname); + Properties nifiProperties = new Properties(); + try (InputStream inputStream = new FileInputStream(new File(hostDir, TlsToolkitStandalone.NIFI_PROPERTIES))) { + nifiProperties.load(inputStream); + } + + String trustStoreType = nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE); + KeyStore trustStore = KeyStore.getInstance(trustStoreType); + try (InputStream inputStream = new FileInputStream(new File(hostDir, "truststore." + trustStoreType))) { + trustStore.load(inputStream, nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD).toCharArray()); + } + + Certificate certificate = trustStore.getCertificate(TlsToolkitStandalone.NIFI_CERT); + assertEquals(rootCert, certificate); + + String keyStoreType = nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE); + String keyStoreFilename = BaseCommandLine.KEYSTORE + keyStoreType; + File keyStoreFile = new File(hostDir, keyStoreFilename); + assertEquals(keyStoreFilename, nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE)); + + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + try (InputStream inputStream = new FileInputStream(keyStoreFile)) { + keyStore.load(inputStream, nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD).toCharArray()); + } + + char[] keyPassword = nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD).toCharArray(); + + KeyStore.Entry entry = keyStore.getEntry(TlsToolkitStandalone.NIFI_KEY, new KeyStore.PasswordProtection(keyPassword)); + assertEquals(KeyStore.PrivateKeyEntry.class, entry.getClass()); + + KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) entry; + + Certificate[] certificateChain = privateKeyEntry.getCertificateChain(); + + assertEquals(2, certificateChain.length); + assertEquals(rootCert, certificateChain[1]); + certificateChain[1].verify(rootCert.getPublicKey()); + certificateChain[0].verify(rootCert.getPublicKey()); + return nifiProperties; + } + + private void runAndAssertExitCode(int exitCode, String... args) { + try { + TlsToolkitStandaloneCommandLine.main(args); + fail("Expecting exit code: " + exitCode); + } catch (ExitException e) { + assertEquals(exitCode, e.getExitCode()); + } + } + + private static class ExitException extends SecurityException { + private final int exitCode; + + public ExitException(int exitCode) { + this.exitCode = exitCode; + } + + public int getExitCode() { + return exitCode; + } + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/PasswordUtilTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/PasswordUtilTest.java new file mode 100644 index 0000000000..e79815ae5c --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/PasswordUtilTest.java @@ -0,0 +1,51 @@ +/* + * 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.toolkit.tls.util; + +import org.junit.Test; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class PasswordUtilTest { + @Test + public void testGeneratePassword() { + SecureRandom secureRandom = mock(SecureRandom.class); + PasswordUtil passwordUtil = new PasswordUtil(secureRandom); + int value = 8675309; + doAnswer(invocation -> { + byte[] bytes = (byte[]) invocation.getArguments()[0]; + assertEquals(32, bytes.length); + Arrays.fill(bytes, (byte) 0); + byte[] val = ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + System.arraycopy(val, 0, bytes, bytes.length - val.length, val.length); + return null; + }).when(secureRandom).nextBytes(any(byte[].class)); + String expected = Base64.getEncoder().encodeToString(BigInteger.valueOf(Integer.valueOf(value).longValue()).toByteArray()).split("=")[0]; + String actual = passwordUtil.generatePassword(); + assertEquals(expected, actual); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java new file mode 100644 index 0000000000..a900a4f07a --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/java/org/apache/nifi/toolkit/tls/util/TlsHelperTest.java @@ -0,0 +1,163 @@ +/* + * 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.toolkit.tls.util; + +import org.apache.nifi.security.util.CertificateUtils; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.OperatorCreationException; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class TlsHelperTest { + private int days; + + private int keySize; + + private String keyPairAlgorithm; + + private String signingAlgorithm; + + private String keyStoreType; + + private SecureRandom secureRandom; + + private KeyPairGenerator keyPairGenerator; + + public static KeyPair loadKeyPair(Reader reader) throws IOException { + try (PEMParser pemParser = new PEMParser(reader)) { + Object object = pemParser.readObject(); + assertEquals(PEMKeyPair.class, object.getClass()); + return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) object); + } + } + + public static KeyPair loadKeyPair(File file) throws IOException { + return loadKeyPair(new FileReader(file)); + } + + public static X509Certificate loadCertificate(Reader reader) throws IOException, CertificateException { + try (PEMParser pemParser = new PEMParser(reader)) { + Object object = pemParser.readObject(); + assertEquals(X509CertificateHolder.class, object.getClass()); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate((X509CertificateHolder) object); + } + } + + public static X509Certificate loadCertificate(File file) throws IOException, CertificateException { + return loadCertificate(new FileReader(file)); + } + + @BeforeClass + public static void beforeClass() { + Security.addProvider(new BouncyCastleProvider()); + } + + @Before + public void setup() throws NoSuchAlgorithmException { + days = 360; + keySize = 2048; + keyPairAlgorithm = "RSA"; + signingAlgorithm = "SHA1WITHRSA"; + keyStoreType = KeyStore.getDefaultType(); + secureRandom = mock(SecureRandom.class); + keyPairGenerator = KeyPairGenerator.getInstance(keyPairAlgorithm); + keyPairGenerator.initialize(keySize); + } + + private Date inFuture(int days) { + return new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(days)); + } + + @Test + public void testGenerateSelfSignedCert() throws GeneralSecurityException, IOException, OperatorCreationException { + String dn = "CN=testDN,O=testOrg"; + + X509Certificate x509Certificate = CertificateUtils.generateSelfSignedX509Certificate(TlsHelper.generateKeyPair(keyPairAlgorithm, keySize), dn, signingAlgorithm, days); + + Date notAfter = x509Certificate.getNotAfter(); + assertTrue(notAfter.after(inFuture(days - 1))); + assertTrue(notAfter.before(inFuture(days + 1))); + + Date notBefore = x509Certificate.getNotBefore(); + assertTrue(notBefore.after(inFuture(-1))); + assertTrue(notBefore.before(inFuture(1))); + + assertEquals(dn, x509Certificate.getIssuerDN().getName()); + assertEquals(signingAlgorithm, x509Certificate.getSigAlgName()); + assertEquals(keyPairAlgorithm, x509Certificate.getPublicKey().getAlgorithm()); + + x509Certificate.checkValidity(); + } + + @Test + public void testIssueCert() throws IOException, CertificateException, NoSuchAlgorithmException, OperatorCreationException, NoSuchProviderException, InvalidKeyException, SignatureException { + X509Certificate issuer = loadCertificate(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("rootCert.crt"))); + KeyPair issuerKeyPair = loadKeyPair(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("rootCert.key"))); + + String dn = "CN=testIssued,O=testOrg"; + + KeyPair keyPair = TlsHelper.generateKeyPair(keyPairAlgorithm, keySize); + X509Certificate x509Certificate = CertificateUtils.generateIssuedCertificate(dn, keyPair.getPublic(), issuer, issuerKeyPair, signingAlgorithm, days); + assertEquals(dn, x509Certificate.getSubjectDN().toString()); + assertEquals(issuer.getSubjectDN().toString(), x509Certificate.getIssuerDN().toString()); + assertEquals(keyPair.getPublic(), x509Certificate.getPublicKey()); + + Date notAfter = x509Certificate.getNotAfter(); + assertTrue(notAfter.after(inFuture(days - 1))); + assertTrue(notAfter.before(inFuture(days + 1))); + + Date notBefore = x509Certificate.getNotBefore(); + assertTrue(notBefore.after(inFuture(-1))); + assertTrue(notBefore.before(inFuture(1))); + + assertEquals(signingAlgorithm, x509Certificate.getSigAlgName()); + assertEquals(keyPairAlgorithm, x509Certificate.getPublicKey().getAlgorithm()); + + x509Certificate.verify(issuerKeyPair.getPublic()); + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/localhost/nifi.properties b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/localhost/nifi.properties new file mode 100644 index 0000000000..41b091c8ab --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/localhost/nifi.properties @@ -0,0 +1,177 @@ +# +# 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. +# + +# Core Properties # +nifi.fake.property=fake value +nifi.version=1.0.0-SNAPSHOT +nifi.flow.configuration.file=./conf/flow.xml.gz +nifi.flow.configuration.archive.dir=./conf/archive/ +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=500 ms +nifi.administrative.yield.duration=30 sec +# If a component has no work to do (is "bored"), how long should we wait before checking again for work? +nifi.bored.yield.duration=10 millis + +nifi.authorizer.configuration.file=./conf/authorizers.xml +nifi.login.identity.provider.configuration.file=./conf/login-identity-providers.xml +nifi.templates.directory=./conf/templates +nifi.ui.banner.text= +nifi.ui.autorefresh.interval=30 sec +nifi.nar.library.directory=./lib +nifi.nar.working.directory=./work/nar/ +nifi.documentation.working.directory=./work/docs/components + +#################### +# State Management # +#################### +nifi.state.management.configuration.file=./conf/state-management.xml +# The ID of the local state provider +nifi.state.management.provider.local=local-provider +# The ID of the cluster-wide state provider. This will be ignored if NiFi is not clustered but must be populated if running in a cluster. +nifi.state.management.provider.cluster=zk-provider +# Specifies whether or not this instance of NiFi should run an embedded ZooKeeper server +nifi.state.management.embedded.zookeeper.start=false +# Properties file that provides the ZooKeeper properties to use if is set to true +nifi.state.management.embedded.zookeeper.properties=./conf/zookeeper.properties + + +# H2 Settings +nifi.database.directory=./database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.flowfile.repository.implementation=org.apache.nifi.controller.repository.WriteAheadFlowFileRepository +nifi.flowfile.repository.directory=./flowfile_repository +nifi.flowfile.repository.partitions=256 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.flowfile.repository.always.sync=false + +nifi.swap.manager.implementation=org.apache.nifi.controller.FileSystemSwapManager +nifi.queue.swap.threshold=20000 +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.repository.implementation=org.apache.nifi.controller.repository.FileSystemRepository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=./content_repository +nifi.content.repository.archive.max.retention.period=12 hours +nifi.content.repository.archive.max.usage.percentage=50% +nifi.content.repository.archive.enabled=true +nifi.content.repository.always.sync=false +nifi.content.viewer.url=/nifi-content-viewer/ + +# Provenance Repository Properties +nifi.provenance.repository.implementation=org.apache.nifi.provenance.PersistentProvenanceRepository + +# Persistent Provenance Repository Properties +nifi.provenance.repository.directory.default=./provenance_repository +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=30 secs +nifi.provenance.repository.rollover.size=100 MB +nifi.provenance.repository.query.threads=2 +nifi.provenance.repository.index.threads=1 +nifi.provenance.repository.compress.on.rollover=true +nifi.provenance.repository.always.sync=false +nifi.provenance.repository.journal.count=16 +# Comma-separated list of fields. Fields that are not indexed will not be searchable. Valid fields are: +# EventType, FlowFileUUID, Filename, TransitURI, ProcessorID, AlternateIdentifierURI, Relationship, Details +nifi.provenance.repository.indexed.fields=EventType, FlowFileUUID, Filename, ProcessorID, Relationship +# FlowFile Attributes that should be indexed and made searchable. Some examples to consider are filename, uuid, mime.type +nifi.provenance.repository.indexed.attributes= +# Large values for the shard size will result in more Java heap usage when searching the Provenance Repository +# but should provide better performance +nifi.provenance.repository.index.shard.size=500 MB +# Indicates the maximum length that a FlowFile attribute can be when retrieving a Provenance Event from +# the repository. If the length of any attribute exceeds this value, it will be truncated when the event is retrieved. +nifi.provenance.repository.max.attribute.length=65536 + +# Volatile Provenance Respository Properties +nifi.provenance.repository.buffer.size=100000 + +# Component Status Repository +nifi.components.status.repository.implementation=org.apache.nifi.controller.status.history.VolatileComponentStatusRepository +nifi.components.status.repository.buffer.size=1440 +nifi.components.status.snapshot.frequency=1 min + +# Site to Site properties +nifi.remote.input.host= +nifi.remote.input.secure=false +nifi.remote.input.socket.port= +nifi.remote.input.http.enabled=true +nifi.remote.input.http.transaction.ttl=30 sec + +# web properties # +nifi.web.war.directory=./lib +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= +nifi.web.jetty.working.directory=./work/jetty +nifi.web.jetty.threads=200 + +# security properties # +nifi.sensitive.props.key= +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=./conf/localhost.jks +nifi.security.keystoreType=jks +nifi.security.keystorePasswd=vidmcgplvih3dn2fqjnckns77g +nifi.security.keyPasswd=qgs57rmnot6p8gm97pfjutnu5g +nifi.security.truststore=./conf/truststore.jks +nifi.security.truststoreType=jks +nifi.security.truststorePasswd=t7rmn1fg8np2ck1sduqdd85opv +nifi.security.needClientAuth= +nifi.security.user.authorizer=file-provider +nifi.security.user.login.identity.provider= +nifi.security.ocsp.responder.url= +nifi.security.ocsp.responder.pemEncodedCertificate= + +# cluster common properties (all nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=10 +nifi.cluster.node.event.history.size=25 +nifi.cluster.node.connection.timeout=5 sec +nifi.cluster.node.read.timeout=5 sec +nifi.cluster.firewall.file= + +# How long a request should be allowed to hold a 'lock' on a component. # +nifi.cluster.request.replication.claim.timeout=15 secs + +# zookeeper properties, used for cluster management # +nifi.zookeeper.connect.string= +nifi.zookeeper.connect.timeout=3 secs +nifi.zookeeper.session.timeout=3 secs +nifi.zookeeper.root.node=/nifi + +# kerberos # +nifi.kerberos.krb5.file= +nifi.kerberos.service.principal= +nifi.kerberos.keytab.location= +nifi.kerberos.authentication.expiration=12 hours diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/localhost/truststore.jks b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/localhost/truststore.jks new file mode 100644 index 0000000000..8d0b4de755 Binary files /dev/null and b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/localhost/truststore.jks differ diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert.crt b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert.crt new file mode 100644 index 0000000000..98bb577bf0 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAkGgAwIBAgIGAVXgcI3oMA0GCSqGSIb3DQEBCwUAMC0xFTATBgNVBAMM +DG5pZmkucm9vdC5jYTEUMBIGA1UECwwLYXBhY2hlLm5pZmkwHhcNMTYwNzEyMTg0 +ODQwWhcNMTcwNzEyMTg0ODQwWjAtMRUwEwYDVQQDDAxuaWZpLnJvb3QuY2ExFDAS +BgNVBAsMC2FwYWNoZS5uaWZpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAuvjnFOoo5fYaq6vMiP2cmoSj3pzpLjvVPsQGP4OH7vy1f3C6IpWkrXjy/kst +sP116WC4PdnLJ0FF4ogR/4b5XpeFuLXrmsGk3dWQac3zunZ1AOvJKflQqCiOfIG8 +9uMknJe0enHq4fhjy4qZrwOmMozGayWa5iRjGfY7mS44luYQvn9dz9G4IIoGVpxJ +rltHDrl0fMcVqY6Kfxb//6jvo74mW0E85hju45JHseEUMHMsZaJAYfKg2dRQryE1 +UIW5Gai2Io17HjWIqWUnht5/CvJG3MKo/3XnVuwyVnbnt5bey+N+NVVsMKTig3m7 +PGK2R6fycKupIMMVZ60FbsLo7wIDAQABo38wfTAOBgNVHQ8BAf8EBAMCAf4wDAYD +VR0TBAUwAwEB/zAdBgNVHQ4EFgQUxPKyo9LTvN7jTJ1x2jVUe4nNz1swHwYDVR0j +BBgwFoAUxPKyo9LTvN7jTJ1x2jVUe4nNz1swHQYDVR0lBBYwFAYIKwYBBQUHAwIG +CCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQBcMRAlnWDco5WbjTV0uSxmS7Dh +RxYgt7YguqA/tdfcn6hhJZ2ZZm5By6nwP4aGFY45tOv2NsjHgchgfJ6Osl3ZxGmF ++JrXW7mveOwZIfZzM2yFCusgkzrGOAWNL2G+lbXpRCnsTuJL6jUbRE1cFwU+iUYo +E8Xfo6XZVNlzeab4WNerAPGssftV9C+0ya9+5+hFmBhzpGkpn5EVicxLAX6fI4K6 +N4ZCBU8DYQilYkE0xgxySSz7Ia1vo8D7Tr30CxoXGsqRLXanW0Jw1wVsLVheTLZw +W3gp2XKOaP/BMFbIFw2iB7REeCao14u9pRAKEodpH+fpUosbFlpd8ysRvFJP +-----END CERTIFICATE----- diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert.key b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert.key new file mode 100644 index 0000000000..64f9bb722c --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuvjnFOoo5fYaq6vMiP2cmoSj3pzpLjvVPsQGP4OH7vy1f3C6 +IpWkrXjy/kstsP116WC4PdnLJ0FF4ogR/4b5XpeFuLXrmsGk3dWQac3zunZ1AOvJ +KflQqCiOfIG89uMknJe0enHq4fhjy4qZrwOmMozGayWa5iRjGfY7mS44luYQvn9d +z9G4IIoGVpxJrltHDrl0fMcVqY6Kfxb//6jvo74mW0E85hju45JHseEUMHMsZaJA +YfKg2dRQryE1UIW5Gai2Io17HjWIqWUnht5/CvJG3MKo/3XnVuwyVnbnt5bey+N+ +NVVsMKTig3m7PGK2R6fycKupIMMVZ60FbsLo7wIDAQABAoIBAAzqSpQQRLj10gvP +tzYzRpEJ3oPvFNBjw6wtQD086bPhcuwVrlXbPl/ZPffA26whJfbz/mYPWFAi2x/1 +xECBY1JcZxlGP7sV6zyDlxEn79EOg7CHmQK0PRUQkB8bmyD2ub2zYP0gR1hKnyId +NdZ4Cw/s13vpQhTpqIrPjnhbT/9kUMMxdWrN36/o/TL18VZMD2E3WGMzX+lbKNLs +QvfAGUpbfbPtmUPFezbDoL5Pqkj4KZ4/5l1RfqQiamNijwxxm/oGbDh8AuyxvnoR +yxDc9OWEoepEb72Qdec4LJnaYNXUtI2rg1QAbcXzuRLtv8o0J0gpVdqm2ROyf3tF +aI+Fk6kCgYEA3jZM5Vta7BHBT1HmYGZQOuQHW+WD7CQgLeXCCB4O4xq/7l7iG2hM +3pt65TGae78KHtJ2LciWrKRuNTruBoKax6MFw16ZfSQSFi0Knn+EbcxcmPQ2i/VO +xpdOVAP9LyEKKGTgH2FTE7RkiGt/oK0eJrYHpHMpAs0jRyCIYGPmt10CgYEA12bg +fuMAhiy3kv/IYtl3W7doGFVzu2C59VWxmkp/WvGQyrVUCURp5PNjlb0LIM4DZnKR +/zCxoP0Sf1TuVmORO1Z+Z0NKJoowYyveScTnn+RqKw7ssx9KgGRXXHuWlDcBv5lI +MP5vYs/5J47arZdbjI3Nbirvv8y4kHt1LkrPWLsCgYB0qV9vnqm6fIvXv/DKNYzz +jhoK9hFvnXvDfPeEWXuJYdbYJ7pflz+cM1avE36+bwq6KdZfrQrl8qHlkl26z0DB +hOYWrwi4OYLBX75OfjYhiwQmTTiB/DTycCdwVnPLFXaGxIciEm+STcfmE0H7Mkg9 +HJ4giVFDpj1aL0tKB8juBQKBgBED7m33jp4KwEGbdP/h/9y94zow3eCKZoYry8jN +Y7wSYwawRkApKFhOpao5cbyYqYoZONE4zn9SHnjyg5VNbjaKwZd0qFHdDq920qtv +I4Ds8ToPhsErkp4Lzx7eIGn64md5O0urfa8HkL4AOeQGldPHi9fNCn1TNa0sI3iR +rklrAoGBALjnqDQFBuT3io0locXPHw6UbIAbD6Pm8Z1+HgK/hskyX/9bmnJn3oyz +jxziJhpy96RoLu0sxBxo5fjHGc54Qn6Ccivu4oM+4rktfALhLuFUfie8tr6nZlnm +mxRyNqJxD8L5Ttb66TiC9snf98MYAqsKD03fPYUAdpPlIbn+xS44 +-----END RSA PRIVATE KEY----- diff --git a/nifi-toolkit/pom.xml b/nifi-toolkit/pom.xml new file mode 100644 index 0000000000..4f9e7d1ee7 --- /dev/null +++ b/nifi-toolkit/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + org.apache.nifi + nifi + 1.0.0-SNAPSHOT + + org.apache.nifi + nifi-toolkit + pom + + nifi-toolkit-tls + nifi-toolkit-assembly + + diff --git a/pom.xml b/pom.xml index 602ac22605..d879adf03e 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ language governing permissions and limitations under the License. --> nifi-docs nifi-maven-archetypes nifi-external + nifi-toolkit http://nifi.apache.org @@ -868,6 +869,11 @@ language governing permissions and limitations under the License. --> nifi-bootstrap 1.0.0-SNAPSHOT + + org.apache.nifi + nifi-toolkit-tls + 1.0.0-SNAPSHOT + org.apache.nifi nifi-resources @@ -1257,6 +1263,11 @@ language governing permissions and limitations under the License. --> nifi-hbase-client-service-api 1.0.0-SNAPSHOT + + org.apache.nifi + nifi-assembly + 1.0.0-SNAPSHOT + com.jayway.jsonpath json-path