Blob Blame History Raw
From fa65ec19458bbd767f54e52f61d920b529936e19 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal@redhat.com>
Date: Wed, 1 Feb 2017 16:15:39 +1000
Subject: [PATCH 1/5] DNSName: add method to get value

To implement a profile default that copies CN to SAN dNSName, we
need to examine existing dNSName values.  To support this, add the
'getValue()' method to 'DNSName'.

Part of: https://fedorahosted.org/pki/ticket/1710

(cherry picked from commit f371114134ee3b6a83b747eecf46e001080b1e9c)
(cherry picked from commit a30f25cbb496b6e24b417a02602e0cdbe079cbd3)
---
 base/util/src/netscape/security/x509/DNSName.java | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/base/util/src/netscape/security/x509/DNSName.java b/base/util/src/netscape/security/x509/DNSName.java
index 361c235..2161adf 100644
--- a/base/util/src/netscape/security/x509/DNSName.java
+++ b/base/util/src/netscape/security/x509/DNSName.java
@@ -79,4 +79,12 @@ public class DNSName implements GeneralNameInterface {
     public String toString() {
         return ("DNSName: " + name);
     }
+
+    /**
+     * Get the raw DNSName value.
+     */
+    public String getValue() {
+        return name;
+    }
+
 }
-- 
1.8.3.1


From 6fa86d4f50b5846f5d6f8a12797f61dd5b629cca Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal@redhat.com>
Date: Wed, 1 Feb 2017 16:17:51 +1000
Subject: [PATCH 2/5] GeneralName: add method to get at inner value

The 'GeneralNameInterface' interface represents a single X.509
General Name value.  Various types are supported.  The 'GeneralName'
class (which also implements 'GeneralNameInterface') is a singleton
container for another 'GeneralNameInterface' value.

To implement a profile component that copies CN to a SAN dNSName, we
need to examine existing General Names in the SAN extension (if
present), to avoid duplicate values.  We can iterate 'GeneralNames',
but if the value is of type 'GeneralName' we need a way to "unwrap"
the value, down to the innermost value which will be of a specific
General Name type.

Add the 'unwrap' method to 'GeneralName'.

Part of: https://fedorahosted.org/pki/ticket/1710

(cherry picked from commit 225dd099efa7e2f752c3f50157aaec71a9834873)
(cherry picked from commit 52704d6564800c6872d3343c9aa5d6180637f070)
---
 base/util/src/netscape/security/x509/GeneralName.java | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/base/util/src/netscape/security/x509/GeneralName.java b/base/util/src/netscape/security/x509/GeneralName.java
index a90ac7b..55b5bfc 100644
--- a/base/util/src/netscape/security/x509/GeneralName.java
+++ b/base/util/src/netscape/security/x509/GeneralName.java
@@ -196,4 +196,19 @@ public class GeneralName implements GeneralNameInterface {
                              constructedForm, (byte) nameType), tmp);
         }
     }
+
+    /**
+     * Unwrap this GeneralName until we reach something that is not
+     * a GeneralName.
+     */
+    public GeneralNameInterface unwrap() {
+        if (this == name)
+            return null;  // can't happen, but just in case...
+
+        if (name instanceof GeneralName)
+            return ((GeneralName) name).unwrap();
+        else
+            return name;
+    }
+
 }
-- 
1.8.3.1


From 6eac5bbccb18fe913c43a0b9ec73707180870bb9 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal@redhat.com>
Date: Wed, 1 Feb 2017 16:25:11 +1000
Subject: [PATCH 3/5] SubjectAlternativeNameExtension: add GeneralNames
 getter/setter

To implement a profile default that copies CN to SAN dNSName, we
need to read and set the 'GeneralNames' of the extension.  This can
be done via the 'get' and 'set' methods but this interface is
awkward and requires the caller to deal with exceptions that aren't
fundamental to the get/set actions.

Add the 'setGeneralNames' and 'getGeneralNames' methods.

Part of: https://fedorahosted.org/pki/ticket/1710

(cherry picked from commit a67816eebbed2332327fbf391f3e23223ee7690e)
(cherry picked from commit 60f4011c3f4511ac8f86b77940d25b5869204353)
---
 .../security/x509/SubjectAlternativeNameExtension.java    | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/base/util/src/netscape/security/x509/SubjectAlternativeNameExtension.java b/base/util/src/netscape/security/x509/SubjectAlternativeNameExtension.java
index d96c821..82f87e1 100644
--- a/base/util/src/netscape/security/x509/SubjectAlternativeNameExtension.java
+++ b/base/util/src/netscape/security/x509/SubjectAlternativeNameExtension.java
@@ -199,6 +199,21 @@ public class SubjectAlternativeNameExtension extends Extension
     }
 
     /**
+     * Set the GeneralNames of this extension.
+     */
+    public void setGeneralNames(GeneralNames names) {
+        clearValue();
+        this.names = names;
+    }
+
+    /**
+     * Get the GeneralNames of this extension.
+     */
+    public GeneralNames getGeneralNames() {
+        return names;
+    }
+
+    /**
      * Get the attribute value.
      */
     public Object get(String name) throws IOException {
-- 
1.8.3.1


From da8cab2d15d5bd5e82ad8bd9a2ff0f51f7bad343 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal@redhat.com>
Date: Wed, 1 Feb 2017 16:30:50 +1000
Subject: [PATCH 4/5] X500Name: add method to get all attributes of a given
 type

To implement a profile default that copies the CN to a SAN dNSName,
we need to examine the CN values present in the Subject DN.
Specifically, we want to look at the "most specific" CN value.  The
'getCommonName' method returns the "least specific" value in the
name, thus is not suitable.

Add the 'getAttributesForOid(ObjectIdentifier)' method, which
returns an ordered list of values of the given name attribute type,
from least specific to most specific.

Part of: https://fedorahosted.org/pki/ticket/1710

(cherry picked from commit 979b6a2da433e97c1ada6434b432aa4aabc47ab5)
(cherry picked from commit 4ba23db518ab285d8a0dce8d4ee493f695867ad8)
---
 base/util/src/netscape/security/x509/X500Name.java | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/base/util/src/netscape/security/x509/X500Name.java b/base/util/src/netscape/security/x509/X500Name.java
index 0f75f48..c8627a9 100644
--- a/base/util/src/netscape/security/x509/X500Name.java
+++ b/base/util/src/netscape/security/x509/X500Name.java
@@ -19,8 +19,10 @@ package netscape.security.x509;
 
 import java.io.IOException;
 import java.security.Principal;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Vector;
 
 import netscape.security.util.DerInputStream;
@@ -451,6 +453,25 @@ public class X500Name implements Principal, GeneralNameInterface {
     }
 
     /**
+     * Return a list of attributes of the given type.
+     *
+     * The "most specific" value comes last.
+     *
+     * If there are no name attributes of the given type, an empty
+     * list is returned.
+     */
+    public List<String> getAttributesForOid(ObjectIdentifier oid)
+            throws IOException {
+        List<String> xs = new ArrayList<>();
+        for (int i = 0; i < names.length; i++) {
+            DerValue v = names[i].findAttribute(oid);
+            if (v != null)
+                xs.add(getString(v));
+        }
+        return xs;
+    }
+
+    /**
      * Returns a Ldap DN String from the X500Name
      * using the specified LdapDNStrconverter.
      * For example, RFC1779String converter can be passed to convert the
-- 
1.8.3.1


From 10799f1af01143ffb27fae06f446bb389c0787e8 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal@redhat.com>
Date: Wed, 1 Feb 2017 16:39:14 +1000
Subject: [PATCH 5/5] Add profile component that copies CN to SAN dNSName

Add the 'CommonNameToSANDefault' profile default class.  When used
on a profile, this will examine the (most-specific) Common Name in
the Subject DN, and if it looks like a DNS name, will add it to the
Subject Alternative Name extension, creating the extension if it
does not already exist.

Also add upgrade scriptlet to add the component to registry.cfg in
existing installations.

Fixes: https://fedorahosted.org/pki/ticket/1710
(cherry picked from commit 9cb00049ec731cca36de822f6c1e834f7febcb4f)
(cherry picked from commit 10d1db00225caf750ccc3c50b9d6e6b7af3655a8)
---
 base/ca/shared/conf/registry.cfg                   |   5 +-
 .../cms/profile/def/CommonNameToSANDefault.java    | 215 +++++++++++++++++++++
 2 files changed, 219 insertions(+), 1 deletion(-)
 create mode 100644 base/server/cms/src/com/netscape/cms/profile/def/CommonNameToSANDefault.java

diff --git a/base/ca/shared/conf/registry.cfg b/base/ca/shared/conf/registry.cfg
index 0bd7c05..280c713 100644
--- a/base/ca/shared/conf/registry.cfg
+++ b/base/ca/shared/conf/registry.cfg
@@ -45,7 +45,7 @@ constraintPolicy.renewGracePeriodConstraintImpl.name=Renewal Grace Period Constr
 constraintPolicy.uniqueKeyConstraintImpl.class=com.netscape.cms.profile.constraint.UniqueKeyConstraint
 constraintPolicy.uniqueKeyConstraintImpl.desc=Unique Public Key Constraint
 constraintPolicy.uniqueKeyConstraintImpl.name=Unique Public Key Constraint
-defaultPolicy.ids=noDefaultImpl,genericExtDefaultImpl,autoAssignDefaultImpl,subjectNameDefaultImpl,validityDefaultImpl,randomizedValidityDefaultImpl,caValidityDefaultImpl,subjectKeyIdentifierExtDefaultImpl,authorityKeyIdentifierExtDefaultImpl,basicConstraintsExtDefaultImpl,keyUsageExtDefaultImpl,nsCertTypeExtDefaultImpl,extendedKeyUsageExtDefaultImpl,ocspNoCheckExtDefaultImpl,issuerAltNameExtDefaultImpl,subjectAltNameExtDefaultImpl,userSubjectNameDefaultImpl,signingAlgDefaultImpl,userKeyDefaultImpl,userValidityDefaultImpl,userExtensionDefaultImpl,userSigningAlgDefaultImpl,authTokenSubjectNameDefaultImpl,subjectInfoAccessExtDefaultImpl,authInfoAccessExtDefaultImpl,nscCommentExtDefaultImpl,freshestCRLExtDefaultImpl,crlDistributionPointsExtDefaultImpl,policyConstraintsExtDefaultImpl,policyMappingsExtDefaultImpl,nameConstraintsExtDefaultImpl,certificateVersionDefaultImpl,certificatePoliciesExtDefaultImpl,subjectDirAttributesExtDefaultImpl,privateKeyPeriodExtDefaultImpl,inhibitAnyPolicyExtDefaultImpl,imageDefaultImpl,nsTokenDeviceKeySubjectNameDefaultImpl,nsTokenUserKeySubjectNameDefaultImpl,authzRealmDefaultImpl
+defaultPolicy.ids=noDefaultImpl,genericExtDefaultImpl,autoAssignDefaultImpl,subjectNameDefaultImpl,validityDefaultImpl,randomizedValidityDefaultImpl,caValidityDefaultImpl,subjectKeyIdentifierExtDefaultImpl,authorityKeyIdentifierExtDefaultImpl,basicConstraintsExtDefaultImpl,keyUsageExtDefaultImpl,nsCertTypeExtDefaultImpl,extendedKeyUsageExtDefaultImpl,ocspNoCheckExtDefaultImpl,issuerAltNameExtDefaultImpl,subjectAltNameExtDefaultImpl,userSubjectNameDefaultImpl,signingAlgDefaultImpl,userKeyDefaultImpl,userValidityDefaultImpl,userExtensionDefaultImpl,userSigningAlgDefaultImpl,authTokenSubjectNameDefaultImpl,subjectInfoAccessExtDefaultImpl,authInfoAccessExtDefaultImpl,nscCommentExtDefaultImpl,freshestCRLExtDefaultImpl,crlDistributionPointsExtDefaultImpl,policyConstraintsExtDefaultImpl,policyMappingsExtDefaultImpl,nameConstraintsExtDefaultImpl,certificateVersionDefaultImpl,certificatePoliciesExtDefaultImpl,subjectDirAttributesExtDefaultImpl,privateKeyPeriodExtDefaultImpl,inhibitAnyPolicyExtDefaultImpl,imageDefaultImpl,nsTokenDeviceKeySubjectNameDefaultImpl,nsTokenUserKeySubjectNameDefaultImpl,authzRealmDefaultImpl,commonNameToSANDefaultImpl
 defaultPolicy.autoAssignDefaultImpl.class=com.netscape.cms.profile.def.AutoAssignDefault
 defaultPolicy.autoAssignDefaultImpl.desc=Auto Request Assignment Default
 defaultPolicy.autoAssignDefaultImpl.name=Auto Request Assignment Default
@@ -166,6 +166,9 @@ defaultPolicy.subjectDirAttributesExtDefaultImpl.name=Subject Directory Attribut
 defaultPolicy.inhibitAnyPolicyExtDefaultImpl.class=com.netscape.cms.profile.def.InhibitAnyPolicyExtDefault
 defaultPolicy.inhibitAnyPolicyExtDefaultImpl.desc=Inhibit Any-Policy Extension Default
 defaultPolicy.inhibitAnyPolicyExtDefaultImpl.name=Inhibit Any-Policy Extension Default
+defaultPolicy.commonNameToSANDefaultImpl.class=com.netscape.cms.profile.def.CommonNameToSANDefault
+defaultPolicy.commonNameToSANDefaultImpl.desc=Copy Common Name to Subject Alternative Name
+defaultPolicy.commonNameToSANDefaultImpl.name=Copy Common Name to Subject Alternative Name
 profile.ids=caEnrollImpl,caCACertEnrollImpl,caServerCertEnrollImpl,caUserCertEnrollImpl
 profile.caEnrollImpl.class=com.netscape.cms.profile.common.CAEnrollProfile
 profile.caEnrollImpl.desc=Certificate Authority Generic Certificate Enrollment Profile
diff --git a/base/server/cms/src/com/netscape/cms/profile/def/CommonNameToSANDefault.java b/base/server/cms/src/com/netscape/cms/profile/def/CommonNameToSANDefault.java
new file mode 100644
index 0000000..33828d1
--- /dev/null
+++ b/base/server/cms/src/com/netscape/cms/profile/def/CommonNameToSANDefault.java
@@ -0,0 +1,215 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2017 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.cms.profile.def;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Locale;
+
+import netscape.security.x509.CertificateSubjectName;
+import netscape.security.x509.DNSName;
+import netscape.security.x509.GeneralName;
+import netscape.security.x509.GeneralNameInterface;
+import netscape.security.x509.GeneralNames;
+import netscape.security.x509.PKIXExtensions;
+import netscape.security.x509.SubjectAlternativeNameExtension;
+import netscape.security.x509.X500Name;
+import netscape.security.x509.X509CertInfo;
+
+import com.netscape.certsrv.apps.CMS;
+import com.netscape.certsrv.profile.EProfileException;
+import com.netscape.certsrv.property.IDescriptor;
+import com.netscape.certsrv.request.IRequest;
+
+/**
+ * This plugin will examine the most specific CN in the Subject DN,
+ * and if it looks like a DNS name, will add it to the SAN extension.
+ *
+ * It will create the SAN extension if necessary.
+ *
+ * If there is already a SAN dnsName value that matches
+ * (case-insensitively) the CN, it will not add the name.
+ *
+ * If there is no CN in the subject DN, does nothing.
+ *
+ * If the most specific CN does not look like a DNS name, does
+ * nothing.
+ *
+ * This profile component should be configured to execute after
+ * other profile components that set or modify the Subject DN or the
+ * SAN extension.
+ */
+public class CommonNameToSANDefault extends EnrollExtDefault {
+
+    private static final String LOG_PREFIX = "CommonNameToSANDefault: ";
+
+    public void populate(IRequest _req, X509CertInfo info)
+            throws EProfileException {
+        // examine the Subject DN
+        CertificateSubjectName subjectName;
+        try {
+            subjectName = (CertificateSubjectName) info.get(X509CertInfo.SUBJECT);
+        } catch (CertificateException | IOException e) {
+            CMS.debug(LOG_PREFIX + "failed to read Subject DN: " + e);
+            return;
+        }
+        X500Name sdn;
+        try {
+            sdn = (X500Name) subjectName.get(CertificateSubjectName.DN_NAME);
+        } catch (IOException e) {
+            CMS.debug(LOG_PREFIX + "failed to retrieve SDN X500Name: " + e);
+            return;
+        }
+        List<String> cns;
+        try {
+            cns = sdn.getAttributesForOid(X500Name.commonName_oid);
+        } catch (IOException e) {
+            // Couldn't read the CN for some reason.
+            // Not a likely scenario so just log and return.
+            CMS.debug(LOG_PREFIX + "failed to decode CN: " + e);
+            return;
+        }
+        if (cns.size() < 1) {
+            CMS.debug(LOG_PREFIX + "No CN in Subject DN; done");
+            return;  // no Common Name; can't do anything
+        }
+
+        String cn = cns.get(cns.size() - 1); // "most specific" CN is at end
+
+        CMS.debug(LOG_PREFIX + "Examining CN: " + cn);
+
+        if (!isValidDNSName(cn)) {
+            CMS.debug(LOG_PREFIX + "CN is not a DNS name; done");
+            return;  // CN does not look like a DNS name
+        }
+
+        SubjectAlternativeNameExtension san = (SubjectAlternativeNameExtension)
+            getExtension(PKIXExtensions.SubjectAlternativeName_Id.toString(), info);
+
+        if (san != null) {
+            // check for existing name matching CN
+            GeneralNames gns = san.getGeneralNames();
+            for (GeneralNameInterface gn : gns) {
+                if (gn instanceof GeneralName)
+                    gn = ((GeneralName) gn).unwrap();
+                if (gn instanceof DNSName) {
+                    String dnsName = ((DNSName) gn).getValue();
+                    if (cn.equalsIgnoreCase(dnsName)) {
+                        CMS.debug(LOG_PREFIX
+                            + "CN already has corresponding SAN dNSName; done");
+                        return;  // CN is already in SAN
+                    }
+                }
+            }
+            gns.add(new DNSName(cn));  // add CN to SAN
+
+            // reset extension value (encoded value may have been cached)
+            san.setGeneralNames(gns);
+            CMS.debug(LOG_PREFIX + "added CN to SAN; done");
+        } else {
+            GeneralNames gns = new GeneralNames();
+            gns.add(new DNSName(cn));
+            try {
+                san = new SubjectAlternativeNameExtension(gns);
+                addExtension(
+                    PKIXExtensions.SubjectAlternativeName_Id.toString(), san, info);
+            } catch (IOException e) {
+                CMS.debug(LOG_PREFIX + "failed to construct SAN ext: " + e);
+                return;
+            }
+            CMS.debug(LOG_PREFIX + "added SAN extension containing CN; done");
+        }
+    }
+
+    public String getText(Locale locale) {
+        return "This default add the Subject DN Common Name to the Subject "
+            + "Alternative Name extension, if it looks like a DNS name.";
+    }
+
+    public IDescriptor getValueDescriptor(Locale locale, String name) {
+        return null;
+    }
+
+    public String getValue(String name, Locale locale, X509CertInfo info) {
+        return null;
+    }
+
+    public void setValue(
+            String name, Locale locale, X509CertInfo info, String value) {
+    }
+
+    /** Validate DNS name syntax per Section 3.5 of RFC 1034
+     * and Section 2.1 of RFC 1123, and the additional rules
+     * of RFC 5280 Section 4.2.1.6.
+     *
+     * Further to those rules, we also ignore CNs that are valid
+     * DNS names but which only have a single part (e.g. TLDs or
+     * host short names).
+     */
+    public static boolean isValidDNSName(String s) {
+        if (s == null)
+            return false;
+
+        if (s.length() < 1 || s.length() > 255)
+            return false;
+
+        String[] parts = s.split("\\.");
+
+        if (parts.length < 2)
+            return false;
+
+        for (int i = 0; i < parts.length; i++) {
+            char[] cs = parts[i].toCharArray();
+
+            if (cs.length < 1 || cs.length > 63)
+                return false;
+
+            if (!isLetter(cs[0]))
+                return false;
+
+            if (!isLetDig(cs[cs.length - 1]))
+                return false;
+
+            for (int j = 0; j < cs.length; j++) {
+                if (!isLetDigHyp(cs[j]))
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
+    public static boolean isLetter(char c) {
+        return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
+    }
+
+    public static boolean isDigit(char c) {
+        return c >= '0' && c <= '9';
+    }
+
+    public static boolean isLetDig(char c) {
+        return isLetter(c) || isDigit(c);
+    }
+
+    public static boolean isLetDigHyp(char c) {
+        return isLetDig(c) || c == '-';
+    }
+
+}
-- 
1.8.3.1