diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 1f1d64c769..769b7bc798 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -187,6 +187,7 @@ Standalone mode is invoked by running `./bin/tls-toolkit.sh standalone -h` which You can use the following command line options with the `tls-toolkit` in standalone mode: * `-a`,`--keyAlgorithm ` Algorithm to use for generated keys (default: `RSA`) +* `--additionalCACertificate ` Path to additional CA certificate (used to sign toolkit CA certificate) in PEM format if necessary * `-B`,`--clientCertPassword ` Password for client certificate. Must either be one value or one for each client DN (auto-generate if not specified) * `-c`,`--certificateAuthorityHostname ` Hostname of NiFi Certificate Authority (default: `localhost`) * `-C`,`--clientCertDn ` Generate client certificate suitable for use in browser with specified DN (Can be specified multiple times) @@ -231,7 +232,6 @@ Create 2 sets of keystore, truststore, nifi.properties for 10 NiFi hostnames in bin/tls-toolkit.sh standalone -n 'nifi[01-10].subdomain[1-4].domain(2)' -C 'CN=username,OU=NIFI' ---- - ==== Client/Server Client/Server mode relies on a long-running Certificate Authority (CA) to issue certificates. The CA can be stopped when you’re not bringing nodes online. @@ -279,7 +279,7 @@ You can use the following command line options with the `tls-toolkit` in client After running the client you will have the CA’s certificate, a keystore, a truststore, and a `config.json` with information about them as well as their passwords. -For a client certificate that can be easily imported into the browser, specify: `-T PKCS12` +For a client certificate that can be easily imported into the browser, specify: `-T PKCS12`. ==== Using An Existing Intermediate Certificate Authority (CA) @@ -288,7 +288,7 @@ In some enterprise scenarios, a security/IT team may provide a signing certifica . Generate or obtain the signed intermediate CA keys in the following format (see additional commands below): * Public certificate in PEM format: `nifi-cert.pem` * Private key in PEM format: `nifi-key.key` -. Place the files in the *toolkit directory*. This is the directory where the tool binary (usually called via the invoking script `tls-toolkit.sh` or `tls-toolkit.bat`) is configured to output the signed certificates. *This is not necessarily the directory where the binary is located or invoked*. +. Place the files in the *toolkit working directory*. This is the directory where the tool is configured to output the signed certificates. *This is not necessarily the directory where the binary is located or invoked*. * For example, given the following scenario, the toolkit command can be run from its location as long as the output directory `-o` is `../hardcoded/`, and the existing `nifi-cert.pem` and `nifi-key.key` will be used. ** e.g. `$ ./toolkit/bin/tls-toolkit.sh standalone -o ./hardcoded/ -n 'node4.nifi.apache.org' -P thisIsABadPassword -S thisIsABadPassword -O` will result in a new directory at `./hardcoded/node4.nifi.apache.org` with a keystore and truststore containing a certificate signed by `./hardcoded/nifi-key.key` * If the `-o` argument is not provided, the default working directory (`.`) must contain `nifi-cert.pem` and `nifi-key.key` @@ -551,7 +551,59 @@ coefficient: . To convert from a Java Keystore (`keystore.jks`) containing private key into PEM encoded files (`$P12_PASSWORD` is the PKCS12 keystore password, `$JKS_PASSWORD` is the Java keystore password you want to set, and `$ALIAS` can be any value -- the NiFi default is `nifi-key`): * `keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.p12 -srcstoretype JKS -deststoretype PKCS12 -destkeypass "$P12_PASSWORD" -deststorepass "$P12_PASSWORD" -srcstorepass "$JKS_PASSWORD" -srcalias "$ALIAS" -destalias "$ALIAS"` * Follow the steps above to convert from `keystore.p12` to `cert.pem` and `key.key` +. To convert from PKCS #8 PEM format to PKCS #1 PEM format: + * If the private key is provided in PKCS #8 format (the file begins with `-----BEGIN PRIVATE KEY-----` rather than `-----BEGIN RSA PRIVATE KEY-----`), the following command will convert it to PKCS #1 format, move the original to `nifi-key-pkcs8.key`, and rename the PKCS #1 version as `nifi-key.key`: + ** `openssl rsa -in nifi-key.key -out nifi-key-pkcs1.key && mv nifi-key.key nifi-key-pkcs8.key && mv nifi-key-pkcs1.key nifi-key.key` +===== Signing with Externally-signed CA Certificates + +To sign generated certificates with a certificate authority (CA) generated outside of the TLS Toolkit, ensure the necessary files are in the right format and location (see above). For example, an organization *Large Organization* has an internal CA (`CN=ca.large.org, OU=Certificate Authority`). This *root CA* is offline and only used to sign other internal CAs. The Large IT team generates an *intermediate CA* (`CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority`) to be used to sign all NiFi node certificates (`CN=node1.nifi.large.org, OU=NiFi`, `CN=node2.nifi.large.org, OU=NiFi`, etc.). + +To use the toolkit to generate these certificates and sign them using the *intermediate CA*, ensure that the following files are present (see <> above): + +* `nifi-cert.pem` -- the public certificate of the *intermediate CA* in PEM format +* `nifi-key.key` -- the Base64-encoded private key of the *intermediate CA* in PKCS #1 PEM format + +If the *intermediate CA* was the *root CA*, it would be *self-signed* -- the signature over the certificate would be issued from the same key. In that case (the same as a toolkit-generated CA), no additional arguments are necessary. However, because the *intermediate CA* is signed by the *root CA*, the public certificate of the *root CA* needs to be provided as well to validate the signature. The `--additionalCACertificate` parameter is used to specify the path to the signing public certificate. The value should be the absolute path to the *root CA* public certificate. + +Example: + +``` +# Generate cert signed by intermediate CA (which is signed by root CA) -- WILL FAIL + +$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \ +-P passwordpassword \ +-S passwordpassword \ +-o /opt/certs/externalCA \ +-O + +2018/08/02 18:48:11 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one. +2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA +2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=Certificate Authority +2018/08/02 18:48:12 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority +2018/08/02 18:48:12 WARN [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority not signed by CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority [certificate does not verify with supplied key] +Error generating TLS configuration. (The signing certificate was not signed by any known certificates) + +# Provide additional CA certificate path for signature verification of intermediate CA + +$ ./bin/tls-toolkit.sh standalone -n 'node1.nifi.apache.org' \ +-P passwordpassword \ +-S passwordpassword \ +-o /opt/certs/externalCA \ +--additionalCACertificate /opt/certs/externalCA/root.pem \ +-O + +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine: No nifiPropertiesFile specified, using embedded one. +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Running standalone certificate generation with output directory /opt/certs/externalCA +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Verifying the certificate signature for CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Attempting to verify certificate CN=nifi_ca.large.org, OU=NiFi, OU=Certificate Authority signature with CN=ca.large.org, OU=Certificate Authority +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.util.TlsHelper: Certificate was signed by CN=ca.large.org, OU=Certificate Authority +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Using existing CA certificate /opt/certs/externalCA/nifi-cert.pem and key /opt/certs/externalCA/nifi-key.key +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Writing new ssl configuration to /opt/certs/externalCA/node1.nifi.apache.org +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: Successfully generated TLS configuration for node1.nifi.apache.org 1 in /opt/certs/externalCA/node1.nifi.apache.org +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: No clientCertDn specified, not generating any client certificates. +2018/08/02 18:48:44 INFO [main] org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone: tls-toolkit standalone completed successfully +``` [[user_authentication]] == User Authentication diff --git a/nifi-toolkit/nifi-toolkit-tls/pom.xml b/nifi-toolkit/nifi-toolkit-tls/pom.xml index 3443f5a8ba..f2b813f373 100644 --- a/nifi-toolkit/nifi-toolkit-tls/pom.xml +++ b/nifi-toolkit/nifi-toolkit-tls/pom.xml @@ -130,6 +130,7 @@ src/test/resources/rootCert.crt src/test/resources/rootCert.key + src/test/resources/rootCert-pkcs8.key diff --git a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java index 7ac44a42f2..d8e86347a9 100644 --- a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java +++ b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/configuration/StandaloneConfig.java @@ -17,10 +17,9 @@ package org.apache.nifi.toolkit.tls.configuration; -import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory; - import java.io.File; import java.util.List; +import org.apache.nifi.toolkit.tls.properties.NiFiPropertiesWriterFactory; /** * Configuration object of the standalone service @@ -34,6 +33,8 @@ public class StandaloneConfig extends TlsConfig { private boolean clientPasswordsGenerated; private boolean overwrite; + // TODO: A lot of these fields are null and cause NPEs in {@link TlsToolkitStandalone} when not executed with expected input + public List getClientDns() { return clientDns; } 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 index 86084c0f70..9332022a08 100644 --- 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 @@ -52,6 +52,7 @@ public class TlsConfig { private String dnPrefix = DEFAULT_DN_PREFIX; private String dnSuffix = DEFAULT_DN_SUFFIX; private boolean reorderDn = DEFAULT_REORDER_DN; + private String additionalCACertificate = ""; public String calcDefaultDn(String hostname) { String dn = dnPrefix + hostname + dnSuffix; @@ -215,4 +216,36 @@ public class TlsConfig { public void setDomainAlternativeNames(String domainAlternativeNames) { this.domainAlternativeNames = domainAlternativeNames; } + + /** + * Returns the path to an additional CA certificate file in PEM format which has been used to sign the CA certificate the toolkit will use. + * + * Example: + * + * nifi-cert.pem [existing PEM file for intermediate CA generated by Org's IT team and signed by org-ca.pem] + * org-ca.pem [PEM file for root CA owned by Org's IT team] + * + * {@code getAdditionalCACertificate() == "/path/to/org-ca.pem"} + * + * @return the path to this file + */ + public String getAdditionalCACertificate() { + return additionalCACertificate; + } + + /** + * Sets the path to an additional CA certificate file in PEM format which has been used to sign the CA certificate the toolkit will use. + * + * Example: + * + * nifi-cert.pem [existing PEM file for intermediate CA generated by Org's IT team and signed by org-ca.pem] + * org-ca.pem [PEM file for root CA owned by Org's IT team] + * + * {@code setAdditionalCACertificate("/path/to/org-ca.pem");} + * + * @param additionalCACertificate the path to this file + */ + public void setAdditionalCACertificate(String additionalCACertificate) { + this.additionalCACertificate = additionalCACertificate; + } } 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 index d3da47677c..ffe4c5d60d 100644 --- 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 @@ -17,24 +17,6 @@ package org.apache.nifi.toolkit.tls.standalone; -import org.apache.nifi.security.util.CertificateUtils; -import org.apache.nifi.security.util.KeystoreType; -import org.apache.nifi.security.util.KeyStoreUtils; -import org.apache.nifi.toolkit.tls.configuration.InstanceDefinition; -import org.apache.nifi.toolkit.tls.configuration.StandaloneConfig; -import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; -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.asn1.x509.Extensions; -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.FileReader; @@ -44,9 +26,29 @@ import java.io.OutputStreamWriter; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyStore; +import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.List; +import org.apache.nifi.security.util.CertificateUtils; +import org.apache.nifi.security.util.KeyStoreUtils; +import org.apache.nifi.security.util.KeystoreType; +import org.apache.nifi.toolkit.tls.configuration.InstanceDefinition; +import org.apache.nifi.toolkit.tls.configuration.StandaloneConfig; +import org.apache.nifi.toolkit.tls.configuration.TlsClientConfig; +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.apache.nifi.util.StringUtils; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.bouncycastle.util.io.pem.PemWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class TlsToolkitStandalone { public static final String NIFI_KEY = "nifi-key"; @@ -65,6 +67,7 @@ public class TlsToolkitStandalone { } public void createNifiKeystoresAndTrustStores(StandaloneConfig standaloneConfig) throws GeneralSecurityException, IOException { + // TODO: This 200 line method should be refactored, as it is difficult to test the various validations separately from the filesystem interaction and generation logic File baseDir = standaloneConfig.getBaseDir(); if (!baseDir.exists() && !baseDir.mkdirs()) { throw new IOException(baseDir + " doesn't exist and unable to create it."); @@ -96,10 +99,35 @@ public class TlsToolkitStandalone { certificate = TlsHelper.parseCertificate(pemEncodedCertificate); } try (FileReader pemEncodedKeyPair = new FileReader(nifiKey)) { - caKeyPair = TlsHelper.parseKeyPair(pemEncodedKeyPair); + caKeyPair = TlsHelper.parseKeyPairFromReader(pemEncodedKeyPair); + } + + // TODO: Do same in client/server + // Load additional signing certificates from config + List signingCertificates = new ArrayList<>(); + + // Read the provided additional CA certificate if it exists and extract the certificate + if (!StringUtils.isBlank(standaloneConfig.getAdditionalCACertificate())) { + X509Certificate signingCertificate; + final File additionalCACertFile = new File(standaloneConfig.getAdditionalCACertificate()); + if (!additionalCACertFile.exists()) { + throw new IOException("The additional CA certificate does not exist at " + additionalCACertFile.getAbsolutePath()); + } + try (FileReader pemEncodedCACertificate = new FileReader(additionalCACertFile)) { + signingCertificate = TlsHelper.parseCertificate(pemEncodedCACertificate); + } + signingCertificates.add(signingCertificate); + } + + // Support self-signed CA certificates + signingCertificates.add(certificate); + + boolean signatureValid = TlsHelper.verifyCertificateSignature(certificate, signingCertificates); + + if (!signatureValid) { + throw new SignatureException("The signing certificate was not signed by any known certificates"); } - certificate.verify(caKeyPair.getPublic()); if (!caKeyPair.getPublic().equals(certificate.getPublicKey())) { throw new IOException("Expected " + nifiKey + " to correspond to CA certificate at " + nifiCert); } 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 index 2e30b48a21..f11ad7b1b3 100644 --- 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 @@ -29,7 +29,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; - import org.apache.commons.cli.CommandLine; import org.apache.nifi.toolkit.tls.commandLine.BaseTlsToolkitCommandLine; import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException; @@ -59,7 +58,8 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { public static final String GLOBAL_PORT_SEQUENCE_ARG = "globalPortSequence"; public static final String NIFI_DN_PREFIX_ARG = "nifiDnPrefix"; public static final String NIFI_DN_SUFFIX_ARG = "nifiDnSuffix"; - public static final String SUBJECT_ALTERNATIVE_NAMES = "subjectAlternativeNames"; + public static final String SUBJECT_ALTERNATIVE_NAMES_ARG = "subjectAlternativeNames"; + public static final String ADDITIONAL_CA_CERTIFICATE_ARG = "additionalCACertificate"; public static final String DEFAULT_OUTPUT_DIRECTORY = calculateDefaultOutputDirectory(Paths.get(".")); @@ -89,6 +89,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { private String dnPrefix; private String dnSuffix; private String domainAlternativeNames; + private String additionalCACertificatePath; public TlsToolkitStandaloneCommandLine() { this(new PasswordUtil()); @@ -107,10 +108,11 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { addOptionWithArg("B", CLIENT_CERT_PASSWORD_ARG, "Password for client certificate. Must either be one value or one for each client DN. (autogenerate if not specified)"); addOptionWithArg("G", GLOBAL_PORT_SEQUENCE_ARG, "Use sequential ports that are calculated for all hosts according to the provided hostname expressions. " + "(Can be specified multiple times, MUST BE SAME FROM RUN TO RUN.)"); - addOptionWithArg(null, SUBJECT_ALTERNATIVE_NAMES, "Comma-separated list of domains to use as Subject Alternative Names in the certificate"); + addOptionWithArg(null, SUBJECT_ALTERNATIVE_NAMES_ARG, "Comma-separated list of domains to use as Subject Alternative Names in the certificate"); addOptionWithArg(null, NIFI_DN_PREFIX_ARG, "String to prepend to hostname(s) when determining DN.", TlsConfig.DEFAULT_DN_PREFIX); addOptionWithArg(null, NIFI_DN_SUFFIX_ARG, "String to append to hostname(s) when determining DN.", TlsConfig.DEFAULT_DN_SUFFIX); addOptionNoArg("O", OVERWRITE_ARG, "Overwrite existing host output."); + addOptionWithArg(null, ADDITIONAL_CA_CERTIFICATE_ARG, "Path to additional CA certificate (used to sign toolkit CA certificate) in PEM format if necessary"); } public static void main(String[] args) { @@ -123,7 +125,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { try { new TlsToolkitStandalone().createNifiKeystoresAndTrustStores(tlsToolkitStandaloneCommandLine.createConfig()); } catch (Exception e) { - tlsToolkitStandaloneCommandLine.printUsage("Error creating generating tls configuration. (" + e.getMessage() + ")"); + tlsToolkitStandaloneCommandLine.printUsage("Error generating TLS configuration. (" + e.getMessage() + ")"); System.exit(ExitCode.ERROR_GENERATING_CONFIG.ordinal()); } System.exit(ExitCode.SUCCESS.ordinal()); @@ -137,7 +139,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { dnPrefix = commandLine.getOptionValue(NIFI_DN_PREFIX_ARG, TlsConfig.DEFAULT_DN_PREFIX); dnSuffix = commandLine.getOptionValue(NIFI_DN_SUFFIX_ARG, TlsConfig.DEFAULT_DN_SUFFIX); - domainAlternativeNames = commandLine.getOptionValue(SUBJECT_ALTERNATIVE_NAMES); + domainAlternativeNames = commandLine.getOptionValue(SUBJECT_ALTERNATIVE_NAMES_ARG); Stream globalOrderExpressions = null; if (commandLine.hasOption(GLOBAL_PORT_SEQUENCE_ARG)) { @@ -166,6 +168,8 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { clientPasswordsGenerated = commandLine.getOptionValues(CLIENT_CERT_PASSWORD_ARG) == null; overwrite = commandLine.hasOption(OVERWRITE_ARG); + additionalCACertificatePath = commandLine.getOptionValue(ADDITIONAL_CA_CERTIFICATE_ARG); + String nifiPropertiesFile = commandLine.getOptionValue(NIFI_PROPERTIES_FILE_ARG, ""); try { if (StringUtils.isEmpty(nifiPropertiesFile)) { @@ -234,6 +238,7 @@ public class TlsToolkitStandaloneCommandLine extends BaseTlsToolkitCommandLine { standaloneConfig.setDnPrefix(dnPrefix); standaloneConfig.setDnSuffix(dnSuffix); standaloneConfig.setDomainAlternativeNames(domainAlternativeNames); + standaloneConfig.setAdditionalCACertificate(additionalCACertificatePath); standaloneConfig.initDefaults(); return standaloneConfig; 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 index d8a7fc0d9a..20bc2d9ff1 100644 --- 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 @@ -34,25 +34,31 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; - import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; - import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.pkcs.RSAPrivateKey; +import org.bouncycastle.asn1.pkcs.RSAPublicKey; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.ExtensionsGenerator; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; 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.PEMException; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; @@ -72,6 +78,7 @@ public class TlsHelper { public static final String JCE_URL = "http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html"; public static final String ILLEGAL_KEY_SIZE = "illegal key size"; private static boolean isUnlimitedStrengthCryptographyEnabled; + private static boolean isVerbose = true; // Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system static { @@ -185,15 +192,80 @@ public class TlsHelper { return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(parsePem(X509CertificateHolder.class, pemEncodedCertificate)); } - public static KeyPair parseKeyPair(Reader pemEncodedKeyPair) throws IOException { - return new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getKeyPair(parsePem(PEMKeyPair.class, pemEncodedKeyPair)); + /** + * Returns the parsed {@link KeyPair} from the provided {@link Reader}. The incoming format can be PKCS #8 or PKCS #1. + * + * @param pemKeyPairReader a reader with access to the serialized key pair + * @return the key pair + * @throws IOException if there is an error reading the key pair + */ + public static KeyPair parseKeyPairFromReader(Reader pemKeyPairReader) throws IOException { + // Instantiate PEMParser from Reader + try (PEMParser pemParser = new PEMParser(pemKeyPairReader)) { + // Read the object (deserialize) + Object parsedObject = pemParser.readObject(); + + // If this is an ASN.1 private key, it's in PKCS #8 format and wraps the actual RSA private key + if (PrivateKeyInfo.class.isInstance(parsedObject)) { + if (isVerbose()) { + logger.info("Provided private key is in PKCS #8 format"); + } + PEMKeyPair keyPair = convertPrivateKeyFromPKCS8ToPKCS1((PrivateKeyInfo) parsedObject); + return getKeyPair(keyPair); + } else if (PEMKeyPair.class.isInstance(parsedObject)) { + // Already in PKCS #1 format + return getKeyPair((PEMKeyPair)parsedObject); + } else { + logger.warn("Expected one of %s or %s but got %s", PrivateKeyInfo.class, PEMKeyPair.class, parsedObject.getClass()); + throw new IOException("Expected private key in PKCS #1 or PKCS #8 unencrypted format"); + } + } + } + + /** + * Returns a {@link KeyPair} instance containing the {@link X509Certificate} public key and the {@link java.security.spec.PKCS8EncodedKeySpec} private key from the PEM-encoded {@link PEMKeyPair}. + * + * @param keyPair the key pair in PEM format + * @return the key pair in a format which provides for direct access to the keys + * @throws PEMException if there is an error converting the key pair + */ + private static KeyPair getKeyPair(PEMKeyPair keyPair) throws PEMException { + return new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getKeyPair(keyPair); + } + + /** + * Returns a {@link PEMKeyPair} object with direct access to the public and private keys given a PKCS #8 private key. + * + * @param privateKeyInfo the PKCS #8 private key info + * @return the PKCS #1 public and private key pair + * @throws IOException if there is an error converting the key pair + */ + private static PEMKeyPair convertPrivateKeyFromPKCS8ToPKCS1(PrivateKeyInfo privateKeyInfo) throws IOException { + // Parse the key wrapping to determine the internal key structure + ASN1Encodable asn1PrivateKey = privateKeyInfo.parsePrivateKey(); + + // Convert the parsed key to an RSA private key + RSAPrivateKey keyStruct = RSAPrivateKey.getInstance(asn1PrivateKey); + + // Create the RSA public key from the modulus and exponent + RSAPublicKey pubSpec = new RSAPublicKey( + keyStruct.getModulus(), keyStruct.getPublicExponent()); + + // Create an algorithm identifier for forming the key pair + AlgorithmIdentifier algId = new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE); + if (isVerbose()) { + logger.info("Converted private key from PKCS #8 to PKCS #1 RSA private key"); + } + + // Create the key pair container + return new PEMKeyPair(new SubjectPublicKeyInfo(algId, pubSpec), new PrivateKeyInfo(algId, keyStruct)); } public static T parsePem(Class clazz, Reader pemReader) throws IOException { try (PEMParser pemParser = new PEMParser(pemReader)) { Object object = pemParser.readObject(); if (!clazz.isInstance(object)) { - throw new IOException("Expected " + clazz); + throw new IOException("Expected " + clazz + " but got " + object.getClass()); } return (T) object; } @@ -251,4 +323,51 @@ public class TlsHelper { return filename.replaceAll("[^\\w\\.\\-\\=]+", "_"); } + /** + * Returns true if the {@code certificate} is signed by one of the {@code signingCertificates}. The list should + * include the certificate itself to allow for self-signed certificates. If it does not, a self-signed certificate + * will return {@code false}. + * + * @param certificate the certificate containing the signature being verified + * @param signingCertificates a list of certificates which may have signed the certificate + * @return true if one of the signing certificates did sign the certificate + */ + public static boolean verifyCertificateSignature(X509Certificate certificate, List signingCertificates) { + String certificateDisplayInfo = getCertificateDisplayInfo(certificate); + if (isVerbose()) { + logger.info("Verifying the certificate signature for " + certificateDisplayInfo); + } + boolean signatureMatches = false; + for (X509Certificate signingCert : signingCertificates) { + final String signingCertDisplayInfo = getCertificateDisplayInfo(signingCert); + try { + if (isVerbose()) { + logger.info("Attempting to verify certificate " + certificateDisplayInfo + " signature with " + signingCertDisplayInfo); + } + PublicKey pub = signingCert.getPublicKey(); + certificate.verify(pub); + if (isVerbose()) { + logger.info("Certificate was signed by " + signingCertDisplayInfo); + } + signatureMatches = true; + break; + } catch (Exception e) { + // Expected if the signature does not match + if (isVerbose()) { + logger.warn("Certificate " + certificateDisplayInfo + " not signed by " + signingCertDisplayInfo + " [" + e.getLocalizedMessage() + "]"); + } + } + } + return signatureMatches; + } + + private static String getCertificateDisplayInfo(X509Certificate certificate) { + return certificate.getSubjectX500Principal().getName(); + } + + private static boolean isVerbose() { + // TODO: When verbose mode is enabled via command-line flag, this will read the variable + return isVerbose; + } + } diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy new file mode 100644 index 0000000000..f73bc8c724 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/standalone/TlsToolkitStandaloneGroovyTest.groovy @@ -0,0 +1,271 @@ +/* + * 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.StandaloneConfig +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator +import org.bouncycastle.util.io.pem.PemWriter +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Files +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.Security +import java.security.SignatureException +import java.security.cert.X509Certificate + +@RunWith(JUnit4.class) +class TlsToolkitStandaloneGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(TlsToolkitStandaloneGroovyTest.class) + + private final String TEST_SRC_DIR = "src/test/resources/" + private final String DEFAULT_KEY_PAIR_ALGORITHM = "RSA" + private final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA" + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder() + + @BeforeClass + static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Test + void testShouldVerifyCertificateSignatureWhenSelfSigned() { + // Arrange + + // Create a temp directory for this test and populate it with the nifi-cert.pem and nifi-key.key files + File baseDir = createBaseDirAndPopulateWithCAFiles() + + // Make a standalone config which doesn't trigger any keystore generation and just has a self-signed cert and key + StandaloneConfig standaloneConfig = new StandaloneConfig() + standaloneConfig.setBaseDir(baseDir) + standaloneConfig.setInstanceDefinitions([]) + standaloneConfig.setClientDns([]) + standaloneConfig.initDefaults() + + TlsToolkitStandalone standalone = new TlsToolkitStandalone() + + // Act + standalone.createNifiKeystoresAndTrustStores(standaloneConfig) + + // Assert + + // The test will fail with an exception if the certificate is not signed by a known certificate + } + + /** + * The certificate under examination is self-signed, but there is another signing cert which will be iterated over first, fail, and then the self-signed signature will be validated. + */ + @Test + void testShouldVerifyCertificateSignatureWithMultipleSigningCerts() { + // Arrange + + // Create a temp directory for this test and populate it with the nifi-cert.pem and nifi-key.key files + File baseDir = createBaseDirAndPopulateWithCAFiles() + + // Create a different cert and persist it to the base dir + X509Certificate otherCert = generateX509Certificate() + File otherCertFile = writeCertificateToPEMFile(otherCert, "${baseDir.path}/other.pem") + logger.info("Wrote other CA cert to ${otherCertFile.path}") + + // Make a standalone config which doesn't trigger any keystore generation and just has a self-signed cert and key + StandaloneConfig standaloneConfig = new StandaloneConfig() + standaloneConfig.setBaseDir(baseDir) + standaloneConfig.setInstanceDefinitions([]) + standaloneConfig.setClientDns([]) + standaloneConfig.initDefaults() + + // Inject the additional CA cert path + standaloneConfig.setAdditionalCACertificate(otherCertFile.path) + + TlsToolkitStandalone standalone = new TlsToolkitStandalone() + + // Act + standalone.createNifiKeystoresAndTrustStores(standaloneConfig) + + // Assert + + // The test will fail with an exception if the certificate is not signed by a known certificate + } + + /** + * The certificate under examination is signed with the external signing cert. + */ + @Test + void testShouldVerifyCertificateSignatureWithAdditionalSigningCert() { + // Arrange + + // Create a temp directory for this test + File baseDir = createBaseDir() + + // Create a root CA, create an intermediate CA, use the root to sign the intermediate and then provide the root + KeyPair rootKeyPair = generateKeyPair() + X509Certificate rootCert = generateX509Certificate("CN=Root CA", rootKeyPair) + + File rootCertFile = writeCertificateToPEMFile(rootCert, "${baseDir.path}/root.pem") + logger.info("Wrote root CA cert to ${rootCertFile.path}") + + KeyPair intermediateKeyPair = generateKeyPair() + X509Certificate intermediateCert = CertificateUtils.generateIssuedCertificate("CN=Intermediate CA", intermediateKeyPair.getPublic(), rootCert, rootKeyPair, DEFAULT_SIGNING_ALGORITHM, 1) + + File intermediateCertFile = writeCertificateToPEMFile(intermediateCert, "${baseDir.path}/nifi-cert.pem") + logger.info("Wrote intermediate CA cert to ${intermediateCertFile.path}") + + // Write the private key of the intermediate cert to nifi-key.key + File intermediateKeyFile = writePrivateKeyToFile(intermediateKeyPair, "${baseDir}/nifi-key.key") + logger.info("Wrote intermediate private key to ${intermediateKeyFile.path}") + + // Make a standalone config which doesn't trigger any keystore generation and just has a signed cert and key + StandaloneConfig standaloneConfig = new StandaloneConfig() + standaloneConfig.setBaseDir(baseDir) + standaloneConfig.setInstanceDefinitions([]) + standaloneConfig.setClientDns([]) + standaloneConfig.initDefaults() + + // Inject the additional CA cert path + standaloneConfig.setAdditionalCACertificate(rootCertFile.path) + + TlsToolkitStandalone standalone = new TlsToolkitStandalone() + + // Act + standalone.createNifiKeystoresAndTrustStores(standaloneConfig) + + // Assert + + // The test will fail with an exception if the certificate is not signed by a known certificate + } + + @Test + void testShouldNotVerifyCertificateSignatureWithWrongSigningCert() { + // Arrange + + // Create a temp directory for this test + File baseDir = createBaseDir() + + // Create a root CA, create an intermediate CA, use the root to sign the intermediate and then do not provide the root + KeyPair rootKeyPair = generateKeyPair() + X509Certificate rootCert = generateX509Certificate("CN=Root CA", rootKeyPair) + + KeyPair intermediateKeyPair = generateKeyPair() + X509Certificate intermediateCert = CertificateUtils.generateIssuedCertificate("CN=Intermediate CA", intermediateKeyPair.getPublic(), rootCert, rootKeyPair, DEFAULT_SIGNING_ALGORITHM, 1) + + File intermediateCertFile = writeCertificateToPEMFile(intermediateCert, "${baseDir.path}/nifi-cert.pem") + logger.info("Wrote intermediate CA cert to ${intermediateCertFile.path}") + + // Write the private key of the intermediate cert to nifi-key.key + File intermediateKeyFile = writePrivateKeyToFile(intermediateKeyPair, "${baseDir.path}/nifi-key.key") + logger.info("Wrote intermediate private key to ${intermediateKeyFile.path}") + + // Make a standalone config which doesn't trigger any keystore generation and just has a signed cert and key + StandaloneConfig standaloneConfig = new StandaloneConfig() + standaloneConfig.setBaseDir(baseDir) + standaloneConfig.setInstanceDefinitions([]) + standaloneConfig.setClientDns([]) + standaloneConfig.initDefaults() + + TlsToolkitStandalone standalone = new TlsToolkitStandalone() + + // Act + def msg = shouldFail(SignatureException) { + standalone.createNifiKeystoresAndTrustStores(standaloneConfig) + } + logger.expected(msg) + + // Assert + assert msg =~ 'The signing certificate was not signed by any known certificates' + } + + private static File writePrivateKeyToFile(KeyPair intermediateKeyPair, String destination) { + File intermediateKeyFile = new File(destination) + PemWriter pemWriter = new PemWriter(new FileWriter(intermediateKeyFile)) + pemWriter.writeObject(new JcaMiscPEMGenerator(intermediateKeyPair)) + pemWriter.close() + intermediateKeyFile + } + + private File createBaseDirAndPopulateWithCAFiles() { + File baseDir = createBaseDir() + + populateBaseDirWithCAFiles(baseDir) + } + + private File createBaseDir() { + File baseDir = tmpDir.newFolder() + logger.info("Created base dir at ${baseDir.path}") + baseDir + } + + private File populateBaseDirWithCAFiles(File baseDir) { + File certificateFile = new File(TEST_SRC_DIR, "rootCert.crt") + File keyFile = new File(TEST_SRC_DIR, "rootCert.key") + File destinationCertFile = new File(baseDir.path, "nifi-cert.pem") + Files.copy(certificateFile.toPath(), destinationCertFile.toPath()) + logger.info("Wrote certificate to ${destinationCertFile.path}") + File destinationKeyFile = new File(baseDir.path, "nifi-key.key") + Files.copy(keyFile.toPath(), destinationKeyFile.toPath()) + logger.info("Wrote private key to ${destinationKeyFile.path}") + + baseDir + } + + /** + * Returns an {@link X509Certificate} with the provided DN and default algorithms. The validity period is only 1 day. + * + * @param dn the DN (defaults to {@code CN=Test Certificate}) + * @return the X509Certificate + */ + private X509Certificate generateX509Certificate(String dn = "CN=Test Certificate", KeyPair keyPair = generateKeyPair()) { + CertificateUtils.generateSelfSignedX509Certificate(keyPair, CertificateUtils.reorderDn(dn), DEFAULT_SIGNING_ALGORITHM, 1) + } + + private KeyPair generateKeyPair() { + KeyPairGenerator instance = KeyPairGenerator.getInstance(DEFAULT_KEY_PAIR_ALGORITHM) + instance.initialize(2048) + instance.generateKeyPair() + } + + /** + * Writes the provided {@link X509Certificate} to the specified file in PEM format. + * + * @param certificate the certificate + * @param destination the path to write the certificate in PEM format + * @return the file + */ + private static File writeCertificateToPEMFile(X509Certificate certificate, String destination) { + File certificateFile = new File(destination) + PemWriter pemWriter = new PemWriter(new FileWriter(certificateFile)) + pemWriter.writeObject(new JcaMiscPEMGenerator(certificate)) + pemWriter.close() + + certificateFile + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy new file mode 100644 index 0000000000..fa9cdaec9a --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/util/TlsHelperGroovyTest.groovy @@ -0,0 +1,140 @@ +/* + * 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.crypto.params.RSAKeyParameters +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.security.auth.x500.X500Principal +import java.security.KeyPair +import java.security.PrivateKey +import java.security.Security +import java.security.cert.X509Certificate + +@RunWith(JUnit4.class) +class TlsHelperGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(TlsHelperGroovyTest.class) + private + final BCRSAPublicKey BAD_PUBLIC_KEY = new BCRSAPublicKey(new RSAKeyParameters(false, new BigInteger("1", 10), new BigInteger("1", 10))) + + @BeforeClass + static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Test + void testShouldVerifyCertificateSignatureWhenSelfSigned() { + // Arrange + File certificateFile = new File("src/test/resources/rootCert.crt") + FileReader certReader = new FileReader(certificateFile) + X509Certificate certificate = TlsHelper.parseCertificate(certReader) + logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}") + + // Act + boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [certificate]) + logger.info("Certificate signature valid: ${isCertificateSigned}") + + // Assert + assert isCertificateSigned + } + + @Test + void testShouldVerifyCertificateSignatureWithMultipleSigningCerts() { + // Arrange + File certificateFile = new File("src/test/resources/rootCert.crt") + FileReader certReader = new FileReader(certificateFile) + X509Certificate certificate = TlsHelper.parseCertificate(certReader) + logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}") + + X509Certificate mockCertificate = [ + getSubjectX500Principal: { -> new X500Principal("CN=Mock Certificate") }, + getPublicKey : { -> BAD_PUBLIC_KEY } + ] as X509Certificate + + // Act + boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [mockCertificate, certificate]) + logger.info("Certificate signature valid: ${isCertificateSigned}") + + // Assert + assert isCertificateSigned + } + + @Test + void testShouldNotVerifyCertificateSignatureWithNoSigningCerts() { + // Arrange + File certificateFile = new File("src/test/resources/rootCert.crt") + FileReader certReader = new FileReader(certificateFile) + X509Certificate certificate = TlsHelper.parseCertificate(certReader) + logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}") + + // Act + boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, []) + logger.info("Certificate signature valid: ${isCertificateSigned}") + + // Assert + assert !isCertificateSigned + } + + @Test + void testShouldNotVerifyCertificateSignatureWithWrongSigningCert() { + // Arrange + File certificateFile = new File("src/test/resources/rootCert.crt") + FileReader certReader = new FileReader(certificateFile) + X509Certificate certificate = TlsHelper.parseCertificate(certReader) + logger.info("Read certificate ${certificate.getSubjectX500Principal().name} from ${certificateFile.path}") + + X509Certificate mockCertificate = [ + getSubjectX500Principal: { -> new X500Principal("CN=Mock Certificate") }, + getPublicKey : { -> BAD_PUBLIC_KEY } + ] as X509Certificate + + // Act + boolean isCertificateSigned = TlsHelper.verifyCertificateSignature(certificate, [mockCertificate]) + logger.info("Certificate signature valid: ${isCertificateSigned}") + + // Assert + assert !isCertificateSigned + } + + @Test + void testParseKeyPairFromReaderShouldHandlePKCS8PrivateKey() { + // Arrange + File keyFile = new File("src/test/resources/rootCert-pkcs8.key") + FileReader keyReader = new FileReader(keyFile) + + final PrivateKey EXPECTED_PRIVATE_KEY = TlsHelper.parseKeyPairFromReader(new FileReader(new File ("src/test/resources/rootCert.key"))).private + + // Act + KeyPair keyPair = TlsHelper.parseKeyPairFromReader(keyReader) + logger.info("Successfully read PKCS #8 unencrypted key from ${keyFile.path}") + + // Assert + assert keyPair.private == EXPECTED_PRIVATE_KEY + } +} diff --git a/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key new file mode 100644 index 0000000000..fc7faefb7e --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-tls/src/test/resources/rootCert-pkcs8.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6+OcU6ijl9hqr +q8yI/ZyahKPenOkuO9U+xAY/g4fu/LV/cLoilaStePL+Sy2w/XXpYLg92csnQUXi +iBH/hvlel4W4teuawaTd1ZBpzfO6dnUA68kp+VCoKI58gbz24yScl7R6cerh+GPL +ipmvA6YyjMZrJZrmJGMZ9juZLjiW5hC+f13P0bggigZWnEmuW0cOuXR8xxWpjop/ +Fv//qO+jviZbQTzmGO7jkkex4RQwcyxlokBh8qDZ1FCvITVQhbkZqLYijXseNYip +ZSeG3n8K8kbcwqj/dedW7DJWdue3lt7L4341VWwwpOKDebs8YrZHp/Jwq6kgwxVn +rQVuwujvAgMBAAECggEADOpKlBBEuPXSC8+3NjNGkQneg+8U0GPDrC1APTzps+Fy +7BWuVds+X9k998DbrCEl9vP+Zg9YUCLbH/XEQIFjUlxnGUY/uxXrPIOXESfv0Q6D +sIeZArQ9FRCQHxubIPa5vbNg/SBHWEqfIh011ngLD+zXe+lCFOmois+OeFtP/2RQ +wzF1as3fr+j9MvXxVkwPYTdYYzNf6Vso0uxC98AZSlt9s+2ZQ8V7NsOgvk+qSPgp +nj/mXVF+pCJqY2KPDHGb+gZsOHwC7LG+ehHLENz05YSh6kRvvZB15zgsmdpg1dS0 +jauDVABtxfO5Eu2/yjQnSClV2qbZE7J/e0Voj4WTqQKBgQDeNkzlW1rsEcFPUeZg +ZlA65Adb5YPsJCAt5cIIHg7jGr/uXuIbaEzem3rlMZp7vwoe0nYtyJaspG41Ou4G +gprHowXDXpl9JBIWLQqef4RtzFyY9DaL9U7Gl05UA/0vIQooZOAfYVMTtGSIa3+g +rR4mtgekcykCzSNHIIhgY+a3XQKBgQDXZuB+4wCGLLeS/8hi2Xdbt2gYVXO7YLn1 +VbGaSn9a8ZDKtVQJRGnk82OVvQsgzgNmcpH/MLGg/RJ/VO5WY5E7Vn5nQ0omijBj +K95JxOef5GorDuyzH0qAZFdce5aUNwG/mUgw/m9iz/knjtqtl1uMjc1uKu+/zLiQ +e3UuSs9YuwKBgHSpX2+eqbp8i9e/8Mo1jPOOGgr2EW+de8N894RZe4lh1tgnul+X +P5wzVq8Tfr5vCrop1l+tCuXyoeWSXbrPQMGE5havCLg5gsFfvk5+NiGLBCZNOIH8 +NPJwJ3BWc8sVdobEhyISb5JNx+YTQfsySD0cniCJUUOmPVovS0oHyO4FAoGAEQPu +bfeOngrAQZt0/+H/3L3jOjDd4IpmhivLyM1jvBJjBrBGQCkoWE6lqjlxvJipihk4 +0TjOf1IeePKDlU1uNorBl3SoUd0Or3bSq28jgOzxOg+GwSuSngvPHt4gafriZ3k7 +S6t9rweQvgA55AaV08eL180KfVM1rSwjeJGuSWsCgYEAuOeoNAUG5PeKjSWhxc8f +DpRsgBsPo+bxnX4eAr+GyTJf/1uacmfejLOPHOImGnL3pGgu7SzEHGjl+McZznhC +foJyK+7igz7iuS18AuEu4VR+J7y2vqdmWeabFHI2onEPwvlO1vrpOIL2yd/3wxgC +qwoPTd89hQB2k+Uhuf7FLjg= +-----END PRIVATE KEY-----