From 920d82615a125ead8f97a0e92ac8def878f122d3 Mon Sep 17 00:00:00 2001
From: Matthew Harmsen <mharmsen@redhat.com>
Date: Thu, 31 Jul 2014 20:15:56 -0700
Subject: [PATCH] Alternative CLI password methods

- PKI TRAC Ticket #555 - Other ways to specify CLI password
---
 base/java-tools/man/man1/pki.1                     | 118 +++++++++--
 .../src/com/netscape/cmstools/cli/MainCLI.java     | 224 +++++++++++++++++++--
 2 files changed, 311 insertions(+), 31 deletions(-)

diff --git a/base/java-tools/man/man1/pki.1 b/base/java-tools/man/man1/pki.1
index 7f57b0e..d4e8afb 100644
--- a/base/java-tools/man/man1/pki.1
+++ b/base/java-tools/man/man1/pki.1
@@ -27,11 +27,14 @@ These services include certificates, groups, keys, security domains, and users.
   
 .SH CLI OPTIONS
 .TP
-.B -c <security database password>
-Specifies the security database password.
+.B -c <client security database password>
+Specifies the client security database password (mutually exclusive to the '-C' option).
 .TP
-.B -d <security database location>
-Specifies the security database location (default: ~/.dogtag/nssdb).
+.B -C <client security database password file>
+Specifies the file which contains the client security database password (mutually exclusive to the '-c' option).
+.TP
+.B -d <client security database location>
+Specifies the client security database location (default: \fI~/.dogtag/nssdb\fP).
 .TP
 .B -h <hostname>
 Specifies the hostname (default: hostname of the local machine).
@@ -39,8 +42,17 @@ Specifies the hostname (default: hostname of the local machine).
 .B --help
 Prints additional help information.
 .TP
-.B -n <nickname>
-Specifies the certificate nickname.
+.B --ignore-cert-status <list>
+Comma-separated list of ignored certificate validity statuses.
+.TP
+.B --message-format <format>
+Message format: xml (default), json.
+.TP
+.B -n <client certificate nickname>
+Specifies the nickname of the client certificate.
+.TP
+.B --output <folder>
+Folder to store HTTP messages.
 .TP
 .B -P <protocol>
 Specifies the protocol (default: http).
@@ -48,8 +60,14 @@ Specifies the protocol (default: http).
 .B -p <port>
 Specifies the port (default: 8080).
 .TP
-.B -U <URL>
-Specifies the server URL.
+.B --reject-cert-status <list>
+Comma-separated list of rejected certificate validity statuses.
+.TP
+.B -t <type>
+PKI subsystem type - [CA, DRM, KRA, OCSP, TKS, or TPS] (non-case-sensitive).
+.TP
+.B -U <URI>
+Specifies the server URI.
 .TP
 .B -u <username>
 Specifies the username.
@@ -61,7 +79,13 @@ Displays verbose information.
 Displays CLI version information.
 .TP
 .B -w <password>
-Specifies the user password.
+Specifies the user password (mutually exclusive to the '-W' and '-y' options).
+.TP
+.B -W <password file>
+Specifies the file which contains the user password (mutually exclusive to the '-w' and '-y' options).
+.TP
+.B -y
+Prompts for the user password (mutually exclusive to the '-W' and '-w' options).
 
 .SH OPERATIONS
 To view available commands and options, simply type \fBpki\fP.  Some commands have sub-commands.
@@ -76,11 +100,11 @@ By default, the CLI connects to a server running on the local machine via the no
 
 .B pki -P <protocol> -h <hostname> -p <port> <command>
 
-Alternatively, the connection parameters can be specified as a URL:
+Alternatively, the connection parameters can be specified as a URI:
 
-.B pki -U <URL> <command>
+.B pki -U <URI> <command>
 
-where the URL is of the format \fI<protocol>://<hostname>:<port>\fP.
+where the URI is of the format \fI<protocol>://<hostname>:<port>\fP.
 
 .SS Authentication
 Some commands require authentication.  These are commands that are restricted to particular sets of users (such as agents or admins) or those operations involving certificate profiles that require authentication.
@@ -89,14 +113,82 @@ To execute a command without authentication:
 
 .B pki <command>
 
+To execute a command using basic authentication (i. e. - username/password), see the \fBBasic Authentication\fP section of this man page.
+
+To execute a command using client authentication (i. e. - client certificate), see the \fBClient Authentication\fP section of this man page.
+
+.SS Basic Authentication
 To authenticate with a username and password:
 
 .B pki -u <username> -w <password> <command>
 
+Rather than being exposed in plaintext on the command-line, user passwords may be stored in a file instead.  For security purposes, user password files should be, at a minimum, operating system protected non-world readable files.  The format of these files merely consists of the plaintext password on a single line.
+
+\fBNote:\fP
+Unlike client security database password files (see explanation in the \fBClient Authentication\fP section), there are no restrictions on using the ':' character within user passwords.
+
+To authenticate with a username by obtaining the user password from a user password file:
+
+.B pki -u <username> -W <user password file> <command>
+
+\fBNote:\fP
+An empty user password file will cause a warning to be issued and the user password to be prompted for!
+
+Finally, to use the \fBpki\fP command-line tool with basic authentication interactively, user passwords may be prompted for:
+
+To authenticate with a username by prompting for a user password:
+
+.B pki -u <username> -y <command>
+
+\fBNote:\fP
+Prompting for a user password is not suitable for automated batch processing.
+
+.SS Client Authentication Setup
+First of all, a client certificate associated with the desired PKI server must be used for client authentication. This can be done by importing the client certificate into an NSS security database and passing the values to relevant options provided by the \fBpki\fP CLI framework.
+
+To achieve this, execute the following commands to set up an NSS security database for use by the \fBpki\fP client, import the client certificate into the NSS database, and list information (including the nickname of the client certificate) stored in the NSS database:
+
+.B    - certutil -N -d <CERT_DB_DIR_PATH>
+
+.B    - pk12util -i <Agent_Cert_P12_FILE_PATH> -d <CERT_DB_DIR_PATH>
+
+.B    - certutil -L -d <CERT_DB_DIR_PATH>
+
+The first command creates a client security database, and asks the client user to enter a password for this NSS database.
+
+The second command imports a client certificate stored in a PKCS12 format into this NSS database; it prompts for the passwords of the PKCS12 file and the client security database.  The simplist example of such a client certificate is to obtain the administrator certificate created during the configuration portion of the basic PKI installation of the associated PKI server (e. g. - located at \fI/root/.dogtag/pki-tomcat/ca_admin_cert.p12\fP on the PKI server machine).
+
+The third command shows the information about the imported client certificate (including its nickname).
+
+\fBNote:\fP
+When issuing the first command using the authentication parameters (after completion of the setup of the client security database), a user may be greeted with a warning message which indicates that an untrusted issuer was encountered.  Simply reply 'Y' to import the CA certificate, and, presuming that the displayed CA server URL is valid, press the carriage return.
+
+.SS Client Authentication
 To authenticate with a client certificate:
 
-.B pki -d <security database location> -c <security database password> -n <certificate nickname> <command>
+.B pki -d <client security database location> -c <client security database password> -n <client certificate nickname> <command>
     
+Alternatively, to prevent exposure via the command-line, a client security database may store their password in a file instead.  For security purposes, client security database password files should be, at a minimum, operating system protected non-world readable files.  Client security database password files generally store a password in the colon-delimited plaintext format 'token:password' (e. g. - 'internal:foobar' where 'internal' is the token, ':' is the delimiter, and 'foobar' is the actual password).  The token keyword 'internal' is the default specification for a token, and refers to the "Internal Key Storage Token".  If a client security database is being used for the sole purposes of the \fBpki\fR command-line tool, a client security database password file also supports the format that merely consists of the plaintext password on a single line (read the \fBImportant\fP notice which follows).
+
+\fBImportant:\fP
+Since client security database password files support the 'token:password' format, the ':' character can only be used as a delimiter (i. e. - it cannot be used as a valid character within either the 'token' or the 'password') even when the client security database password file merely consists of a plaintext password on a single line.
+
+To authenticate with a client certificate by obtaining the client security database password from a client security database password file:
+
+.B pki -d <client security database location> -C <client security database password file> -n <client certificate nickname> <command>
+
+\fBNote:\fP
+A warning will be issued for a client security database password file that fails to adequately define a password, or one that exists as an empty file, and may cause the client security database password to be prompted for!
+
+Finally, if a client certificate has been specified on the command-line, and neither the '-C <client security database password file>' nor the '-c <client security database password' options have been utilized, the client security database password may be prompted for.
+
+To authenticate with a client certificate by interactively prompting for a client security database password:
+
+.B pki -d <client security database location> -n <client certificate nickname> <command>
+
+\fBNote:\fP
+Prompting for a client security database password is not suitable for automated batch processing.
+
 .SH FILES
 .I /usr/bin/pki
 
diff --git a/base/java-tools/src/com/netscape/cmstools/cli/MainCLI.java b/base/java-tools/src/com/netscape/cmstools/cli/MainCLI.java
index e83d440..e9be90d 100644
--- a/base/java-tools/src/com/netscape/cmstools/cli/MainCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/cli/MainCLI.java
@@ -18,15 +18,22 @@
 
 package com.netscape.cmstools.cli;
 
+import java.io.BufferedReader;
+import java.io.Console;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
+import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang.StringUtils;
 import org.mozilla.jss.CryptoManager;
 import org.mozilla.jss.CryptoManager.NotInitializedException;
@@ -50,6 +57,8 @@ import com.netscape.cmstools.user.UserCLI;
  */
 public class MainCLI extends CLI {
 
+    public static final String[] PKI_SUBSYSTEMS = { "CA", "KRA", "DRM", "OCSP", "TKS", "TPS" };
+
     public ClientConfig config = new ClientConfig();
 
     public Collection<Integer> rejectedCertStatuses = new HashSet<Integer>();
@@ -59,6 +68,8 @@ public class MainCLI extends CLI {
 
     String output;
 
+    private ArrayList<String> errMsgs = new ArrayList<>();
+
     public MainCLI() throws Exception {
         super("pki", "PKI command-line interface");
 
@@ -91,7 +102,6 @@ public class MainCLI extends CLI {
     }
 
     public void printHelp() {
-
         formatter.printHelp(name+" [OPTIONS..] <command> [ARGS..]", options);
         System.out.println();
 
@@ -114,6 +124,16 @@ public class MainCLI extends CLI {
         }
     }
 
+    public void printHelp(ArrayList<String> errors) {
+        for (String message: errors) {
+            System.err.println();
+            System.err.println(message);
+            System.err.println();
+        }
+
+        printHelp();
+    }
+
     public void createOptions() throws UnknownHostException {
 
         Option option = new Option("U", true, "Server URI");
@@ -132,19 +152,23 @@ public class MainCLI extends CLI {
         option.setArgName("port");
         options.addOption(option);
 
-        option = new Option("t", true, "Subsystem type");
+        option = new Option("t", true, "PKI subsystem type - [CA, DRM, KRA, OCSP, TKS, or TPS] (non-case-sensitive)");
         option.setArgName("type");
         options.addOption(option);
 
-        option = new Option("d", true, "Security database location (default: ~/.dogtag/nssdb)");
+        option = new Option("d", true, "Client security database location (default: ~/.dogtag/nssdb)");
         option.setArgName("database");
         options.addOption(option);
 
-        option = new Option("c", true, "Security database password");
-        option.setArgName("password");
+        option = new Option("c", true, "Client security database password (mutually exclusive to '-C' option)");
+        option.setArgName("certpassword");
+        options.addOption(option);
+
+        option = new Option("C", true, "Client security database password file (mutually exclusive to '-c' option)");
+        option.setArgName("certpasswordfile");
         options.addOption(option);
 
-        option = new Option("n", true, "Certificate nickname");
+        option = new Option("n", true, "Client certificate nickname");
         option.setArgName("nickname");
         options.addOption(option);
 
@@ -152,10 +176,14 @@ public class MainCLI extends CLI {
         option.setArgName("username");
         options.addOption(option);
 
-        option = new Option("w", true, "Password");
+        option = new Option("w", true, "Password (mutually exclusive to '-W' and '-y' options)");
         option.setArgName("password");
         options.addOption(option);
 
+        option = new Option("W", true, "Password file (mutually exclusive to '-w' and '-y' options)");
+        option.setArgName("passwordfile");
+        options.addOption(option);
+
         option = new Option(null, "output", true, "Folder to store HTTP messages");
         option.setArgName("folder");
         options.addOption(option);
@@ -173,10 +201,112 @@ public class MainCLI extends CLI {
         options.addOption(option);
 
         options.addOption("v", false, "Verbose");
+        options.addOption("y", false, "Password prompt (mutually exclusive to '-W' and '-w' options)");
         options.addOption(null, "help", false, "Help");
         options.addOption(null, "version", false, "Version");
     }
 
+    public String prompt_for_password() throws IOException {
+        char[] password = null;
+        Console console = System.console();
+        System.out.print("Enter Password: ");
+        password = console.readPassword();
+        return new String(password);
+    }
+
+    public String read_plaintext_password_from_file(String pwfile) {
+        String password = null;
+
+        try {
+            FileInputStream fis = new FileInputStream(pwfile);
+            InputStreamReader isr = new InputStreamReader(fis);
+            BufferedReader br = new BufferedReader(isr);
+
+            password = br.readLine();
+            if (password == null) {
+                System.err.println("\nWarning:  File '" + pwfile + "' is empty!\n");
+
+                // Attempt to prompt for the password
+                password = prompt_for_password();
+            }
+
+            br.close();
+            isr.close();
+            fis.close();
+        } catch (Exception e) {
+            errMsgs.add("Error: " + e.getMessage());
+            printHelp(errMsgs);
+            System.exit(-1);
+        }
+
+        return password;
+    }
+
+    public String read_client_security_database_password_from_file(String pwfile) {
+        String token = "internal";
+        String password = null;
+
+        try {
+            FileInputStream fis = new FileInputStream(pwfile);
+            InputStreamReader isr = new InputStreamReader(fis);
+            BufferedReader br = new BufferedReader(isr);
+
+            String line = br.readLine();
+            if (line != null) {
+                // Process line
+                if (line.contains(":")) {
+                    // Check for format 'token:password'
+                    if (StringUtils.countMatches(line, ":") == 1) {
+                        String[] parts = StringUtils.split(line, ":");
+                        // Check for undefined 'token'
+                        if (line.startsWith(":")) {
+                            // Issue a warning and continue
+                            System.err.println("\nWarning:  File '" + pwfile + "' does not define a token!\n");
+                        } else {
+                             // Set 'token'
+                             token = parts[0];
+                        }
+                        // Check for undefined 'password'
+                        if (line.endsWith(":")) {
+                            // Issue a warning and continue
+                            System.err.println("\nWarning:  File '" + pwfile + "' does not define a password!\n");
+                        } else {
+                            // Set 'password'
+                            if (line.startsWith(":")) {
+                                password = parts[0];
+                            } else {
+                                password = parts[1];
+                            }
+                        }
+                    } else {
+                        errMsgs.add("Error:  File '" + pwfile + "' contains an invalid password!");
+                        printHelp(errMsgs);
+                        System.exit(-1);
+                    }
+                } else {
+                    // Set 'password'
+                    password = line;
+                }
+            } else {
+                // Issue a warning and continue
+                System.err.println("\nWarning:  File '" + pwfile + "' is empty!\n");
+            }
+
+            // NOTE:  An empty file, or a file which fails to define a
+            //        password will probably yield a password prompt!
+
+            br.close();
+            isr.close();
+            fis.close();
+        } catch (Exception e) {
+            errMsgs.add("Error: " + e.getMessage());
+            printHelp(errMsgs);
+            System.exit(-1);
+        }
+
+        return password;
+    }
+
     public void parseOptions(CommandLine cmd) throws Exception {
 
         verbose = cmd.hasOption("v");
@@ -192,8 +322,21 @@ public class MainCLI extends CLI {
         if (uri == null)
             uri = protocol + "://" + hostname + ":" + port;
 
-        if (subsystem != null)
-            uri = uri + "/" + subsystem;
+        if (subsystem != null) {
+            if (!ArrayUtils.contains(PKI_SUBSYSTEMS, subsystem.toUpperCase())) {
+                errMsgs.add("Error:  PKI subsystem '" + subsystem + "' is not a valid PKI subsystem.");
+                printHelp(errMsgs);
+                System.exit(-1);
+            }
+
+            // For URI purposes, always convert "DRM" --> "KRA"
+            if( subsystem.toUpperCase().equals("DRM") ) {
+                subsystem = "KRA";
+            }
+
+            // For URI purposes, always convert subsystem to lowercase
+            uri = uri + "/" + subsystem.toLowerCase();
+        }
 
         config.setServerURI(uri);
 
@@ -202,8 +345,28 @@ public class MainCLI extends CLI {
         String certDatabase = cmd.getOptionValue("d");
         String certNickname = cmd.getOptionValue("n");
         String certPassword = cmd.getOptionValue("c");
+        String certPasswordFile = cmd.getOptionValue("C");
         String username = cmd.getOptionValue("u");
         String password = cmd.getOptionValue("w");
+        String passwordFile = cmd.getOptionValue("W");
+        boolean passwordPrompt = cmd.hasOption("y");
+
+        // check for mutually exclusive options
+        boolean MutuallyExclusiveOptions = true;
+        if ((certPasswordFile != null) && (certPassword != null)) {
+            errMsgs.add("Error:  The '-C' and '-c' options are mutually exclusive!");
+            MutuallyExclusiveOptions = false;
+        }
+        if (((passwordFile != null) && (password != null))       ||
+            ((passwordFile != null) && passwordPrompt) ||
+            ((password != null) && passwordPrompt)) {
+            errMsgs.add("Error:  The '-W', '-w', and '-y' options are all mutually exclusive!");
+            MutuallyExclusiveOptions = false;
+        }
+        if (!MutuallyExclusiveOptions) {
+            printHelp(errMsgs);
+            System.exit(-1);
+        }
 
         // convert into absolute path
         if (certDatabase != null)
@@ -212,14 +375,39 @@ public class MainCLI extends CLI {
         if (certNickname != null)
             config.setCertNickname(certNickname);
 
-        if (certPassword != null)
+        if (certPassword != null) {
+            // set client security database password
             config.setCertPassword(certPassword);
+        } else if (certPasswordFile != null) {
+            // read client security database password from specified file
+            certPassword = read_client_security_database_password_from_file(certPasswordFile);
+            // set client security database password
+            config.setCertPassword(certPassword);
+        }
 
         if (username != null)
             config.setUsername(username);
 
-        if (password != null)
+        if (password != null) {
+            // set user password
+            config.setPassword(password);
+        } else if (passwordFile != null) {
+            // read user password from specified file
+            password = read_plaintext_password_from_file(passwordFile);
+            // set user password
             config.setPassword(password);
+        } else if (passwordPrompt) {
+            try {
+                // prompt for user password
+                password = prompt_for_password();
+                // set user password
+                config.setPassword(password);
+            } catch (Exception e) {
+                errMsgs.add("Error: " + e.getMessage());
+                printHelp(errMsgs);
+                System.exit(-1);
+            }
+        }
 
         String list = cmd.getOptionValue("reject-cert-status");
         convertCertStatusList(list, rejectedCertStatuses);
@@ -228,17 +416,17 @@ public class MainCLI extends CLI {
         convertCertStatusList(list, ignoredCertStatuses);
 
         if (config.getCertDatabase() == null) {
-            // Use default security database
+            // Use default client security database
             this.certDatabase = new File(
                     System.getProperty("user.home") + File.separator +
                     ".dogtag" + File.separator + "nssdb");
 
         } else {
-            // Use existing security database
+            // Use existing client security database
             this.certDatabase = new File(config.getCertDatabase());
         }
 
-        if (verbose) System.out.println("Security database: "+this.certDatabase.getAbsolutePath());
+        if (verbose) System.out.println("Client security database: "+this.certDatabase.getAbsolutePath());
 
         String messageFormat = cmd.getOptionValue("message-format");
         config.setMessageFormat(messageFormat);
@@ -264,12 +452,12 @@ public class MainCLI extends CLI {
 
     public void init() throws Exception {
 
-        // Main program should initialize security database
+        // Main program should initialize client security database
         if (certDatabase.exists()) {
             CryptoManager.initialize(certDatabase.getAbsolutePath());
         }
 
-        // If password is specified, use password to access security database
+        // If password is specified, use password to access client security database
         if (config.getCertPassword() != null) {
             try {
                 CryptoManager manager = CryptoManager.getInstance();
@@ -279,11 +467,11 @@ public class MainCLI extends CLI {
 
             } catch (NotInitializedException e) {
                 // The original exception doesn't contain a message.
-                throw new Error("Security database does not exist.");
+                throw new Error("Client security database does not exist.");
 
             } catch (IncorrectPasswordException e) {
                 // The original exception doesn't contain a message.
-                throw new IncorrectPasswordException("Incorrect security database password.");
+                throw new IncorrectPasswordException("Incorrect client security database password.");
             }
 
         }
-- 
1.9.3

