>From 63af93d4b7ba2bdda405bb585ed1e4c096e7ceb2 Mon Sep 17 00:00:00 2001
From: Christina Fu <cfu@redhat.com>
Date: Fri, 19 May 2017 11:55:14 -0700
Subject: [PATCH] Ticket#2618 feature: pre-signed CMC renewal request

This patch provides the feature implementation to allow CA to process pre-signed CMC renewal requests. In the world of CMC, renewal request are full CMC requests that are signed by previously issued signing certificate.
The implementation approach is to use the caFullCMCUserSignedCert with the enhanced profile constraint: UniqueKeyConstraint.
UniqueKeyConstraint has been updated to disallow renewal of same key shared by a revoked certificate.  It also saves the origNotAfter of the newest certificate sharing the same key in the request to be used by the RenewGracePeriodConstraint.
The profile caFullCMCUserSignedCert.cfg has been updated to have both UniqueKeyConstraint and RenewGracePeriodConstraint.  They must be placed in the correct order. By default in the UniqueKeyConstraint the constraint parameter allowSameKeyRenewal=true.
---
 .../shared/profiles/ca/caFullCMCUserSignedCert.cfg |  13 ++-
 .../src/com/netscape/cmstools/CMCRequest.java      |  14 +--
 .../constraint/RenewGracePeriodConstraint.java     |  23 ++--
 .../profile/constraint/UniqueKeyConstraint.java    | 116 ++++++++++++++++-----
 4 files changed, 124 insertions(+), 42 deletions(-)

diff --git a/base/ca/shared/profiles/ca/caFullCMCUserSignedCert.cfg b/base/ca/shared/profiles/ca/caFullCMCUserSignedCert.cfg
index 229a3cd..63a4bca 100644
--- a/base/ca/shared/profiles/ca/caFullCMCUserSignedCert.cfg
+++ b/base/ca/shared/profiles/ca/caFullCMCUserSignedCert.cfg
@@ -10,12 +10,23 @@ input.i2.class_id=submitterInfoInputImpl
 output.list=o1
 output.o1.class_id=certOutputImpl
 policyset.list=cmcUserCertSet
-policyset.cmcUserCertSet.list=1,2,3,4,5,6,7,8
+policyset.cmcUserCertSet.list=1,9,10,2,3,4,5,6,7,8
 policyset.cmcUserCertSet.1.constraint.class_id=cmcUserSignedSubjectNameConstraintImpl
 policyset.cmcUserCertSet.1.constraint.name=CMC User Signed Subject Name Constraint
 policyset.cmcUserCertSet.1.default.class_id=cmcUserSignedSubjectNameDefaultImpl
 policyset.cmcUserCertSet.1.default.name=User Signed Subject Name Default
 policyset.cmcUserCertSet.1.default.params.name=
+policyset.cmcUserCertSet.9.constraint.class_id=uniqueKeyConstraintImpl
+policyset.cmcUserCertSet.9.constraint.name=Unique Key Constraint
+policyset.cmcUserCertSet.9.constraint.params.allowSameKeyRenewal=true
+policyset.cmcUserCertSet.9.default.class_id=noDefaultImpl
+policyset.cmcUserCertSet.9.default.name=No Default
+policyset.cmcUserCertSet.10.constraint.class_id=renewGracePeriodConstraintImpl
+policyset.cmcUserCertSet.10.constraint.name=Renewal Grace Period Constraint
+policyset.cmcUserCertSet.10.constraint.params.renewal.graceBefore=30
+policyset.cmcUserCertSet.10.constraint.params.renewal.graceAfter=30
+policyset.cmcUserCertSet.10.default.class_id=noDefaultImpl
+policyset.cmcUserCertSet.10.default.name=No Default
 policyset.cmcUserCertSet.2.constraint.class_id=validityConstraintImpl
 policyset.cmcUserCertSet.2.constraint.name=Validity Constraint
 policyset.cmcUserCertSet.2.constraint.params.notAfterCheck=false
diff --git a/base/java-tools/src/com/netscape/cmstools/CMCRequest.java b/base/java-tools/src/com/netscape/cmstools/CMCRequest.java
index 6e27cb1..9c41403 100644
--- a/base/java-tools/src/com/netscape/cmstools/CMCRequest.java
+++ b/base/java-tools/src/com/netscape/cmstools/CMCRequest.java
@@ -2014,10 +2014,12 @@ public class CMCRequest {
                 certname.append(tokenName);
                 certname.append(":");
             }
-            certname.append(nickname);
-            signerCert = cm.findCertByNickname(certname.toString());
-            if (signerCert != null) {
-                System.out.println("got signerCert: "+ certname.toString());
+            if (!selfSign.equals("true") && nickname != null) {
+                certname.append(nickname);
+                signerCert = cm.findCertByNickname(certname.toString());
+                if (signerCert != null) {
+                    System.out.println("got signerCert: "+ certname.toString());
+                }
             }
 
             ContentInfo cmcblob = null;
@@ -2239,11 +2241,11 @@ public class CMCRequest {
             // sign the request
             SignedData signedData = null;
             if (selfSign.equalsIgnoreCase("true")) {
-                // selfSign signes with private key
+                // selfSign signs with private key
                 System.out.println("selfSign is true...");
                 signedData = signData(privk, pkidata);
             } else {
-                // none selfSign signes with  existing cert
+                // none selfSign signs with  existing cert
                 System.out.println("selfSign is false...");
                 signedData = signData(signerCert, tokenName, nickname, cm, pkidata);
             }
diff --git a/base/server/cms/src/com/netscape/cms/profile/constraint/RenewGracePeriodConstraint.java b/base/server/cms/src/com/netscape/cms/profile/constraint/RenewGracePeriodConstraint.java
index d140396..de076d5 100644
--- a/base/server/cms/src/com/netscape/cms/profile/constraint/RenewGracePeriodConstraint.java
+++ b/base/server/cms/src/com/netscape/cms/profile/constraint/RenewGracePeriodConstraint.java
@@ -87,14 +87,17 @@ public class RenewGracePeriodConstraint extends EnrollConstraint {
 
     public void validate(IRequest req, X509CertInfo info)
             throws ERejectException {
+        String method = "RenewGracePeriodConstraint: validate: ";
+        String msg = "";
+
         String origExpDate_s = req.getExtDataInString("origNotAfter");
         // probably not for renewal
         if (origExpDate_s == null) {
+            CMS.debug(method + " original cert expiration date not found...return without validation");
             return;
-        } else {
-            CMS.debug("validate RenewGracePeriod: original cert expiration date found... renewing");
+        } else { //should occur when it's not renewal
+            CMS.debug(method + " original cert expiration date found... validating");
         }
-        CMS.debug("ValidilityConstraint: validateRenewGraceperiod begins");
         BigInteger origExpDate_BI = new BigInteger(origExpDate_s);
         Date origExpDate = new Date(origExpDate_BI.longValue());
         String renew_grace_before_s = getConfig(CONFIG_RENEW_GRACE_BEFORE);
@@ -122,7 +125,7 @@ public class RenewGracePeriodConstraint extends EnrollConstraint {
 
         Date current = CMS.getCurrentDate();
         long millisDiff = origExpDate.getTime() - current.getTime();
-        CMS.debug("validateRenewGracePeriod: millisDiff="
+        CMS.debug(method + " millisDiff="
                 + millisDiff + " origExpDate=" + origExpDate.getTime() + " current=" + current.getTime());
 
         /*
@@ -134,17 +137,17 @@ public class RenewGracePeriodConstraint extends EnrollConstraint {
          */
         if (millisDiff >= 0) {
             if ((renew_grace_before > 0) && (millisDiff > renew_grace_before_BI.longValue())) {
+                msg = renew_grace_before + " days before and " +
+                        renew_grace_after + " days after original cert expiration date";
                 throw new ERejectException(CMS.getUserMessage(getLocale(req),
-                        "CMS_PROFILE_RENEW_OUTSIDE_GRACE_PERIOD",
-                        renew_grace_before + " days before and " +
-                                renew_grace_after + " days after original cert expiration date"));
+                        "CMS_PROFILE_RENEW_OUTSIDE_GRACE_PERIOD", msg));
             }
         } else {
             if ((renew_grace_after > 0) && ((0 - millisDiff) > renew_grace_after_BI.longValue())) {
+                msg = renew_grace_before + " days before and " +
+                        renew_grace_after + " days after original cert expiration date";
                 throw new ERejectException(CMS.getUserMessage(getLocale(req),
-                        "CMS_PROFILE_RENEW_OUTSIDE_GRACE_PERIOD",
-                        renew_grace_before + " days before and " +
-                                renew_grace_after + " days after original cert expiration date"));
+                        "CMS_PROFILE_RENEW_OUTSIDE_GRACE_PERIOD", msg));
             }
         }
     }
diff --git a/base/server/cms/src/com/netscape/cms/profile/constraint/UniqueKeyConstraint.java b/base/server/cms/src/com/netscape/cms/profile/constraint/UniqueKeyConstraint.java
index 869f0e2..a8fe41e 100644
--- a/base/server/cms/src/com/netscape/cms/profile/constraint/UniqueKeyConstraint.java
+++ b/base/server/cms/src/com/netscape/cms/profile/constraint/UniqueKeyConstraint.java
@@ -17,16 +17,11 @@
 // --- END COPYRIGHT BLOCK ---
 package com.netscape.cms.profile.constraint;
 
+import java.math.BigInteger;
+import java.util.Date;
 import java.util.Enumeration;
 import java.util.Locale;
 
-import netscape.security.x509.CertificateSubjectName;
-import netscape.security.x509.CertificateX509Key;
-import netscape.security.x509.X500Name;
-import netscape.security.x509.X509CertImpl;
-import netscape.security.x509.X509CertInfo;
-import netscape.security.x509.X509Key;
-
 import com.netscape.certsrv.apps.CMS;
 import com.netscape.certsrv.base.IConfigStore;
 import com.netscape.certsrv.ca.ICertificateAuthority;
@@ -41,6 +36,13 @@ import com.netscape.certsrv.property.IDescriptor;
 import com.netscape.certsrv.request.IRequest;
 import com.netscape.cms.profile.def.NoDefault;
 
+import netscape.security.x509.CertificateSubjectName;
+import netscape.security.x509.CertificateX509Key;
+import netscape.security.x509.X500Name;
+import netscape.security.x509.X509CertImpl;
+import netscape.security.x509.X509CertInfo;
+import netscape.security.x509.X509Key;
+
 /**
  * This constraint is to check for publickey uniqueness.
  * The config param "allowSameKeyRenewal" enables the
@@ -102,9 +104,29 @@ public class UniqueKeyConstraint extends EnrollConstraint {
     /**
      * Validates the request. The request is not modified
      * during the validation.
+     *
+     * It will try to capture orig cert expiration info for renewal later.
+     * Renewal can be either renewal with same key or new key.
+     *
+     * In case of renewing with same key, the old cert record
+     * can be retrieved and used to fill original info such as
+     * original expiration date for use with RenewGracePeriodConstraint.
+     *
+     * In case of renewing with new key, it would be no different from
+     * regular enrollment
+     *
+     * Search by ICertRecord.ATTR_X509CERT_PUBLIC_KEY_DATA
+     * would tell us if its reusing the same key or not.
+     * If any cert with the same key in the repository is found
+     * to be revoked, then the request is rejected
+     *
+     * This contraint has to go before the RenewGracePeriodConstraint,
+     * but after any of the SubjectName Default and Constraint
      */
     public void validate(IRequest request, X509CertInfo info)
             throws ERejectException {
+        String method = "UniqueKeyConstraint: validate: ";
+        String msg = "";
         boolean rejected = false;
         int size = 0;
         ICertRecordList list;
@@ -114,6 +136,8 @@ public class UniqueKeyConstraint extends EnrollConstraint {
         getConfigBoolean(CONFIG_REVOKE_DUPKEY_CERT);
         */
         mAllowSameKeyRenewal = getConfigBoolean(CONFIG_ALLOW_SAME_KEY_RENEWAL);
+        msg = msg + ": allowSameKeyRenewal=" + mAllowSameKeyRenewal + ";";
+        CMS.debug(method + msg);
 
         try {
             CertificateX509Key infokey = (CertificateX509Key)
@@ -131,18 +155,18 @@ public class UniqueKeyConstraint extends EnrollConstraint {
 
         } catch (Exception e) {
             throw new ERejectException(
-                        CMS.getUserMessage(
-                                getLocale(request),
-                                "CMS_PROFILE_INTERNAL_ERROR", e.toString()));
+                    CMS.getUserMessage(
+                            getLocale(request),
+                            "CMS_PROFILE_INTERNAL_ERROR", method + e.toString()));
         }
 
         /*
          * It does not matter if the corresponding cert's status
-         * is valid or not, we don't want a key that was once
-         * generated before
+         * is valid or not, if mAllowSameKeyRenewal is false,
+         * we don't want a key that was once generated before
          */
         if (size > 0) {
-            CMS.debug("UniqueKeyConstraint: found existing cert with duplicate key.");
+            CMS.debug(method + "found existing cert with same key");
 
             /*
                 The following code revokes the existing certs that have
@@ -189,45 +213,87 @@ public class UniqueKeyConstraint extends EnrollConstraint {
 
                         sjname_in_req =
                                 (X500Name) subName.get(CertificateSubjectName.DN_NAME);
-                        CMS.debug("UniqueKeyConstraint: cert request subject DN =" + sjname_in_req.toString());
+                        CMS.debug(method +" cert request subject DN =" + sjname_in_req.toString());
                         Enumeration<ICertRecord> e = list.getCertRecords(0, size - 1);
+                        Date latestOrigNotAfter = null;
+                        Date origNotAfter = null;
+                        boolean first = true;
                         while (e != null && e.hasMoreElements()) {
                             ICertRecord rec = e.nextElement();
-                            X509CertImpl cert = rec.getCertificate();
+                            BigInteger serial = rec.getSerialNumber();
+
+                            if (rec.getStatus().equals(ICertRecord.STATUS_REVOKED)
+                                    || rec.getStatus().equals(ICertRecord.STATUS_REVOKED_EXPIRED)) {
+                                msg = msg + "revoked cert cannot be renewed: serial=" + serial.toString() + ";";
+                                CMS.debug(method + msg);
+                                rejected = true;
+                                // this has to break
+                                break;
+                            }
+                            if (!rec.getStatus().equals(ICertRecord.STATUS_VALID)
+                                    && !rec.getStatus().equals(ICertRecord.STATUS_EXPIRED)) {
+                                CMS.debug(method + "invalid cert cannot be renewed; continue:" + serial.toString());
+                                // can still find another one to renew
+                                continue;
+                            }
+                            // only VALID or EXPIRED certs could have reached here
+                            X509CertImpl origCert = rec.getCertificate();
                             String certDN =
-                                    cert.getSubjectDN().toString();
-                            CMS.debug("UniqueKeyConstraint: cert retrieved from ldap has subject DN =" + certDN);
+                                    origCert.getSubjectDN().toString();
+                            CMS.debug(method + " cert retrieved from ldap has subject DN =" + certDN);
 
                             sjname_in_db = new X500Name(certDN);
 
                             if (sjname_in_db.equals(sjname_in_req) == false) {
+                                msg = msg + "subject name not match in same key renewal;";
                                 rejected = true;
                                 break;
                             } else {
-                                rejected = false;
+                                CMS.debug("subject name match in same key renewal");
+                            }
+
+                            // find the latest expiration date to keep for
+                            // Renewal Grace Period Constraint later
+                            origNotAfter = origCert.getNotAfter();
+                            CMS.debug(method + "origNotAfter =" + origNotAfter.toString());
+                            if (first) {
+                                latestOrigNotAfter = origNotAfter;
+                                first = false;
+                            } else if (latestOrigNotAfter.before(origNotAfter)) {
+                                CMS.debug(method + "newer cert found");
+                                latestOrigNotAfter = origNotAfter;
                             }
+
+                            // yes, this could be overwritten by later
+                            // found cert(s) that has violations
+                            rejected = false;
                         } // while
+
+                        if (latestOrigNotAfter != null) {
+                            // set origNotAfter for RenewGracePeriodConstraint
+                            CMS.debug(method + "setting latest original cert expiration in request");
+                            request.setExtData("origNotAfter", BigInteger.valueOf(latestOrigNotAfter.getTime()));
+                        }
                     } else { //subName is null
+                        msg =  msg +"subject name not found in cert request info;";
                         rejected = true;
                     }
                 } catch (Exception ex1) {
-                    CMS.debug("UniqueKeyConstraint: error in allowSameKeyRenewal: " + ex1.toString());
+                    CMS.debug(method +  msg + ex1.toString());
                     rejected = true;
                 } // try
 
             } else {
+                msg = msg + "found existing cert with same key;";
                 rejected = true;
             }// allowSameKeyRenewal
         } // (size > 0)
 
         if (rejected == true) {
-            CMS.debug("UniqueKeyConstraint: rejected");
-            throw new ERejectException(
-                           CMS.getUserMessage(
-                                   getLocale(request),
-                                   "CMS_PROFILE_DUPLICATE_KEY"));
+            CMS.debug(method + " rejected");
+            throw new ERejectException(msg);
         } else {
-            CMS.debug("UniqueKeyConstraint: approved");
+            CMS.debug(method + " approved");
         }
     }
 
-- 
2.7.4

