From ef3bd5e0820f4db1445a2c44e51c523dfaa448a1 Mon Sep 17 00:00:00 2001
From: Endi Sukma Dewata <edewata@redhat.com>
Date: Fri, 19 Apr 2013 11:45:10 -0400
Subject: [PATCH] Added options to reject/ignore cert validity statuses.

New options have been added to the CLI to reject or ignore certain
cert validity statuses such as UNTRUSTED_ISSUER or BAD_CERT_DOMAIN.
The options can also be defined in pki.conf as a system-wide policy.

Ticket #491
---
 .../com/netscape/certsrv/client/PKIConnection.java | 89 ++++++++++++++++++----
 base/java-tools/pki                                |  5 +-
 .../src/com/netscape/cmstools/cli/MainCLI.java     | 47 +++++++++++-
 3 files changed, 124 insertions(+), 17 deletions(-)

diff --git a/base/common/src/com/netscape/certsrv/client/PKIConnection.java b/base/common/src/com/netscape/certsrv/client/PKIConnection.java
index 4eba7231ace656a68e1f5858d9b146ce2ea27e92..556779ec896ad357c6bc24f519435ca55aa80a58 100644
--- a/base/common/src/com/netscape/certsrv/client/PKIConnection.java
+++ b/base/common/src/com/netscape/certsrv/client/PKIConnection.java
@@ -17,7 +17,9 @@ import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Enumeration;
+import java.util.HashSet;
 import java.util.List;
 
 import javax.ws.rs.core.MediaType;
@@ -46,6 +48,7 @@ import org.apache.http.conn.scheme.SchemeSocketFactory;
 import org.apache.http.entity.BufferedHttpEntity;
 import org.apache.http.impl.client.ClientParamsStack;
 import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
 import org.apache.http.impl.client.DefaultRedirectStrategy;
 import org.apache.http.impl.client.EntityEnclosingRequestWrapper;
 import org.apache.http.impl.client.RequestWrapper;
@@ -78,6 +81,12 @@ public class PKIConnection {
 
     ClientConfig config;
 
+    Collection<Integer> rejectedCertStatuses;
+    Collection<Integer> ignoredCertStatuses;
+
+    // List to prevent displaying the same warnings/errors again.
+    Collection<Integer> statuses = new HashSet<Integer>();
+
     DefaultHttpClient httpClient = new DefaultHttpClient();
 
     ResteasyProviderFactory providerFactory;
@@ -97,6 +106,9 @@ public class PKIConnection {
         Scheme scheme = new Scheme("https", 443, new JSSProtocolSocketFactory());
         httpClient.getConnectionManager().getSchemeRegistry().register(scheme);
 
+        // Don't retry operations.
+        httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
+
         if (config.getUsername() != null && config.getPassword() != null) {
             List<String> authPref = new ArrayList<String>();
             authPref.add(AuthPolicy.BASIC);
@@ -264,6 +276,7 @@ public class PKIConnection {
     }
 
     private class ServerCertApprovalCB implements SSLCertificateApprovalCallback {
+
         // NOTE:  The following helper method defined as
         //        'public String displayReason(int reason)'
         //        should be moved into the JSS class called
@@ -296,11 +309,33 @@ public class PKIConnection {
             return null;
         }
 
+        public String getMessage(X509Certificate serverCert, int reason) {
+
+            if (reason == SSLCertificateApprovalCallback.ValidityStatus.BAD_CERT_DOMAIN) {
+
+                return "BAD_CERT_DOMAIN encountered on '"+serverCert.getSubjectDN()+"' indicates a common-name mismatch";
+            }
+
+            if (reason == SSLCertificateApprovalCallback.ValidityStatus.UNTRUSTED_ISSUER) {
+                return "UNTRUSTED ISSUER encountered on '" +
+                        serverCert.getSubjectDN() + "' indicates a non-trusted CA cert '" +
+                        serverCert.getIssuerDN() + "'";
+            }
+
+            if (reason == SSLCertificateApprovalCallback.ValidityStatus.CA_CERT_INVALID) {
+                return "CA_CERT_INVALID encountered on '"+serverCert.getSubjectDN()+"' results in a denied SSL server cert!";
+            }
+
+            String reasonName = displayReason(reason);
+            if (reasonName != null) {
+                return reasonName+" encountered on '"+serverCert.getSubjectDN()+"' results in a denied SSL server cert!";
+            }
+
+            return "Unknown/undefined reason "+reason+" encountered on '"+serverCert.getSubjectDN()+"' results in a denied SSL server cert!";
+        }
+
         public boolean handleUntrustedIssuer(X509Certificate serverCert) {
             try {
-                System.err.println("WARNING: UNTRUSTED ISSUER encountered on '" +
-                        serverCert.getSubjectDN() + "' indicates a non-trusted CA cert '" +
-                        serverCert.getIssuerDN() + "'");
                 System.out.print("Import CA certificate (Y/n)? ");
 
                 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
@@ -359,7 +394,6 @@ public class PKIConnection {
                 SSLCertificateApprovalCallback.ValidityStatus status) {
 
             boolean approval = true;
-            String reasonName = null;
 
             if (verbose) System.out.println("Server certificate: "+serverCert.getSubjectDN());
 
@@ -375,7 +409,15 @@ public class PKIConnection {
                 item = (SSLCertificateApprovalCallback.ValidityItem) errors.nextElement();
                 int reason = item.getReason();
 
-                if (reason == SSLCertificateApprovalCallback.ValidityStatus.UNTRUSTED_ISSUER) {
+                if (isRejected(reason)) {
+                    if (!statuses.contains(reason))
+                        System.err.println("ERROR: " + getMessage(serverCert, reason));
+                    approval = false;
+
+                } else if (isIgnored(reason)) {
+                    // Ignore validity status
+
+                } else if (reason == SSLCertificateApprovalCallback.ValidityStatus.UNTRUSTED_ISSUER) {
                     // Ignore the "UNTRUSTED_ISSUER" validity status
                     // during PKI instance creation since we are
                     // utilizing an untrusted temporary CA cert.
@@ -383,13 +425,17 @@ public class PKIConnection {
                         // Otherwise, issue a WARNING, but allow this process
                         // to continue since we haven't installed a trusted CA
                         // cert for this operation.
-                        handleUntrustedIssuer(serverCert);
+                        if (!statuses.contains(reason)) {
+                            System.err.println("WARNING: " + getMessage(serverCert, reason));
+                            handleUntrustedIssuer(serverCert);
+                        }
                     }
 
                 } else if (reason == SSLCertificateApprovalCallback.ValidityStatus.BAD_CERT_DOMAIN) {
                     // Issue a WARNING, but allow this process to continue on
                     // common-name mismatches.
-                    System.err.println("WARNING: BAD_CERT_DOMAIN encountered on '"+serverCert.getSubjectDN()+"' indicates a common-name mismatch");
+                    if (!statuses.contains(reason))
+                        System.err.println("WARNING: " + getMessage(serverCert, reason));
 
                 } else if (reason == SSLCertificateApprovalCallback.ValidityStatus.CA_CERT_INVALID) {
                     // Ignore the "CA_CERT_INVALID" validity status
@@ -400,7 +446,8 @@ public class PKIConnection {
                         // certificate so that the connection is terminated.
                         // (Expect an IOException on the outstanding
                         //  read()/write() on the socket).
-                        System.err.println("ERROR: CA_CERT_INVALID encountered on '"+serverCert.getSubjectDN()+"' results in a denied SSL server cert!");
+                        if (!statuses.contains(reason))
+                            System.err.println("ERROR: " + getMessage(serverCert, reason));
                         approval = false;
                     }
 
@@ -408,14 +455,12 @@ public class PKIConnection {
                     // Set approval false to deny this certificate so that
                     // the connection is terminated. (Expect an IOException
                     // on the outstanding read()/write() on the socket).
-                    reasonName = displayReason(reason);
-                    if (reasonName != null ) {
-                        System.err.println("ERROR: "+reasonName+" encountered on '"+serverCert.getSubjectDN()+"' results in a denied SSL server cert!");
-                    } else {
-                        System.err.println("ERROR: Unknown/undefined reason "+reason+" encountered on '"+serverCert.getSubjectDN()+"' results in a denied SSL server cert!");
-                    }
+                    if (!statuses.contains(reason))
+                        System.err.println("ERROR: " + getMessage(serverCert, reason));
                     approval = false;
                 }
+
+                statuses.add(reason);
             }
 
             return approval;
@@ -540,6 +585,22 @@ public class PKIConnection {
         return request.post(String.class);
     }
 
+    public void setRejectedCertStatuses(Collection<Integer> rejectedCertStatuses) {
+        this.rejectedCertStatuses = rejectedCertStatuses;
+    }
+
+    public boolean isRejected(Integer certStatus) {
+        return this.rejectedCertStatuses != null && this.rejectedCertStatuses.contains(certStatus);
+    }
+
+    public void setIgnoredCertStatuses(Collection<Integer> ignoredCertStatuses) {
+        this.ignoredCertStatuses = ignoredCertStatuses;
+    }
+
+    public boolean isIgnored(Integer certStatus) {
+        return this.ignoredCertStatuses != null && this.ignoredCertStatuses.contains(certStatus);
+    }
+
     public File getOutput() {
         return output;
     }
diff --git a/base/java-tools/pki b/base/java-tools/pki
index 197fa610079f3e77f7eaa52e152faa0fdaede50f..07eb4ee28f998685ab80fe4d6c786e2a55aeb8c6 100755
--- a/base/java-tools/pki
+++ b/base/java-tools/pki
@@ -107,12 +107,15 @@ if( $ARCHITECTURE eq "x86_64" ) {
 ##      based upon the preset LD_LIBRARY_PATH and CP environment variables.  ##
 ###############################################################################
 
+my $PKI_CLI_OPTIONS = `source /etc/pki/pki.conf && echo \$PKI_CLI_OPTIONS`;
+chomp($PKI_CLI_OPTIONS);
+
 my @args = ();
 foreach (@ARGV) {
     push(@args, quotemeta($_));
 }
 
-my $command = "java -cp $ENV{CLASSPATH} com.netscape.cmstools.cli.MainCLI @args";
+my $command = "java -cp $ENV{CLASSPATH} com.netscape.cmstools.cli.MainCLI $PKI_CLI_OPTIONS @args";
 
 system($command) and do {
     exit $? >> 8;
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 c6daa7d29945b73f85fd22d3c64d290198a57e87..aa4327fe60ac1bf5b77319b8bf30fe901a176402 100644
--- a/base/java-tools/src/com/netscape/cmstools/cli/MainCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/cli/MainCLI.java
@@ -19,9 +19,11 @@
 package com.netscape.cmstools.cli;
 
 import java.io.File;
+import java.lang.reflect.Field;
 import java.net.InetAddress;
-import java.net.URISyntaxException;
 import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.HashSet;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
@@ -29,6 +31,7 @@ import org.apache.commons.cli.Options;
 import org.apache.commons.lang.StringUtils;
 import org.mozilla.jss.CryptoManager;
 import org.mozilla.jss.crypto.CryptoToken;
+import org.mozilla.jss.ssl.SSLCertificateApprovalCallback;
 import org.mozilla.jss.util.IncorrectPasswordException;
 import org.mozilla.jss.util.Password;
 
@@ -49,6 +52,9 @@ public class MainCLI extends CLI {
 
     public ClientConfig config = new ClientConfig();
 
+    public Collection<Integer> rejectedCertStatuses;
+    public Collection<Integer> ignoredCertStatuses;
+
     public PKIConnection connection;
     public AccountClient accountClient;
 
@@ -140,12 +146,20 @@ public class MainCLI extends CLI {
         option.setArgName("folder");
         options.addOption(option);
 
+        option = new Option(null, "reject-cert-status", true, "Comma-separated list of rejected certificate validity statuses");
+        option.setArgName("list");
+        options.addOption(option);
+
+        option = new Option(null, "ignore-cert-status", true, "Comma-separated list of ignored certificate validity statuses");
+        option.setArgName("list");
+        options.addOption(option);
+
         options.addOption("v", false, "Verbose");
         options.addOption(null, "help", false, "Help");
         options.addOption(null, "version", false, "Version");
     }
 
-    public void parseOptions(CommandLine cmd) throws URISyntaxException, UnknownHostException {
+    public void parseOptions(CommandLine cmd) throws Exception {
 
         verbose = cmd.hasOption("v");
         output = cmd.getOptionValue("output");
@@ -179,11 +193,40 @@ public class MainCLI extends CLI {
 
         if (password != null)
             config.setPassword(password);
+
+        String list = cmd.getOptionValue("reject-cert-status");
+        rejectedCertStatuses = convertCertStatusList(list);
+
+        list = cmd.getOptionValue("ignore-cert-status");
+        ignoredCertStatuses = convertCertStatusList(list);
+    }
+
+    public Collection<Integer> convertCertStatusList(String list) throws Exception {
+
+        if (list == null) return null;
+
+        Collection<Integer> statuses = new HashSet<Integer>();
+
+        Class<SSLCertificateApprovalCallback.ValidityStatus> clazz = SSLCertificateApprovalCallback.ValidityStatus.class;
+
+        for (String status : list.split(",")) {
+            try {
+                Field field = clazz.getField(status);
+                statuses.add(field.getInt(null));
+
+            } catch (NoSuchFieldException e) {
+                throw new Error("Invalid cert status \"" + status + "\".", e);
+            }
+        }
+
+        return statuses;
     }
 
     public void connect() throws Exception {
         connection = new PKIConnection(config);
         connection.setVerbose(verbose);
+        connection.setRejectedCertStatuses(rejectedCertStatuses);
+        connection.setIgnoredCertStatuses(ignoredCertStatuses);
 
         if (output != null) {
             File file = new File(output);
-- 
1.8.1.4

