You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by GitBox <gi...@apache.org> on 2020/11/16 23:04:19 UTC

[GitHub] [nifi] VedaKadam opened a new pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

VedaKadam opened a new pull request #4670:
URL: https://github.com/apache/nifi/pull/4670


   Thank you for submitting a contribution to Apache NiFi.
   
   Please provide a short description of the PR here:
   Verifies each node has the correct configuration files and passwords available, and that the key/certificate contents of the keystore and truststore are correct for that node
   
   #### Description of PR
   
   _Enables X functionality; fixes bug NIFI-YYYY._
   
   In order to streamline the review of the contribution we ask you
   to ensure the following steps have been taken:
   
   ### For all changes:
   - [x] Is there a JIRA ticket associated with this PR? Is it referenced 
        in the commit message?
   
   - [x] Does your PR title start with **NIFI-XXXX** where XXXX is the JIRA number you are trying to resolve? Pay particular attention to the hyphen "-" character.
   
   - [x] Has your PR been rebased against the latest commit within the target branch (typically `main`)?
   
   - [ ] Is your initial contribution a single, squashed commit? _Additional commits in response to PR reviewer feedback should be made on this branch and pushed to allow change tracking. Do not `squash` or use `--force` when pushing to allow for clean monitoring of changes._
   
   ### For code changes:
   - [x] Have you ensured that the full suite of tests is executed via `mvn -Pcontrib-check clean install` at the root `nifi` folder?
   - [x] Have you written or updated unit tests to verify your changes?
   - [x] Have you verified that the full build is successful on JDK 8?
   - [x] Have you verified that the full build is successful on JDK 11?
   - [ ] If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under [ASF 2.0](http://www.apache.org/legal/resolved.html#category-a)? 
   - [x] If applicable, have you updated the `LICENSE` file, including the main `LICENSE` file under `nifi-assembly`?
   - [x] If applicable, have you updated the `NOTICE` file, including the main `NOTICE` file found under `nifi-assembly`?
   - [ ] If adding new Properties, have you added `.displayName` in addition to .name (programmatic access) for each of the new properties?
   
   ### For documentation related changes:
   - [ ] Have you ensured that format looks appropriate for the output in which it is rendered?
   
   ### Note:
   Please ensure that once the PR is submitted, you check GitHub Actions CI for build issues and submit an update to your PR as soon as possible.
   


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526376869



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");

Review comment:
       Yes, correcting.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] exceptionfactory commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
exceptionfactory commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r525504215



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());

Review comment:
       Recommend logging the KeyStoreException and adding a custom message along the lines of:
   
   ```suggestion
               logger.error("Unable to get X.509 Certificate from Trust Store for alias [{}]", alias, e);
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526393927



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);

Review comment:
       Yes, changing.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526979642



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());

Review comment:
       Yes, changing




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526998832



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {
+            sanListDNS = sanMap.entrySet().stream().filter(t -> "dNSName".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+            sanListIP = sanMap.entrySet().stream().filter(t -> "iPAddress".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+        } else {
+            logger.error("[2] No DNS or IPAddress entry present in SAN");
+            return new Tuple<>("[2] SAN is empty. ==> Add a SAN entry matching " + specifiedHostname, Output.WRONG);
+        }
+
+        //specifiedHostname is a domain name
+        if (!specifiedHostnameIsIP) {
+
+            //SAN has the specified domain name
+            if (sanListDNS.size() != 0 && sanListDNS.contains(specifiedHostname)) {
+                logger.info("[2] SAN: DNS = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListDNS.size() == 0) {
+                    logger.warn("[2] SAN: SAN doesn't have DNS entry. Checking IP entries.");
+                } else {
+                    logger.warn("[2] SAN: SAN DNS entry doesn't match with host '" + specifiedHostname + "' in nifi.properties. Checking IP entries.");
+                }
+                //check for IP entries in SAN to match with resolved specified hostname
+                if (sanListIP.size() != 0) {
+                    try {
+                        String ipAddress = InetAddress.getByName(specifiedHostname).getHostAddress();
+                        if (sanListIP.contains(ipAddress)) {
+                            logger.info("    SAN: IP = " + ipAddress + " in SAN  matches with host in nifi.properties after resolution\n");
+                            return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+                        } else {
+                            logger.error("    No IP address entries found in SAN that represent " + specifiedHostname);
+                            logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                            return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                        }
+                    } catch (UnknownHostException e) {
+                        logger.error("    " + e.getLocalizedMessage() + "\n");
+                        return new Tuple<>("[2] Unable to resolve hostname in nifi.properties to IP ", Output.NEEDS_ATTENTION);
+                    }
+
+                } else {
+                    //No IP entries present in SAN
+                    logger.error("    No IP address entries found in SAN to resolve.");
+                    logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        } else { //nifi.web.https.host is an IP address
+            if (sanListIP.size() != 0 && sanListIP.contains(specifiedHostname)) {
+                logger.info("[2] SAN: IP = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListIP.size() == 0) {
+                    logger.error("[2] SAN: SAN doesn't have IP entry");
+                    logger.error("    Add IP entry in SAN for host IP: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN has no IP entries. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                } else {
+                    return new Tuple<>("[2] SAN IP entries do not represent hostname in nifi.properties. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        }
+    }
+
+    private static Tuple<String, Output> checkEKU(X509Certificate x509Certificate) {
+        List<String> eKU = null;
+        try {
+            eKU = x509Certificate.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            logger.error("Error in EKU check: " + e.getLocalizedMessage());
+            return new Tuple<>("Error in EKU check: " + e.getLocalizedMessage(), Output.WRONG);
+        }
+        if (eKU != null) {
+            if (!eKU.contains(ekuMap.get("serverAuth")) && !eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.error("[3] EKU: serverAuth and clientAuth absent");
+                logger.error("    Add serverAuth and clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate.", Output.WRONG);
+            }
+
+            if (eKU.contains(ekuMap.get("serverAuth")) && eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.info("[3] EKU: serverAuth and clientAuth present\n");
+                return new Tuple<>("[3] EKUs are correct. ", Output.CORRECT);
+            } else if (!eKU.contains(ekuMap.get("serverAuth"))) {
+                logger.error("[3] EKU: serverAuth is absent");
+                logger.error("    Add serverAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU serverAuth needs to be added to the certificate. ", Output.WRONG);
+            } else {
+                logger.error("[3] EKU: clientAuth is absent ");
+                logger.error("    Add clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU clientAuth needs to be added to the certificate", Output.WRONG);
+            }
+
+        } else {
+            logger.warn("[3] EKU: No extended key usage found. Add serverAuth and clientAuth usage to the EKU of the certificate.\n");
+            return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate. ", Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private static Tuple<String, Output> checkValidity(X509Certificate x509Certificate) {
+        String message;
+        try {
+            x509Certificate.checkValidity();
+            logger.info("[4] Validity: Certificate is VALID");
+
+            DateFormat dateFormat = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy");
+            Date dateObj = new Date();
+            Date expiry = x509Certificate.getNotAfter();
+
+            long mSecTillExpiry = Math.abs(expiry.getTime() - dateObj.getTime());
+            long daysTillExpiry = TimeUnit.DAYS.convert(mSecTillExpiry, TimeUnit.MILLISECONDS);
+
+            if (daysTillExpiry < 30) {
+                logger.warn("    Certificate expires in less than 30 days\n");
+            } else if (daysTillExpiry < 60) {
+                logger.warn("    Certificate expires in less than 60 days\n");
+            } else if (daysTillExpiry < 90) {
+                logger.warn("    Certificate expires in less than 90 days\n");
+            } else {
+                logger.info("    Certificate expires in " + daysTillExpiry + "  days\n");
+            }
+            return new Tuple<>("[4] Certificate is VALID", Output.CORRECT);
+        } catch (CertificateExpiredException e) {
+            message = "[4] Validity: Certificate is INVALID: Validity date expired " + x509Certificate.getNotAfter();
+        } catch (CertificateNotYetValidException e) {
+            message = "[4] Validity: Certificate is INVALID: Certificate is not valid before " + x509Certificate.getNotBefore();
+        }
+        logger.error(message + "\n");
+        return new Tuple<>(message, Output.WRONG);
+    }
+
+    private static Tuple<String, Output> checkKeySize(X509Certificate x509Certificate) {
+        PublicKey publicKey = x509Certificate.getPublicKey();
+
+        String finding = "[5] ";
+        String padding = "    ";
+        Output output;
+        String message;
+
+        // Determine key length and print
+        int keyLength = determineKeyLength(publicKey);
+        String keyLengthMessage = publicKey.getAlgorithm() + " Key length: " + keyLength;
+        logger.info(padding + keyLengthMessage);
+
+        // If unsupported key algorithm, print warning
+        if (!(publicKey instanceof RSAPublicKey || publicKey instanceof DSAPublicKey)) {
+            //TODO: Add different algorithm key length checks
+            message = finding + keyLengthMessage;
+            logger.warn(finding + "Key length not checked for " + publicKey.getAlgorithm() + "\n");
+            output = Output.NEEDS_ATTENTION;
+        } else {
+            // If supported key length, check for validity
+            if (keyLength >= 2048) {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is VALID";
+                logger.info(message + "\n");
+                output = Output.CORRECT;
+            } else {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is INVALID (key length below minimum 2048 bits)";
+                logger.error(message + "\n");
+                output = Output.WRONG;
+            }
+        }
+        return new Tuple<>(message, output);
+    }
+
+    private static Tuple<String, Output> checkSignature(List<X509Certificate> certificateList, X509Certificate x509Certificate) {
+        String number = "[6] ";
+        String message;
+        Output output;
+        if (TlsHelper.verifyCertificateSignature(x509Certificate, certificateList)) {
+            message = number + "Signature is VALID";
+            logger.info(message + "\n");
+            output = Output.CORRECT;
+        } else {
+            message = number + "Signature is INVALID";
+            logger.error(message + "\n");
+            output = Output.WRONG;
+        }
+        return new Tuple<>(message, output);
+    }
+
+
+    private static int determineKeyLength(PublicKey publicKey) {
+        switch (publicKey.getAlgorithm().toUpperCase()) {

Review comment:
       Yes, changing to instance of




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526380294



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {

Review comment:
       I think for toolkit we are only using .jks . I referred this link: https://nifi.apache.org/docs/nifi-docs/html/walkthroughs.html#securing-nifi-with-provided-certificates 
   = "PKCS12 keystores are usable by NiFi, but JKS format is handled more robustly and causes fewer edge cases. JKS keystores cannot be formed directly from PEM files, so the PKCS12 keystore serves as an intermediate form"
   Since this is for common deployment scenario, for now I have used only .jks keystores. This can be something to add further. Adding it as TODO.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526359408



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {

Review comment:
       Yes, changing.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r527004277



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisCommandLineTest.groovy
##########
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.nifi.toolkit.tls.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisCommandLineTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+
+    @Test
+    void shouldExitMainWithNoArgs() {
+
+        //Arrange
+        exit.expectSystemExitWithStatus(ExitCode.INVALID_ARGS.ordinal())
+        exit.checkAssertionAfterwards({
+            assert true
+        })
+
+        //exit.checkAssertionAfterwards(new VedaAssertion())

Review comment:
       No, it was used for a test. Removing.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] exceptionfactory commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
exceptionfactory commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526397819



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());

Review comment:
       Sorry for the lack of clarity, I recommend passing the exception object to the logger so that the stack trace is also logged:
   
   ```suggestion
               logger.error("Something went wrong: {}", e.getLocalizedMessage(), e);
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r527002762



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandaloneTest.groovy
##########
@@ -0,0 +1,660 @@
+/*
+ * 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.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.security.util.KeyStoreUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.util.TlsHelper
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.Extension
+import org.bouncycastle.asn1.x509.Extensions
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+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.jcajce.JcaContentSignerBuilder
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+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.KeyStore
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisStandaloneTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+    public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    private static final KeyPair keyPair = TlsHelper.generateKeyPair("RSA", 2048)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+        //setupTmpDir() ???
+    }
+
+    static X509Certificate signAndBuildCert(String dn, String signingAlgorithm, KeyPair keyPair) {
+        ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+        return cert
+    }
+
+    static X509v3CertificateBuilder certBuilder(Date startDate, String dn, KeyPair keyPair, int hours) {
+        Date endDate = new Date(startDate.getTime() + TimeUnit.HOURS.toMillis(hours));
+
+        SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                CertificateUtils.getUniqueSerialNumber(),
+                startDate, endDate,
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                subPubKeyInfo)
+        return certBuilder
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+    @Ignore("No assertions to make here")

Review comment:
       For future addition of command-line arguments, it will be helpful to test out printing the help message to check for any dimension changes of helpFormatter.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526395590



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());

Review comment:
       I'm not sure if I understand this one. I have added the KeystoreException **e** to the `logger.error` as `e.getLocalizedMessage()`




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526965016



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());

Review comment:
       Yes I'll add that.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] github-actions[bot] commented on pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
github-actions[bot] commented on pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#issuecomment-819929625


   We're marking this PR as stale due to lack of updates in the past few months. If after another couple of weeks the stale label has not been removed this PR will be closed. This stale marker and eventual auto close does not indicate a judgement of the PR just lack of reviewer bandwidth and helps us keep the PR queue more manageable.  If you would like this PR re-opened you can do so and a committer can remove the stale tag.  Or you can open a new PR.  Try to help review other PRs to increase PR review bandwidth which in turn helps yours.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526351267



##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -160,26 +180,33 @@ public static String extractUsername(String dn) {
      */
     public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException {
 
-        final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+        /*
+         * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
+         *
+         * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.
+         */
+
+        ArrayList<String> sanEntries = new ArrayList<>(getSubjectAlternativeNamesMap(certificate).keySet());

Review comment:
       Yes, correcting.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526380294



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {

Review comment:
       I think for toolkit we prefer using .jks . I referred this link: https://nifi.apache.org/docs/nifi-docs/html/walkthroughs.html#securing-nifi-with-provided-certificates 
   = "PKCS12 keystores are usable by NiFi, but JKS format is handled more robustly and causes fewer edge cases. JKS keystores cannot be formed directly from PEM files, so the PKCS12 keystore serves as an intermediate form"
   Since this is for common deployment scenario, for now I have used only .jks keystores. This can be something to add further. Adding it as TODO.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#issuecomment-731247911


   Thank you for the detailed review @exceptionfactory and @thenatog. I completely agree with all points mentioned and will amend the code as per.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526395590



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());

Review comment:
       I'm not sure if I understand this one. I have the KeystoreException **e** to the `logger.error` as `e.getLocalizedMessage()`




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] thenatog edited a comment on pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
thenatog edited a comment on pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#issuecomment-730635492


   I've tested out using the diagnosis tool and had some feedback on its usage. I've compiled my comments alongside the logs I saw for different tests I tried. Let me know if you can't access: https://docs.google.com/spreadsheets/d/1kAbM4LLA3NgRjKAfCXg7GqS8dvJnFA6sajrAkBezMt4/edit?usp=sharing
   
   I think with this tool, the key things to focus on should be these:
   
   - The tool is being created to assist users with checking configuration that can be difficult to get right. The tool needs to be easy to use. The usage guide of the tool wasn't completely clear when I tried out using it.
   - The tool diagnoses errors with configuration, so the tool itself needs to be error free (as best as possible). Otherwise if there are errors with the tool, how will a user (a user who may already be struggling to get their configuration right) know whether the tool is to blame or the configuration is to blame? It needs to be pretty robust.
   - Sometimes I got a summary of results, sometimes I didn't, depending on what error the tool experienced. It wasn't immediately clear if the tool failed, or my configuration did. We need to gracefully fail. In as many cases as possible, there should always some form of diagnosis summary at the end of running the tool with information on how to proceed/fix the problem. 
   
   I can appreciate that the tls-toolkit code as it stands needs redesigning/refactoring, and maybe you can let me know if the above feature requests are difficult to implement with the way it is right now. However, we should do our absolute best to make sure this diagnosis tool is robust as possible. A tool that diagnoses errors should have few errors of its own, and its correct usage should be clear and simple. Ideally, we don't want to have to provide support to users on how to use a tool that was created to support them in the first place.
   
   Having said that, this is a great idea and I think with a few adjustments it will be really useful to diagnose problems our users frequently have. Especially so if we can do a more complex option for clustered nodes.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] exceptionfactory commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
exceptionfactory commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r525469318



##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -160,26 +180,33 @@ public static String extractUsername(String dn) {
      */
     public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException {
 
-        final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+        /*
+         * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
+         *
+         * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.
+         */
+
+        ArrayList<String> sanEntries = new ArrayList<>(getSubjectAlternativeNamesMap(certificate).keySet());

Review comment:
       The variable sanEntries can be declared as a List instead of an ArrayList.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisCommandLine.java
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class TlsToolkitGetDiagnosisCommandLine {
+
+    public static final String DESCRIPTION = "Diagnoses issues in common deployment scenario of TLS toolkit";
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitStandaloneCommandLine.class);
+
+

Review comment:
       Recommend running code formatting on this class to remove multiple new lines in several places.

##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -120,12 +123,29 @@
         return Collections.unmodifiableMap(orderMap);
     }
 
+    private static Map<Integer, String> createSANOrderMap() {
+        Map<Integer, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put(count++, "otherName");
+        orderMap.put(count++, "rfc822Name");
+        orderMap.put(count++, "dNSName");
+        orderMap.put(count++, "x400Address");
+        orderMap.put(count++, "directoryName");
+        orderMap.put(count++, "ediPartyName");
+        orderMap.put(count++, "uniformResourceIdentifier");
+        orderMap.put(count++, "iPAddress");
+        orderMap.put(count, "registeredID");
+        return Collections.unmodifiableMap(orderMap);

Review comment:
       Another option for implementing an ordered map of Subject Alternative Names would be a Java enum.  Using the enum ordinal value would provide implicit ordering based on how values are declared, as opposed to having an incremented counter used to build this map.  Having a SortedSubjectAlternativeName enum would also make known values more clearly defined.

##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -160,26 +180,33 @@ public static String extractUsername(String dn) {
      */
     public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException {
 
-        final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+        /*
+         * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
+         *
+         * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.

Review comment:
       This comment seems like it applies to the getSubjectAlternativeNamesMap method, so it probably should be moved, or removed if no longer applicable.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisCommandLine.java
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class TlsToolkitGetDiagnosisCommandLine {
+
+    public static final String DESCRIPTION = "Diagnoses issues in common deployment scenario of TLS toolkit";
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitStandaloneCommandLine.class);
+
+
+    public static void main(String[] args) {
+
+        TlsToolkitGetDiagnosisCommandLine commandLine = new TlsToolkitGetDiagnosisCommandLine();
+        try {
+            commandLine.chooseMain(args);
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        }
+
+    }
+
+    public void chooseMain(String[] args) throws CommandLineParseException {
+
+
+        if(args.length < 1){
+           //How to print errors and exit

Review comment:
       Recommend removing this comment.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {

Review comment:
       A more specific name like OutputStatus would help clarify the purpose of this enum.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");

Review comment:
       Recommend using System.println() to remove newline characters, and either computing the total number of checks or setting and reusing a static variable for the total number of checks.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());

Review comment:
       Recommend adding the KeyStoreException as a parameter to logger.error() for better troubleshooting.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");

Review comment:
       Using System.println() before and after would remove the need to include platform-specific newline characters.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandaloneTest.groovy
##########
@@ -0,0 +1,660 @@
+/*
+ * 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.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.security.util.KeyStoreUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.util.TlsHelper
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.Extension
+import org.bouncycastle.asn1.x509.Extensions
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+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.jcajce.JcaContentSignerBuilder
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+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.KeyStore
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisStandaloneTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+    public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    private static final KeyPair keyPair = TlsHelper.generateKeyPair("RSA", 2048)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+        //setupTmpDir() ???
+    }
+
+    static X509Certificate signAndBuildCert(String dn, String signingAlgorithm, KeyPair keyPair) {
+        ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+        return cert
+    }
+
+    static X509v3CertificateBuilder certBuilder(Date startDate, String dn, KeyPair keyPair, int hours) {
+        Date endDate = new Date(startDate.getTime() + TimeUnit.HOURS.toMillis(hours));
+
+        SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                CertificateUtils.getUniqueSerialNumber(),
+                startDate, endDate,
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                subPubKeyInfo)
+        return certBuilder
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+    @Ignore("No assertions to make here")

Review comment:
       Is there a reason to leave this ignored test method in place?

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);

Review comment:
       Recommend setting and reusing static variables to prefix this and other check messages in the class.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);

Review comment:
       The prefix string could be replaced the the **number** variable, which could also be refactored to a static final class variable.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());

Review comment:
       Is it necessary to use the Bouncy Castle X500Name class as opposed to just passing the Java X500Principal.toString() to CertificateUtils.extractUsername()?

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());

Review comment:
       Recommend logging the KeyStoreException and adding a custom message along the lines of "Unable to get X.509 Certificate from Trust Store for alias [%s]".

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {
+            sanListDNS = sanMap.entrySet().stream().filter(t -> "dNSName".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+            sanListIP = sanMap.entrySet().stream().filter(t -> "iPAddress".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+        } else {
+            logger.error("[2] No DNS or IPAddress entry present in SAN");
+            return new Tuple<>("[2] SAN is empty. ==> Add a SAN entry matching " + specifiedHostname, Output.WRONG);
+        }
+
+        //specifiedHostname is a domain name
+        if (!specifiedHostnameIsIP) {
+
+            //SAN has the specified domain name
+            if (sanListDNS.size() != 0 && sanListDNS.contains(specifiedHostname)) {
+                logger.info("[2] SAN: DNS = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListDNS.size() == 0) {
+                    logger.warn("[2] SAN: SAN doesn't have DNS entry. Checking IP entries.");
+                } else {
+                    logger.warn("[2] SAN: SAN DNS entry doesn't match with host '" + specifiedHostname + "' in nifi.properties. Checking IP entries.");
+                }
+                //check for IP entries in SAN to match with resolved specified hostname
+                if (sanListIP.size() != 0) {
+                    try {
+                        String ipAddress = InetAddress.getByName(specifiedHostname).getHostAddress();
+                        if (sanListIP.contains(ipAddress)) {
+                            logger.info("    SAN: IP = " + ipAddress + " in SAN  matches with host in nifi.properties after resolution\n");
+                            return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+                        } else {
+                            logger.error("    No IP address entries found in SAN that represent " + specifiedHostname);
+                            logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                            return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                        }
+                    } catch (UnknownHostException e) {
+                        logger.error("    " + e.getLocalizedMessage() + "\n");
+                        return new Tuple<>("[2] Unable to resolve hostname in nifi.properties to IP ", Output.NEEDS_ATTENTION);
+                    }
+
+                } else {
+                    //No IP entries present in SAN
+                    logger.error("    No IP address entries found in SAN to resolve.");
+                    logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        } else { //nifi.web.https.host is an IP address
+            if (sanListIP.size() != 0 && sanListIP.contains(specifiedHostname)) {
+                logger.info("[2] SAN: IP = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListIP.size() == 0) {
+                    logger.error("[2] SAN: SAN doesn't have IP entry");
+                    logger.error("    Add IP entry in SAN for host IP: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN has no IP entries. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                } else {
+                    return new Tuple<>("[2] SAN IP entries do not represent hostname in nifi.properties. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        }
+    }
+
+    private static Tuple<String, Output> checkEKU(X509Certificate x509Certificate) {
+        List<String> eKU = null;
+        try {
+            eKU = x509Certificate.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            logger.error("Error in EKU check: " + e.getLocalizedMessage());
+            return new Tuple<>("Error in EKU check: " + e.getLocalizedMessage(), Output.WRONG);
+        }
+        if (eKU != null) {
+            if (!eKU.contains(ekuMap.get("serverAuth")) && !eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.error("[3] EKU: serverAuth and clientAuth absent");
+                logger.error("    Add serverAuth and clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate.", Output.WRONG);
+            }
+
+            if (eKU.contains(ekuMap.get("serverAuth")) && eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.info("[3] EKU: serverAuth and clientAuth present\n");
+                return new Tuple<>("[3] EKUs are correct. ", Output.CORRECT);
+            } else if (!eKU.contains(ekuMap.get("serverAuth"))) {
+                logger.error("[3] EKU: serverAuth is absent");
+                logger.error("    Add serverAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU serverAuth needs to be added to the certificate. ", Output.WRONG);
+            } else {
+                logger.error("[3] EKU: clientAuth is absent ");
+                logger.error("    Add clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU clientAuth needs to be added to the certificate", Output.WRONG);
+            }
+
+        } else {
+            logger.warn("[3] EKU: No extended key usage found. Add serverAuth and clientAuth usage to the EKU of the certificate.\n");
+            return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate. ", Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private static Tuple<String, Output> checkValidity(X509Certificate x509Certificate) {
+        String message;
+        try {
+            x509Certificate.checkValidity();
+            logger.info("[4] Validity: Certificate is VALID");
+
+            DateFormat dateFormat = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy");
+            Date dateObj = new Date();
+            Date expiry = x509Certificate.getNotAfter();
+
+            long mSecTillExpiry = Math.abs(expiry.getTime() - dateObj.getTime());
+            long daysTillExpiry = TimeUnit.DAYS.convert(mSecTillExpiry, TimeUnit.MILLISECONDS);
+
+            if (daysTillExpiry < 30) {
+                logger.warn("    Certificate expires in less than 30 days\n");
+            } else if (daysTillExpiry < 60) {
+                logger.warn("    Certificate expires in less than 60 days\n");
+            } else if (daysTillExpiry < 90) {
+                logger.warn("    Certificate expires in less than 90 days\n");
+            } else {
+                logger.info("    Certificate expires in " + daysTillExpiry + "  days\n");
+            }
+            return new Tuple<>("[4] Certificate is VALID", Output.CORRECT);
+        } catch (CertificateExpiredException e) {
+            message = "[4] Validity: Certificate is INVALID: Validity date expired " + x509Certificate.getNotAfter();
+        } catch (CertificateNotYetValidException e) {
+            message = "[4] Validity: Certificate is INVALID: Certificate is not valid before " + x509Certificate.getNotBefore();
+        }
+        logger.error(message + "\n");
+        return new Tuple<>(message, Output.WRONG);
+    }
+
+    private static Tuple<String, Output> checkKeySize(X509Certificate x509Certificate) {
+        PublicKey publicKey = x509Certificate.getPublicKey();
+
+        String finding = "[5] ";
+        String padding = "    ";
+        Output output;
+        String message;
+
+        // Determine key length and print
+        int keyLength = determineKeyLength(publicKey);
+        String keyLengthMessage = publicKey.getAlgorithm() + " Key length: " + keyLength;
+        logger.info(padding + keyLengthMessage);
+
+        // If unsupported key algorithm, print warning
+        if (!(publicKey instanceof RSAPublicKey || publicKey instanceof DSAPublicKey)) {
+            //TODO: Add different algorithm key length checks
+            message = finding + keyLengthMessage;
+            logger.warn(finding + "Key length not checked for " + publicKey.getAlgorithm() + "\n");
+            output = Output.NEEDS_ATTENTION;
+        } else {
+            // If supported key length, check for validity
+            if (keyLength >= 2048) {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is VALID";
+                logger.info(message + "\n");
+                output = Output.CORRECT;
+            } else {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is INVALID (key length below minimum 2048 bits)";
+                logger.error(message + "\n");
+                output = Output.WRONG;
+            }
+        }
+        return new Tuple<>(message, output);
+    }
+
+    private static Tuple<String, Output> checkSignature(List<X509Certificate> certificateList, X509Certificate x509Certificate) {
+        String number = "[6] ";
+        String message;
+        Output output;
+        if (TlsHelper.verifyCertificateSignature(x509Certificate, certificateList)) {
+            message = number + "Signature is VALID";
+            logger.info(message + "\n");
+            output = Output.CORRECT;
+        } else {
+            message = number + "Signature is INVALID";
+            logger.error(message + "\n");
+            output = Output.WRONG;
+        }
+        return new Tuple<>(message, output);
+    }
+
+
+    private static int determineKeyLength(PublicKey publicKey) {
+        switch (publicKey.getAlgorithm().toUpperCase()) {
+            case "RSA":
+                return ((RSAPublicKey) publicKey).getModulus().bitLength();
+            case "DSA":
+                return ((DSAPublicKey) publicKey).getParams().getP().bitLength();
+            case "EC":
+                return ((BCECPublicKey) publicKey).getParameters().getCurve().getFieldSize();
+            default:
+                logger.warn("Cannot determine key length for unknown algorithm " + publicKey.getAlgorithm());
+                return -1;

Review comment:
       Recommend setting a static variable for UNKNOWN_KEY_LENGTH.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);

Review comment:
       Recommend  setting and reusing a static variable for the failure exit code.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {

Review comment:
       Should the method also support looking for the .p12 extension to support finding PKCS12 files?

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisCommandLineTest.groovy
##########
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.nifi.toolkit.tls.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisCommandLineTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+
+    @Test
+    void shouldExitMainWithNoArgs() {
+
+        //Arrange
+        exit.expectSystemExitWithStatus(ExitCode.INVALID_ARGS.ordinal())
+        exit.checkAssertionAfterwards({
+            assert true
+        })
+
+        //exit.checkAssertionAfterwards(new VedaAssertion())

Review comment:
       Should this commented line remain in place?

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/resources/diagnosis/bootstrap.conf
##########
@@ -0,0 +1,86 @@
+#
+# 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.
+#
+
+# Java command to use when running NiFi
+java=java
+
+# Username to use when running NiFi. This value will be ignored on Windows.
+run.as=
+
+# Configure where NiFi's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling NiFi to shutdown before explicitly killing the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# The G1GC is known to cause some problems in Java 8 and earlier, but the issues were addressed in Java 9. If using Java 8 or earlier,
+# it is recommended that G1GC not be used, especially in conjunction with the Write Ahead Provenance Repository. However, if using a newer
+# version of Java, it can result in better performance without significant "stop-the-world" delays.
+#java.arg.13=-XX:+UseG1GC
+
+#Set headless mode by default
+java.arg.14=-Djava.awt.headless=true
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.bootstrap.sensitive.key=
+
+# Sets the provider of SecureRandom to /dev/urandom to prevent blocking on VMs
+java.arg.15=-Djava.security.egd=file:/dev/urandom
+
+# Requires JAAS to use only the provided JAAS configuration to authenticate a Subject, without using any "fallback" methods (such as prompting for username/password)
+# Please see https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/single-signon.html, section "EXCEPTIONS TO THE MODEL"
+java.arg.16=-Djavax.security.auth.useSubjectCredsOnly=true
+
+# Zookeeper 3.5 now includes an Admin Server that starts on port 8080, since NiFi is already using that port disable by default.
+# Please see https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_adminserver_config for configuration options.
+java.arg.17.=-Dzookeeper.admin.enableServer=false
+
+###
+# Notification Services for notifying interested parties when NiFi is stopped, started, dies
+###
+
+# XML File that contains the definitions of the notification services
+notification.services.file=./conf/bootstrap-notification-services.xml
+
+# In the case that we are unable to send a notification for an event, how many times should we retry?
+notification.max.attempts=5
+
+# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi is started?
+#nifi.start.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi is stopped?
+#nifi.stop.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi dies?
+#nifi.dead.notification.services=email-notification

Review comment:
       The contents of this file could be truncated to remove a number of the commented lines since it is only used for testing.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {

Review comment:
       Creating a SortedSubjectAlternativeName enum with String labels would allow for reuse of known values in this method.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandaloneTest.groovy
##########
@@ -0,0 +1,660 @@
+/*
+ * 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.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.security.util.KeyStoreUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.util.TlsHelper
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.Extension
+import org.bouncycastle.asn1.x509.Extensions
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+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.jcajce.JcaContentSignerBuilder
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+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.KeyStore
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisStandaloneTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+    public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    private static final KeyPair keyPair = TlsHelper.generateKeyPair("RSA", 2048)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+        //setupTmpDir() ???
+    }
+
+    static X509Certificate signAndBuildCert(String dn, String signingAlgorithm, KeyPair keyPair) {
+        ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+        return cert
+    }
+
+    static X509v3CertificateBuilder certBuilder(Date startDate, String dn, KeyPair keyPair, int hours) {
+        Date endDate = new Date(startDate.getTime() + TimeUnit.HOURS.toMillis(hours));
+
+        SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                CertificateUtils.getUniqueSerialNumber(),
+                startDate, endDate,
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                subPubKeyInfo)
+        return certBuilder
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+    @Ignore("No assertions to make here")
+    @Test
+    void testPrintUsage() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        //Act
+        standalone.printUsage("This is an error message");
+
+        //Assert
+    }
+
+    @Test
+    void testShouldParseCommandLine() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-n src/test/resources/diagnosis/nifi.properties"
+
+        //Act
+        standalone.parseCommandLine(args.split(" ") as String[])
+
+        //Assert
+        assert standalone.niFiPropertiesPath == "src/test/resources/diagnosis/nifi.properties"

Review comment:
       Recommend declaring and reusing a variable for the properties path in order to avoid unexpected failures.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {
+            sanListDNS = sanMap.entrySet().stream().filter(t -> "dNSName".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+            sanListIP = sanMap.entrySet().stream().filter(t -> "iPAddress".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+        } else {
+            logger.error("[2] No DNS or IPAddress entry present in SAN");
+            return new Tuple<>("[2] SAN is empty. ==> Add a SAN entry matching " + specifiedHostname, Output.WRONG);
+        }
+
+        //specifiedHostname is a domain name
+        if (!specifiedHostnameIsIP) {
+
+            //SAN has the specified domain name
+            if (sanListDNS.size() != 0 && sanListDNS.contains(specifiedHostname)) {
+                logger.info("[2] SAN: DNS = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListDNS.size() == 0) {
+                    logger.warn("[2] SAN: SAN doesn't have DNS entry. Checking IP entries.");
+                } else {
+                    logger.warn("[2] SAN: SAN DNS entry doesn't match with host '" + specifiedHostname + "' in nifi.properties. Checking IP entries.");
+                }
+                //check for IP entries in SAN to match with resolved specified hostname
+                if (sanListIP.size() != 0) {
+                    try {
+                        String ipAddress = InetAddress.getByName(specifiedHostname).getHostAddress();
+                        if (sanListIP.contains(ipAddress)) {
+                            logger.info("    SAN: IP = " + ipAddress + " in SAN  matches with host in nifi.properties after resolution\n");
+                            return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+                        } else {
+                            logger.error("    No IP address entries found in SAN that represent " + specifiedHostname);
+                            logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                            return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                        }
+                    } catch (UnknownHostException e) {
+                        logger.error("    " + e.getLocalizedMessage() + "\n");
+                        return new Tuple<>("[2] Unable to resolve hostname in nifi.properties to IP ", Output.NEEDS_ATTENTION);
+                    }
+
+                } else {
+                    //No IP entries present in SAN
+                    logger.error("    No IP address entries found in SAN to resolve.");
+                    logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        } else { //nifi.web.https.host is an IP address
+            if (sanListIP.size() != 0 && sanListIP.contains(specifiedHostname)) {
+                logger.info("[2] SAN: IP = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListIP.size() == 0) {
+                    logger.error("[2] SAN: SAN doesn't have IP entry");
+                    logger.error("    Add IP entry in SAN for host IP: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN has no IP entries. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                } else {
+                    return new Tuple<>("[2] SAN IP entries do not represent hostname in nifi.properties. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        }
+    }
+
+    private static Tuple<String, Output> checkEKU(X509Certificate x509Certificate) {
+        List<String> eKU = null;
+        try {
+            eKU = x509Certificate.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            logger.error("Error in EKU check: " + e.getLocalizedMessage());
+            return new Tuple<>("Error in EKU check: " + e.getLocalizedMessage(), Output.WRONG);
+        }
+        if (eKU != null) {
+            if (!eKU.contains(ekuMap.get("serverAuth")) && !eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.error("[3] EKU: serverAuth and clientAuth absent");
+                logger.error("    Add serverAuth and clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate.", Output.WRONG);
+            }
+
+            if (eKU.contains(ekuMap.get("serverAuth")) && eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.info("[3] EKU: serverAuth and clientAuth present\n");
+                return new Tuple<>("[3] EKUs are correct. ", Output.CORRECT);
+            } else if (!eKU.contains(ekuMap.get("serverAuth"))) {
+                logger.error("[3] EKU: serverAuth is absent");
+                logger.error("    Add serverAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU serverAuth needs to be added to the certificate. ", Output.WRONG);
+            } else {
+                logger.error("[3] EKU: clientAuth is absent ");
+                logger.error("    Add clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU clientAuth needs to be added to the certificate", Output.WRONG);
+            }
+
+        } else {
+            logger.warn("[3] EKU: No extended key usage found. Add serverAuth and clientAuth usage to the EKU of the certificate.\n");
+            return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate. ", Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private static Tuple<String, Output> checkValidity(X509Certificate x509Certificate) {
+        String message;
+        try {
+            x509Certificate.checkValidity();
+            logger.info("[4] Validity: Certificate is VALID");
+
+            DateFormat dateFormat = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy");
+            Date dateObj = new Date();
+            Date expiry = x509Certificate.getNotAfter();
+
+            long mSecTillExpiry = Math.abs(expiry.getTime() - dateObj.getTime());
+            long daysTillExpiry = TimeUnit.DAYS.convert(mSecTillExpiry, TimeUnit.MILLISECONDS);
+
+            if (daysTillExpiry < 30) {
+                logger.warn("    Certificate expires in less than 30 days\n");
+            } else if (daysTillExpiry < 60) {
+                logger.warn("    Certificate expires in less than 60 days\n");
+            } else if (daysTillExpiry < 90) {
+                logger.warn("    Certificate expires in less than 90 days\n");
+            } else {
+                logger.info("    Certificate expires in " + daysTillExpiry + "  days\n");
+            }
+            return new Tuple<>("[4] Certificate is VALID", Output.CORRECT);
+        } catch (CertificateExpiredException e) {
+            message = "[4] Validity: Certificate is INVALID: Validity date expired " + x509Certificate.getNotAfter();
+        } catch (CertificateNotYetValidException e) {
+            message = "[4] Validity: Certificate is INVALID: Certificate is not valid before " + x509Certificate.getNotBefore();
+        }
+        logger.error(message + "\n");
+        return new Tuple<>(message, Output.WRONG);
+    }
+
+    private static Tuple<String, Output> checkKeySize(X509Certificate x509Certificate) {
+        PublicKey publicKey = x509Certificate.getPublicKey();
+
+        String finding = "[5] ";
+        String padding = "    ";
+        Output output;
+        String message;
+
+        // Determine key length and print
+        int keyLength = determineKeyLength(publicKey);
+        String keyLengthMessage = publicKey.getAlgorithm() + " Key length: " + keyLength;
+        logger.info(padding + keyLengthMessage);
+
+        // If unsupported key algorithm, print warning
+        if (!(publicKey instanceof RSAPublicKey || publicKey instanceof DSAPublicKey)) {
+            //TODO: Add different algorithm key length checks
+            message = finding + keyLengthMessage;
+            logger.warn(finding + "Key length not checked for " + publicKey.getAlgorithm() + "\n");
+            output = Output.NEEDS_ATTENTION;
+        } else {
+            // If supported key length, check for validity
+            if (keyLength >= 2048) {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is VALID";
+                logger.info(message + "\n");
+                output = Output.CORRECT;
+            } else {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is INVALID (key length below minimum 2048 bits)";
+                logger.error(message + "\n");
+                output = Output.WRONG;
+            }
+        }
+        return new Tuple<>(message, output);
+    }
+
+    private static Tuple<String, Output> checkSignature(List<X509Certificate> certificateList, X509Certificate x509Certificate) {
+        String number = "[6] ";
+        String message;
+        Output output;
+        if (TlsHelper.verifyCertificateSignature(x509Certificate, certificateList)) {
+            message = number + "Signature is VALID";
+            logger.info(message + "\n");
+            output = Output.CORRECT;
+        } else {
+            message = number + "Signature is INVALID";
+            logger.error(message + "\n");
+            output = Output.WRONG;
+        }
+        return new Tuple<>(message, output);
+    }
+
+
+    private static int determineKeyLength(PublicKey publicKey) {
+        switch (publicKey.getAlgorithm().toUpperCase()) {

Review comment:
       Recommend replacing algorithm String checks with publicKey instanceof checks to avoid reliance on String values.

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandaloneTest.groovy
##########
@@ -0,0 +1,660 @@
+/*
+ * 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.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.security.util.KeyStoreUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.util.TlsHelper
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.Extension
+import org.bouncycastle.asn1.x509.Extensions
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+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.jcajce.JcaContentSignerBuilder
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+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.KeyStore
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisStandaloneTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+    public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    private static final KeyPair keyPair = TlsHelper.generateKeyPair("RSA", 2048)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+        //setupTmpDir() ???
+    }
+
+    static X509Certificate signAndBuildCert(String dn, String signingAlgorithm, KeyPair keyPair) {
+        ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+        return cert
+    }
+
+    static X509v3CertificateBuilder certBuilder(Date startDate, String dn, KeyPair keyPair, int hours) {
+        Date endDate = new Date(startDate.getTime() + TimeUnit.HOURS.toMillis(hours));
+
+        SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                CertificateUtils.getUniqueSerialNumber(),
+                startDate, endDate,
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                subPubKeyInfo)
+        return certBuilder
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+    @Ignore("No assertions to make here")
+    @Test
+    void testPrintUsage() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        //Act
+        standalone.printUsage("This is an error message");
+
+        //Assert
+    }
+
+    @Test
+    void testShouldParseCommandLine() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-n src/test/resources/diagnosis/nifi.properties"
+
+        //Act
+        standalone.parseCommandLine(args.split(" ") as String[])
+
+        //Assert
+        assert standalone.niFiPropertiesPath == "src/test/resources/diagnosis/nifi.properties"
+    }
+
+    @Test
+    void testParseCommandLineShouldFail() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-w wrongservice -p"
+
+        //Act
+        def msg = shouldFail(CommandLineParseException) {
+            standalone.parseCommandLine(args.split(" ") as String[])
+        }
+
+        assert msg == "Error parsing command line. (Unrecognized option: -w)"
+    }
+
+    @Test
+    void testParseCommandLineShouldDetectHelpArg() {
+        //Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-h -n wrongservice"
+
+        //Act
+        standalone.parseCommandLine(args.split(" ") as String[])
+    }
+
+    @Test
+    void testShouldLoadNiFiProperties() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        String[] args = ["-n", niFiPropertiesPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+
+        //Assert
+        assert properties
+        assert properties.size() > 0
+    }
+
+    @Test
+    void testShouldLoadNiFiPropertiesFromEncryptedNifiFile() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/encrypted_nifi.properties"
+        String bootstrapPath = "src/test/resources/diagnosis/bootstrap_with_key.conf"
+        String[] args = ["-n", niFiPropertiesPath, "-b", bootstrapPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+        logger.info("Parsed boostrap.conf location: ${standalone.bootstrapPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+
+        //Assert
+        assert properties
+        assert properties.size() > 0
+    }
+
+    @Test
+    void testShouldCheckDoesFileExist() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        String[] args = ["-n", niFiPropertiesPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+        def keystorePath = properties.getProperty("nifi.security.keystore")
+        def doesFileExist = standalone.doesFileExist(keystorePath, standalone.niFiPropertiesPath, ".jks")
+
+        //Assert
+        assert doesFileExist
+    }
+
+    @Test
+    void testShouldFailDoesFileExist() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesWrongPath = "src/test/resources/diagnosis/nifi_wrong_keystore_path.properties"
+        String[] args = ["-n", niFiPropertiesWrongPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+        def keystorePath = properties.getProperty("nifi.security.keystore")
+        def doesFileExist = standalone.doesFileExist(keystorePath, standalone.niFiPropertiesPath, ".jks")
+
+        //Assert
+        assert !doesFileExist
+    }
+
+    @Test
+    void testShouldCheckPasswordForKeystore() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        standalone.niFiPropertiesPath = niFiPropertiesPath
+        standalone.bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+        standalone.niFiProperties = standalone.loadNiFiProperties()
+        def keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore")
+        def keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType")
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd")
+
+        //Act
+        def keystore = TlsToolkitGetDiagnosisStandalone.checkPasswordForKeystoreAndLoadKeystore(keystorePassword, keystorePath, keystoreType)
+
+        //Assert
+        assert keystore != null
+    }
+
+    @Test
+    void testCheckPasswordForKeystoreShouldFail() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        standalone.niFiPropertiesPath = niFiPropertiesPath
+        standalone.bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+        standalone.niFiProperties = standalone.loadNiFiProperties()
+        def keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore")
+        def keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType")
+        char[] keystorePassword = ['c' * 16] as char[]
+
+        //Act
+        def keystore = TlsToolkitGetDiagnosisStandalone.checkPasswordForKeystoreAndLoadKeystore(keystorePassword, keystorePath, keystoreType)
+
+        //Assert
+        assert !keystore
+    }
+
+    @Test
+    void testShouldExtractPrimaryPrivateKeyEntry() {
+
+        //Arrange
+        KeyStore ks = KeyStore.getInstance("JKS")
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        def password = "password" as char[]
+        ks.load(null, password);
+        KeyPair keyPair1 = keyPair
+        KeyPair keyPair2 = keyPair
+
+        def chain1 = [[
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain1") },
+                              getPublicKey           : { -> keyPair1.getPublic() }
+                      ],
+                      [
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain1Root") },
+                              getPublicKey           : { -> keyPair1.getPublic() }
+                      ]
+        ] as X509Certificate[]
+
+        def chain2 = [[
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain2") },
+                              getPublicKey           : { -> keyPair2.getPublic() }
+                      ],
+                      [
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain2Root") },
+                              getPublicKey           : { -> keyPair2.getPublic() }
+                      ]
+
+        ] as X509Certificate[]
+
+        ks.setKeyEntry("test1", keyPair1.getPrivate(), password, chain1)
+        ks.setKeyEntry("test2", keyPair2.getPrivate(), password, chain2)
+        standalone.keystore = ks
+
+        //Act
+        def primaryPrivateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(ks, password)
+
+        //Assert
+        assert primaryPrivateKeyEntry.getCertificate() instanceof X509Certificate
+        X509Certificate test = (X509Certificate) primaryPrivateKeyEntry.getCertificate()
+        assert CertificateUtils.extractUsername(test.getSubjectX500Principal().getName().toString()) == "ForChain2"
+
+    }
+
+
+    @Test
+    void testShouldExtractPrimaryPrivateKeyEntryForSingleEntry() {
+        //Arrange
+        KeyStore ks = KeyStore.getInstance("JKS")
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        def password = "password" as char[]
+        ks.load(null, password);
+        KeyPair keyPair1 = keyPair
+        X509Certificate[] chain1 = new X509Certificate[2];
+        chain1[0] = [
+                getSubjectX500Principal: { -> new X500Principal("CN=ForChain1") },
+                getPublicKey           : { -> keyPair1.getPublic() }
+        ] as X509Certificate
+
+        chain1[1] = [
+                getSubjectX500Principal: { -> new X500Principal("CN=ForChain1Root") },
+                getPublicKey           : { -> keyPair1.getPublic() }
+        ] as X509Certificate
+
+        ks.setKeyEntry("test1", keyPair1.getPrivate(), password, chain1)
+        standalone.keystore = ks
+
+        //Act
+        def primaryPrivateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(ks, password)
+
+        //Assert
+        assert primaryPrivateKeyEntry.getCertificate() instanceof X509Certificate
+        X509Certificate test = (X509Certificate) primaryPrivateKeyEntry.getCertificate()
+        assert CertificateUtils.extractUsername(test.getSubjectX500Principal().getName().toString()) == "ForChain1"
+    }
+
+    @Test
+    void testExtractPrimaryPrivateKeyEntryForEmptyKeystore() {
+        //Arrange
+        KeyStore ks = KeyStore.getInstance("JKS")
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        def password = "password" as char[]
+        ks.load(null, password);
+
+        //Act
+        def output = standalone.extractPrimaryPrivateKeyEntry(ks, password)
+
+        //
+        assert output == null
+    }
+
+    @Test
+    void testShouldCheckCNAllScenarios() {
+        //Tests for CN compared with hostname in nifi.properties for: Exact Match, Wildcard, and Wrong Match
+        //Arrange
+        KeyPair keyPair = keyPair
+        def certificateCorrect = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=fakeCN", "SHA256WITHRSA", 365)
+        def certificateWildcard = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=*.fakeCN", "SHA256WITHRSA", 365)
+        def certificateWrong = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=fakeCN", "SHA256WITHRSA", 365)
+        //Act
+        def outputCorrect = TlsToolkitGetDiagnosisStandalone.checkCN(certificateCorrect, "fakeCN")
+        def outputWrong = TlsToolkitGetDiagnosisStandalone.checkCN(certificateWrong, "WrongCN")
+        def outputWildcard = TlsToolkitGetDiagnosisStandalone.checkCN(certificateWildcard, "*.fakeCN")
+
+        //Assert
+        assert outputCorrect.getValue().toString() == "CORRECT"
+        assert outputWrong.getValue().toString() == "WRONG"
+        assert outputWildcard.getValue().toString() == "NEEDS_ATTENTION"
+    }
+
+    @Test
+    void testShouldCheckSANAllScenarios() {
+        //Arrange
+        KeyPair keyPair = keyPair
+        String dn = "CN=fakeCN"
+        ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIGNING_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+
+        Extensions extensions = TlsHelper.createDomainAlternativeNamesExtensions(["120.60.23.24", "127.0.0.1"] as List<String>, dn)
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName))
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+
+        def correctHostnames = [
+                "fakeCN",          //specifiedHostname == CN == SAN(DNS entry)
+                "120.60.23.24",    //specifiedHostname == SAN(IP entry)
+                "localhost",      //specifiedHostname will resolve to entry in SAN(IP entry)
+        ]
+
+        def wrongHostnames = [
+                "nifi.apache.org", //specifiedHostname(DNS) not present SAN(DNS or IP entry)
+                "121.60.23.24",    //specifiedHostname(IP) not present SAN(IP entry)
+        ]
+
+        def needsAttentionHostnames = [
+                "nifi.fake"        //specifiedHostname cannot be resolved to IP
+        ]
+
+        //Act
+        def correctOutputs = correctHostnames.collect() {
+            def output = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, it)
+            output
+        }
+
+        def wrongOutputs = wrongHostnames.collect() {
+            def output = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, it)
+            output
+        }
+
+        def needsAttentionOutputs = needsAttentionHostnames.collect() {
+            def output = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, it)
+            output
+        }
+
+        //Assert
+        assert correctOutputs.every { it.getValue().toString() == "CORRECT" }
+        assert wrongOutputs.every { it.getValue().toString() == "WRONG" }
+        assert needsAttentionOutputs.every { it.getValue().toString() == "NEEDS_ATTENTION" }
+    }
+
+    @Test
+    void testShouldCheckSANForNoIPEntries() {
+        //Arrange
+        KeyPair keyPair = keyPair
+        String dn = "CN=fakeCN"
+        ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIGNING_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+
+        Extensions extensions = TlsHelper.createDomainAlternativeNamesExtensions(["anotherCN", "nifi.apache.org"] as List<String>, dn)
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName))
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+
+        //Act
+        def outputDNS = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, "newCN")
+        def outputIP = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, "120.34.34.20")
+        //Assert
+        outputDNS.getValue().toString() == "WRONG"
+        outputIP.getValue().toString() == "WRONG"
+
+    }
+
+    @Test
+    void testShouldCheckEKUAllScenarios() {
+        //Arrange
+        ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIGNING_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+
+        //Both clientAuth and serverAuth
+        def correctEKUs = [
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth] as KeyPurposeId[])
+        ]
+
+        def wrongEKUs = [
+                //Either severAuth or clientAuth
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_serverAuth] as KeyPurposeId[]),
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_clientAuth] as KeyPurposeId[]),
+                //Other auth
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_codeSigning, KeyPurposeId.id_kp_emailProtection] as KeyPurposeId[])
+        ]
+
+        //No EKU
+        X509v3CertificateBuilder certBuilderNoEKU = certBuilder(new Date(), "CN=fakeCN", keyPair, 365 * 24)
+        X509Certificate certNoEKU = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilderNoEKU.build(sigGen))
+
+
+        //Act
+        def correctOutputs = correctEKUs.collect() {
+            X509v3CertificateBuilder certBuilder = certBuilder(new Date(), "CN=fakeCN", keyPair, 365 * 24)
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, it)
+            X509Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+            TlsToolkitGetDiagnosisStandalone.checkEKU(certificate)
+        }
+
+
+        def wrongOutputs = wrongEKUs.collect() {
+            X509v3CertificateBuilder certBuilder = certBuilder(new Date(), "CN=fakeCN", keyPair, 365 * 24)
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, it)
+            X509Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+            TlsToolkitGetDiagnosisStandalone.checkEKU(certificate)
+        }
+
+
+        def outputNoEKU = TlsToolkitGetDiagnosisStandalone.checkEKU(certNoEKU)
+
+        //Assert
+        assert correctOutputs.every { it.getValue().toString() == "CORRECT" }

Review comment:
       Is there a reason for comparing the String value as opposed to comparing the value to the expected Output enum?

##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {
+            sanListDNS = sanMap.entrySet().stream().filter(t -> "dNSName".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+            sanListIP = sanMap.entrySet().stream().filter(t -> "iPAddress".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+        } else {
+            logger.error("[2] No DNS or IPAddress entry present in SAN");
+            return new Tuple<>("[2] SAN is empty. ==> Add a SAN entry matching " + specifiedHostname, Output.WRONG);
+        }
+
+        //specifiedHostname is a domain name
+        if (!specifiedHostnameIsIP) {
+
+            //SAN has the specified domain name
+            if (sanListDNS.size() != 0 && sanListDNS.contains(specifiedHostname)) {
+                logger.info("[2] SAN: DNS = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListDNS.size() == 0) {
+                    logger.warn("[2] SAN: SAN doesn't have DNS entry. Checking IP entries.");
+                } else {
+                    logger.warn("[2] SAN: SAN DNS entry doesn't match with host '" + specifiedHostname + "' in nifi.properties. Checking IP entries.");
+                }
+                //check for IP entries in SAN to match with resolved specified hostname
+                if (sanListIP.size() != 0) {
+                    try {
+                        String ipAddress = InetAddress.getByName(specifiedHostname).getHostAddress();
+                        if (sanListIP.contains(ipAddress)) {
+                            logger.info("    SAN: IP = " + ipAddress + " in SAN  matches with host in nifi.properties after resolution\n");
+                            return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+                        } else {
+                            logger.error("    No IP address entries found in SAN that represent " + specifiedHostname);
+                            logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                            return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                        }
+                    } catch (UnknownHostException e) {
+                        logger.error("    " + e.getLocalizedMessage() + "\n");
+                        return new Tuple<>("[2] Unable to resolve hostname in nifi.properties to IP ", Output.NEEDS_ATTENTION);
+                    }
+
+                } else {
+                    //No IP entries present in SAN
+                    logger.error("    No IP address entries found in SAN to resolve.");
+                    logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        } else { //nifi.web.https.host is an IP address
+            if (sanListIP.size() != 0 && sanListIP.contains(specifiedHostname)) {
+                logger.info("[2] SAN: IP = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListIP.size() == 0) {
+                    logger.error("[2] SAN: SAN doesn't have IP entry");
+                    logger.error("    Add IP entry in SAN for host IP: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN has no IP entries. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                } else {
+                    return new Tuple<>("[2] SAN IP entries do not represent hostname in nifi.properties. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        }
+    }
+
+    private static Tuple<String, Output> checkEKU(X509Certificate x509Certificate) {
+        List<String> eKU = null;
+        try {
+            eKU = x509Certificate.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            logger.error("Error in EKU check: " + e.getLocalizedMessage());
+            return new Tuple<>("Error in EKU check: " + e.getLocalizedMessage(), Output.WRONG);
+        }
+        if (eKU != null) {
+            if (!eKU.contains(ekuMap.get("serverAuth")) && !eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.error("[3] EKU: serverAuth and clientAuth absent");
+                logger.error("    Add serverAuth and clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate.", Output.WRONG);
+            }
+
+            if (eKU.contains(ekuMap.get("serverAuth")) && eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.info("[3] EKU: serverAuth and clientAuth present\n");
+                return new Tuple<>("[3] EKUs are correct. ", Output.CORRECT);
+            } else if (!eKU.contains(ekuMap.get("serverAuth"))) {
+                logger.error("[3] EKU: serverAuth is absent");
+                logger.error("    Add serverAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU serverAuth needs to be added to the certificate. ", Output.WRONG);
+            } else {
+                logger.error("[3] EKU: clientAuth is absent ");
+                logger.error("    Add clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU clientAuth needs to be added to the certificate", Output.WRONG);
+            }
+
+        } else {
+            logger.warn("[3] EKU: No extended key usage found. Add serverAuth and clientAuth usage to the EKU of the certificate.\n");
+            return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate. ", Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private static Tuple<String, Output> checkValidity(X509Certificate x509Certificate) {
+        String message;
+        try {
+            x509Certificate.checkValidity();
+            logger.info("[4] Validity: Certificate is VALID");
+
+            DateFormat dateFormat = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy");
+            Date dateObj = new Date();
+            Date expiry = x509Certificate.getNotAfter();
+
+            long mSecTillExpiry = Math.abs(expiry.getTime() - dateObj.getTime());
+            long daysTillExpiry = TimeUnit.DAYS.convert(mSecTillExpiry, TimeUnit.MILLISECONDS);
+
+            if (daysTillExpiry < 30) {
+                logger.warn("    Certificate expires in less than 30 days\n");
+            } else if (daysTillExpiry < 60) {
+                logger.warn("    Certificate expires in less than 60 days\n");
+            } else if (daysTillExpiry < 90) {
+                logger.warn("    Certificate expires in less than 90 days\n");
+            } else {
+                logger.info("    Certificate expires in " + daysTillExpiry + "  days\n");
+            }
+            return new Tuple<>("[4] Certificate is VALID", Output.CORRECT);
+        } catch (CertificateExpiredException e) {
+            message = "[4] Validity: Certificate is INVALID: Validity date expired " + x509Certificate.getNotAfter();
+        } catch (CertificateNotYetValidException e) {
+            message = "[4] Validity: Certificate is INVALID: Certificate is not valid before " + x509Certificate.getNotBefore();
+        }
+        logger.error(message + "\n");
+        return new Tuple<>(message, Output.WRONG);
+    }
+
+    private static Tuple<String, Output> checkKeySize(X509Certificate x509Certificate) {
+        PublicKey publicKey = x509Certificate.getPublicKey();
+
+        String finding = "[5] ";
+        String padding = "    ";
+        Output output;
+        String message;
+
+        // Determine key length and print
+        int keyLength = determineKeyLength(publicKey);
+        String keyLengthMessage = publicKey.getAlgorithm() + " Key length: " + keyLength;
+        logger.info(padding + keyLengthMessage);
+
+        // If unsupported key algorithm, print warning
+        if (!(publicKey instanceof RSAPublicKey || publicKey instanceof DSAPublicKey)) {
+            //TODO: Add different algorithm key length checks
+            message = finding + keyLengthMessage;
+            logger.warn(finding + "Key length not checked for " + publicKey.getAlgorithm() + "\n");
+            output = Output.NEEDS_ATTENTION;
+        } else {
+            // If supported key length, check for validity
+            if (keyLength >= 2048) {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is VALID";
+                logger.info(message + "\n");
+                output = Output.CORRECT;
+            } else {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is INVALID (key length below minimum 2048 bits)";
+                logger.error(message + "\n");
+                output = Output.WRONG;
+            }
+        }
+        return new Tuple<>(message, output);
+    }
+
+    private static Tuple<String, Output> checkSignature(List<X509Certificate> certificateList, X509Certificate x509Certificate) {
+        String number = "[6] ";
+        String message;
+        Output output;
+        if (TlsHelper.verifyCertificateSignature(x509Certificate, certificateList)) {
+            message = number + "Signature is VALID";
+            logger.info(message + "\n");
+            output = Output.CORRECT;
+        } else {
+            message = number + "Signature is INVALID";
+            logger.error(message + "\n");
+            output = Output.WRONG;
+        }
+        return new Tuple<>(message, output);
+    }
+
+
+    private static int determineKeyLength(PublicKey publicKey) {
+        switch (publicKey.getAlgorithm().toUpperCase()) {
+            case "RSA":
+                return ((RSAPublicKey) publicKey).getModulus().bitLength();
+            case "DSA":
+                return ((DSAPublicKey) publicKey).getParams().getP().bitLength();
+            case "EC":
+                return ((BCECPublicKey) publicKey).getParameters().getCurve().getFieldSize();
+            default:
+                logger.warn("Cannot determine key length for unknown algorithm " + publicKey.getAlgorithm());
+                return -1;
+        }
+    }
+
+    private Tuple<String, KeyStore.Entry> retrieveEntryFromKeystore(KeyStore.PasswordProtection keystorePasswordProtection, String alias) {
+        try {
+            return new Tuple<String, KeyStore.Entry>(alias, keystore.getEntry(alias, keystorePasswordProtection));
+        } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
+            e.getLocalizedMessage();

Review comment:
       It appears that should message should be passed to the logger.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] github-actions[bot] closed pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
github-actions[bot] closed pull request #4670:
URL: https://github.com/apache/nifi/pull/4670


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526351831



##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -160,26 +180,33 @@ public static String extractUsername(String dn) {
      */
     public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException {
 
-        final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+        /*
+         * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
+         *
+         * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.

Review comment:
       Yes this applies to the other method I included (getSubjectAlternativeNamesMap). Shifting the comments to that method.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526990641



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {

Review comment:
       Changed and used default int returned by altNames in getSubjectAlternativeNameMap(). No need for an enum.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r527037153



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandaloneTest.groovy
##########
@@ -0,0 +1,660 @@
+/*
+ * 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.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.security.util.KeyStoreUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.util.TlsHelper
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.Extension
+import org.bouncycastle.asn1.x509.Extensions
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+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.jcajce.JcaContentSignerBuilder
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+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.KeyStore
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisStandaloneTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+    public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    private static final KeyPair keyPair = TlsHelper.generateKeyPair("RSA", 2048)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+        //setupTmpDir() ???
+    }
+
+    static X509Certificate signAndBuildCert(String dn, String signingAlgorithm, KeyPair keyPair) {
+        ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+        return cert
+    }
+
+    static X509v3CertificateBuilder certBuilder(Date startDate, String dn, KeyPair keyPair, int hours) {
+        Date endDate = new Date(startDate.getTime() + TimeUnit.HOURS.toMillis(hours));
+
+        SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                CertificateUtils.getUniqueSerialNumber(),
+                startDate, endDate,
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                subPubKeyInfo)
+        return certBuilder
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+    @Ignore("No assertions to make here")
+    @Test
+    void testPrintUsage() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        //Act
+        standalone.printUsage("This is an error message");
+
+        //Assert
+    }
+
+    @Test
+    void testShouldParseCommandLine() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-n src/test/resources/diagnosis/nifi.properties"
+
+        //Act
+        standalone.parseCommandLine(args.split(" ") as String[])
+
+        //Assert
+        assert standalone.niFiPropertiesPath == "src/test/resources/diagnosis/nifi.properties"
+    }
+
+    @Test
+    void testParseCommandLineShouldFail() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-w wrongservice -p"
+
+        //Act
+        def msg = shouldFail(CommandLineParseException) {
+            standalone.parseCommandLine(args.split(" ") as String[])
+        }
+
+        assert msg == "Error parsing command line. (Unrecognized option: -w)"
+    }
+
+    @Test
+    void testParseCommandLineShouldDetectHelpArg() {
+        //Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-h -n wrongservice"
+
+        //Act
+        standalone.parseCommandLine(args.split(" ") as String[])
+    }
+
+    @Test
+    void testShouldLoadNiFiProperties() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        String[] args = ["-n", niFiPropertiesPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+
+        //Assert
+        assert properties
+        assert properties.size() > 0
+    }
+
+    @Test
+    void testShouldLoadNiFiPropertiesFromEncryptedNifiFile() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/encrypted_nifi.properties"
+        String bootstrapPath = "src/test/resources/diagnosis/bootstrap_with_key.conf"
+        String[] args = ["-n", niFiPropertiesPath, "-b", bootstrapPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+        logger.info("Parsed boostrap.conf location: ${standalone.bootstrapPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+
+        //Assert
+        assert properties
+        assert properties.size() > 0
+    }
+
+    @Test
+    void testShouldCheckDoesFileExist() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        String[] args = ["-n", niFiPropertiesPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+        def keystorePath = properties.getProperty("nifi.security.keystore")
+        def doesFileExist = standalone.doesFileExist(keystorePath, standalone.niFiPropertiesPath, ".jks")
+
+        //Assert
+        assert doesFileExist
+    }
+
+    @Test
+    void testShouldFailDoesFileExist() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesWrongPath = "src/test/resources/diagnosis/nifi_wrong_keystore_path.properties"
+        String[] args = ["-n", niFiPropertiesWrongPath] as String[]
+        standalone.parseCommandLine(args)
+        logger.info("Parsed nifi.properties location: ${standalone.niFiPropertiesPath}")
+
+        //Act
+        NiFiProperties properties = standalone.loadNiFiProperties()
+        def keystorePath = properties.getProperty("nifi.security.keystore")
+        def doesFileExist = standalone.doesFileExist(keystorePath, standalone.niFiPropertiesPath, ".jks")
+
+        //Assert
+        assert !doesFileExist
+    }
+
+    @Test
+    void testShouldCheckPasswordForKeystore() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        standalone.niFiPropertiesPath = niFiPropertiesPath
+        standalone.bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+        standalone.niFiProperties = standalone.loadNiFiProperties()
+        def keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore")
+        def keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType")
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd")
+
+        //Act
+        def keystore = TlsToolkitGetDiagnosisStandalone.checkPasswordForKeystoreAndLoadKeystore(keystorePassword, keystorePath, keystoreType)
+
+        //Assert
+        assert keystore != null
+    }
+
+    @Test
+    void testCheckPasswordForKeystoreShouldFail() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String niFiPropertiesPath = "src/test/resources/diagnosis/nifi.properties"
+        standalone.niFiPropertiesPath = niFiPropertiesPath
+        standalone.bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+        standalone.niFiProperties = standalone.loadNiFiProperties()
+        def keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore")
+        def keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType")
+        char[] keystorePassword = ['c' * 16] as char[]
+
+        //Act
+        def keystore = TlsToolkitGetDiagnosisStandalone.checkPasswordForKeystoreAndLoadKeystore(keystorePassword, keystorePath, keystoreType)
+
+        //Assert
+        assert !keystore
+    }
+
+    @Test
+    void testShouldExtractPrimaryPrivateKeyEntry() {
+
+        //Arrange
+        KeyStore ks = KeyStore.getInstance("JKS")
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        def password = "password" as char[]
+        ks.load(null, password);
+        KeyPair keyPair1 = keyPair
+        KeyPair keyPair2 = keyPair
+
+        def chain1 = [[
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain1") },
+                              getPublicKey           : { -> keyPair1.getPublic() }
+                      ],
+                      [
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain1Root") },
+                              getPublicKey           : { -> keyPair1.getPublic() }
+                      ]
+        ] as X509Certificate[]
+
+        def chain2 = [[
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain2") },
+                              getPublicKey           : { -> keyPair2.getPublic() }
+                      ],
+                      [
+                              getSubjectX500Principal: { -> new X500Principal("CN=ForChain2Root") },
+                              getPublicKey           : { -> keyPair2.getPublic() }
+                      ]
+
+        ] as X509Certificate[]
+
+        ks.setKeyEntry("test1", keyPair1.getPrivate(), password, chain1)
+        ks.setKeyEntry("test2", keyPair2.getPrivate(), password, chain2)
+        standalone.keystore = ks
+
+        //Act
+        def primaryPrivateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(ks, password)
+
+        //Assert
+        assert primaryPrivateKeyEntry.getCertificate() instanceof X509Certificate
+        X509Certificate test = (X509Certificate) primaryPrivateKeyEntry.getCertificate()
+        assert CertificateUtils.extractUsername(test.getSubjectX500Principal().getName().toString()) == "ForChain2"
+
+    }
+
+
+    @Test
+    void testShouldExtractPrimaryPrivateKeyEntryForSingleEntry() {
+        //Arrange
+        KeyStore ks = KeyStore.getInstance("JKS")
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        def password = "password" as char[]
+        ks.load(null, password);
+        KeyPair keyPair1 = keyPair
+        X509Certificate[] chain1 = new X509Certificate[2];
+        chain1[0] = [
+                getSubjectX500Principal: { -> new X500Principal("CN=ForChain1") },
+                getPublicKey           : { -> keyPair1.getPublic() }
+        ] as X509Certificate
+
+        chain1[1] = [
+                getSubjectX500Principal: { -> new X500Principal("CN=ForChain1Root") },
+                getPublicKey           : { -> keyPair1.getPublic() }
+        ] as X509Certificate
+
+        ks.setKeyEntry("test1", keyPair1.getPrivate(), password, chain1)
+        standalone.keystore = ks
+
+        //Act
+        def primaryPrivateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(ks, password)
+
+        //Assert
+        assert primaryPrivateKeyEntry.getCertificate() instanceof X509Certificate
+        X509Certificate test = (X509Certificate) primaryPrivateKeyEntry.getCertificate()
+        assert CertificateUtils.extractUsername(test.getSubjectX500Principal().getName().toString()) == "ForChain1"
+    }
+
+    @Test
+    void testExtractPrimaryPrivateKeyEntryForEmptyKeystore() {
+        //Arrange
+        KeyStore ks = KeyStore.getInstance("JKS")
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        def password = "password" as char[]
+        ks.load(null, password);
+
+        //Act
+        def output = standalone.extractPrimaryPrivateKeyEntry(ks, password)
+
+        //
+        assert output == null
+    }
+
+    @Test
+    void testShouldCheckCNAllScenarios() {
+        //Tests for CN compared with hostname in nifi.properties for: Exact Match, Wildcard, and Wrong Match
+        //Arrange
+        KeyPair keyPair = keyPair
+        def certificateCorrect = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=fakeCN", "SHA256WITHRSA", 365)
+        def certificateWildcard = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=*.fakeCN", "SHA256WITHRSA", 365)
+        def certificateWrong = CertificateUtils.generateSelfSignedX509Certificate(keyPair, "CN=fakeCN", "SHA256WITHRSA", 365)
+        //Act
+        def outputCorrect = TlsToolkitGetDiagnosisStandalone.checkCN(certificateCorrect, "fakeCN")
+        def outputWrong = TlsToolkitGetDiagnosisStandalone.checkCN(certificateWrong, "WrongCN")
+        def outputWildcard = TlsToolkitGetDiagnosisStandalone.checkCN(certificateWildcard, "*.fakeCN")
+
+        //Assert
+        assert outputCorrect.getValue().toString() == "CORRECT"
+        assert outputWrong.getValue().toString() == "WRONG"
+        assert outputWildcard.getValue().toString() == "NEEDS_ATTENTION"
+    }
+
+    @Test
+    void testShouldCheckSANAllScenarios() {
+        //Arrange
+        KeyPair keyPair = keyPair
+        String dn = "CN=fakeCN"
+        ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIGNING_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+
+        Extensions extensions = TlsHelper.createDomainAlternativeNamesExtensions(["120.60.23.24", "127.0.0.1"] as List<String>, dn)
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName))
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+
+        def correctHostnames = [
+                "fakeCN",          //specifiedHostname == CN == SAN(DNS entry)
+                "120.60.23.24",    //specifiedHostname == SAN(IP entry)
+                "localhost",      //specifiedHostname will resolve to entry in SAN(IP entry)
+        ]
+
+        def wrongHostnames = [
+                "nifi.apache.org", //specifiedHostname(DNS) not present SAN(DNS or IP entry)
+                "121.60.23.24",    //specifiedHostname(IP) not present SAN(IP entry)
+        ]
+
+        def needsAttentionHostnames = [
+                "nifi.fake"        //specifiedHostname cannot be resolved to IP
+        ]
+
+        //Act
+        def correctOutputs = correctHostnames.collect() {
+            def output = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, it)
+            output
+        }
+
+        def wrongOutputs = wrongHostnames.collect() {
+            def output = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, it)
+            output
+        }
+
+        def needsAttentionOutputs = needsAttentionHostnames.collect() {
+            def output = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, it)
+            output
+        }
+
+        //Assert
+        assert correctOutputs.every { it.getValue().toString() == "CORRECT" }
+        assert wrongOutputs.every { it.getValue().toString() == "WRONG" }
+        assert needsAttentionOutputs.every { it.getValue().toString() == "NEEDS_ATTENTION" }
+    }
+
+    @Test
+    void testShouldCheckSANForNoIPEntries() {
+        //Arrange
+        KeyPair keyPair = keyPair
+        String dn = "CN=fakeCN"
+        ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIGNING_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+
+        Extensions extensions = TlsHelper.createDomainAlternativeNamesExtensions(["anotherCN", "nifi.apache.org"] as List<String>, dn)
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName))
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+
+        //Act
+        def outputDNS = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, "newCN")
+        def outputIP = TlsToolkitGetDiagnosisStandalone.checkSAN(cert, "120.34.34.20")
+        //Assert
+        outputDNS.getValue().toString() == "WRONG"
+        outputIP.getValue().toString() == "WRONG"
+
+    }
+
+    @Test
+    void testShouldCheckEKUAllScenarios() {
+        //Arrange
+        ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIGNING_ALGORITHM).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+
+        //Both clientAuth and serverAuth
+        def correctEKUs = [
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth] as KeyPurposeId[])
+        ]
+
+        def wrongEKUs = [
+                //Either severAuth or clientAuth
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_serverAuth] as KeyPurposeId[]),
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_clientAuth] as KeyPurposeId[]),
+                //Other auth
+                new ExtendedKeyUsage([KeyPurposeId.id_kp_codeSigning, KeyPurposeId.id_kp_emailProtection] as KeyPurposeId[])
+        ]
+
+        //No EKU
+        X509v3CertificateBuilder certBuilderNoEKU = certBuilder(new Date(), "CN=fakeCN", keyPair, 365 * 24)
+        X509Certificate certNoEKU = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilderNoEKU.build(sigGen))
+
+
+        //Act
+        def correctOutputs = correctEKUs.collect() {
+            X509v3CertificateBuilder certBuilder = certBuilder(new Date(), "CN=fakeCN", keyPair, 365 * 24)
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, it)
+            X509Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+            TlsToolkitGetDiagnosisStandalone.checkEKU(certificate)
+        }
+
+
+        def wrongOutputs = wrongEKUs.collect() {
+            X509v3CertificateBuilder certBuilder = certBuilder(new Date(), "CN=fakeCN", keyPair, 365 * 24)
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, it)
+            X509Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+            TlsToolkitGetDiagnosisStandalone.checkEKU(certificate)
+        }
+
+
+        def outputNoEKU = TlsToolkitGetDiagnosisStandalone.checkEKU(certNoEKU)
+
+        //Assert
+        assert correctOutputs.every { it.getValue().toString() == "CORRECT" }

Review comment:
       No. Changing to enum comparison instead of String




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526434425



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);

Review comment:
       Yes, changing.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526352181



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisCommandLine.java
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class TlsToolkitGetDiagnosisCommandLine {
+
+    public static final String DESCRIPTION = "Diagnoses issues in common deployment scenario of TLS toolkit";
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitStandaloneCommandLine.class);
+
+
+    public static void main(String[] args) {
+
+        TlsToolkitGetDiagnosisCommandLine commandLine = new TlsToolkitGetDiagnosisCommandLine();
+        try {
+            commandLine.chooseMain(args);
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        }
+
+    }
+
+    public void chooseMain(String[] args) throws CommandLineParseException {
+
+
+        if(args.length < 1){
+           //How to print errors and exit

Review comment:
       Yes, correcting




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526352018



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisCommandLine.java
##########
@@ -0,0 +1,68 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class TlsToolkitGetDiagnosisCommandLine {
+
+    public static final String DESCRIPTION = "Diagnoses issues in common deployment scenario of TLS toolkit";
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitStandaloneCommandLine.class);
+
+

Review comment:
       Yes, correcting.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] exceptionfactory commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
exceptionfactory commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526395136



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {

Review comment:
       That sounds good.  The NiFi documentation should be updated as Java 9 changed the default preferred key store type to PKCS12 as described in [JEP 229](http://openjdk.java.net/jeps/229).




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526376795



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");

Review comment:
       Yes, changing.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526434800



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);

Review comment:
       Yes, changing




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r527030717



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/test/groovy/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandaloneTest.groovy
##########
@@ -0,0 +1,660 @@
+/*
+ * 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.diagnosis
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.security.util.CertificateUtils
+import org.apache.nifi.security.util.KeyStoreUtils
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.toolkit.tls.util.TlsHelper
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.asn1.x500.X500Name
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage
+import org.bouncycastle.asn1.x509.Extension
+import org.bouncycastle.asn1.x509.Extensions
+import org.bouncycastle.asn1.x509.KeyPurposeId
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+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.jcajce.JcaContentSignerBuilder
+import org.junit.Assume
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+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.KeyStore
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+
+
+@RunWith(JUnit4.class)
+class TlsToolkitGetDiagnosisStandaloneTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisCommandLineTest.class)
+    public static final String DEFAULT_SIGNING_ALGORITHM = "SHA256WITHRSA"
+
+    private static final KeyPair keyPair = TlsHelper.generateKeyPair("RSA", 2048)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+        //setupTmpDir() ???
+    }
+
+    static X509Certificate signAndBuildCert(String dn, String signingAlgorithm, KeyPair keyPair) {
+        ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate())
+        X509v3CertificateBuilder certBuilder = certBuilder(new Date(), dn, keyPair, 365 * 24)
+        X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen))
+        return cert
+    }
+
+    static X509v3CertificateBuilder certBuilder(Date startDate, String dn, KeyPair keyPair, int hours) {
+        Date endDate = new Date(startDate.getTime() + TimeUnit.HOURS.toMillis(hours));
+
+        SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
+        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                CertificateUtils.getUniqueSerialNumber(),
+                startDate, endDate,
+                CertificateUtils.reverseX500Name(new X500Name(dn)),
+                subPubKeyInfo)
+        return certBuilder
+    }
+
+    void setUp() {
+        super.setUp()
+    }
+
+    void tearDown() {
+    }
+
+    @Ignore("No assertions to make here")
+    @Test
+    void testPrintUsage() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+
+        //Act
+        standalone.printUsage("This is an error message");
+
+        //Assert
+    }
+
+    @Test
+    void testShouldParseCommandLine() {
+        //Arrange
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone()
+        String args = "-n src/test/resources/diagnosis/nifi.properties"
+
+        //Act
+        standalone.parseCommandLine(args.split(" ") as String[])
+
+        //Assert
+        assert standalone.niFiPropertiesPath == "src/test/resources/diagnosis/nifi.properties"

Review comment:
       Yes, changing




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r527001300



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {
+            sanListDNS = sanMap.entrySet().stream().filter(t -> "dNSName".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+            sanListIP = sanMap.entrySet().stream().filter(t -> "iPAddress".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+        } else {
+            logger.error("[2] No DNS or IPAddress entry present in SAN");
+            return new Tuple<>("[2] SAN is empty. ==> Add a SAN entry matching " + specifiedHostname, Output.WRONG);
+        }
+
+        //specifiedHostname is a domain name
+        if (!specifiedHostnameIsIP) {
+
+            //SAN has the specified domain name
+            if (sanListDNS.size() != 0 && sanListDNS.contains(specifiedHostname)) {
+                logger.info("[2] SAN: DNS = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListDNS.size() == 0) {
+                    logger.warn("[2] SAN: SAN doesn't have DNS entry. Checking IP entries.");
+                } else {
+                    logger.warn("[2] SAN: SAN DNS entry doesn't match with host '" + specifiedHostname + "' in nifi.properties. Checking IP entries.");
+                }
+                //check for IP entries in SAN to match with resolved specified hostname
+                if (sanListIP.size() != 0) {
+                    try {
+                        String ipAddress = InetAddress.getByName(specifiedHostname).getHostAddress();
+                        if (sanListIP.contains(ipAddress)) {
+                            logger.info("    SAN: IP = " + ipAddress + " in SAN  matches with host in nifi.properties after resolution\n");
+                            return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+                        } else {
+                            logger.error("    No IP address entries found in SAN that represent " + specifiedHostname);
+                            logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                            return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                        }
+                    } catch (UnknownHostException e) {
+                        logger.error("    " + e.getLocalizedMessage() + "\n");
+                        return new Tuple<>("[2] Unable to resolve hostname in nifi.properties to IP ", Output.NEEDS_ATTENTION);
+                    }
+
+                } else {
+                    //No IP entries present in SAN
+                    logger.error("    No IP address entries found in SAN to resolve.");
+                    logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        } else { //nifi.web.https.host is an IP address
+            if (sanListIP.size() != 0 && sanListIP.contains(specifiedHostname)) {
+                logger.info("[2] SAN: IP = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListIP.size() == 0) {
+                    logger.error("[2] SAN: SAN doesn't have IP entry");
+                    logger.error("    Add IP entry in SAN for host IP: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN has no IP entries. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                } else {
+                    return new Tuple<>("[2] SAN IP entries do not represent hostname in nifi.properties. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        }
+    }
+
+    private static Tuple<String, Output> checkEKU(X509Certificate x509Certificate) {
+        List<String> eKU = null;
+        try {
+            eKU = x509Certificate.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            logger.error("Error in EKU check: " + e.getLocalizedMessage());
+            return new Tuple<>("Error in EKU check: " + e.getLocalizedMessage(), Output.WRONG);
+        }
+        if (eKU != null) {
+            if (!eKU.contains(ekuMap.get("serverAuth")) && !eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.error("[3] EKU: serverAuth and clientAuth absent");
+                logger.error("    Add serverAuth and clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate.", Output.WRONG);
+            }
+
+            if (eKU.contains(ekuMap.get("serverAuth")) && eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.info("[3] EKU: serverAuth and clientAuth present\n");
+                return new Tuple<>("[3] EKUs are correct. ", Output.CORRECT);
+            } else if (!eKU.contains(ekuMap.get("serverAuth"))) {
+                logger.error("[3] EKU: serverAuth is absent");
+                logger.error("    Add serverAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU serverAuth needs to be added to the certificate. ", Output.WRONG);
+            } else {
+                logger.error("[3] EKU: clientAuth is absent ");
+                logger.error("    Add clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU clientAuth needs to be added to the certificate", Output.WRONG);
+            }
+
+        } else {
+            logger.warn("[3] EKU: No extended key usage found. Add serverAuth and clientAuth usage to the EKU of the certificate.\n");
+            return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate. ", Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private static Tuple<String, Output> checkValidity(X509Certificate x509Certificate) {
+        String message;
+        try {
+            x509Certificate.checkValidity();
+            logger.info("[4] Validity: Certificate is VALID");
+
+            DateFormat dateFormat = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy");
+            Date dateObj = new Date();
+            Date expiry = x509Certificate.getNotAfter();
+
+            long mSecTillExpiry = Math.abs(expiry.getTime() - dateObj.getTime());
+            long daysTillExpiry = TimeUnit.DAYS.convert(mSecTillExpiry, TimeUnit.MILLISECONDS);
+
+            if (daysTillExpiry < 30) {
+                logger.warn("    Certificate expires in less than 30 days\n");
+            } else if (daysTillExpiry < 60) {
+                logger.warn("    Certificate expires in less than 60 days\n");
+            } else if (daysTillExpiry < 90) {
+                logger.warn("    Certificate expires in less than 90 days\n");
+            } else {
+                logger.info("    Certificate expires in " + daysTillExpiry + "  days\n");
+            }
+            return new Tuple<>("[4] Certificate is VALID", Output.CORRECT);
+        } catch (CertificateExpiredException e) {
+            message = "[4] Validity: Certificate is INVALID: Validity date expired " + x509Certificate.getNotAfter();
+        } catch (CertificateNotYetValidException e) {
+            message = "[4] Validity: Certificate is INVALID: Certificate is not valid before " + x509Certificate.getNotBefore();
+        }
+        logger.error(message + "\n");
+        return new Tuple<>(message, Output.WRONG);
+    }
+
+    private static Tuple<String, Output> checkKeySize(X509Certificate x509Certificate) {
+        PublicKey publicKey = x509Certificate.getPublicKey();
+
+        String finding = "[5] ";
+        String padding = "    ";
+        Output output;
+        String message;
+
+        // Determine key length and print
+        int keyLength = determineKeyLength(publicKey);
+        String keyLengthMessage = publicKey.getAlgorithm() + " Key length: " + keyLength;
+        logger.info(padding + keyLengthMessage);
+
+        // If unsupported key algorithm, print warning
+        if (!(publicKey instanceof RSAPublicKey || publicKey instanceof DSAPublicKey)) {
+            //TODO: Add different algorithm key length checks
+            message = finding + keyLengthMessage;
+            logger.warn(finding + "Key length not checked for " + publicKey.getAlgorithm() + "\n");
+            output = Output.NEEDS_ATTENTION;
+        } else {
+            // If supported key length, check for validity
+            if (keyLength >= 2048) {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is VALID";
+                logger.info(message + "\n");
+                output = Output.CORRECT;
+            } else {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is INVALID (key length below minimum 2048 bits)";
+                logger.error(message + "\n");
+                output = Output.WRONG;
+            }
+        }
+        return new Tuple<>(message, output);
+    }
+
+    private static Tuple<String, Output> checkSignature(List<X509Certificate> certificateList, X509Certificate x509Certificate) {
+        String number = "[6] ";
+        String message;
+        Output output;
+        if (TlsHelper.verifyCertificateSignature(x509Certificate, certificateList)) {
+            message = number + "Signature is VALID";
+            logger.info(message + "\n");
+            output = Output.CORRECT;
+        } else {
+            message = number + "Signature is INVALID";
+            logger.error(message + "\n");
+            output = Output.WRONG;
+        }
+        return new Tuple<>(message, output);
+    }
+
+
+    private static int determineKeyLength(PublicKey publicKey) {
+        switch (publicKey.getAlgorithm().toUpperCase()) {
+            case "RSA":
+                return ((RSAPublicKey) publicKey).getModulus().bitLength();
+            case "DSA":
+                return ((DSAPublicKey) publicKey).getParams().getP().bitLength();
+            case "EC":
+                return ((BCECPublicKey) publicKey).getParameters().getCurve().getFieldSize();
+            default:
+                logger.warn("Cannot determine key length for unknown algorithm " + publicKey.getAlgorithm());
+                return -1;
+        }
+    }
+
+    private Tuple<String, KeyStore.Entry> retrieveEntryFromKeystore(KeyStore.PasswordProtection keystorePasswordProtection, String alias) {
+        try {
+            return new Tuple<String, KeyStore.Entry>(alias, keystore.getEntry(alias, keystorePasswordProtection));
+        } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
+            e.getLocalizedMessage();

Review comment:
       Yes, correcting
   




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r527001402



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());
+        String subjectCN = CertificateUtils.extractUsername(x500Name.toString());
+
+        if (subjectCN.contains("*.")) {
+            logger.info("[1] CN: Subject CN = " + subjectCN + " is a wildcard\n");
+            logger.info("    Check SAN entry for '" + specifiedHostname + "'");
+            logger.warn("    Wildcard certificates are not recommended nor supported for NiFi");
+            return new Tuple<>("[1] CN is wildcard. Check SAN", Output.NEEDS_ATTENTION);
+        } else if (subjectCN.equals(specifiedHostname)) {
+            //Exact match
+            logger.info("[1] CN: Subject CN = " + subjectCN + " matches with host in nifi.properties\n");
+            return new Tuple<>("[1] CN is CORRECT", Output.CORRECT);
+        } else {
+            logger.error("[1] Subject CN = " + subjectCN + " doesn't match with hostname in nifi.properties file");
+            logger.error("    Check nifi.web.https.host value.");
+            logger.error("    Current nifi.web.https.host = " + specifiedHostname + "\n");
+            return new Tuple<>("[1] CN is different than hostname. Compare CN with nifi.web.https.host in nifi.properties", Output.WRONG);
+        }
+    }
+
+    private static Tuple<String, Output> checkSAN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        boolean specifiedHostnameIsIP = false;
+
+        //Check if specified hostname is IP
+        if (InetAddressUtils.isIPv4Address(specifiedHostname) || InetAddressUtils.isIPv6Address(specifiedHostname)) {
+            specifiedHostnameIsIP = true;
+        }
+
+        //Get all SANs
+        Map<String, String> sanMap = null;
+        try {
+            sanMap = CertificateUtils.getSubjectAlternativeNamesMap(x509Certificate);
+        } catch (CertificateParsingException e) {
+            logger.error("Error in SAN check: " + e.getLocalizedMessage());
+            return new Tuple<>("[2] SAN: Error in SAN check: " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+
+        //Check and load IP or DNS SAN entries
+        List<String> sanListDNS;
+        List<String> sanListIP;
+        if (sanMap.containsValue(("dNSName")) || sanMap.containsValue(("iPAddress"))) {
+            sanListDNS = sanMap.entrySet().stream().filter(t -> "dNSName".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+            sanListIP = sanMap.entrySet().stream().filter(t -> "iPAddress".equals(t.getValue())).map(Map.Entry::getKey).collect(Collectors.toList());
+        } else {
+            logger.error("[2] No DNS or IPAddress entry present in SAN");
+            return new Tuple<>("[2] SAN is empty. ==> Add a SAN entry matching " + specifiedHostname, Output.WRONG);
+        }
+
+        //specifiedHostname is a domain name
+        if (!specifiedHostnameIsIP) {
+
+            //SAN has the specified domain name
+            if (sanListDNS.size() != 0 && sanListDNS.contains(specifiedHostname)) {
+                logger.info("[2] SAN: DNS = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListDNS.size() == 0) {
+                    logger.warn("[2] SAN: SAN doesn't have DNS entry. Checking IP entries.");
+                } else {
+                    logger.warn("[2] SAN: SAN DNS entry doesn't match with host '" + specifiedHostname + "' in nifi.properties. Checking IP entries.");
+                }
+                //check for IP entries in SAN to match with resolved specified hostname
+                if (sanListIP.size() != 0) {
+                    try {
+                        String ipAddress = InetAddress.getByName(specifiedHostname).getHostAddress();
+                        if (sanListIP.contains(ipAddress)) {
+                            logger.info("    SAN: IP = " + ipAddress + " in SAN  matches with host in nifi.properties after resolution\n");
+                            return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+                        } else {
+                            logger.error("    No IP address entries found in SAN that represent " + specifiedHostname);
+                            logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                            return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                        }
+                    } catch (UnknownHostException e) {
+                        logger.error("    " + e.getLocalizedMessage() + "\n");
+                        return new Tuple<>("[2] Unable to resolve hostname in nifi.properties to IP ", Output.NEEDS_ATTENTION);
+                    }
+
+                } else {
+                    //No IP entries present in SAN
+                    logger.error("    No IP address entries found in SAN to resolve.");
+                    logger.error("    Add DNS/IP entry in SAN for hostname: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN entries do not represent hostname in nifi.properties. Add DNS/IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        } else { //nifi.web.https.host is an IP address
+            if (sanListIP.size() != 0 && sanListIP.contains(specifiedHostname)) {
+                logger.info("[2] SAN: IP = " + specifiedHostname + " in SAN matches with host in nifi.properties\n");
+                return new Tuple<>("[2] SAN entry represents " + specifiedHostname, Output.CORRECT);
+            } else {
+                if (sanListIP.size() == 0) {
+                    logger.error("[2] SAN: SAN doesn't have IP entry");
+                    logger.error("    Add IP entry in SAN for host IP: " + specifiedHostname + "\n");
+                    return new Tuple<>("[2] SAN has no IP entries. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                } else {
+                    return new Tuple<>("[2] SAN IP entries do not represent hostname in nifi.properties. Add IP entry in SAN for hostname: " + specifiedHostname, Output.WRONG);
+                }
+            }
+        }
+    }
+
+    private static Tuple<String, Output> checkEKU(X509Certificate x509Certificate) {
+        List<String> eKU = null;
+        try {
+            eKU = x509Certificate.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            logger.error("Error in EKU check: " + e.getLocalizedMessage());
+            return new Tuple<>("Error in EKU check: " + e.getLocalizedMessage(), Output.WRONG);
+        }
+        if (eKU != null) {
+            if (!eKU.contains(ekuMap.get("serverAuth")) && !eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.error("[3] EKU: serverAuth and clientAuth absent");
+                logger.error("    Add serverAuth and clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate.", Output.WRONG);
+            }
+
+            if (eKU.contains(ekuMap.get("serverAuth")) && eKU.contains(ekuMap.get("clientAuth"))) {
+                logger.info("[3] EKU: serverAuth and clientAuth present\n");
+                return new Tuple<>("[3] EKUs are correct. ", Output.CORRECT);
+            } else if (!eKU.contains(ekuMap.get("serverAuth"))) {
+                logger.error("[3] EKU: serverAuth is absent");
+                logger.error("    Add serverAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU serverAuth needs to be added to the certificate. ", Output.WRONG);
+            } else {
+                logger.error("[3] EKU: clientAuth is absent ");
+                logger.error("    Add clientAuth to the EKU of the certificate\n");
+                return new Tuple<>("[3] EKU clientAuth needs to be added to the certificate", Output.WRONG);
+            }
+
+        } else {
+            logger.warn("[3] EKU: No extended key usage found. Add serverAuth and clientAuth usage to the EKU of the certificate.\n");
+            return new Tuple<>("[3] EKUs serverAuth and clientAuth needs to be added to the certificate. ", Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private static Tuple<String, Output> checkValidity(X509Certificate x509Certificate) {
+        String message;
+        try {
+            x509Certificate.checkValidity();
+            logger.info("[4] Validity: Certificate is VALID");
+
+            DateFormat dateFormat = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy");
+            Date dateObj = new Date();
+            Date expiry = x509Certificate.getNotAfter();
+
+            long mSecTillExpiry = Math.abs(expiry.getTime() - dateObj.getTime());
+            long daysTillExpiry = TimeUnit.DAYS.convert(mSecTillExpiry, TimeUnit.MILLISECONDS);
+
+            if (daysTillExpiry < 30) {
+                logger.warn("    Certificate expires in less than 30 days\n");
+            } else if (daysTillExpiry < 60) {
+                logger.warn("    Certificate expires in less than 60 days\n");
+            } else if (daysTillExpiry < 90) {
+                logger.warn("    Certificate expires in less than 90 days\n");
+            } else {
+                logger.info("    Certificate expires in " + daysTillExpiry + "  days\n");
+            }
+            return new Tuple<>("[4] Certificate is VALID", Output.CORRECT);
+        } catch (CertificateExpiredException e) {
+            message = "[4] Validity: Certificate is INVALID: Validity date expired " + x509Certificate.getNotAfter();
+        } catch (CertificateNotYetValidException e) {
+            message = "[4] Validity: Certificate is INVALID: Certificate is not valid before " + x509Certificate.getNotBefore();
+        }
+        logger.error(message + "\n");
+        return new Tuple<>(message, Output.WRONG);
+    }
+
+    private static Tuple<String, Output> checkKeySize(X509Certificate x509Certificate) {
+        PublicKey publicKey = x509Certificate.getPublicKey();
+
+        String finding = "[5] ";
+        String padding = "    ";
+        Output output;
+        String message;
+
+        // Determine key length and print
+        int keyLength = determineKeyLength(publicKey);
+        String keyLengthMessage = publicKey.getAlgorithm() + " Key length: " + keyLength;
+        logger.info(padding + keyLengthMessage);
+
+        // If unsupported key algorithm, print warning
+        if (!(publicKey instanceof RSAPublicKey || publicKey instanceof DSAPublicKey)) {
+            //TODO: Add different algorithm key length checks
+            message = finding + keyLengthMessage;
+            logger.warn(finding + "Key length not checked for " + publicKey.getAlgorithm() + "\n");
+            output = Output.NEEDS_ATTENTION;
+        } else {
+            // If supported key length, check for validity
+            if (keyLength >= 2048) {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is VALID";
+                logger.info(message + "\n");
+                output = Output.CORRECT;
+            } else {
+                message = finding + "Key length: " + keyLength + " for algorithm " + publicKey.getAlgorithm() + " is INVALID (key length below minimum 2048 bits)";
+                logger.error(message + "\n");
+                output = Output.WRONG;
+            }
+        }
+        return new Tuple<>(message, output);
+    }
+
+    private static Tuple<String, Output> checkSignature(List<X509Certificate> certificateList, X509Certificate x509Certificate) {
+        String number = "[6] ";
+        String message;
+        Output output;
+        if (TlsHelper.verifyCertificateSignature(x509Certificate, certificateList)) {
+            message = number + "Signature is VALID";
+            logger.info(message + "\n");
+            output = Output.CORRECT;
+        } else {
+            message = number + "Signature is INVALID";
+            logger.error(message + "\n");
+            output = Output.WRONG;
+        }
+        return new Tuple<>(message, output);
+    }
+
+
+    private static int determineKeyLength(PublicKey publicKey) {
+        switch (publicKey.getAlgorithm().toUpperCase()) {
+            case "RSA":
+                return ((RSAPublicKey) publicKey).getModulus().bitLength();
+            case "DSA":
+                return ((DSAPublicKey) publicKey).getParams().getP().bitLength();
+            case "EC":
+                return ((BCECPublicKey) publicKey).getParameters().getCurve().getFieldSize();
+            default:
+                logger.warn("Cannot determine key length for unknown algorithm " + publicKey.getAlgorithm());
+                return -1;

Review comment:
       Yes, changing




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526989534



##########
File path: nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/diagnosis/TlsToolkitGetDiagnosisStandalone.java
##########
@@ -0,0 +1,664 @@
+/*
+ * 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.diagnosis;
+
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.nifi.properties.NiFiPropertiesLoader;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.CertificateUtils;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException;
+import org.apache.nifi.toolkit.tls.commandLine.ExitCode;
+import org.apache.nifi.toolkit.tls.util.TlsHelper;
+import org.apache.nifi.util.NiFiProperties;
+
+import org.apache.nifi.util.StringUtils;
+import org.apache.nifi.util.Tuple;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+
+public class TlsToolkitGetDiagnosisStandalone {
+
+    private static final String NIFI_PROPERTIES_ARG = "nifiProperties";
+    private static final String HELP_ARG = "help";
+    private static final String QUIET_ARG = "quiet";
+    private static final String BOOTSTRAP_ARG = "bootstrap";
+    private static final String CN = "CN";
+    private static final String SAN = "SAN";
+    private static final String EKU = "EKU";
+    private static final String VALIDITY = "VALIDITY";
+    private static final String KEYSIZE = "KEYSIZE";
+    private static final String SIGN = "SIGN";
+    private static final String TRUSTSTORE = "TRUSTSTORE";
+    private final Options options;
+
+    private String keystorePath;
+    private String keystoreType;
+    private KeyStore keystore;
+
+    private String truststorePath;
+    private String truststoreType;
+    private KeyStore truststore;
+
+    private String niFiPropertiesPath;
+    private String bootstrapPath;
+    private NiFiProperties niFiProperties;
+
+    private static Map<String, String> createEKUMap() {
+        Map<String, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put("serverAuth", "1.3.6.1.5.5.7.3.1");
+        orderMap.put("clientAuth", "1.3.6.1.5.5.7.3.2");
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    private static Map<String, String> ekuMap = createEKUMap();
+
+    enum Output {
+        CORRECT,
+        WRONG,
+        NEEDS_ATTENTION
+    }
+
+    private static Map<String, Tuple<String, Output>> outputSummary = new LinkedHashMap<>();
+    private static final Logger logger = LoggerFactory.getLogger(TlsToolkitGetDiagnosisStandalone.class);
+
+    public TlsToolkitGetDiagnosisStandalone() {
+        this.options = buildOptions();
+    }
+
+    private static Options buildOptions() {
+        Options options = new Options();
+        options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("This field specifies nifi.properties file name").build());
+        options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build());
+        options.addOption(Option.builder("q").longOpt(QUIET_ARG).hasArg(false).desc("Suppresses log info messages").build());
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_ARG).hasArg(true).desc("Suppresses log info messages").build());
+        return options;
+    }
+
+    private void parseCommandLine(String[] args) throws CommandLineParseException {
+        CommandLineParser parser = new DefaultParser();
+
+        try {
+            CommandLine commandLine = parser.parse(options, args);
+            if (commandLine.hasOption(HELP_ARG)) {
+                printUsage("");
+                System.exit(0);
+            }
+            //nifi.properties present?
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG);
+                logger.info("Parsed nifi.properties path: " + niFiPropertiesPath);
+
+                if (commandLine.hasOption(BOOTSTRAP_ARG)) {
+                    bootstrapPath = commandLine.getOptionValue(BOOTSTRAP_ARG);
+                } else {
+                    logger.info("No bootstrap.conf provided. Looking in nifi.properties directory");
+                    bootstrapPath = new File(niFiPropertiesPath).getParent() + "/bootstrap.conf";
+                }
+
+                logger.info("Parsed bootstrap.conf path: " + bootstrapPath);
+            }
+
+        } catch (ParseException e) {
+            logger.error("Encountered an error while parsing command line");
+            printAndThrowParsingException("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE);
+        }
+    }
+
+    public static void printUsage(String errorMessage) {
+        if (!errorMessage.isEmpty()) {
+            System.out.println(errorMessage);
+            System.out.println();
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.setOptionComparator(null);
+        // preserve manual ordering of options when printing instead of alphabetical
+        helpFormatter.printHelp(TlsToolkitGetDiagnosisStandalone.class.getCanonicalName(), buildOptions(), true);
+    }
+
+    public static void printAndThrowParsingException(String errorMessage, ExitCode exitCode) throws CommandLineParseException {
+        printUsage(errorMessage);
+        throw new CommandLineParseException(errorMessage, exitCode);
+    }
+
+    private static void displaySummaryReport() {
+        int correct = 0, wrong = 0, needsAttention = 0;
+        System.out.println("\n***********STANDALONE DIAGNOSIS SUMMARY***********\n");
+        for (Map.Entry<String, Tuple<String, Output>> each : outputSummary.entrySet()) {
+            String output = each.getValue().getValue().toString();
+            String type = StringUtils.rightPad(each.getKey(), 12);
+            System.out.println(type + " ==>   " + each.getValue().getKey());
+            switch (output) {
+                case "WRONG":
+                    wrong++;
+                    break;
+                case "CORRECT":
+                    correct++;
+                    break;
+                case "NEEDS_ATTENTION":
+                    needsAttention++;
+                    break;
+            }
+        }
+        System.out.println("\nCORRECT checks:         " + correct + "/7");
+        System.out.println("WRONG checks:           " + wrong + "/7");
+        System.out.println("NEEDS ATTENTION checks: " + needsAttention + "/7");
+        System.out.println("**************************************************\n");
+    }
+
+
+    public static void main(String[] args) {
+        TlsToolkitGetDiagnosisStandalone standalone = new TlsToolkitGetDiagnosisStandalone();
+
+        // TODO: If -v was added, change the logging config value
+
+        //Parse
+        try {
+            standalone.parseCommandLine(args);
+            standalone.niFiProperties = standalone.loadNiFiProperties();
+        } catch (CommandLineParseException e) {
+            System.exit(e.getExitCode().ordinal());
+        } catch (IOException e) {
+            printUsage(e.getLocalizedMessage());
+            System.exit(-1);
+        }
+
+        //Get keystore and truststore path
+        standalone.keystorePath = standalone.niFiProperties.getProperty("nifi.security.keystore");
+        standalone.truststorePath = standalone.niFiProperties.getProperty("nifi.security.truststore");
+        char[] keystorePassword = standalone.niFiProperties.getProperty("nifi.security.keystorePasswd").toCharArray();
+        standalone.keystoreType = standalone.niFiProperties.getProperty("nifi.security.keystoreType");
+        standalone.truststoreType = standalone.niFiProperties.getProperty("nifi.security.truststoreType");
+        char[] truststorePassword = standalone.niFiProperties.getProperty("nifi.security.truststorePasswd").toCharArray();
+
+        //Verify keystore and truststore are located at the correct file path
+        if ((doesFileExist(standalone.keystorePath, standalone.niFiPropertiesPath, ".jks")
+                && doesFileExist(standalone.truststorePath, standalone.niFiPropertiesPath, ".jks"))) {
+
+            //check keystore and truststore password
+            standalone.keystore = checkPasswordForKeystoreAndLoadKeystore(keystorePassword, standalone.keystorePath, standalone.keystoreType);
+            standalone.truststore = checkPasswordForKeystoreAndLoadKeystore(truststorePassword, standalone.truststorePath, standalone.truststoreType);
+            if (!(standalone.keystore == null) && !(standalone.truststore == null)) {
+                // TODO: Refactor "dangerous" logic to method which throws exceptions
+                KeyStore.PrivateKeyEntry privateKeyEntry = standalone.extractPrimaryPrivateKeyEntry(standalone.keystore, keystorePassword);
+                if (privateKeyEntry != null) {
+                    if (standalone.identifyHostUsingKeystore(privateKeyEntry)) {
+                        outputSummary.put(TRUSTSTORE, standalone.checkTruststore(privateKeyEntry));
+
+                        displaySummaryReport();
+                    } else {
+                        System.exit(-1);
+                    }
+                } else {
+                    System.exit(-1);
+                }
+            } else {
+                System.exit(-1);
+            }
+        } else {
+            System.exit(-1);
+        }
+    }
+
+    private KeyStore.PrivateKeyEntry extractPrimaryPrivateKeyEntry(KeyStore keystore, char[] keystorePassword) {
+        try {
+            KeyStore.PasswordProtection keystorePasswordProtection = new KeyStore.PasswordProtection(keystorePassword);
+            List<String> keystoreAliases = Collections.list(keystore.aliases());
+            Map<String, KeyStore.Entry> privateEntries = keystoreAliases.stream()
+                    .map(alias -> retrieveEntryFromKeystore(keystorePasswordProtection, alias))
+                    .filter(Objects::nonNull)
+                    .filter(t -> t.getValue() instanceof KeyStore.PrivateKeyEntry)
+                    .collect(Collectors.toMap(Tuple::getKey, Tuple::getValue));
+
+            //Check # of privateKeyEntry(s)
+            if (privateEntries.size() == 0) {
+                logger.error("No privateKeyEntry in keystore. Cannot explore keystore identification.");
+                return null;
+            } else if (privateEntries.size() > 1) {
+                logger.info("Keystore has multiple privateKeyEntries. Using the first privateKeyEntry in the list: " + new ArrayList<>(privateEntries.keySet()).get(0));
+                logger.warn("Recommended to have a single PrivateKeyEntry in keystore");
+                logger.warn("Available PrivateKeyEntries: " + StringUtils.join(privateEntries.keySet(), ", "));
+            } else {
+                logger.info("Keystore has single privateKeyEntry: " + new ArrayList<>(privateEntries.keySet()).get(0));
+            }
+            return ((KeyStore.PrivateKeyEntry) new ArrayList<>(privateEntries.values()).get(0));
+        } catch (KeyStoreException e) {
+            logger.error("Something went wrong: " + e.getLocalizedMessage());
+            return null;
+        }
+    }
+
+    private boolean identifyHostUsingKeystore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate();
+
+        if (x509Certificate != null) {
+            String specifiedHostname = niFiProperties.getProperty("nifi.web.https.host");
+            if (specifiedHostname.contains("*.")) {
+                logger.error("Hostname in nifi.properties file is a WILDCARD: Cannot proceed with diagnosis");
+                return false;
+            }
+            // [1] CN
+            outputSummary.put(CN, checkCN(x509Certificate, specifiedHostname));
+            // [2] SAN
+            outputSummary.put(SAN, checkSAN(x509Certificate, specifiedHostname));
+            //[3] EKU
+            outputSummary.put(EKU, checkEKU(x509Certificate));
+            //[4] Validity dates
+            outputSummary.put(VALIDITY, checkValidity(x509Certificate));
+            //[5] Key size
+            outputSummary.put(KEYSIZE, checkKeySize(x509Certificate));
+            //[6] Signature
+            List<X509Certificate> certificateList = Arrays.stream(((X509Certificate[]) privateKeyEntry.getCertificateChain())).sequential().collect(Collectors.toList());
+            outputSummary.put(SIGN, checkSignature(certificateList, x509Certificate));
+            return true;
+        } else {
+            logger.error("Error loading X509 certificate: Check privateKeyEntry of keystore");
+            return false;
+        }
+    }
+
+    private Tuple<String, Output> checkTruststore(KeyStore.PrivateKeyEntry privateKeyEntry) {
+
+        String number = "[7] ";
+        try {
+            List<String> truststoreAliases = Collections.list(truststore.aliases());
+            List<X509Certificate> trustedCertificateEntries = truststoreAliases.stream().map(this::getTrustedCertificates).collect(Collectors.toList());
+
+            X509Certificate privateKeyEntryCert = (X509Certificate) privateKeyEntry.getCertificate();
+
+            if (TlsHelper.verifyCertificateSignature(privateKeyEntryCert, trustedCertificateEntries)) {
+                logger.info(number + "truststore contains a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore identifies privateKeyEntry in keystore", Output.CORRECT);
+            } else {
+                logger.error(number + "truststore does not contain a public certificate identifying privateKeyEntry in keystore\n");
+                return new Tuple<>(number + "Truststore does not identify privateKeyEntry in keystore", Output.WRONG);
+            }
+        } catch (KeyStoreException e) {
+            logger.error(number + e.getLocalizedMessage());
+            return new Tuple<>("[7] " + e.getLocalizedMessage(), Output.NEEDS_ATTENTION);
+        }
+    }
+
+    private X509Certificate getTrustedCertificates(String alias) {
+        try {
+            return (X509Certificate) truststore.getCertificate(alias);
+        } catch (KeyStoreException e) {
+            logger.error(e.getLocalizedMessage());
+        }
+        return null;
+    }
+
+    private static Tuple<String, Output> checkCN(X509Certificate x509Certificate, String specifiedHostname) {
+
+        X500Name x500Name = new X500Name(x509Certificate.getSubjectX500Principal().getName());

Review comment:
       It wasn't necessary. `Surely X500Principal.toString()` to `CertificateUtils.extractUsername()` is easier. Changing to that.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] thenatog commented on pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
thenatog commented on pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#issuecomment-730635492


   I've tested out using the diagnosis tool and had some feedback on its usage. I've compiled my comments alongside the logs I saw for different tests I tried. Let me know if you can't access: https://docs.google.com/spreadsheets/d/1kAbM4LLA3NgRjKAfCXg7GqS8dvJnFA6sajrAkBezMt4/edit?usp=sharing
   
   I think with this tool, the key things to focus on should be these:
   
   - The tool is being created to assist users with checking configuration that can be difficult to get right. The tool needs to be easy to use. The usage guide of the tool wasn't completely clear when I tried out using it.
   - The tool diagnoses errors with configuration, so the tool itself needs to be error free (as best as possible). Otherwise if there are errors with the tool, how will a user (a user who may already be struggling to get their configuration right) know whether the tool is to blame or the configuration is to blame? It needs to pretty robust.
   - Sometimes I got a summary of results, sometimes I didn't, depending on what error the tool experienced. It wasn't immediately clear if the tool failed, or my configuration did. We need to gracefully fail. In as many cases as possible, there should always some form of diagnosis summary at the end of running the tool with information on how to proceed/fix the problem. 
   
   I can appreciate that the tls-toolkit code as it stands needs redesigning/refactoring, and maybe the above feature requests are difficult to implement with the way it is right now. However, we should do our absolute best to make sure this diagnosis tool is robust as possible. A tool that diagnoses errors should have few errors of its own, and its correct usage should be clear and simple. Ideally, we don't want to have to provide support to users on how to use a tool that was created to support them in the first place.
   
   Having said that, this is a great idea and I think with a few adjustments it will be really useful to diagnose problems our users frequently have. Especially so if we can do a more complex option for clustered nodes.


----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] thenatog commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
thenatog commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526497429



##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -120,12 +123,29 @@
         return Collections.unmodifiableMap(orderMap);
     }
 
+    private static Map<Integer, String> createSANOrderMap() {
+        Map<Integer, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put(count++, "otherName");
+        orderMap.put(count++, "rfc822Name");
+        orderMap.put(count++, "dNSName");
+        orderMap.put(count++, "x400Address");
+        orderMap.put(count++, "directoryName");
+        orderMap.put(count++, "ediPartyName");
+        orderMap.put(count++, "uniformResourceIdentifier");
+        orderMap.put(count++, "iPAddress");
+        orderMap.put(count, "registeredID");
+        return Collections.unmodifiableMap(orderMap);

Review comment:
       It would have been ideal to use GeneralName here but the ASN1 classes seem pretty unintuitive. I couldn't figure out a great way to employ it.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] thenatog commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
thenatog commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526498722



##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -160,26 +180,33 @@ public static String extractUsername(String dn) {
      */
     public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException {
 
-        final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+        /*
+         * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
+         *
+         * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.
+         */
+
+        ArrayList<String> sanEntries = new ArrayList<>(getSubjectAlternativeNamesMap(certificate).keySet());
+        Collections.sort(sanEntries);
+        return sanEntries;
+    }
+
+    public static Map<String, String> getSubjectAlternativeNamesMap(X509Certificate cert) throws CertificateParsingException {
+
+        final Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
+
         if (altNames == null) {
-            return new ArrayList<>();
+            return new HashMap<>();
         }
 
-        final List<String> result = new ArrayList<>();
-        for (final List<?> generalName : altNames) {
-            /**
-             * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
-             *
-             * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.
-             */
-            final Object value = generalName.get(1);
-            if (value instanceof String) {
-                result.add(((String) value).toLowerCase());
-            }
+        Map<String, String> sanMap = altNames.stream()
+                .map(nameType -> new Tuple<Object, Object>(nameType.get(0), nameType.get(1)))
+                .filter(Objects::nonNull)
+                .filter(t -> t.getValue() instanceof String)
+                .collect(Collectors.toMap(x -> (String) x.getValue(), x -> sanOrderMap.get( x.getKey() )));

Review comment:
       Couldn't figure out a great way to fit use of GeneralName from the BouncyCastle ASN1 library in here. 




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [nifi] VedaKadam commented on a change in pull request #4670: NIFI-7673 Standalone diagnosis mode verifies independent node

Posted by GitBox <gi...@apache.org>.
VedaKadam commented on a change in pull request #4670:
URL: https://github.com/apache/nifi/pull/4670#discussion_r526327787



##########
File path: nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
##########
@@ -120,12 +123,29 @@
         return Collections.unmodifiableMap(orderMap);
     }
 
+    private static Map<Integer, String> createSANOrderMap() {
+        Map<Integer, String> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put(count++, "otherName");
+        orderMap.put(count++, "rfc822Name");
+        orderMap.put(count++, "dNSName");
+        orderMap.put(count++, "x400Address");
+        orderMap.put(count++, "directoryName");
+        orderMap.put(count++, "ediPartyName");
+        orderMap.put(count++, "uniformResourceIdentifier");
+        orderMap.put(count++, "iPAddress");
+        orderMap.put(count, "registeredID");
+        return Collections.unmodifiableMap(orderMap);

Review comment:
       I was changing it to enum and realized I could just work with int and it doesn't require a map or enum. Changing it now.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org