[Pki-devel] [PATCH] Lightweight CAs

Fraser Tweedale ftweedal at redhat.com
Thu Sep 24 14:20:02 UTC 2015


Latest patches attached.  Relative to previous patchset this one:

- fixes a compile error in CATest.java
- fixes a ton of warnings and some poorly ordered imports
- adds ACLs and ACL enforcement for privileged operations
  on AuthorityResource

Here's an ldif snippet for adding the ACLs to an existing database

dn: cn=aclResources,o=ipaca
changetype: modify
add: resourceACLS
resourceACLS: certServer.ca.authorities:list,read:allow (list,read) user="anybody":Anybody may list and read lightweight authorities
resourceACLS: certServer.ca.authorities:create,modify:allow (create,modify) group="Administrators":Administrators may create and modify lightweight authorities

Cheers,
Fraser

On Fri, Sep 18, 2015 at 02:11:27PM -0500, Endi Sukma Dewata wrote:
> On 9/18/2015 1:46 PM, Ade Lee wrote:
> >>6. Assuming authority DN is unique, we can add --issuer <DN> option
> >>tothese commands:
> >>* pki ca-cert-find --issuer <dn>
> >>* pki ca-cert-request-submit --issuer <dn>
> >>* pki client-cert-find --issuer <dn>
> >>* pki client-cert-request --issuer <dn>
> >>
> >
> >If we do this, then we need to be sure that the DN is normalized - both
> >on input -- ie. when the subca is created (we need to do this in any
> >case) and also on processing in the CLI.
> >
> >I'm ok with offering this as an option (maybe --issuer_dn), but the
> >primary (and initially required option) will be using UUID.  We can
> >defer this mechanism to another ticket/patch.  Please open one.
> 
> Per IRC discussion we agreed with these options:
> * --issuer-id <ID>
> * --issuer-dn <DN>
> to be added to the ca-cert-* and client-cert-request commands.
> 
> For the client-cert-find command we can only provide this option:
> * --issuer-dn <DN>
> since issuer ID is irrelevant on the client.
> 
> Personally I think the issuer DN would be more useful since that's the value
> that you see in certificates, so it's more consistent everywhere, and no
> need to do a lookup to find the issuer ID. Also, although most likely we
> will copy & paste the ID or DN anyway, the DN is easier to read and confirm
> that you're submitting the request to the right authority.
> 
> -- 
> Endi S. Dewata
-------------- next part --------------
From f5bf94e2cac23970e5a8b673a0dfbe9100610102 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Wed, 28 Jan 2015 02:41:10 -0500
Subject: [PATCH] Lightweight CAs: initial support

This commit adds initial support for "lightweight CAs" - CAs that
inhabit an existing CA instance and share the request queue and
certificate database of the "top-level CA".

We initially support only sub-CAs under the top-level CA - either
direct sub-CAs or nested.  The general design will support hosting
unrelated CAs but creation or import of unrelated CAs is not yet
implemented.

Part of: https://fedorahosted.org/pki/ticket/1213
---
 base/ca/shared/conf/acl.ldif                       |   2 +
 base/ca/shared/conf/acl.properties                 |   4 +
 base/ca/shared/conf/db.ldif                        |   5 +
 base/ca/src/com/netscape/ca/CAService.java         |  53 +-
 .../src/com/netscape/ca/CertificateAuthority.java  | 664 ++++++++++++++++++---
 base/ca/src/com/netscape/ca/SigningUnit.java       |  22 +-
 .../dogtagpki/server/ca/rest/AuthorityService.java | 275 +++++++++
 .../dogtagpki/server/ca/rest/CAApplication.java    |   3 +
 .../server/ca/rest/CertRequestService.java         |   5 +
 .../netscape/certsrv/authority/AuthorityData.java  | 114 ++++
 .../certsrv/authority/AuthorityResource.java       |  91 +++
 .../src/com/netscape/certsrv/ca/AuthorityID.java   |  36 ++
 .../netscape/certsrv/ca/CADisabledException.java   |  15 +
 .../netscape/certsrv/ca/CANotFoundException.java   |  14 +
 .../com/netscape/certsrv/ca/CATypeException.java   |  16 +
 .../src/com/netscape/certsrv/ca/ICAService.java    |  11 +-
 .../netscape/certsrv/ca/ICertificateAuthority.java |  63 ++
 .../certsrv/ca/IssuerUnavailableException.java     |  15 +
 .../netscape/certsrv/profile/IEnrollProfile.java   |   5 +
 .../netscape/certsrv/security/ISigningUnit.java    |   8 +
 .../cms/profile/common/CAEnrollProfile.java        |  11 +-
 .../netscape/cms/profile/common/EnrollProfile.java |   3 +
 .../def/AuthorityKeyIdentifierExtDefault.java      |  28 +-
 .../netscape/cms/profile/def/CAEnrollDefault.java  |   4 +-
 .../com/netscape/cms/servlet/base/PKIService.java  |   5 +-
 .../netscape/cms/servlet/cert/CertRequestDAO.java  |   2 +-
 .../cms/servlet/cert/EnrollmentProcessor.java      |  15 +-
 .../cms/servlet/cert/RequestProcessor.java         |  36 +-
 .../com/netscape/cms/servlet/csadmin/CertUtil.java |  38 +-
 base/server/share/conf/schema-authority.ldif       |   8 +
 base/server/share/conf/schema.ldif                 |  13 +
 31 files changed, 1452 insertions(+), 132 deletions(-)
 create mode 100644 base/ca/src/org/dogtagpki/server/ca/rest/AuthorityService.java
 create mode 100644 base/common/src/com/netscape/certsrv/authority/AuthorityData.java
 create mode 100644 base/common/src/com/netscape/certsrv/authority/AuthorityResource.java
 create mode 100644 base/common/src/com/netscape/certsrv/ca/AuthorityID.java
 create mode 100644 base/common/src/com/netscape/certsrv/ca/CADisabledException.java
 create mode 100644 base/common/src/com/netscape/certsrv/ca/CANotFoundException.java
 create mode 100644 base/common/src/com/netscape/certsrv/ca/CATypeException.java
 create mode 100644 base/common/src/com/netscape/certsrv/ca/IssuerUnavailableException.java
 create mode 100644 base/server/share/conf/schema-authority.ldif

diff --git a/base/ca/shared/conf/acl.ldif b/base/ca/shared/conf/acl.ldif
index 0da10939fc64dc88b32016c308fc13a1bab2d14f..54c9f1d5c64b6578de83f1b7ffdff922a69975f4 100644
--- a/base/ca/shared/conf/acl.ldif
+++ b/base/ca/shared/conf/acl.ldif
@@ -57,3 +57,5 @@ resourceACLS: certServer.ca.certs:execute:allow (execute) group="Certificate Man
 resourceACLS: certServer.ca.groups:execute:allow (execute) group="Administrators":Admins may execute group operations
 resourceACLS: certServer.ca.selftests:read,execute:allow (read,execute) group="Administrators":Only admins can access selftests.
 resourceACLS: certServer.ca.users:execute:allow (execute) group="Administrators":Admins may execute user operations
+resourceACLS: certServer.ca.authorities:list,read:allow (list,read) user="anybody":Anybody may list and read lightweight authorities
+resourceACLS: certServer.ca.authorities:create,modify:allow (create,modify) group="Administrators":Administrators may create and modify lightweight authorities
diff --git a/base/ca/shared/conf/acl.properties b/base/ca/shared/conf/acl.properties
index d14d1832c7cff7db055f2274f1ed30223d16cad8..f0b5b9f650ad2fc4bde531ade94347a7280d3089 100644
--- a/base/ca/shared/conf/acl.properties
+++ b/base/ca/shared/conf/acl.properties
@@ -21,3 +21,7 @@ securityDomain.installToken = certServer.securitydomain.domainxml,read
 selftests.read = certServer.ca.selftests,read
 selftests.execute = certServer.ca.selftests,execute
 users = certServer.ca.users,execute
+authorities.create = certServer.ca.authorities,create
+authorities.list = certServer.ca.authorities,list
+authorities.modify = certServer.ca.authorities,modify
+authorities.read = certServer.ca.authorities,read
diff --git a/base/ca/shared/conf/db.ldif b/base/ca/shared/conf/db.ldif
index 8a2e0b07274a83b317fb1ba56e8ef32b96857118..704b8d11be7dcffd7d57fb3ec90c11f3c0ef9cbc 100644
--- a/base/ca/shared/conf/db.ldif
+++ b/base/ca/shared/conf/db.ldif
@@ -164,3 +164,8 @@ dn: ou=certificateProfiles,ou=ca,{rootSuffix}
 objectClass: top
 objectClass: organizationalUnit
 ou: certificateProfiles
+
+dn: ou=authorities,ou=ca,{rootSuffix}
+objectClass: top
+objectClass: organizationalUnit
+ou: authorities
diff --git a/base/ca/src/com/netscape/ca/CAService.java b/base/ca/src/com/netscape/ca/CAService.java
index 36f0bd592e333a276da84662c1e64a2921c5e7d2..a49d641cec839b4dac33fe7a6be49bf86c3560a8 100644
--- a/base/ca/src/com/netscape/ca/CAService.java
+++ b/base/ca/src/com/netscape/ca/CAService.java
@@ -65,7 +65,9 @@ import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.IConfigStore;
 import com.netscape.certsrv.base.MetaInfo;
 import com.netscape.certsrv.base.SessionContext;
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.ca.ECAException;
+import com.netscape.certsrv.ca.CANotFoundException;
 import com.netscape.certsrv.ca.ICAService;
 import com.netscape.certsrv.ca.ICRLIssuingPoint;
 import com.netscape.certsrv.ca.ICertificateAuthority;
@@ -565,18 +567,15 @@ public class CAService implements ICAService, IService {
     /// CA related routines.
     ///
 
-    public X509CertImpl issueX509Cert(X509CertInfo certi)
-            throws EBaseException {
-        return issueX509Cert(certi, null, null);
-    }
-
     /**
      * issue cert for enrollment.
      */
-    public X509CertImpl issueX509Cert(X509CertInfo certi, String profileId, String rid)
+    public X509CertImpl issueX509Cert(
+                AuthorityID aid, X509CertInfo certi,
+                String profileId, String rid)
             throws EBaseException {
         CMS.debug("issueX509Cert");
-        X509CertImpl certImpl = issueX509Cert("", certi, false, null);
+        X509CertImpl certImpl = issueX509Cert(aid, "", certi, false, null);
 
         CMS.debug("storeX509Cert " + certImpl.getSerialNumber());
         storeX509Cert(profileId, rid, certImpl);
@@ -615,9 +614,21 @@ public class CAService implements ICAService, IService {
      * renewal is expected to have original cert serial no. in cert info
      * field.
      */
-    X509CertImpl issueX509Cert(String rid, X509CertInfo certi,
-            boolean renewal, BigInteger oldSerialNo)
-            throws EBaseException {
+    X509CertImpl issueX509Cert(
+                String rid, X509CertInfo certi,
+                boolean renewal, BigInteger oldSerialNo
+            ) throws EBaseException {
+        return issueX509Cert(null, rid, certi, renewal, oldSerialNo);
+    }
+
+    private X509CertImpl issueX509Cert(
+                AuthorityID aid, String rid, X509CertInfo certi,
+                boolean renewal, BigInteger oldSerialNo
+            ) throws EBaseException {
+        ICertificateAuthority ca = mCA.getCA(aid);
+        if (ca == null)
+            throw new CANotFoundException("No such CA: " + aid);
+
         String algname = null;
         X509CertImpl cert = null;
 
@@ -642,7 +653,7 @@ public class CAService implements ICAService, IService {
             // set default cert version. If policies added a extensions
             // the version would already be set to version 3.
             if (certi.get(X509CertInfo.VERSION) == null) {
-                certi.set(X509CertInfo.VERSION, mCA.getDefaultCertVersion());
+                certi.set(X509CertInfo.VERSION, ca.getDefaultCertVersion());
             }
 
             // set default validity if not set.
@@ -665,7 +676,7 @@ public class CAService implements ICAService, IService {
                 }
 
                 begin = CMS.getCurrentDate();
-                end = new Date(begin.getTime() + mCA.getDefaultValidity());
+                end = new Date(begin.getTime() + ca.getDefaultValidity());
                 certi.set(CertificateValidity.NAME,
                         new CertificateValidity(begin, end));
             }
@@ -705,7 +716,7 @@ public class CAService implements ICAService, IService {
             }
 
             Date caNotAfter =
-                    mCA.getSigningUnit().getCertImpl().getNotAfter();
+                    ca.getSigningUnit().getCertImpl().getNotAfter();
 
             if (begin.after(caNotAfter)) {
                 mCA.log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_PAST_VALIDITY"));
@@ -714,7 +725,7 @@ public class CAService implements ICAService, IService {
 
             if (end.after(caNotAfter)) {
                 if (!is_ca) {
-                    if (!mCA.isEnablePastCATime()) {
+                    if (!ca.isEnablePastCATime()) {
                         end = caNotAfter;
                         certi.set(CertificateValidity.NAME,
                                 new CertificateValidity(begin, caNotAfter));
@@ -734,7 +745,7 @@ public class CAService implements ICAService, IService {
                     certi.get(X509CertInfo.ALGORITHM_ID);
 
             if (algor == null || algor.toString().equals(CertInfo.SERIALIZE_ALGOR.toString())) {
-                algname = mCA.getSigningUnit().getDefaultAlgorithm();
+                algname = ca.getSigningUnit().getDefaultAlgorithm();
                 algid = AlgorithmId.get(algname);
                 certi.set(X509CertInfo.ALGORITHM_ID,
                         new CertificateAlgorithmId(algid));
@@ -820,16 +831,16 @@ public class CAService implements ICAService, IService {
         }
 
         try {
-            if (mCA.getIssuerObj() != null) {
+            if (ca.getIssuerObj() != null) {
                 // this ensures the isserDN has the same encoding as the
                 // subjectDN of the CA signing cert
                 CMS.debug("CAService: issueX509Cert: setting issuerDN using exact CA signing cert subjectDN encoding");
                 certi.set(X509CertInfo.ISSUER,
-                        mCA.getIssuerObj());
+                        ca.getIssuerObj());
             } else {
-                CMS.debug("CAService: issueX509Cert: mCA.getIssuerObj() is null, creating new CertificateIssuerName");
+                CMS.debug("CAService: issueX509Cert: ca.getIssuerObj() is null, creating new CertificateIssuerName");
                 certi.set(X509CertInfo.ISSUER,
-                        new CertificateIssuerName(mCA.getX500Name()));
+                        new CertificateIssuerName(ca.getX500Name()));
             }
         } catch (CertificateException e) {
             mCA.log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_SET_ISSUER", e.toString()));
@@ -861,8 +872,8 @@ public class CAService implements ICAService, IService {
             }
         }
 
-        CMS.debug("About to mCA.sign cert.");
-        cert = mCA.sign(certi, algname);
+        CMS.debug("About to ca.sign cert.");
+        cert = ca.sign(certi, algname);
         return cert;
     }
 
diff --git a/base/ca/src/com/netscape/ca/CertificateAuthority.java b/base/ca/src/com/netscape/ca/CertificateAuthority.java
index acf07b9bde2a05f7c62740293a0c66cf92f50771..a58788bc5b85ba5c393c4c5a4798e21c24608e21 100644
--- a/base/ca/src/com/netscape/ca/CertificateAuthority.java
+++ b/base/ca/src/com/netscape/ca/CertificateAuthority.java
@@ -23,36 +23,26 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.math.BigInteger;
+import java.security.KeyPair;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.security.cert.CRLException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateParsingException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.Vector;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
-import netscape.security.util.DerOutputStream;
-import netscape.security.util.DerValue;
-import netscape.security.x509.AlgorithmId;
-import netscape.security.x509.CertificateChain;
-import netscape.security.x509.CertificateIssuerName;
-import netscape.security.x509.CertificateSubjectName;
-import netscape.security.x509.CertificateVersion;
-import netscape.security.x509.X500Name;
-import netscape.security.x509.X509CRLImpl;
-import netscape.security.x509.X509CertImpl;
-import netscape.security.x509.X509CertInfo;
-import netscape.security.x509.X509ExtensionException;
-import netscape.security.x509.X509Key;
-
 import org.mozilla.jss.CryptoManager;
 import org.mozilla.jss.asn1.ASN1Util;
 import org.mozilla.jss.asn1.GeneralizedTime;
@@ -60,6 +50,9 @@ import org.mozilla.jss.asn1.INTEGER;
 import org.mozilla.jss.asn1.InvalidBERException;
 import org.mozilla.jss.asn1.OBJECT_IDENTIFIER;
 import org.mozilla.jss.asn1.OCTET_STRING;
+import org.mozilla.jss.crypto.CryptoToken;
+import org.mozilla.jss.crypto.KeyPairAlgorithm;
+import org.mozilla.jss.crypto.KeyPairGenerator;
 import org.mozilla.jss.crypto.SignatureAlgorithm;
 import org.mozilla.jss.crypto.TokenException;
 import org.mozilla.jss.pkix.cert.Extension;
@@ -73,15 +66,21 @@ import com.netscape.certsrv.base.IConfigStore;
 import com.netscape.certsrv.base.ISubsystem;
 import com.netscape.certsrv.base.Nonces;
 import com.netscape.certsrv.base.PKIException;
+import com.netscape.certsrv.ca.AuthorityID;
+import com.netscape.certsrv.ca.CADisabledException;
+import com.netscape.certsrv.ca.CANotFoundException;
+import com.netscape.certsrv.ca.CATypeException;
 import com.netscape.certsrv.ca.ECAException;
 import com.netscape.certsrv.ca.ICRLIssuingPoint;
 import com.netscape.certsrv.ca.ICertificateAuthority;
+import com.netscape.certsrv.ca.IssuerUnavailableException;
 import com.netscape.certsrv.dbs.IDBSubsystem;
 import com.netscape.certsrv.dbs.certdb.ICertRecord;
 import com.netscape.certsrv.dbs.certdb.ICertificateRepository;
 import com.netscape.certsrv.dbs.crldb.ICRLRepository;
 import com.netscape.certsrv.dbs.replicadb.IReplicaIDRepository;
 import com.netscape.certsrv.ldap.ELdapException;
+import com.netscape.certsrv.ldap.ILdapConnFactory;
 import com.netscape.certsrv.logging.ILogger;
 import com.netscape.certsrv.ocsp.IOCSPService;
 import com.netscape.certsrv.policy.IPolicyProcessor;
@@ -96,6 +95,8 @@ import com.netscape.certsrv.request.IRequestScheduler;
 import com.netscape.certsrv.request.IService;
 import com.netscape.certsrv.security.ISigningUnit;
 import com.netscape.certsrv.util.IStatsSubsystem;
+import com.netscape.cms.servlet.csadmin.CertUtil;
+import com.netscape.cmscore.base.PropConfigStore;
 import com.netscape.cmscore.dbs.CRLRepository;
 import com.netscape.cmscore.dbs.CertRecord;
 import com.netscape.cmscore.dbs.CertificateRepository;
@@ -106,6 +107,7 @@ import com.netscape.cmscore.listeners.ListenerPlugin;
 import com.netscape.cmscore.request.RequestSubsystem;
 import com.netscape.cmscore.security.KeyCertUtil;
 import com.netscape.cmscore.util.Debug;
+import com.netscape.cmsutil.crypto.CryptoUtil;
 import com.netscape.cmsutil.ocsp.BasicOCSPResponse;
 import com.netscape.cmsutil.ocsp.CertID;
 import com.netscape.cmsutil.ocsp.CertStatus;
@@ -123,6 +125,29 @@ import com.netscape.cmsutil.ocsp.SingleResponse;
 import com.netscape.cmsutil.ocsp.TBSRequest;
 import com.netscape.cmsutil.ocsp.UnknownInfo;
 
+import netscape.ldap.LDAPAttribute;
+import netscape.ldap.LDAPAttributeSet;
+import netscape.ldap.LDAPConnection;
+import netscape.ldap.LDAPEntry;
+import netscape.ldap.LDAPException;
+import netscape.ldap.LDAPModification;
+import netscape.ldap.LDAPModificationSet;
+import netscape.ldap.LDAPSearchResults;
+import netscape.security.util.DerOutputStream;
+import netscape.security.util.DerValue;
+import netscape.security.x509.AlgorithmId;
+import netscape.security.x509.CertificateChain;
+import netscape.security.x509.CertificateIssuerName;
+import netscape.security.x509.CertificateSubjectName;
+import netscape.security.x509.CertificateVersion;
+import netscape.security.x509.X500Name;
+import netscape.security.x509.X509CRLImpl;
+import netscape.security.x509.X509CertImpl;
+import netscape.security.x509.X509CertInfo;
+import netscape.security.x509.X509ExtensionException;
+import netscape.security.x509.X509Key;
+
+
 /**
  * A class represents a Certificate Authority that is
  * responsible for certificate specific operations.
@@ -136,6 +161,13 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
 
     public final static OBJECT_IDENTIFIER OCSP_NONCE = new OBJECT_IDENTIFIER("1.3.6.1.5.5.7.48.1.2");
 
+    private static final Map<AuthorityID, ICertificateAuthority> caMap = new TreeMap<>();
+    protected CertificateAuthority topCA = null;
+    protected AuthorityID authorityID = null;
+    protected AuthorityID authorityParentID = null;
+    protected String authorityDescription = null;
+    protected boolean authorityEnabled = true;
+
     protected ISubsystem mOwner = null;
     protected IConfigStore mConfig = null;
     protected ILogger mLogger = CMS.getLogger();
@@ -234,6 +266,41 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      * Constructs a CA subsystem.
      */
     public CertificateAuthority() {
+        topCA = this;
+    }
+
+    /**
+     * Construct and initialise a sub-CA
+     */
+    private CertificateAuthority(
+            CertificateAuthority topCA,
+            AuthorityID aid,
+            AuthorityID parentAID,
+            String signingKeyNickname,
+            String authorityDescription,
+            boolean authorityEnabled
+            ) throws EBaseException {
+        setId(topCA.getId());
+        this.topCA = topCA;
+        this.authorityID = aid;
+        this.authorityParentID = parentAID;
+        this.authorityDescription = authorityDescription;
+        this.authorityEnabled = authorityEnabled;
+        mNickname = signingKeyNickname;
+        init(topCA.mOwner, topCA.mConfig);
+    }
+
+    private boolean isTopCA() {
+        return topCA == this;
+    }
+
+    private void ensureEnabled() throws CADisabledException {
+        if (!authorityEnabled)
+            throw new CADisabledException("Authority is disabled");
+    }
+
+    public boolean getAuthorityEnabled() {
+        return authorityEnabled;
     }
 
     /**
@@ -334,8 +401,22 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
             mOwner = owner;
             mConfig = config;
 
-            // init cert & crl database.
-            initCaDatabases();
+            // init cert & crl database
+            initCertDatabase();
+            initCrlDatabase();
+
+            // init replica id repository
+            if (isTopCA()) {
+                String replicaReposDN = mConfig.getString(PROP_REPLICAID_DN, null);
+                if (replicaReposDN == null) {
+                    replicaReposDN = "ou=Replica," + getDBSubsystem().getBaseDN();
+                }
+                mReplicaRepot = new ReplicaIDRepository(
+                        DBSubsystem.getInstance(), 1, replicaReposDN);
+                CMS.debug("Replica Repot inited");
+            } else {
+                mReplicaRepot = topCA.mReplicaRepot;
+            }
 
             // init signing unit & CA cert.
             try {
@@ -358,51 +439,37 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
             if (CMS.isPreOpMode())
                 return;
 
-            // set certificate status to 10 minutes
-            mCertRepot.setCertStatusUpdateInterval(
+            /* The top-level CA owns these resources so skip these
+             * steps for sub-CAs.
+             */
+            if (isTopCA()) {
+                /* These methods configure and start threads related to
+                 * CertificateRepository.  Ideally all of the config would
+                 * be pushed into CertificateRepository constructor and a
+                 * single 'start' method would start the threads.
+                 */
+                // set certificate status to 10 minutes
+                mCertRepot.setCertStatusUpdateInterval(
                     mRequestQueue.getRequestRepository(),
                     mConfig.getInteger("certStatusUpdateInterval", 10 * 60),
                     mConfig.getBoolean("listenToCloneModifications", false));
-            mCertRepot.setConsistencyCheck(
+                mCertRepot.setConsistencyCheck(
                     mConfig.getBoolean("ConsistencyCheck", false));
-            mCertRepot.setSkipIfInConsistent(
+                mCertRepot.setSkipIfInConsistent(
                     mConfig.getBoolean("SkipIfInConsistent", false));
 
-            // set serial number update task to run every 10 minutes
-            mCertRepot.setSerialNumberUpdateInterval(
+                // set serial number update task to run every 10 minutes
+                mCertRepot.setSerialNumberUpdateInterval(
                     mRequestQueue.getRequestRepository(),
                     mConfig.getInteger("serialNumberUpdateInterval", 10 * 60));
 
-            mService.init(config.getSubStore("connector"));
+                mService.init(config.getSubStore("connector"));
 
-            initMiscellaneousListeners();
-
-            // instantiate CRL publisher
-            IConfigStore cpStore = null;
-
-            mByName = config.getBoolean("byName", true);
-
-            cpStore = config.getSubStore("crlPublisher");
-            if (cpStore != null && cpStore.size() > 0) {
-                String publisherClass = cpStore.getString("class");
-
-                if (publisherClass != null) {
-                    try {
-                        @SuppressWarnings("unchecked")
-                        Class<ICRLPublisher> pc = (Class<ICRLPublisher>) Class.forName(publisherClass);
-
-                        mCRLPublisher = pc.newInstance();
-                        mCRLPublisher.init(this, cpStore);
-                    } catch (ClassNotFoundException ee) {
-                        log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_NO_PUBLISHER", ee.toString()));
-                    } catch (IllegalAccessException ee) {
-                        log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_NO_PUBLISHER", ee.toString()));
-                    } catch (InstantiationException ee) {
-                        log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_NO_PUBLISHER", ee.toString()));
-                    }
-                }
+                initMiscellaneousListeners();
             }
 
+            initCRLPublisher();
+
             // initialize publisher processor (publish remote admin
             // rely on this subsystem, so it has to be initialized)
             initPublish();
@@ -412,6 +479,9 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
             // being functional.
             initCRL();
 
+            if (isTopCA())
+                loadLightweightCAs();
+
         } catch (EBaseException e) {
             if (CMS.isPreOpMode())
                 return;
@@ -420,6 +490,37 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
         }
     }
 
+    private void initCRLPublisher() throws EBaseException {
+        // instantiate CRL publisher
+        if (!isTopCA()) {
+            mByName = topCA.mByName;
+            mCRLPublisher = topCA.mCRLPublisher;
+            return;
+        }
+
+        mByName = mConfig.getBoolean("byName", true);
+        IConfigStore cpStore = mConfig.getSubStore("crlPublisher");
+        if (cpStore != null && cpStore.size() > 0) {
+            String publisherClass = cpStore.getString("class");
+
+            if (publisherClass != null) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    Class<ICRLPublisher> pc = (Class<ICRLPublisher>) Class.forName(publisherClass);
+
+                    mCRLPublisher = pc.newInstance();
+                    mCRLPublisher.init(this, cpStore);
+                } catch (ClassNotFoundException ee) {
+                    log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_NO_PUBLISHER", ee.toString()));
+                } catch (IllegalAccessException ee) {
+                    log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_NO_PUBLISHER", ee.toString()));
+                } catch (InstantiationException ee) {
+                    log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_CA_CA_NO_PUBLISHER", ee.toString()));
+                }
+            }
+        }
+    }
+
     /**
      * return CA's request queue processor
      */
@@ -540,14 +641,11 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
         mService.startup();
         mRequestQueue.recover();
 
-        // Note that this could be null.
-
-        // setup Admin operations
-
-        initNotificationListeners();
-
-        startPublish();
-        //		startCRL();
+        if (isTopCA()) {
+            // setup Admin operations
+            initNotificationListeners();
+            startPublish();
+        }
     }
 
     /**
@@ -555,6 +653,8 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      * <P>
      */
     public void shutdown() {
+        if (!isTopCA()) return;  // sub-CAs don't own these resources
+
         Enumeration<ICRLIssuingPoint> enums = mCRLIssuePoints.elements();
         while (enums.hasMoreElements()) {
             CRLIssuingPoint point = (CRLIssuingPoint) enums.nextElement();
@@ -967,6 +1067,7 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      */
     public X509CRLImpl sign(X509CRLImpl crl, String algname)
             throws EBaseException {
+        ensureEnabled();
         X509CRLImpl signedcrl = null;
 
         IStatsSubsystem statsSub = (IStatsSubsystem) CMS.getSubsystem("stats");
@@ -1039,6 +1140,7 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      */
     public X509CertImpl sign(X509CertInfo certInfo, String algname)
             throws EBaseException {
+        ensureEnabled();
 
         X509CertImpl signedcert = null;
 
@@ -1123,6 +1225,7 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      */
     public byte[] sign(byte[] data, String algname)
             throws EBaseException {
+        ensureEnabled();
         return mSigningUnit.sign(data, algname);
     }
 
@@ -1228,13 +1331,13 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
                 mIssuerObj = new CertificateIssuerName((X500Name)mSubjectObj.get(CertificateIssuerName.DN_NAME));
             }
 
-            mSigningUnit.init(this, caSigningCfg);
+            mSigningUnit.init(this, caSigningCfg, mNickname);
             CMS.debug("CA signing unit inited");
 
             // for identrus
             IConfigStore CrlStore = mConfig.getSubStore(PROP_CRL_SIGNING_SUBSTORE);
 
-            if (CrlStore != null && CrlStore.size() > 0) {
+            if (isTopCA() && CrlStore != null && CrlStore.size() > 0) {
                 mCRLSigningUnit = new SigningUnit();
                 mCRLSigningUnit.init(this, mConfig.getSubStore(PROP_CRL_SIGNING_SUBSTORE));
             } else {
@@ -1304,7 +1407,7 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
 
             IConfigStore OCSPStore = mConfig.getSubStore(PROP_OCSP_SIGNING_SUBSTORE);
 
-            if (OCSPStore != null && OCSPStore.size() > 0) {
+            if (isTopCA() && OCSPStore != null && OCSPStore.size() > 0) {
                 mOCSPSigningUnit = new SigningUnit();
                 mOCSPSigningUnit.init(this, mConfig.getSubStore(PROP_OCSP_SIGNING_SUBSTORE));
                 CMS.debug("Separate OCSP signing unit inited");
@@ -1443,8 +1546,13 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
     /**
      * init cert & crl database
      */
-    private void initCaDatabases()
+    private void initCertDatabase()
             throws EBaseException {
+        if (!isTopCA()) {
+            mCertRepot = topCA.mCertRepot;
+            return;
+        }
+
         int certdb_inc = mConfig.getInteger(PROP_CERTDB_INC, 5);
 
         String certReposDN = mConfig.getString(PROP_CERT_REPOS_DN, null);
@@ -1471,8 +1579,17 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
         mCertRepot.setTransitRecordPageSize(transitRecordPageSize);
 
         CMS.debug("Cert Repot inited");
+    }
 
-        // init crl repot.
+    /**
+     * init cert & crl database
+     */
+    private void initCrlDatabase()
+            throws EBaseException {
+        if (!isTopCA()) {
+            mCRLRepot = topCA.mCRLRepot;
+            return;
+        }
 
         int crldb_inc = mConfig.getInteger(PROP_CRLDB_INC, 5);
 
@@ -1482,14 +1599,6 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
                     "ou=crlIssuingPoints, ou=" + getId() + ", " +
                             getDBSubsystem().getBaseDN());
         CMS.debug("CRL Repot inited");
-
-        String replicaReposDN = mConfig.getString(PROP_REPLICAID_DN, null);
-        if (replicaReposDN == null) {
-            replicaReposDN = "ou=Replica," + getDBSubsystem().getBaseDN();
-        }
-        mReplicaRepot = new ReplicaIDRepository(
-                DBSubsystem.getInstance(), 1, replicaReposDN);
-        CMS.debug("Replica Repot inited");
     }
 
     private void startPublish()
@@ -1513,6 +1622,11 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      */
     private void initPublish()
             throws EBaseException {
+        if (!isTopCA()) {
+            mPublisherProcessor = topCA.mPublisherProcessor;
+            return;
+        }
+
         IConfigStore c = null;
 
         try {
@@ -1676,6 +1790,15 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
      */
     private void initRequestQueue()
             throws EBaseException {
+        if (!isTopCA()) {
+            mPolicy = topCA.mPolicy;
+            mService = topCA.mService;
+            mNotify = topCA.mNotify;
+            mPNotify = topCA.mPNotify;
+            mRequestQueue = topCA.mRequestQueue;
+            return;
+        }
+
         mPolicy = new CAPolicy();
         mPolicy.init(this, mConfig.getSubStore(PROP_POLICY));
         CMS.debug("CA policy inited");
@@ -1734,6 +1857,11 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
     @SuppressWarnings("unchecked")
     private void initCRL()
             throws EBaseException {
+        if (!isTopCA()) {
+            mCRLIssuePoints = topCA.mCRLIssuePoints;
+            mMasterCRLIssuePoint = topCA.mMasterCRLIssuePoint;
+            return;
+        }
         IConfigStore crlConfig = mConfig.getSubStore(PROP_CRL_SUBSTORE);
 
         if ((crlConfig == null) || (crlConfig.size() <= 0)) {
@@ -1799,6 +1927,111 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
         log(ILogger.LL_INFO, "CRL Issuing Points inited");
     }
 
+    /**
+     * Find, instantiate and register sub-CAs.
+     *
+     * This method must only be called by the top-level CA.
+     */
+    private synchronized void loadLightweightCAs() throws EBaseException {
+        ILdapConnFactory dbFactory = CMS.getLdapBoundConnFactory("loadLightweightCAs");
+        dbFactory.init(CMS.getConfigStore().getSubStore("internaldb"));
+        LDAPConnection conn = dbFactory.getConn();
+
+        String searchDN = "ou=authorities,ou=" + getId()
+            + "," + getDBSubsystem().getBaseDN();
+        LDAPSearchResults results = null;
+        boolean foundTopCA = false;
+        boolean haveLightweightCAsContainer = true;
+        try {
+            results = conn.search(
+                searchDN, LDAPConnection.SCOPE_ONE,
+                "(objectclass=authority)", null, false);
+
+            while (results.hasMoreElements()) {
+                LDAPEntry entry = results.next();
+                LDAPAttribute aidAttr = entry.getAttribute("authorityID");
+                LDAPAttribute nickAttr = entry.getAttribute("authorityKeyNickname");
+                LDAPAttribute dnAttr = entry.getAttribute("authorityDN");
+                LDAPAttribute parentAIDAttr = entry.getAttribute("authorityParentID");
+                LDAPAttribute parentDNAttr = entry.getAttribute("authorityParentDN");
+
+                if (aidAttr == null || nickAttr == null || dnAttr == null)
+                    throw new ECAException("Malformed authority object; required attribute(s) missing: " + entry.getDN());
+
+                AuthorityID aid = new AuthorityID((String)
+                    aidAttr.getStringValues().nextElement());
+
+                X500Name dn = null;
+                try {
+                    dn = new X500Name((String) dnAttr.getStringValues().nextElement());
+                } catch (IOException e) {
+                    throw new ECAException("Malformed authority object; invalid authorityDN: " + entry.getDN());
+                }
+
+                String desc = null;
+                LDAPAttribute descAttr = entry.getAttribute("description");
+                if (descAttr != null)
+                    desc = (String) descAttr.getStringValues().nextElement();
+
+                if (dn.equals(mName)) {
+                    foundTopCA = true;
+                    this.authorityID = aid;
+                    this.authorityDescription = desc;
+                    caMap.put(aid, this);
+                    continue;
+                }
+
+                @SuppressWarnings("unused")
+                X500Name parentDN = null;
+                if (parentDNAttr != null) {
+                    try {
+                        parentDN = new X500Name((String) parentDNAttr.getStringValues().nextElement());
+                    } catch (IOException e) {
+                        throw new ECAException("Malformed authority object; invalid authorityParentDN: " + entry.getDN());
+                    }
+                }
+
+                String keyNick = (String) nickAttr.getStringValues().nextElement();
+                AuthorityID parentAID = null;
+                if (parentAIDAttr != null)
+                    parentAID = new AuthorityID((String)
+                        parentAIDAttr.getStringValues().nextElement());
+
+                boolean enabled = true;
+                LDAPAttribute enabledAttr = entry.getAttribute("authorityEnabled");
+                if (enabledAttr != null) {
+                    String enabledString = (String)
+                        enabledAttr.getStringValues().nextElement();
+                    enabled = enabledString.equalsIgnoreCase("TRUE");
+                }
+
+                CertificateAuthority subCA = new CertificateAuthority(
+                    this, aid, parentAID, keyNick, desc, enabled);
+                caMap.put(aid, subCA);
+            }
+        } catch (LDAPException e) {
+            if (e.getLDAPResultCode() == LDAPException.NO_SUCH_OBJECT) {
+                CMS.debug(
+                    "Missing lightweight CAs container '" + searchDN
+                    + "'.  Disabling lightweight CAs.");
+                haveLightweightCAsContainer = false;
+            } else {
+                throw new ECAException("Failed to execute LDAP search for lightweight CAs: " + e);
+            }
+        } finally {
+            dbFactory.returnConn(conn);
+            dbFactory.reset();
+        }
+
+        if (haveLightweightCAsContainer && !foundTopCA) {
+            CMS.debug("loadLightweightCAs: no entry for top-level CA");
+            CMS.debug("loadLightweightCAs: adding entry for top-level CA");
+            AuthorityID aid = addTopCAEntry();
+            this.authorityID = aid;
+            caMap.put(aid, this);
+        }
+    }
+
     public String getOfficialName() {
         return OFFICIAL_NAME;
     }
@@ -1960,6 +2193,7 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
     }
 
     private BasicOCSPResponse sign(ResponseData rd) throws EBaseException {
+        ensureEnabled();
         try (DerOutputStream out = new DerOutputStream()) {
             DerOutputStream tmp = new DerOutputStream();
 
@@ -2083,4 +2317,294 @@ public class CertificateAuthority implements ICertificateAuthority, ICertAuthori
 
         return new SingleResponse(cid, certStatus, thisUpdate, nextUpdate);
     }
+
+    /**
+     * Enumerate all sub-CAs.
+     */
+    public synchronized List<ICertificateAuthority> getCAs() {
+        List<ICertificateAuthority> cas = new ArrayList<>();
+        for (ICertificateAuthority ca : caMap.values()) {
+            cas.add(ca);
+        }
+        return cas;
+    }
+
+    /**
+     * Get the sub-CA by ID.
+     *
+     * @param aid The ID of the CA to retrieve, or null
+     *             to retreive the top-level CA.
+     *
+     * @return the authority, or null if not found
+     */
+    public ICertificateAuthority getCA(AuthorityID aid) {
+        return aid == null ? topCA : caMap.get(aid);
+    }
+
+    public ICertificateAuthority getCA(X500Name dn) {
+        for (ICertificateAuthority ca : getCAs()) {
+            if (ca.getX500Name().equals(dn))
+                return ca;
+        }
+        return null;
+    }
+
+    public AuthorityID getAuthorityID() {
+        return authorityID;
+    }
+
+    public AuthorityID getAuthorityParentID() {
+        return authorityParentID;
+    }
+
+    public String getAuthorityDescription() {
+        return authorityDescription;
+    }
+
+    /**
+     * Create a new sub-CA.
+     *
+     * @param subjectDN Subject DN for new CA
+     * @param parentAID ID of parent CA, or null to create
+     *                  sub-CA immediately below top-level CA
+     * @param description Optional string description of CA
+     */
+    public ICertificateAuthority createCA(
+            String subjectDN, AuthorityID parentAID,
+            String description)
+            throws EBaseException {
+        ICertificateAuthority parentCA = getCA(parentAID);
+        if (parentCA == null)
+            throw new CANotFoundException(
+                "Parent CA \"" + parentAID + "\" does not exist");
+
+        ICertificateAuthority subCA = parentCA.createSubCA(
+                subjectDN, description);
+        synchronized (this) {
+            caMap.put(subCA.getAuthorityID(), subCA);
+        }
+        return subCA;
+    }
+
+    /**
+     * Create a new sub-CA IMMEDIATELY beneath this one.
+     *
+     * This method DOES NOT add the new CA to caMap; it is the
+     * caller's responsibility.
+     */
+    public ICertificateAuthority createSubCA(
+            String subjectDN, String description)
+            throws EBaseException {
+
+        // check uniqueness of DN
+        X500Name subjectX500Name = null;
+        try {
+            subjectX500Name = new X500Name(subjectDN);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(
+                "Invalid Subject DN: " + subjectDN);
+        }
+        for (ICertificateAuthority ca : caMap.values()) {
+            if (ca.getX500Name().equals(subjectX500Name))
+                throw new IssuerUnavailableException(
+                    "CA with Subject DN '" + subjectDN + "' already exists");
+        }
+
+        // generate authority ID and nickname
+        AuthorityID aid = new AuthorityID();
+        String aidString = aid.toString();
+        String nickname = topCA.getNickname() + " " + aidString;
+
+        // build database entry
+        String dn = "cn=" + aidString + ",ou=authorities,ou="
+            + getId() + "," + getDBSubsystem().getBaseDN();
+        CMS.debug("createSubCA: DN = " + dn);
+        String parentDNString = null;
+        try {
+            parentDNString = mName.toLdapDNString();
+        } catch (IOException e) {
+            throw new EBaseException("Failed to convert issuer DN to string: " + e);
+        }
+
+        LDAPAttribute[] attrs = {
+            new LDAPAttribute("objectclass", "authority"),
+            new LDAPAttribute("cn", aidString),
+            new LDAPAttribute("authorityID", aidString),
+            new LDAPAttribute("authorityKeyNickname", nickname),
+            new LDAPAttribute("authorityEnabled", "TRUE"),
+            new LDAPAttribute("authorityDN", subjectDN),
+            new LDAPAttribute("authorityParentDN", parentDNString)
+        };
+        LDAPAttributeSet attrSet = new LDAPAttributeSet(attrs);
+        if (this.authorityID != null)
+            attrSet.add(new LDAPAttribute(
+                "authorityParentID", this.authorityID.toString()));
+        if (description != null)
+            attrSet.add(new LDAPAttribute("description", description));
+        LDAPEntry ldapEntry = new LDAPEntry(dn, attrSet);
+
+        // connect to database
+        ILdapConnFactory dbFactory = CMS.getLdapBoundConnFactory("createSubCA");
+        dbFactory.init(CMS.getConfigStore().getSubStore("internaldb"));
+        LDAPConnection conn = dbFactory.getConn();
+
+        try {
+            // add entry to database
+            conn.add(ldapEntry);
+
+            try {
+                // Generate sub-CA signing key
+                CryptoManager cryptoManager = CryptoManager.getInstance();
+                // TODO read PROP_TOKEN_NAME config
+                CryptoToken token = cryptoManager.getInternalKeyStorageToken();
+                // TODO algorithm parameter
+                KeyPairGenerator gen = token.getKeyPairGenerator(KeyPairAlgorithm.RSA);
+                gen.initialize(2048);
+                KeyPair keypair = gen.genKeyPair();
+                PublicKey pub = keypair.getPublic();
+                X509Key x509key = CryptoUtil.convertPublicKeyToX509Key(pub);
+
+                // Sign certificate
+                String algName = mSigningUnit.getDefaultAlgorithm();
+                IConfigStore cs = new PropConfigStore("cs");
+                cs.put(".profile", "caCert.profile");
+                cs.put(".dn", subjectDN);
+                cs.put(".keyalgorithm", algName);
+                X509CertImpl cert =
+                    CertUtil.createLocalCertWithCA(cs, x509key, "", "", "local", this);
+
+                // Add certificate to nssdb
+                cryptoManager.importCertPackage(cert.getEncoded(), nickname);
+            } catch (Exception e) {
+                // something went wrong; delete just-added entry
+                conn.delete(dn);
+                throw new ECAException("Error creating sub-CA certificate: " + e);
+            }
+        } catch (LDAPException e) {
+            throw new EBaseException("Error adding sub-CA entry to database: " + e);
+        } finally {
+            dbFactory.returnConn(conn);
+            dbFactory.reset();
+        }
+
+        return new CertificateAuthority(
+            topCA, aid, this.authorityID, nickname, description, true);
+    }
+
+    private AuthorityID addTopCAEntry() throws EBaseException {
+        if (!isTopCA())
+            throw new EBaseException("Can only invoke from top-level CA");
+
+        // generate authority ID
+        AuthorityID aid = new AuthorityID();
+        String aidString = aid.toString();
+
+        // build database entry
+        String dn = "cn=" + aidString + ",ou=authorities,ou="
+            + getId() + "," + getDBSubsystem().getBaseDN();
+        String dnString = null;
+        try {
+            dnString = mName.toLdapDNString();
+        } catch (IOException e) {
+            throw new EBaseException("Failed to convert issuer DN to string: " + e);
+        }
+
+        LDAPAttribute[] attrs = {
+            new LDAPAttribute("objectclass", "authority"),
+            new LDAPAttribute("cn", aidString),
+            new LDAPAttribute("authorityID", aidString),
+            new LDAPAttribute("authorityKeyNickname", getNickname()),
+            new LDAPAttribute("authorityEnabled", "TRUE"),
+            new LDAPAttribute("authorityDN", dnString),
+            new LDAPAttribute("description", "Top-level (host) CA")
+        };
+        LDAPAttributeSet attrSet = new LDAPAttributeSet(attrs);
+        LDAPEntry ldapEntry = new LDAPEntry(dn, attrSet);
+
+        // connect to database
+        ILdapConnFactory dbFactory = CMS.getLdapBoundConnFactory("addTopCAEntry");
+        dbFactory.init(CMS.getConfigStore().getSubStore("internaldb"));
+        LDAPConnection conn = dbFactory.getConn();
+
+        try {
+            conn.add(ldapEntry);
+        } catch (LDAPException e) {
+            throw new ELdapException("Error adding top-level CA entry to database: " + e);
+        } finally {
+            dbFactory.returnConn(conn);
+            dbFactory.reset();
+        }
+
+        return aid;
+    }
+
+    /**
+     * Update lightweight authority attributes.
+     *
+     * Pass null values to exclude an attribute from the update.
+     *
+     * If a passed value matches the current value, it is excluded
+     * from the update.
+     *
+     * To remove optional string values, pass the empty string.
+     */
+    public void modifyAuthority(Boolean enabled, String desc)
+            throws EBaseException {
+        if (isTopCA() && enabled != null && !enabled)
+            throw new CATypeException("Cannot disable the top-level CA");
+
+        LDAPModificationSet mods = new LDAPModificationSet();
+
+        boolean nextEnabled = authorityEnabled;
+        if (enabled != null && enabled.booleanValue() != authorityEnabled) {
+            mods.add(
+                LDAPModification.REPLACE,
+                new LDAPAttribute("authorityEnabled", enabled ? "TRUE" : "FALSE"));
+            nextEnabled = enabled;
+        }
+
+        String nextDesc = authorityDescription;
+        if (desc != null) {
+            if (!desc.isEmpty() && authorityDescription != null
+                    && !desc.equals(authorityDescription)) {
+                mods.add(
+                    LDAPModification.REPLACE,
+                    new LDAPAttribute("description", desc));
+                nextDesc = desc;
+            } else if (desc.isEmpty() && authorityDescription != null) {
+                mods.add(
+                    LDAPModification.DELETE,
+                    new LDAPAttribute("description", authorityDescription));
+                nextDesc = null;
+            } else if (!desc.isEmpty() && authorityDescription == null) {
+                mods.add(
+                    LDAPModification.ADD,
+                    new LDAPAttribute("description", desc));
+                nextDesc = desc;
+            }
+        }
+
+        if (mods.size() > 0) {
+            String dn = "cn=" + authorityID.toString() + ",ou=authorities,ou="
+                + getId() + "," + getDBSubsystem().getBaseDN();
+
+            // connect to database
+            ILdapConnFactory dbFactory = CMS.getLdapBoundConnFactory("updateAuthority");
+            dbFactory.init(CMS.getConfigStore().getSubStore("internaldb"));
+            LDAPConnection conn = dbFactory.getConn();
+            try {
+                conn.modify(dn, mods);
+            } catch (LDAPException e) {
+                throw new EBaseException("Error adding sub-CA entry to database: " + e);
+            } finally {
+                dbFactory.returnConn(conn);
+                dbFactory.reset();
+            }
+
+            // update was successful; update CA's state
+            authorityEnabled = nextEnabled;
+            authorityDescription = nextDesc;
+        }
+    }
+
 }
diff --git a/base/ca/src/com/netscape/ca/SigningUnit.java b/base/ca/src/com/netscape/ca/SigningUnit.java
index 2466fb652a46a3b5faede616cb397d18e592f5a0..0410bd2909bc71ca7d7443b3c9db0b085d62eaea 100644
--- a/base/ca/src/com/netscape/ca/SigningUnit.java
+++ b/base/ca/src/com/netscape/ca/SigningUnit.java
@@ -123,16 +123,14 @@ public final class SigningUnit implements ISigningUnit {
         return mConfig.getString(PROP_TOKEN_NAME);
     }
 
-    public String getNickName() throws EBaseException {
-        try {
-            return mConfig.getString(PROP_RENAMED_CERT_NICKNAME);
-        } catch (EBaseException e) {
-            return mConfig.getString(PROP_CERT_NICKNAME);
-        }
-    }
 
     public void init(ISubsystem owner, IConfigStore config)
             throws EBaseException {
+        init(owner, config, null);
+    }
+
+    public void init(ISubsystem owner, IConfigStore config, String nickname)
+            throws EBaseException {
         mOwner = owner;
         mConfig = config;
 
@@ -140,7 +138,15 @@ public final class SigningUnit implements ISigningUnit {
         try {
             mManager = CryptoManager.getInstance();
 
-            mNickname = getNickName();
+            if (nickname == null) {
+                try {
+                    mNickname = mConfig.getString(PROP_RENAMED_CERT_NICKNAME);
+                } catch (EBaseException e) {
+                    mNickname = mConfig.getString(PROP_CERT_NICKNAME);
+                }
+            } else {
+                mNickname = nickname;
+            }
 
             tokenname = config.getString(PROP_TOKEN_NAME);
             if (tokenname.equalsIgnoreCase(Constants.PR_INTERNAL_TOKEN) ||
diff --git a/base/ca/src/org/dogtagpki/server/ca/rest/AuthorityService.java b/base/ca/src/org/dogtagpki/server/ca/rest/AuthorityService.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ada9139f469903a61a0b10c2d4b43f9e07f8c77
--- /dev/null
+++ b/base/ca/src/org/dogtagpki/server/ca/rest/AuthorityService.java
@@ -0,0 +1,275 @@
+//--- 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) 2015 Red Hat, Inc.
+//All rights reserved.
+//--- END COPYRIGHT BLOCK ---
+
+package org.dogtagpki.server.ca.rest;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.GenericEntity;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import com.netscape.certsrv.apps.CMS;
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.certsrv.authority.AuthorityResource;
+import com.netscape.certsrv.base.BadRequestException;
+import com.netscape.certsrv.base.ConflictingOperationException;
+import com.netscape.certsrv.base.EBaseException;
+import com.netscape.certsrv.base.ForbiddenException;
+import com.netscape.certsrv.base.PKIException;
+import com.netscape.certsrv.base.ResourceNotFoundException;
+import com.netscape.certsrv.ca.AuthorityID;
+import com.netscape.certsrv.ca.CANotFoundException;
+import com.netscape.certsrv.ca.CATypeException;
+import com.netscape.certsrv.ca.ICertificateAuthority;
+import com.netscape.certsrv.ca.IssuerUnavailableException;
+import com.netscape.cms.servlet.base.PKIService;
+import com.netscape.cmsutil.util.Utils;
+
+/**
+ * @author ftweedal
+ */
+public class AuthorityService extends PKIService implements AuthorityResource {
+
+    ICertificateAuthority topCA;
+
+    public AuthorityService() {
+        topCA = (ICertificateAuthority) CMS.getSubsystem("ca");
+    }
+
+    @Context
+    private UriInfo uriInfo;
+
+    @Context
+    private HttpHeaders headers;
+
+    @Context
+    private Request request;
+
+    @Context
+    private HttpServletRequest servletRequest;
+
+    /*
+    private final static String LOGGING_SIGNED_AUDIT_CERT_PROFILE_APPROVAL =
+            "LOGGING_SIGNED_AUDIT_CERT_PROFILE_APPROVAL_4";
+    private final static String LOGGING_SIGNED_AUDIT_CONFIG_CERT_PROFILE =
+            "LOGGING_SIGNED_AUDIT_CONFIG_CERT_PROFILE_3";
+    */
+
+    @Override
+    public Response listCAs() {
+        List<AuthorityData> results = new ArrayList<>();
+        for (ICertificateAuthority ca : topCA.getCAs())
+            results.add(readAuthorityData(ca));
+
+        GenericEntity<List<AuthorityData>> entity =
+            new GenericEntity<List<AuthorityData>>(results) {};
+        return Response.ok(entity).build();
+    }
+
+    @Override
+    public Response getCA(String aidString) {
+        AuthorityID aid;
+        try {
+            aid = new AuthorityID(aidString);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Bad CA ID: " + aidString);
+        }
+
+        ICertificateAuthority ca = topCA.getCA(aid);
+        if (ca == null)
+            throw new ResourceNotFoundException("CA \"" + aidString + "\" not found");
+
+        return createOKResponse(readAuthorityData(ca));
+    }
+
+    @Override
+    public Response getCert(String aidString) {
+        AuthorityID aid;
+        try {
+            aid = new AuthorityID(aidString);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Bad CA ID: " + aidString);
+        }
+
+        ICertificateAuthority ca = topCA.getCA(aid);
+        if (ca == null)
+            throw new ResourceNotFoundException("CA \"" + aidString + "\" not found");
+
+        try {
+            return Response.ok(ca.getCaX509Cert().getEncoded()).build();
+        } catch (CertificateEncodingException e) {
+            // this really is a 500 Internal Server Error
+            throw new PKIException("Error encoding certificate: " + e);
+        }
+    }
+
+    @Override
+    public Response getCertPEM(String aidString) {
+        byte[] der = (byte[]) getCert(aidString).getEntity();
+        return Response.ok(toPem("CERTIFICATE", der)).build();
+    }
+
+    @Override
+    public Response getChain(String aidString) {
+        AuthorityID aid;
+        try {
+            aid = new AuthorityID(aidString);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Bad CA ID: " + aidString);
+        }
+
+        ICertificateAuthority ca = topCA.getCA(aid);
+        if (ca == null)
+            throw new ResourceNotFoundException("CA \"" + aidString + "\" not found");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try {
+            ca.getCACertChain().encode(out);
+        } catch (IOException e) {
+            throw new PKIException("Error encoding certificate chain: " + e);
+        }
+
+        return Response.ok(out.toByteArray()).build();
+    }
+
+    @Override
+    public Response getChainPEM(String aidString) {
+        byte[] der = (byte[]) getCert(aidString).getEntity();
+        return Response.ok(toPem("PKCS7", der)).build();
+    }
+
+    @Override
+    public Response createCA(AuthorityData data) {
+        String parentAIDString = data.getParentID();
+        AuthorityID parentAID = null;
+        try {
+            parentAID = new AuthorityID(parentAIDString);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Bad Authority ID: " + parentAIDString);
+        }
+
+        try {
+            ICertificateAuthority subCA = topCA.createCA(
+                data.getDN(), parentAID, data.getDescription());
+            return createOKResponse(readAuthorityData(subCA));
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException(e.toString());
+        } catch (CANotFoundException e) {
+            throw new ResourceNotFoundException(e.toString());
+        } catch (IssuerUnavailableException e) {
+            throw new ConflictingOperationException(e.toString());
+        } catch (Exception e) {
+            CMS.debug(e);
+            throw new PKIException("Error creating CA: " + e.toString());
+        }
+    }
+
+    @Override
+    public Response modifyCA(String aidString, AuthorityData data) {
+        AuthorityID aid = null;
+        try {
+            aid = new AuthorityID(aidString);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Bad CA ID: " + aidString);
+        }
+
+        ICertificateAuthority ca = topCA.getCA(aid);
+        if (ca == null)
+            throw new ResourceNotFoundException("CA \"" + aidString + "\" not found");
+
+        try {
+            ca.modifyAuthority(data.getEnabled(), data.getDescription());
+            return createOKResponse(readAuthorityData(ca));
+        } catch (CATypeException e) {
+            throw new ForbiddenException(e.toString());
+        } catch (EBaseException e) {
+            CMS.debug(e);
+            throw new PKIException("Error modifying authority: " + e.toString());
+        }
+    }
+
+    @Override
+    public Response enableCA(String aidString) {
+        return modifyCA(
+            aidString,
+            new AuthorityData(null, null, null, true, null));
+    }
+
+    @Override
+    public Response disableCA(String aidString) {
+        return modifyCA(
+            aidString,
+            new AuthorityData(null, null, null, false, null));
+    }
+
+    private static AuthorityData readAuthorityData(ICertificateAuthority ca)
+            throws PKIException {
+        String dn;
+        try {
+            dn = ca.getX500Name().toLdapDNString();
+        } catch (IOException e) {
+            throw new PKIException("Error reading CA data: could not determine Issuer DN");
+        }
+
+        AuthorityID parentAID = ca.getAuthorityParentID();
+        return new AuthorityData(
+            dn,
+            ca.getAuthorityID().toString(),
+            parentAID != null ? parentAID.toString() : null,
+            ca.getAuthorityEnabled(),
+            ca.getAuthorityDescription()
+        );
+    }
+
+    private String toPem(String name, byte[] data) {
+        return "-----BEGIN " + name + "-----\n" +
+                Utils.base64encode(data) +
+                "-----END " + name + "-----\n";
+    }
+
+    /* TODO work out what audit messages are needed
+    public void auditProfileChangeState(String profileId, String op, String status) {
+        String msg = CMS.getLogMessage(
+                LOGGING_SIGNED_AUDIT_CERT_PROFILE_APPROVAL,
+                auditor.getSubjectID(),
+                status,
+                profileId,
+                op);
+        auditor.log(msg);
+    }
+
+    public void auditProfileChange(String scope, String type, String id, String status, Map<String, String> params) {
+        String msg = CMS.getLogMessage(
+                LOGGING_SIGNED_AUDIT_CONFIG_CERT_PROFILE,
+                auditor.getSubjectID(),
+                status,
+                auditor.getParamString(scope, type, id, params));
+        auditor.log(msg);
+    }
+    */
+
+}
diff --git a/base/ca/src/org/dogtagpki/server/ca/rest/CAApplication.java b/base/ca/src/org/dogtagpki/server/ca/rest/CAApplication.java
index 16eae7877059c7dc42479276b3111db1ce7f582d..235ea105bef0c738bccd53276a744b95a76f0627 100644
--- a/base/ca/src/org/dogtagpki/server/ca/rest/CAApplication.java
+++ b/base/ca/src/org/dogtagpki/server/ca/rest/CAApplication.java
@@ -34,6 +34,9 @@ public class CAApplication extends Application {
         // installer
         classes.add(CAInstallerService.class);
 
+        // sub-ca management
+        classes.add(AuthorityService.class);
+
         // certs and requests
         classes.add(CertService.class);
         classes.add(CertRequestService.class);
diff --git a/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java b/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java
index 95f1f4c20086ddb45846f65b1db157bff238708a..1da1ce1713541e52164e9e8fbcbf39ca2332540d 100644
--- a/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java
+++ b/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java
@@ -38,9 +38,11 @@ import com.netscape.certsrv.authentication.EAuthException;
 import com.netscape.certsrv.authorization.EAuthzException;
 import com.netscape.certsrv.base.BadRequestDataException;
 import com.netscape.certsrv.base.BadRequestException;
+import com.netscape.certsrv.base.ConflictingOperationException;
 import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.PKIException;
 import com.netscape.certsrv.base.UnauthorizedException;
+import com.netscape.certsrv.ca.CADisabledException;
 import com.netscape.certsrv.cert.CertEnrollmentRequest;
 import com.netscape.certsrv.cert.CertRequestInfo;
 import com.netscape.certsrv.cert.CertRequestInfos;
@@ -210,6 +212,9 @@ public class CertRequestService extends PKIService implements CertRequestResourc
         } catch (BadRequestDataException e) {
             CMS.debug("changeRequestState: bad request data: " + e);
             throw new BadRequestException(e.toString());
+        } catch (CADisabledException e) {
+            CMS.debug("changeRequestState: CA disabled: " + e);
+            throw new ConflictingOperationException(e.toString());
         } catch (EPropertyException e) {
             CMS.debug("changeRequestState: execution error " + e);
             throw new PKIException(CMS.getUserMessage(getLocale(headers),
diff --git a/base/common/src/com/netscape/certsrv/authority/AuthorityData.java b/base/common/src/com/netscape/certsrv/authority/AuthorityData.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2479f2977657108faa23dc7c3a1f7b0fa33e104
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/authority/AuthorityData.java
@@ -0,0 +1,114 @@
+// --- 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) 2015 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+/**
+ * @author ftweedal
+ */
+package com.netscape.certsrv.authority;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+ at XmlRootElement(name = "ca")
+ at XmlAccessorType(XmlAccessType.FIELD)
+public class AuthorityData {
+
+    public static Marshaller marshaller;
+    public static Unmarshaller unmarshaller;
+
+    static {
+        try {
+            marshaller = JAXBContext.newInstance(AuthorityData.class).createMarshaller();
+            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+            unmarshaller = JAXBContext.newInstance(AuthorityData.class).createUnmarshaller();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    @XmlAttribute
+    protected String aid;
+
+    public String getID() {
+        return aid;
+    }
+
+
+    @XmlAttribute
+    protected String parentAID;
+
+    public String getParentID() {
+        return parentAID;
+    }
+
+
+    @XmlAttribute
+    protected String dn;
+
+    public String getDN() {
+        return dn;
+    }
+
+
+    @XmlAttribute
+    protected Boolean enabled;
+
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+
+    @XmlAttribute
+    protected String description;
+
+    public String getDescription() {
+        return description;
+    }
+
+
+    protected Link link;
+
+    public Link getLink() {
+        return link;
+    }
+
+    public void setLink(Link link) {
+        this.link = link;
+    }
+
+    protected AuthorityData() {
+    }
+
+    public AuthorityData(
+            String dn, String aid, String parentAID,
+            Boolean enabled, String description) {
+        this.dn = dn;
+        this.aid = aid;
+        this.parentAID = parentAID;
+        this.enabled = enabled;
+        this.description = description;
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/authority/AuthorityResource.java b/base/common/src/com/netscape/certsrv/authority/AuthorityResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1fb2a7b7f46601aa89143579badbfc6f6c8d555
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/authority/AuthorityResource.java
@@ -0,0 +1,91 @@
+package com.netscape.certsrv.authority;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.ClientResponseType;
+
+import com.netscape.certsrv.acls.ACLMapping;
+import com.netscape.certsrv.authentication.AuthMethodMapping;
+
+ at Path("authorities")
+ at AuthMethodMapping("authorities")
+public interface AuthorityResource {
+
+    @GET
+    public Response listCAs();
+    /*
+            @QueryParam("start") Integer start,
+            @QueryParam("size") Integer size);
+            */
+
+    @GET
+    @Path("{id}")
+    @ClientResponseType(entityType=AuthorityData.class)
+    public Response getCA(@PathParam("id") String caIDString);
+
+    @GET
+    @Path("{id}/cert")
+    @Produces("application/pkix-cert")
+    @ClientResponseType(entityType=byte[].class)
+    public Response getCert(@PathParam("id") String caIDString);
+
+    @GET
+    @Path("{id}/cert")
+    @Produces("application/x-pem-file")
+    @ClientResponseType(entityType=String.class)
+    public Response getCertPEM(@PathParam("id") String caIDString);
+
+    @GET
+    @Path("{id}/chain")
+    @Produces("application/pkcs7-mime")
+    @ClientResponseType(entityType=byte[].class)
+    public Response getChain(@PathParam("id") String caIDString);
+
+    @GET
+    @Path("{id}/chain")
+    @Produces("application/x-pem-file")
+    @ClientResponseType(entityType=String.class)
+    public Response getChainPEM(@PathParam("id") String caIDString);
+
+    @POST
+    @ClientResponseType(entityType=AuthorityData.class)
+    @ACLMapping("authorities.create")
+    public Response createCA(AuthorityData data);
+
+    /**
+     * Modify a CA (supports partial updates).
+     *
+     * AuthorityID, AuthorityParentID and DN are immutable;
+     * differences in these values are ignored.
+     *
+     * Other values, if null, are ignored, otherwise they are
+     * set to the new value.  To remove the description, use an
+     * empty string.
+     */
+    @PUT
+    @Path("{id}")
+    @ClientResponseType(entityType=AuthorityData.class)
+    @ACLMapping("authorities.modify")
+    public Response modifyCA(
+        @PathParam("id") String caIDString,
+        AuthorityData data);
+
+    @POST
+    @Path("{id}/enable")
+    @ClientResponseType(entityType=AuthorityData.class)
+    @ACLMapping("authorities.modify")
+    public Response enableCA(@PathParam("id") String caIDString);
+
+    @POST
+    @Path("{id}/disable")
+    @ClientResponseType(entityType=AuthorityData.class)
+    @ACLMapping("authorities.modify")
+    public Response disableCA(@PathParam("id") String caIDString);
+
+}
diff --git a/base/common/src/com/netscape/certsrv/ca/AuthorityID.java b/base/common/src/com/netscape/certsrv/ca/AuthorityID.java
new file mode 100644
index 0000000000000000000000000000000000000000..daac587b75843f5faf75e556d4d0135c8ffc8fd7
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/ca/AuthorityID.java
@@ -0,0 +1,36 @@
+package com.netscape.certsrv.ca;
+
+import java.util.UUID;
+
+/**
+ * Identifier for a CertificateAuthority.
+ */
+public class AuthorityID implements Comparable<AuthorityID> {
+
+    protected UUID uuid;
+
+    /**
+     * Parse a AuthorityID from the given string
+     */
+    public AuthorityID(String s) {
+        if (s == null)
+            throw new IllegalArgumentException("null AuthorityID string");
+        uuid = UUID.fromString(s);
+    }
+
+    /**
+     * Construct a random AuthorityID
+     */
+    public AuthorityID() {
+        uuid = UUID.randomUUID();
+    }
+
+    public String toString() {
+        return uuid.toString();
+    }
+
+    public int compareTo(AuthorityID aid) {
+        return uuid.compareTo(aid.uuid);
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/ca/CADisabledException.java b/base/common/src/com/netscape/certsrv/ca/CADisabledException.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b3f16b90a8d28b87e993575037a2a19517e17b9
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/ca/CADisabledException.java
@@ -0,0 +1,15 @@
+package com.netscape.certsrv.ca;
+
+/**
+ * Exception to throw when a (sub-)CA cannot perform an operation
+ * because it is disabled.
+ */
+public class CADisabledException extends ECAException {
+
+    private static final long serialVersionUID = -8827509070155037699L;
+
+    public CADisabledException(String msgFormat) {
+        super(msgFormat);
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/ca/CANotFoundException.java b/base/common/src/com/netscape/certsrv/ca/CANotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..f292077ece3089d398ad0cf3a008b7e5218a6bcd
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/ca/CANotFoundException.java
@@ -0,0 +1,14 @@
+package com.netscape.certsrv.ca;
+
+/**
+ * Exception to throw when a (sub-)CA cannot be found.
+ */
+public class CANotFoundException extends ECAException {
+
+    private static final long serialVersionUID = -4618887355685066120L;
+
+    public CANotFoundException(String msgFormat) {
+        super(msgFormat);
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/ca/CATypeException.java b/base/common/src/com/netscape/certsrv/ca/CATypeException.java
new file mode 100644
index 0000000000000000000000000000000000000000..19eb680e88aee3baac51abd95439c46487bf5720
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/ca/CATypeException.java
@@ -0,0 +1,16 @@
+package com.netscape.certsrv.ca;
+
+/**
+ * Exception to throw when an operation cannot be completed
+ * because the CA is the wrong type (e.g., an operation that
+ * only applies to lightweight CAs).
+ */
+public class CATypeException extends ECAException {
+
+    private static final long serialVersionUID = -6004456461295692150L;
+
+    public CATypeException(String msgFormat) {
+        super(msgFormat);
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/ca/ICAService.java b/base/common/src/com/netscape/certsrv/ca/ICAService.java
index 1d179e07692eee2f32780b33489975a571670685..a4b4a63038872fbf6d97cfc3fbcadce5234208a6 100644
--- a/base/common/src/com/netscape/certsrv/ca/ICAService.java
+++ b/base/common/src/com/netscape/certsrv/ca/ICAService.java
@@ -23,6 +23,7 @@ import netscape.security.x509.X509CertInfo;
 
 import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.IConfigStore;
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.connector.IConnector;
 import com.netscape.certsrv.request.IRequest;
 
@@ -59,13 +60,15 @@ public interface ICAService {
      * Issues certificate base on enrollment information,
      * creates certificate record, and stores all necessary data.
      *
+     * @param caID CA ID
      * @param certi information obtain from revocation request
+     * @param profileId Name of profile used
+     * @param rid Request ID
      * @exception EBaseException failed to issue certificate or create certificate record
      */
-    public X509CertImpl issueX509Cert(X509CertInfo certi)
-            throws EBaseException;
-
-    public X509CertImpl issueX509Cert(X509CertInfo certi, String profileId, String rid)
+    public X509CertImpl issueX509Cert(
+                AuthorityID aid, X509CertInfo certi,
+                String profileId, String rid)
             throws EBaseException;
 
     /**
diff --git a/base/common/src/com/netscape/certsrv/ca/ICertificateAuthority.java b/base/common/src/com/netscape/certsrv/ca/ICertificateAuthority.java
index f87f15420b3ea6e02e5ce47b5c350e86f67ccc9f..d63f92aa2b2b9040fb14dac8cced4ecc8a5039ec 100644
--- a/base/common/src/com/netscape/certsrv/ca/ICertificateAuthority.java
+++ b/base/common/src/com/netscape/certsrv/ca/ICertificateAuthority.java
@@ -18,6 +18,7 @@
 package com.netscape.certsrv.ca;
 
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
@@ -515,4 +516,66 @@ public interface ICertificateAuthority extends ISubsystem {
 
     public CertificateIssuerName getIssuerObj();
     public CertificateSubjectName getSubjectObj();
+
+    /**
+     * Enumerate all sub-CA handles.
+     */
+    public List<ICertificateAuthority> getCAs();
+
+    /**
+     * Get the CA ID of this CA.  Returns null for the top-level CA.
+     */
+    public AuthorityID getAuthorityID();
+
+    /**
+     * Get the CA ID of this CA's parent CA.  Returns null for
+     * authorities signed by the top-level CA.
+     */
+    public AuthorityID getAuthorityParentID();
+
+    /**
+     * Return CA description.  May be null.
+     */
+    public boolean getAuthorityEnabled();
+
+    /**
+     * Return CA description.  May be null.
+     */
+    public String getAuthorityDescription();
+
+    /**
+     * Get the CA by ID.  Returns null if CA not found.
+     */
+    public ICertificateAuthority getCA(AuthorityID aid);
+
+    /**
+     * Get the CA by DN.  Returns null if CA not found.
+     */
+    public ICertificateAuthority getCA(X500Name dn);
+
+    /**
+     * Create a new sub-CA under the specified parent CA.
+     */
+    public ICertificateAuthority createCA(
+            String dn, AuthorityID parentAID, String desc)
+        throws EBaseException;
+
+    /**
+     * Create a new sub-CA IMMEDIATELY beneath this one.
+     *
+     * This method DOES NOT add the new CA to caMap; it is the
+     * caller's responsibility.
+     */
+    public ICertificateAuthority createSubCA(
+            String dn, String desc)
+        throws EBaseException;
+
+    /**
+     * Update authority configurables.
+     *
+     * @param enabled Whether CA is enabled or disabled
+     * @param desc Description; null or empty removes it
+     */
+    public void modifyAuthority(Boolean enabled, String desc)
+        throws EBaseException;
 }
diff --git a/base/common/src/com/netscape/certsrv/ca/IssuerUnavailableException.java b/base/common/src/com/netscape/certsrv/ca/IssuerUnavailableException.java
new file mode 100644
index 0000000000000000000000000000000000000000..75bf88251bfadb687c7bf85944a806339f6f3b9f
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/ca/IssuerUnavailableException.java
@@ -0,0 +1,15 @@
+package com.netscape.certsrv.ca;
+
+/**
+ * Exception to throw during CA creation when requested CA
+ * (issuer DN) already exists.
+ */
+public class IssuerUnavailableException extends ECAException {
+
+    private static final long serialVersionUID = -6247493607604418446L;
+
+    public IssuerUnavailableException(String msgFormat) {
+        super(msgFormat);
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/profile/IEnrollProfile.java b/base/common/src/com/netscape/certsrv/profile/IEnrollProfile.java
index 69a39d7e23232a1a0cc6e2fe98305d452234bb8c..a861a2e73a2e361971f010f63bd0ca615ba08e80 100644
--- a/base/common/src/com/netscape/certsrv/profile/IEnrollProfile.java
+++ b/base/common/src/com/netscape/certsrv/profile/IEnrollProfile.java
@@ -175,6 +175,11 @@ public interface IEnrollProfile extends IProfile {
     public static final String REQUEST_ALGORITHM_PARAMS = "req_algorithm_params";
 
     /**
+     * ID of requested certificate authority (unused for top-level CA)
+     */
+    public static final String REQUEST_AUTHORITY_ID = "req_authority_id";
+
+    /**
      * Set Default X509CertInfo in the request.
      *
      * @param request profile-based certificate request.
diff --git a/base/common/src/com/netscape/certsrv/security/ISigningUnit.java b/base/common/src/com/netscape/certsrv/security/ISigningUnit.java
index 34d2a5109170c560b5a449d08f43eeeda5035b88..75b45bb8b31cde22881e0ddc310c0bdb51a66338 100644
--- a/base/common/src/com/netscape/certsrv/security/ISigningUnit.java
+++ b/base/common/src/com/netscape/certsrv/security/ISigningUnit.java
@@ -17,6 +17,7 @@
 // --- END COPYRIGHT BLOCK ---
 package com.netscape.certsrv.security;
 
+import java.security.PrivateKey;
 import java.security.PublicKey;
 
 import netscape.security.x509.X509CertImpl;
@@ -161,4 +162,11 @@ public interface ISigningUnit {
      * @return public key
      */
     public PublicKey getPublicKey();
+
+    /**
+     * Retrieves the public key associated in this unit.
+     *
+     * @return public key
+     */
+    public PrivateKey getPrivateKey();
 }
diff --git a/base/server/cms/src/com/netscape/cms/profile/common/CAEnrollProfile.java b/base/server/cms/src/com/netscape/cms/profile/common/CAEnrollProfile.java
index d0bfdb8a64ee857a3f5ff544e41de905b4660f55..53edca3a93c28a4fdd6c476bbdd2dc3d83869505 100644
--- a/base/server/cms/src/com/netscape/cms/profile/common/CAEnrollProfile.java
+++ b/base/server/cms/src/com/netscape/cms/profile/common/CAEnrollProfile.java
@@ -29,6 +29,7 @@ import com.netscape.certsrv.apps.CMS;
 import com.netscape.certsrv.authority.IAuthority;
 import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.SessionContext;
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.ca.ICAService;
 import com.netscape.certsrv.ca.ICertificateAuthority;
 import com.netscape.certsrv.connector.IConnector;
@@ -95,8 +96,8 @@ public class CAEnrollProfile extends EnrollProfile {
         CMS.debug("CAEnrollProfile: execute reqId=" +
                 request.getRequestId().toString());
         ICertificateAuthority ca = (ICertificateAuthority) getAuthority();
+
         ICAService caService = (ICAService) ca.getCAService();
-
         if (caService == null) {
             throw new EProfileException("No CA Service");
         }
@@ -190,9 +191,13 @@ public class CAEnrollProfile extends EnrollProfile {
         if (setId != null) {
             sc.put("profileSetId", setId);
         }
+        AuthorityID aid = null;
+        String aidString = request.getExtDataInString(REQUEST_AUTHORITY_ID);
+        if (aidString != null)
+            aid = new AuthorityID(aidString);
         try {
-            theCert = caService.issueX509Cert(info, getId() /* profileId */,
-                    id /* requestId */);
+            theCert = caService.issueX509Cert(
+                aid, info, getId() /* profileId */, id /* requestId */);
         } catch (EBaseException e) {
             CMS.debug(e.toString());
 
diff --git a/base/server/cms/src/com/netscape/cms/profile/common/EnrollProfile.java b/base/server/cms/src/com/netscape/cms/profile/common/EnrollProfile.java
index fe3b424a4b8e13215d4029d328d4a1e280be63ff..523e0117a55567d2f807dd3eb2e69c48d7eb7344 100644
--- a/base/server/cms/src/com/netscape/cms/profile/common/EnrollProfile.java
+++ b/base/server/cms/src/com/netscape/cms/profile/common/EnrollProfile.java
@@ -190,6 +190,9 @@ public abstract class EnrollProfile extends BasicProfile
             if (locale != null) {
                 result[i].setExtData(REQUEST_LOCALE, locale.getLanguage());
             }
+
+            // set requested CA
+            result[i].setExtData(REQUEST_AUTHORITY_ID, ctx.get(REQUEST_AUTHORITY_ID));
         }
         return result;
     }
diff --git a/base/server/cms/src/com/netscape/cms/profile/def/AuthorityKeyIdentifierExtDefault.java b/base/server/cms/src/com/netscape/cms/profile/def/AuthorityKeyIdentifierExtDefault.java
index 095f8bb5ffa2f950b58c868a6daee99991a80daa..bd71a4ef8cf710008fc861a022a553d5064c37ba 100644
--- a/base/server/cms/src/com/netscape/cms/profile/def/AuthorityKeyIdentifierExtDefault.java
+++ b/base/server/cms/src/com/netscape/cms/profile/def/AuthorityKeyIdentifierExtDefault.java
@@ -20,20 +20,23 @@ package com.netscape.cms.profile.def;
 import java.io.IOException;
 import java.util.Locale;
 
-import netscape.security.x509.AuthorityKeyIdentifierExtension;
-import netscape.security.x509.KeyIdentifier;
-import netscape.security.x509.PKIXExtensions;
-import netscape.security.x509.X509CertInfo;
-
 import com.netscape.certsrv.apps.CMS;
 import com.netscape.certsrv.base.IConfigStore;
+import com.netscape.certsrv.ca.AuthorityID;
+import com.netscape.certsrv.ca.ICertificateAuthority;
 import com.netscape.certsrv.profile.EProfileException;
+import com.netscape.certsrv.profile.IEnrollProfile;
 import com.netscape.certsrv.profile.IProfile;
 import com.netscape.certsrv.property.Descriptor;
 import com.netscape.certsrv.property.EPropertyException;
 import com.netscape.certsrv.property.IDescriptor;
 import com.netscape.certsrv.request.IRequest;
 
+import netscape.security.x509.AuthorityKeyIdentifierExtension;
+import netscape.security.x509.KeyIdentifier;
+import netscape.security.x509.PKIXExtensions;
+import netscape.security.x509.X509CertInfo;
+
 /**
  * This class implements an enrollment default policy
  * that populates Authority Key Identifier extension
@@ -161,18 +164,27 @@ public class AuthorityKeyIdentifierExtDefault extends CAEnrollDefault {
      */
     public void populate(IRequest request, X509CertInfo info)
             throws EProfileException {
-        AuthorityKeyIdentifierExtension ext = createExtension(info);
+        ICertificateAuthority ca = (ICertificateAuthority)
+                CMS.getSubsystem(CMS.SUBSYSTEM_CA);
+        String aidString = request.getExtDataInString(
+                IEnrollProfile.REQUEST_AUTHORITY_ID);
+        if (aidString != null)
+            ca = ca.getCA(new AuthorityID(aidString));
+        if (ca == null)
+            throw new EProfileException("Could not reach requested CA");
 
+        AuthorityKeyIdentifierExtension ext = createExtension(ca, info);
         addExtension(PKIXExtensions.AuthorityKey_Id.toString(), ext, info);
     }
 
-    public AuthorityKeyIdentifierExtension createExtension(X509CertInfo info) {
+    public AuthorityKeyIdentifierExtension createExtension(
+            ICertificateAuthority ca, X509CertInfo info) {
         KeyIdentifier kid = null;
         String localKey = getConfig("localKey");
         if (localKey != null && localKey.equals("true")) {
             kid = getKeyIdentifier(info);
         } else {
-            kid = getCAKeyIdentifier();
+            kid = getCAKeyIdentifier(ca);
         }
 
         if (kid == null)
diff --git a/base/server/cms/src/com/netscape/cms/profile/def/CAEnrollDefault.java b/base/server/cms/src/com/netscape/cms/profile/def/CAEnrollDefault.java
index 1d1d05ed55ef30114781521ac607eae118546250..696830ead842767892f77bd8f8c9ea6f667225aa 100644
--- a/base/server/cms/src/com/netscape/cms/profile/def/CAEnrollDefault.java
+++ b/base/server/cms/src/com/netscape/cms/profile/def/CAEnrollDefault.java
@@ -68,9 +68,7 @@ public abstract class CAEnrollDefault extends EnrollDefault {
         return null;
     }
 
-    public KeyIdentifier getCAKeyIdentifier() {
-        ICertificateAuthority ca = (ICertificateAuthority)
-                CMS.getSubsystem(CMS.SUBSYSTEM_CA);
+    public KeyIdentifier getCAKeyIdentifier(ICertificateAuthority ca) {
         X509CertImpl caCert = ca.getCACert();
         if (caCert == null) {
             // during configuration, we dont have the CA certificate
diff --git a/base/server/cms/src/com/netscape/cms/servlet/base/PKIService.java b/base/server/cms/src/com/netscape/cms/servlet/base/PKIService.java
index 4ebf075cb709e813fb6a919c507e9847455e70b2..fe77fd567922a49641938cde99d533c091398b75 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/base/PKIService.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/base/PKIService.java
@@ -56,7 +56,10 @@ public class PKIService {
     public static List<MediaType> MESSAGE_FORMATS = Arrays.asList(
             MediaType.APPLICATION_XML_TYPE,
             MediaType.APPLICATION_JSON_TYPE,
-            MediaType.APPLICATION_FORM_URLENCODED_TYPE
+            MediaType.APPLICATION_FORM_URLENCODED_TYPE,
+            MediaType.valueOf("application/pkix-cert"),
+            MediaType.valueOf("application/pkcs7-mime"),
+            MediaType.valueOf("application/x-pem-file")
     );
 
     public final static int MIN_FILTER_LENGTH = 3;
diff --git a/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java b/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java
index c94ee14961ef39681a53f506b24e4ca5ab06a27e..27d8b8262cb7bbcffa3706cba5318ca8aa0ad75b 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java
@@ -175,7 +175,7 @@ public class CertRequestDAO extends CMSRequestDAO {
             results = processor.processRenewal(data, request);
         } else {
             EnrollmentProcessor processor = new EnrollmentProcessor("caProfileSubmit", locale);
-            results = processor.processEnrollment(data, request);
+            results = processor.processEnrollment(data, request, null);
         }
 
         IRequest reqs[] = (IRequest[]) results.get(CAProcessor.ARG_REQUESTS);
diff --git a/base/server/cms/src/com/netscape/cms/servlet/cert/EnrollmentProcessor.java b/base/server/cms/src/com/netscape/cms/servlet/cert/EnrollmentProcessor.java
index 960f997cd4badd18bdd25393e9175fc935d52edb..e5b9a14df99f29da8ad5c4f76c088c98ff766540 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/cert/EnrollmentProcessor.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/cert/EnrollmentProcessor.java
@@ -30,6 +30,8 @@ import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.EPropertyNotFound;
 import com.netscape.certsrv.base.SessionContext;
 import com.netscape.certsrv.cert.CertEnrollmentRequest;
+import com.netscape.certsrv.ca.AuthorityID;
+import com.netscape.certsrv.profile.IEnrollProfile;
 import com.netscape.certsrv.profile.IProfile;
 import com.netscape.certsrv.profile.IProfileAuthenticator;
 import com.netscape.certsrv.profile.IProfileContext;
@@ -98,7 +100,7 @@ public class EnrollmentProcessor extends CertProcessor {
         }
 
         CertEnrollmentRequest data = CertEnrollmentRequestFactory.create(cmsReq, profile, locale);
-        return processEnrollment(data, cmsReq.getHttpReq());
+        return processEnrollment(data, cmsReq.getHttpReq(), null);
     }
 
     /**
@@ -118,8 +120,11 @@ public class EnrollmentProcessor extends CertProcessor {
      * @param cmsReq the object holding the request and response information
      * @exception EBaseException an error has occurred
      */
-    public HashMap<String, Object> processEnrollment(CertEnrollmentRequest data, HttpServletRequest request)
-            throws EBaseException {
+    public HashMap<String, Object> processEnrollment(
+            CertEnrollmentRequest data,
+            HttpServletRequest request,
+            AuthorityID aid)
+        throws EBaseException {
 
         try {
             if (CMS.debugOn()) {
@@ -146,6 +151,10 @@ public class EnrollmentProcessor extends CertProcessor {
             }
 
             IProfileContext ctx = profile.createContext();
+
+            if (aid != null)
+                ctx.set(IEnrollProfile.REQUEST_AUTHORITY_ID, aid.toString());
+
             CMS.debug("EnrollmentProcessor: set Inputs into profile Context");
             setInputsIntoContext(data, profile, ctx);
 
diff --git a/base/server/cms/src/com/netscape/cms/servlet/cert/RequestProcessor.java b/base/server/cms/src/com/netscape/cms/servlet/cert/RequestProcessor.java
index 2826f477e358a5e16657e985d7f13079cdb14a33..ca2d6f6739906aad06289708a05f0983155b72e8 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/cert/RequestProcessor.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/cert/RequestProcessor.java
@@ -36,6 +36,10 @@ import com.netscape.certsrv.base.BadRequestException;
 import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.EPropertyNotFound;
 import com.netscape.certsrv.base.IConfigStore;
+import com.netscape.certsrv.ca.AuthorityID;
+import com.netscape.certsrv.ca.CADisabledException;
+import com.netscape.certsrv.ca.CANotFoundException;
+import com.netscape.certsrv.ca.ICertificateAuthority;
 import com.netscape.certsrv.cert.CertReviewResponse;
 import com.netscape.certsrv.logging.ILogger;
 import com.netscape.certsrv.profile.EDeferException;
@@ -327,6 +331,31 @@ public class RequestProcessor extends CertProcessor {
     }
 
     /**
+     * Ensure validity of AuthorityID and that CA exists and is enabled.
+     */
+    private void ensureCAEnabled(String aidString) throws EBaseException {
+        AuthorityID aid = null;
+        try {
+            aid = new AuthorityID(aidString);
+        } catch (IllegalArgumentException e) {
+            // this shouldn't happen because request was already accepted
+            throw new BadRequestDataException("Invalid AuthorityID in request data");
+        }
+        ICertificateAuthority ca = (ICertificateAuthority)
+            CMS.getSubsystem("ca");
+        if (ca == null)
+            // this shouldn't happen
+            throw new CANotFoundException("Could not get top-level CA");  // shouldn't happen
+        ca = ca.getCA(aid);
+        if (ca == null)
+            // this shouldn't happen because request was already accepted
+            throw new CANotFoundException("Unknown CA: " + aidString);
+        if (!ca.getAuthorityEnabled())
+            // authority was disabled after request was accepted
+            throw new CADisabledException("CA '" + aidString + "' is disabled");
+    }
+
+    /**
      * Approve request
      * <P>
      *
@@ -346,11 +375,16 @@ public class RequestProcessor extends CertProcessor {
      *                occurred
      */
     private void approveRequest(IRequest req, CertReviewResponse data, IProfile profile, Locale locale)
-            throws EProfileException {
+            throws EBaseException {
         String auditMessage = null;
         String auditSubjectID = auditSubjectID();
         String auditRequesterID = auditRequesterID(req);
 
+        // ensure target CA is enabled
+        String aidString = req.getExtDataInString(IEnrollProfile.REQUEST_AUTHORITY_ID);
+        if (aidString != null)
+            ensureCAEnabled(aidString);
+
         try {
             profile.execute(req);
             req.setRequestStatus(RequestStatus.COMPLETE);
diff --git a/base/server/cms/src/com/netscape/cms/servlet/csadmin/CertUtil.java b/base/server/cms/src/com/netscape/cms/servlet/csadmin/CertUtil.java
index 36b0e4d0d44ec8987856ebaaa3f4919c4a3f7071..c0729d88100e64d06c099bc4f1d73a14bcb9918f 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/csadmin/CertUtil.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/csadmin/CertUtil.java
@@ -434,8 +434,19 @@ public class CertUtil {
                (signingKeyType.equals("dsa") && algorithm.contains("DSA")));
     }
 
+    public static X509CertImpl createLocalCertWithCA(IConfigStore config, X509Key x509key,
+            String prefix, String certTag, String type, ICertificateAuthority ca) throws IOException {
+        return createLocalCert(config, x509key, prefix, certTag, type, ca, null);
+    }
+
     public static X509CertImpl createLocalCert(IConfigStore config, X509Key x509key,
             String prefix, String certTag, String type, Context context) throws IOException {
+        return createLocalCert(config, x509key, prefix, certTag, type, null, context);
+    }
+
+    public static X509CertImpl createLocalCert(IConfigStore config, X509Key x509key,
+            String prefix, String certTag, String type,
+            ICertificateAuthority ca, Context context) throws IOException {
 
         CMS.debug("Creating local certificate... certTag=" + certTag);
         String profile = null;
@@ -446,13 +457,14 @@ public class CertUtil {
         }
 
         X509CertImpl cert = null;
-        ICertificateAuthority ca = null;
         ICertificateRepository cr = null;
         RequestId reqId = null;
         String profileId = null;
         IRequestQueue queue = null;
         IRequest req = null;
 
+        boolean caProvided = ca != null;
+
         try {
             Boolean injectSAN = config.getBoolean(
                                       "service.injectSAN", false);
@@ -468,7 +480,8 @@ public class CertUtil {
             } else {
                 keyAlgorithm = config.getString(prefix + certTag + ".keyalgorithm");
             }
-            ca = (ICertificateAuthority) CMS.getSubsystem(
+            if (!caProvided)
+                ca = (ICertificateAuthority) CMS.getSubsystem(
                     ICertificateAuthority.ID);
             cr = ca.getCertificateRepository();
             BigInteger serialNo = cr.getNextSerialNumber();
@@ -496,9 +509,9 @@ public class CertUtil {
             }
             CMS.debug("Cert Template: " + info.toString());
 
-            String instanceRoot = config.getString("instanceRoot");
+            String instanceRoot = CMS.getConfigStore().getString("instanceRoot");
 
-            String configurationRoot = config.getString("configurationRoot");
+            String configurationRoot = CMS.getConfigStore().getString("configurationRoot");
 
             CertInfoProfile processor = new CertInfoProfile(
                     instanceRoot + configurationRoot + profile);
@@ -541,11 +554,18 @@ public class CertUtil {
 
             processor.populate(req, info);
 
-            String caPriKeyID = config.getString(
-                    prefix + "signing" + ".privkey.id");
-            byte[] keyIDb = CryptoUtil.string2byte(caPriKeyID);
-            PrivateKey caPrik = CryptoUtil.findPrivateKeyFromID(
-                    keyIDb);
+            PrivateKey caPrik = null;
+            if (caProvided) {
+                java.security.PrivateKey pk = ca.getSigningUnit().getPrivateKey();
+                if (!(pk instanceof PrivateKey))
+                    throw new IOException("CA Private key must be a JSS PrivateKey");
+                caPrik = (PrivateKey) pk;
+            } else {
+                String caPriKeyID = config.getString(
+                        prefix + "signing" + ".privkey.id");
+                byte[] keyIDb = CryptoUtil.string2byte(caPriKeyID);
+                caPrik = CryptoUtil.findPrivateKeyFromID(keyIDb);
+            }
 
             if (caPrik == null) {
                 CMS.debug("CertUtil::createSelfSignedCert() - "
diff --git a/base/server/share/conf/schema-authority.ldif b/base/server/share/conf/schema-authority.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..7d261f18fbc9475983bf93b1cddcc184d7f9d178
--- /dev/null
+++ b/base/server/share/conf/schema-authority.ldif
@@ -0,0 +1,8 @@
+dn: cn=schema
+attributeTypes: ( authorityID-oid NAME 'authorityID' DESC 'Authority ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityKeyNickname-oid NAME 'authorityKeyNickname' DESC 'Authority key nickname' SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'user-defined' )
+attributeTypes: ( authorityParentID-oid NAME 'authorityParentID' DESC 'Authority Parent ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityEnabled-oid NAME 'authorityEnabled' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityDN-oid NAME 'authorityDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityParentDN-oid NAME 'authorityParentDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
+objectClasses: ( authority-oid NAME 'authority' DESC 'Certificate Authority' SUP top STRUCTURAL MUST ( cn $ authorityID $ authorityKeyNickname $ authorityEnabled $ authorityDN ) MAY ( authorityParentID $ authorityParentDN $ description ) X-ORIGIN 'user defined' )
diff --git a/base/server/share/conf/schema.ldif b/base/server/share/conf/schema.ldif
index 475758c5d66bf681e589995505a561bf4e4c40ef..a15601ae7a362635bc398b92b9bfda1c72f0dfc8 100644
--- a/base/server/share/conf/schema.ldif
+++ b/base/server/share/conf/schema.ldif
@@ -667,3 +667,16 @@ dn: cn=schema
 changetype: modify
 add: objectClasses
 objectClasses: ( certProfile-oid NAME 'certProfile' DESC 'Certificate profile' SUP top STRUCTURAL MUST cn MAY ( classId $ certProfileConfig ) X-ORIGIN 'user defined' )
+
+dn: cn=schema
+changetype: modify
+add: attributeTypes
+attributeTypes: ( authorityID-oid NAME 'authorityID' DESC 'Authority ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityKeyNickname-oid NAME 'authorityKeyNickname' DESC 'Authority key nickname' SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'user-defined' )
+attributeTypes: ( authorityParentID-oid NAME 'authorityParentID' DESC 'Authority Parent ID' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityEnabled-oid NAME 'authorityEnabled' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityDN-oid NAME 'authorityDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
+attributeTypes: ( authorityParentDN-oid NAME 'authorityParentDN' DESC 'Authority Enabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'user defined' )
+-
+add: objectClasses
+objectClasses: ( authority-oid NAME 'authority' DESC 'Certificate Authority' SUP top STRUCTURAL MUST ( cn $ authorityID $ authorityKeyNickname $ authorityEnabled $ authorityDN ) MAY ( authorityParentID $ authorityParentDN $ description ) X-ORIGIN 'user defined' )
-- 
2.4.3

-------------- next part --------------
From 4c67f33f613c9b4ab5e0a75cc8709b1acaf5c2f4 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Wed, 10 Jun 2015 03:02:35 -0400
Subject: [PATCH] Lightweight CAs: add ca-authority CLI

Add CLI commands for creating, listing and showing lightweight CAs.

Part of: https://fedorahosted.org/pki/ticket/1213
---
 .../certsrv/authority/AuthorityClient.java         | 62 +++++++++++++++
 .../src/com/netscape/certsrv/ca/CAClient.java      |  3 +-
 .../netscape/cmstools/authority/AuthorityCLI.java  | 49 ++++++++++++
 .../cmstools/authority/AuthorityCreateCLI.java     | 89 ++++++++++++++++++++++
 .../cmstools/authority/AuthorityDisableCLI.java    | 56 ++++++++++++++
 .../cmstools/authority/AuthorityEnableCLI.java     | 56 ++++++++++++++
 .../cmstools/authority/AuthorityFindCLI.java       | 62 +++++++++++++++
 .../cmstools/authority/AuthorityShowCLI.java       | 55 +++++++++++++
 .../src/com/netscape/cmstools/cli/CACLI.java       |  2 +
 9 files changed, 433 insertions(+), 1 deletion(-)
 create mode 100644 base/common/src/com/netscape/certsrv/authority/AuthorityClient.java
 create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java
 create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityCreateCLI.java
 create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityDisableCLI.java
 create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityEnableCLI.java
 create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityFindCLI.java
 create mode 100644 base/java-tools/src/com/netscape/cmstools/authority/AuthorityShowCLI.java

diff --git a/base/common/src/com/netscape/certsrv/authority/AuthorityClient.java b/base/common/src/com/netscape/certsrv/authority/AuthorityClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..86de3352e2424211125c146edf759481448a2694
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/authority/AuthorityClient.java
@@ -0,0 +1,62 @@
+//--- 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) 2015 Red Hat, Inc.
+//All rights reserved.
+//--- END COPYRIGHT BLOCK ---
+package com.netscape.certsrv.authority;
+
+import java.net.URISyntaxException;
+import java.util.List;
+
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import com.netscape.certsrv.client.Client;
+import com.netscape.certsrv.client.PKIClient;
+
+/**
+ * @author Fraser Tweedale <ftweedal at redhat.com>
+ */
+public class AuthorityClient extends Client {
+
+    public AuthorityResource proxy;
+
+    public AuthorityClient(PKIClient client, String subsystem) throws URISyntaxException {
+        super(client, subsystem, "authority");
+        proxy = createProxy(AuthorityResource.class);
+    }
+
+    public List<AuthorityData> listCAs() {
+        Response response = proxy.listCAs();
+        GenericType<List<AuthorityData>> type = new GenericType<List<AuthorityData>>() {};
+        return client.getEntity(response, type);
+    }
+
+    public AuthorityData getCA(String caIDString) {
+        Response response = proxy.getCA(caIDString);
+        return client.getEntity(response, AuthorityData.class);
+    }
+
+    public AuthorityData createCA(AuthorityData data) {
+        Response response = proxy.createCA(data);
+        return client.getEntity(response, AuthorityData.class);
+    }
+
+    public AuthorityData modifyCA(AuthorityData data) {
+        Response response = proxy.modifyCA(data.getID(), data);
+        return client.getEntity(response, AuthorityData.class);
+    }
+
+}
diff --git a/base/common/src/com/netscape/certsrv/ca/CAClient.java b/base/common/src/com/netscape/certsrv/ca/CAClient.java
index e1a0a8c02f8a840acbdea924c164020b88557fc4..1fbd2a0b286ed09854373846510c392c5202307a 100644
--- a/base/common/src/com/netscape/certsrv/ca/CAClient.java
+++ b/base/common/src/com/netscape/certsrv/ca/CAClient.java
@@ -26,6 +26,7 @@ import com.netscape.certsrv.group.GroupClient;
 import com.netscape.certsrv.profile.ProfileClient;
 import com.netscape.certsrv.selftests.SelfTestClient;
 import com.netscape.certsrv.user.UserClient;
+import com.netscape.certsrv.authority.AuthorityClient;
 
 public class CAClient extends SubsystemClient {
 
@@ -35,7 +36,7 @@ public class CAClient extends SubsystemClient {
     }
 
     public void init() throws URISyntaxException {
-
+        addClient(new AuthorityClient(client, name));
         addClient(new CertClient(client, name));
         addClient(new GroupClient(client, name));
         addClient(new ProfileClient(client, name));
diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd0609204dd3e757925c6fec9488f293d31b2c9f
--- /dev/null
+++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCLI.java
@@ -0,0 +1,49 @@
+package com.netscape.cmstools.authority;
+
+import com.netscape.certsrv.authority.AuthorityClient;
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.cmstools.cli.CLI;
+import com.netscape.cmstools.cli.MainCLI;
+
+public class AuthorityCLI extends CLI {
+
+    public AuthorityClient authorityClient;
+
+    public AuthorityCLI(CLI parent) {
+        super("authority", "CA management commands", parent);
+
+        addModule(new AuthorityFindCLI(this));
+        addModule(new AuthorityShowCLI(this));
+        addModule(new AuthorityCreateCLI(this));
+        addModule(new AuthorityDisableCLI(this));
+        addModule(new AuthorityEnableCLI(this));
+    }
+
+    public String getFullName() {
+        if (parent instanceof MainCLI) {
+            // do not include MainCLI's name
+            return name;
+        } else {
+            return parent.getFullName() + "-" + name;
+        }
+    }
+
+    public void execute(String[] args) throws Exception {
+        client = parent.getClient();
+        authorityClient = new AuthorityClient(client, "ca");
+        super.execute(args);
+    }
+
+    protected static void printAuthorityData(AuthorityData data) {
+        System.out.println("  Authority DN: " + data.getDN());
+        System.out.println("  ID:           " + data.getID());
+        String parentAID = data.getParentID();
+        if (parentAID != null)
+            System.out.println("  Parent DN:    " + data.getParentID());
+        System.out.println("  Enabled:      " + data.getEnabled());
+        String desc = data.getDescription();
+        if (desc != null)
+            System.out.println("  Description:  " + desc);
+    }
+
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCreateCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCreateCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..9799c7db8eb00d59384754684aea2c3a3bdeec67
--- /dev/null
+++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityCreateCLI.java
@@ -0,0 +1,89 @@
+package com.netscape.cmstools.authority;
+
+import java.util.Arrays;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.ParseException;
+
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.certsrv.ca.AuthorityID;
+import com.netscape.cmstools.cli.CLI;
+
+public class AuthorityCreateCLI extends CLI {
+
+    public AuthorityCLI authorityCLI;
+
+    public AuthorityCreateCLI(AuthorityCLI authorityCLI) {
+        super("create", "Create CAs", authorityCLI);
+        this.authorityCLI = authorityCLI;
+
+        Option optParent = new Option(null, "parent", true, "ID of parent CA");
+        optParent.setArgName("id");
+        options.addOption(optParent);
+
+        Option optDesc = new Option(null, "desc", true, "Optional description");
+        optDesc.setArgName("string");
+        options.addOption(optDesc);
+    }
+
+    public void printHelp() {
+        formatter.printHelp(getFullName() + " <dn>", options);
+    }
+
+    public void execute(String[] args) throws Exception {
+        // Always check for "--help" prior to parsing
+        if (Arrays.asList(args).contains("--help")) {
+            // Display usage
+            printHelp();
+            System.exit(0);
+        }
+
+        CommandLine cmd = null;
+
+        try {
+            cmd = parser.parse(options, args);
+        } catch (ParseException e) {
+            System.err.println("Error: " + e.getMessage());
+            printHelp();
+            System.exit(-1);
+        }
+
+        String[] cmdArgs = cmd.getArgs();
+        if (cmdArgs.length != 1) {
+            if (cmdArgs.length < 1)
+                System.err.println("No DN specified.");
+            else
+                System.err.println("Too many arguments.");
+            printHelp();
+            System.exit(-1);
+        }
+
+        String parentAIDString = null;
+        if (cmd.hasOption("parent")) {
+            parentAIDString = cmd.getOptionValue("parent");
+            try {
+                new AuthorityID(parentAIDString);
+            } catch (IllegalArgumentException e) {
+                System.err.println("Bad CA ID: " + parentAIDString);
+                printHelp();
+                System.exit(-1);
+            }
+        } else {
+            System.err.println("Must specify parent authority");
+            printHelp();
+            System.exit(-1);
+        }
+
+        String desc = null;
+        if (cmd.hasOption("desc"))
+            desc = cmd.getOptionValue("desc");
+
+        String dn = cmdArgs[0];
+        AuthorityData data = new AuthorityData(
+            dn, null, parentAIDString, true /* enabled */, desc);
+        AuthorityData newData = authorityCLI.authorityClient.createCA(data);
+        AuthorityCLI.printAuthorityData(newData);
+    }
+
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityDisableCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityDisableCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3d24439f0dfefc85b210e65abb252ab4c7c7b25
--- /dev/null
+++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityDisableCLI.java
@@ -0,0 +1,56 @@
+package com.netscape.cmstools.authority;
+
+import java.util.Arrays;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.ParseException;
+
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.cmstools.cli.CLI;
+
+public class AuthorityDisableCLI extends CLI {
+
+    public AuthorityCLI authorityCLI;
+
+    public AuthorityDisableCLI(AuthorityCLI authorityCLI) {
+        super("disable", "Disable CAs", authorityCLI);
+        this.authorityCLI = authorityCLI;
+    }
+
+    public void printHelp() {
+        formatter.printHelp(getFullName() + " <ID>", options);
+    }
+
+    public void execute(String[] args) throws Exception {
+        // Always check for "--help" prior to parsing
+        if (Arrays.asList(args).contains("--help")) {
+            // Display usage
+            printHelp();
+            System.exit(0);
+        }
+
+        CommandLine cmd = null;
+
+        try {
+            cmd = parser.parse(options, args);
+        } catch (ParseException e) {
+            System.err.println("Error: " + e.getMessage());
+            printHelp();
+            System.exit(-1);
+        }
+
+        String[] cmdArgs = cmd.getArgs();
+
+        if (cmdArgs.length < 1) {
+            System.err.println("Error: No ID specified.");
+            printHelp();
+            System.exit(-1);
+        }
+
+        AuthorityData data = new AuthorityData(
+            null, cmdArgs[0], null, false, null);
+        data = authorityCLI.authorityClient.modifyCA(data);
+        AuthorityCLI.printAuthorityData(data);
+    }
+
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityEnableCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityEnableCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..23eb9167b761c519b2e1b6ae1f933248476541e0
--- /dev/null
+++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityEnableCLI.java
@@ -0,0 +1,56 @@
+package com.netscape.cmstools.authority;
+
+import java.util.Arrays;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.ParseException;
+
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.cmstools.cli.CLI;
+
+public class AuthorityEnableCLI extends CLI {
+
+    public AuthorityCLI authorityCLI;
+
+    public AuthorityEnableCLI(AuthorityCLI authorityCLI) {
+        super("enable", "Enable CAs", authorityCLI);
+        this.authorityCLI = authorityCLI;
+    }
+
+    public void printHelp() {
+        formatter.printHelp(getFullName() + " <ID>", options);
+    }
+
+    public void execute(String[] args) throws Exception {
+        // Always check for "--help" prior to parsing
+        if (Arrays.asList(args).contains("--help")) {
+            // Display usage
+            printHelp();
+            System.exit(0);
+        }
+
+        CommandLine cmd = null;
+
+        try {
+            cmd = parser.parse(options, args);
+        } catch (ParseException e) {
+            System.err.println("Error: " + e.getMessage());
+            printHelp();
+            System.exit(-1);
+        }
+
+        String[] cmdArgs = cmd.getArgs();
+
+        if (cmdArgs.length < 1) {
+            System.err.println("Error: No ID specified.");
+            printHelp();
+            System.exit(-1);
+        }
+
+        AuthorityData data = new AuthorityData(
+            null, cmdArgs[0], null, true, null);
+        data = authorityCLI.authorityClient.modifyCA(data);
+        AuthorityCLI.printAuthorityData(data);
+    }
+
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityFindCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityFindCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1aa99fc627e8e0ccfd1f12a23610a13dd5cfbbb
--- /dev/null
+++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityFindCLI.java
@@ -0,0 +1,62 @@
+package com.netscape.cmstools.authority;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.ParseException;
+
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.cmstools.cli.CLI;
+import com.netscape.cmstools.cli.MainCLI;
+
+public class AuthorityFindCLI extends CLI {
+
+    public AuthorityCLI authorityCLI;
+
+    public AuthorityFindCLI(AuthorityCLI authorityCLI) {
+        super("find", "Find CAs", authorityCLI);
+        this.authorityCLI = authorityCLI;
+    }
+
+    public void printHelp() {
+        formatter.printHelp(getFullName(), options);
+    }
+
+    public void execute(String[] args) throws Exception {
+        // Always check for "--help" prior to parsing
+        if (Arrays.asList(args).contains("--help")) {
+            // Display usage
+            printHelp();
+            System.exit(0);
+        }
+
+        @SuppressWarnings("unused")
+        CommandLine cmd = null;
+
+        try {
+            cmd = parser.parse(options, args);
+        } catch (ParseException e) {
+            System.err.println("Error: " + e.getMessage());
+            printHelp();
+            System.exit(-1);
+        }
+
+        List<AuthorityData> datas = authorityCLI.authorityClient.listCAs();
+
+        MainCLI.printMessage(datas.size() + " entries matched");
+        if (datas.size() == 0) return;
+
+        boolean first = true;
+        for (AuthorityData data : datas) {
+            if (first)
+                first = false;
+            else
+                System.out.println();
+            AuthorityCLI.printAuthorityData(data);
+        }
+
+        MainCLI.printMessage("Number of entries returned " + datas.size());
+    }
+
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/authority/AuthorityShowCLI.java b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityShowCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..2993430f0a8b2d0750720d2b251cad26a6707751
--- /dev/null
+++ b/base/java-tools/src/com/netscape/cmstools/authority/AuthorityShowCLI.java
@@ -0,0 +1,55 @@
+package com.netscape.cmstools.authority;
+
+import java.util.Arrays;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.ParseException;
+
+import com.netscape.certsrv.authority.AuthorityData;
+import com.netscape.cmstools.cli.CLI;
+
+public class AuthorityShowCLI extends CLI {
+
+    public AuthorityCLI authorityCLI;
+
+    public AuthorityShowCLI(AuthorityCLI authorityCLI) {
+        super("show", "Show CAs", authorityCLI);
+        this.authorityCLI = authorityCLI;
+    }
+
+    public void printHelp() {
+        formatter.printHelp(getFullName() + " <ID>", options);
+    }
+
+    public void execute(String[] args) throws Exception {
+        // Always check for "--help" prior to parsing
+        if (Arrays.asList(args).contains("--help")) {
+            // Display usage
+            printHelp();
+            System.exit(0);
+        }
+
+        CommandLine cmd = null;
+
+        try {
+            cmd = parser.parse(options, args);
+        } catch (ParseException e) {
+            System.err.println("Error: " + e.getMessage());
+            printHelp();
+            System.exit(-1);
+        }
+
+        String[] cmdArgs = cmd.getArgs();
+
+        if (cmdArgs.length < 1) {
+            System.err.println("Error: No ID specified.");
+            printHelp();
+            System.exit(-1);
+        }
+
+        String caIDString = cmdArgs[0];
+        AuthorityData data = authorityCLI.authorityClient.getCA(caIDString);
+        AuthorityCLI.printAuthorityData(data);
+    }
+
+}
diff --git a/base/java-tools/src/com/netscape/cmstools/cli/CACLI.java b/base/java-tools/src/com/netscape/cmstools/cli/CACLI.java
index 17fb4866f38f05f7ead02b6145ef7d09140a90c5..5c41f00c2eb6e393cc95d3b174cb14eefc7307ae 100644
--- a/base/java-tools/src/com/netscape/cmstools/cli/CACLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/cli/CACLI.java
@@ -20,6 +20,7 @@ package com.netscape.cmstools.cli;
 
 import com.netscape.certsrv.ca.CAClient;
 import com.netscape.certsrv.client.Client;
+import com.netscape.cmstools.authority.AuthorityCLI;
 import com.netscape.cmstools.cert.CertCLI;
 import com.netscape.cmstools.group.GroupCLI;
 import com.netscape.cmstools.profile.ProfileCLI;
@@ -37,6 +38,7 @@ public class CACLI extends SubsystemCLI {
     public CACLI(CLI parent) {
         super("ca", "CA management commands", parent);
 
+        addModule(new AuthorityCLI(this));
         addModule(new CertCLI(this));
         addModule(new GroupCLI(this));
         addModule(new KRAConnectorCLI(this));
-- 
2.4.3

-------------- next part --------------
From 874e7860b4dce6f92e9e99c5561a8f725b265d37 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Tue, 1 Sep 2015 09:57:42 -0400
Subject: [PATCH] Lightweight CAs: REST cert request param to specify authority

Add the optional "ca" query parameter for REST cert request
submission.  Also update the ca-cert-request-submit CLI command with
an option to provide an AuthorityID.

Part of: https://fedorahosted.org/pki/ticket/1213
---
 .../src/com/netscape/cms/servlet/test/CATest.java  |  4 +-
 .../server/ca/rest/CertRequestService.java         | 41 ++++++++++++++++++--
 .../src/com/netscape/certsrv/cert/CertClient.java  | 16 +++++++-
 .../netscape/certsrv/cert/CertRequestResource.java |  5 ++-
 .../cmstools/cert/CertRequestSubmitCLI.java        | 44 +++++++++++++++++++++-
 .../cmstools/client/ClientCertRequestCLI.java      |  2 +-
 .../netscape/cms/servlet/cert/CertRequestDAO.java  | 12 ++++--
 7 files changed, 111 insertions(+), 13 deletions(-)

diff --git a/base/ca/functional/src/com/netscape/cms/servlet/test/CATest.java b/base/ca/functional/src/com/netscape/cms/servlet/test/CATest.java
index 15023cad939abb11927abc64fe5916e04cb65661..92c776d17a3993fdd50ecbdfd3e5e3b5692888f2 100644
--- a/base/ca/functional/src/com/netscape/cms/servlet/test/CATest.java
+++ b/base/ca/functional/src/com/netscape/cms/servlet/test/CATest.java
@@ -288,7 +288,7 @@ public class CATest {
     private static void enrollAndApproveCertRequest(CertClient client, CertEnrollmentRequest data) {
         CertRequestInfos reqInfo = null;
         try {
-            reqInfo = client.enrollRequest(data);
+            reqInfo = client.enrollRequest(data, null, null);
         } catch (Exception e) {
             e.printStackTrace();
             log(e.toString());
@@ -308,7 +308,7 @@ public class CATest {
     private static void enrollCertRequest(CertClient client, CertEnrollmentRequest data) {
         CertRequestInfos reqInfo = null;
         try {
-            reqInfo = client.enrollRequest(data);
+            reqInfo = client.enrollRequest(data, null, null);
         } catch (Exception e) {
             e.printStackTrace();
             log(e.toString());
diff --git a/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java b/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java
index 1da1ce1713541e52164e9e8fbcbf39ca2332540d..7cb4ff71e18b6e29bf55c11dc99bbfb9b83dd60f 100644
--- a/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java
+++ b/base/ca/src/org/dogtagpki/server/ca/rest/CertRequestService.java
@@ -18,6 +18,7 @@
 
 package org.dogtagpki.server.ca.rest;
 
+import java.io.IOException;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Enumeration;
@@ -41,8 +42,11 @@ import com.netscape.certsrv.base.BadRequestException;
 import com.netscape.certsrv.base.ConflictingOperationException;
 import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.PKIException;
+import com.netscape.certsrv.base.ResourceNotFoundException;
 import com.netscape.certsrv.base.UnauthorizedException;
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.ca.CADisabledException;
+import com.netscape.certsrv.ca.ICertificateAuthority;
 import com.netscape.certsrv.cert.CertEnrollmentRequest;
 import com.netscape.certsrv.cert.CertRequestInfo;
 import com.netscape.certsrv.cert.CertRequestInfos;
@@ -63,6 +67,7 @@ import com.netscape.certsrv.request.RequestNotFoundException;
 import com.netscape.cms.servlet.base.PKIService;
 import com.netscape.cms.servlet.cert.CertRequestDAO;
 import com.netscape.cmsutil.ldap.LDAPUtil;
+import netscape.security.x509.X500Name;
 
 /**
  * @author alee
@@ -115,13 +120,43 @@ public class CertRequestService extends PKIService implements CertRequestResourc
     }
 
     @Override
-    public Response enrollCert(CertEnrollmentRequest data) {
-
+    public Response enrollCert(CertEnrollmentRequest data, String aidString, String adnString) {
         if (data == null) {
             CMS.debug("enrollCert: data is null");
             throw new BadRequestException("Unable to create enrollment reequest: Invalid input data");
         }
 
+        if (aidString != null && adnString != null)
+            throw new BadRequestException("Cannot provide both issuer-id and issuer-dn");
+
+        AuthorityID aid = null;
+        ICertificateAuthority ca = (ICertificateAuthority)
+            CMS.getSubsystem(CMS.SUBSYSTEM_CA);
+        if (aidString != null) {
+            try {
+                aid = new AuthorityID(aidString);
+            } catch (IllegalArgumentException e) {
+                throw new BadRequestException("invalid AuthorityID: " + aidString);
+            }
+            ca = ca.getCA(aid);
+            if (ca == null)
+                throw new ResourceNotFoundException("CA not found: " + aidString);
+        }
+        if (adnString != null) {
+            X500Name adn = null;
+            try {
+                adn = new X500Name(adnString);
+            } catch (IOException e) {
+                throw new BadRequestException("invalid DN: " + adnString);
+            }
+            ca = ca.getCA(adn);
+            if (ca == null)
+                throw new ResourceNotFoundException("CA not found: " + adnString);
+            aid = ca.getAuthorityID();
+        }
+        if (!ca.getAuthorityEnabled())
+            throw new ConflictingOperationException("CA not enabled: " + aid.toString());
+
         data.setRemoteHost(servletRequest.getRemoteHost());
         data.setRemoteAddr(servletRequest.getRemoteAddr());
 
@@ -129,7 +164,7 @@ public class CertRequestService extends PKIService implements CertRequestResourc
 
         CertRequestInfos infos;
         try {
-            infos = dao.submitRequest(data, servletRequest, uriInfo, getLocale(headers));
+            infos = dao.submitRequest(aid, data, servletRequest, uriInfo, getLocale(headers));
         } catch (EAuthException e) {
             CMS.debug("enrollCert: authentication failed: " + e);
             throw new UnauthorizedException(e.toString());
diff --git a/base/common/src/com/netscape/certsrv/cert/CertClient.java b/base/common/src/com/netscape/certsrv/cert/CertClient.java
index 42b04b7021f0063894c340c177915d799b621ddd..1d4ccd2cf7e83a8ed3b33253b1416110d5504125 100644
--- a/base/common/src/com/netscape/certsrv/cert/CertClient.java
+++ b/base/common/src/com/netscape/certsrv/cert/CertClient.java
@@ -17,16 +17,19 @@
 //--- END COPYRIGHT BLOCK ---
 package com.netscape.certsrv.cert;
 
+import java.io.IOException;
 import java.net.URISyntaxException;
 
 import javax.ws.rs.core.Response;
 
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.client.Client;
 import com.netscape.certsrv.client.PKIClient;
 import com.netscape.certsrv.client.SubsystemClient;
 import com.netscape.certsrv.dbs.certdb.CertId;
 import com.netscape.certsrv.profile.ProfileDataInfos;
 import com.netscape.certsrv.request.RequestId;
+import netscape.security.x509.X500Name;
 
 /**
  * @author Endi S. Dewata
@@ -85,8 +88,17 @@ public class CertClient extends Client {
         return client.getEntity(response, CertRequestInfo.class);
     }
 
-    public CertRequestInfos enrollRequest(CertEnrollmentRequest data) {
-        Response response = certRequestClient.enrollCert(data);
+    public CertRequestInfos enrollRequest(
+            CertEnrollmentRequest data, AuthorityID aid, X500Name adn) {
+        String aidString = aid != null ? aid.toString() : null;
+        String adnString = null;
+        if (adn != null) {
+            try {
+                adnString = adn.toLdapDNString();
+            } catch (IOException e) {
+            }
+        }
+        Response response = certRequestClient.enrollCert(data, aidString, adnString);
         return client.getEntity(response, CertRequestInfos.class);
     }
 
diff --git a/base/common/src/com/netscape/certsrv/cert/CertRequestResource.java b/base/common/src/com/netscape/certsrv/cert/CertRequestResource.java
index 7f08b4af392e3e56419abdad7cb66bd191688222..493f6f53a5a5e30804532305b199d44a66eecd24 100644
--- a/base/common/src/com/netscape/certsrv/cert/CertRequestResource.java
+++ b/base/common/src/com/netscape/certsrv/cert/CertRequestResource.java
@@ -37,7 +37,10 @@ public interface CertRequestResource {
     @POST
     @Path("certrequests")
     @ClientResponseType(entityType=CertRequestInfos.class)
-    public Response enrollCert(CertEnrollmentRequest data);
+    public Response enrollCert(
+        CertEnrollmentRequest data,
+        @QueryParam("issuer-id") String caIDString,
+        @QueryParam("issuer-dn") String caDNString);
 
     /**
      * Used to retrieve cert request info for a specific request
diff --git a/base/java-tools/src/com/netscape/cmstools/cert/CertRequestSubmitCLI.java b/base/java-tools/src/com/netscape/cmstools/cert/CertRequestSubmitCLI.java
index 608490bb73d7df482d87e67e9c15322ddc2e5f5a..e6b59a5d7fcaced100699ea067a8f95b992722f3 100644
--- a/base/java-tools/src/com/netscape/cmstools/cert/CertRequestSubmitCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/cert/CertRequestSubmitCLI.java
@@ -2,18 +2,22 @@ package com.netscape.cmstools.cert;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.Scanner;
 
 import javax.xml.bind.JAXBException;
 
 import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
 import org.apache.commons.cli.ParseException;
 
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.cert.CertEnrollmentRequest;
 import com.netscape.certsrv.cert.CertRequestInfos;
 import com.netscape.cmstools.cli.CLI;
 import com.netscape.cmstools.cli.MainCLI;
+import netscape.security.x509.X500Name;
 
 public class CertRequestSubmitCLI extends CLI {
 
@@ -22,6 +26,14 @@ public class CertRequestSubmitCLI extends CLI {
     public CertRequestSubmitCLI(CertCLI certCLI) {
         super("request-submit", "Submit certificate request", certCLI);
         this.certCLI = certCLI;
+
+        Option optAID = new Option(null, "issuer-id", true, "Authority ID (top-level CA if omitted)");
+        optAID.setArgName("id");
+        options.addOption(optAID);
+
+        Option optADN = new Option(null, "issuer-dn", true, "Authority DN (top-level CA if omitted)");
+        optADN.setArgName("dn");
+        options.addOption(optADN);
     }
 
     public void printHelp() {
@@ -55,9 +67,39 @@ public class CertRequestSubmitCLI extends CLI {
             System.exit(-1);
         }
 
+        AuthorityID aid = null;
+        if (cmd.hasOption("issuer-id")) {
+            String aidString = cmd.getOptionValue("issuer-id");
+            try {
+                aid = new AuthorityID(aidString);
+            } catch (IllegalArgumentException e) {
+                System.err.println("Bad AuthorityID: " + aidString);
+                printHelp();
+                System.exit(-1);
+            }
+        }
+
+        X500Name adn = null;
+        if (cmd.hasOption("issuer-dn")) {
+            String adnString = cmd.getOptionValue("issuer-dn");
+            try {
+                adn = new X500Name(adnString);
+            } catch (IOException e) {
+                System.err.println("Bad DN: " + adnString);
+                printHelp();
+                System.exit(-1);
+            }
+        }
+
+        if (aid != null && adn != null) {
+            System.err.println("--issuer-id and --issuer-dn options are mutually exclusive");
+            printHelp();
+            System.exit(-1);
+        }
+
         try {
             CertEnrollmentRequest erd = getEnrollmentRequest(cmdArgs[0]);
-            CertRequestInfos cri = certCLI.certClient.enrollRequest(erd);
+            CertRequestInfos cri = certCLI.certClient.enrollRequest(erd, aid, adn);
             MainCLI.printMessage("Submitted certificate request");
             CertCLI.printCertRequestInfos(cri);
 
diff --git a/base/java-tools/src/com/netscape/cmstools/client/ClientCertRequestCLI.java b/base/java-tools/src/com/netscape/cmstools/client/ClientCertRequestCLI.java
index e6bd0d98120295ef8e798925f4e9aceb3a0d43f6..db71c8a0f7db4644290efb766178b76668c22377 100644
--- a/base/java-tools/src/com/netscape/cmstools/client/ClientCertRequestCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/client/ClientCertRequestCLI.java
@@ -283,7 +283,7 @@ public class ClientCertRequestCLI extends CLI {
             System.out.println("Sending certificate request.");
         }
 
-        CertRequestInfos infos = certClient.enrollRequest(request);
+        CertRequestInfos infos = certClient.enrollRequest(request, null, null);
 
         MainCLI.printMessage("Submitted certificate request");
         CertCLI.printCertRequestInfos(infos);
diff --git a/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java b/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java
index 27d8b8262cb7bbcffa3706cba5318ca8aa0ad75b..a2e4b583d318ac8412361850d91233b77a447e13 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/cert/CertRequestDAO.java
@@ -30,6 +30,7 @@ import javax.ws.rs.core.UriInfo;
 
 import com.netscape.certsrv.apps.CMS;
 import com.netscape.certsrv.base.EBaseException;
+import com.netscape.certsrv.ca.AuthorityID;
 import com.netscape.certsrv.ca.ICertificateAuthority;
 import com.netscape.certsrv.cert.CertEnrollmentRequest;
 import com.netscape.certsrv.cert.CertRequestInfo;
@@ -164,8 +165,13 @@ public class CertRequestDAO extends CMSRequestDAO {
      * @throws EBaseException
      * @throws ServletException
      */
-    public CertRequestInfos submitRequest(CertEnrollmentRequest data, HttpServletRequest request, UriInfo uriInfo,
-            Locale locale) throws EBaseException {
+    public CertRequestInfos submitRequest(
+            AuthorityID aid,
+            CertEnrollmentRequest data,
+            HttpServletRequest request,
+            UriInfo uriInfo,
+            Locale locale)
+        throws EBaseException {
 
         CertRequestInfos ret = new CertRequestInfos();
 
@@ -175,7 +181,7 @@ public class CertRequestDAO extends CMSRequestDAO {
             results = processor.processRenewal(data, request);
         } else {
             EnrollmentProcessor processor = new EnrollmentProcessor("caProfileSubmit", locale);
-            results = processor.processEnrollment(data, request, null);
+            results = processor.processEnrollment(data, request, aid);
         }
 
         IRequest reqs[] = (IRequest[]) results.get(CAProcessor.ARG_REQUESTS);
-- 
2.4.3



More information about the Pki-devel mailing list