Blob Blame History Raw
From 0a7411f3bf1e65d2e0897c2504f376d8cf0c59af Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata@redhat.com>
Date: Thu, 12 Nov 2015 00:23:26 +0100
Subject: [PATCH] Added support for existing CA case.

The deployment procedure for external CA has been modified
such that it generates the CA CSR before starting the server.
This allows the same procedure to be used to import CA
certificate from an existing server. It also removes the
requirement to keep the server running while waiting to get
the CSR signed by an external CA.

The pki ca-cert-request-submit command has been modified to
provide options to specify the profile name and the CSR which
will be used to create and populate the request object. This
way it's no longer necessary to download the request template
and insert the CSR manually.

A new pki-server subsystem-cert-export command has been added
to export a system certificate, the CSR, and the key. This
command can be used to migrate a system certificate into another
instance.

The man page have been updated to reflect these changes.

The installation code for external CA case has been fixed such
that IPA can detect step 1 completion properly.

The code that handles certificate data conversion has been fixed
to reformat base-64 data for PEM output properly.

The installation summary for step 1 has been updated to provide
more accurate information.

https://fedorahosted.org/pki/ticket/456
---
 base/common/python/pki/nssdb.py                    | 532 +++++++++++++++++++++
 .../certsrv/system/ConfigurationRequest.java       |  12 +
 base/java-tools/man/man1/pki-cert.1                |  23 +-
 .../cmstools/cert/CertRequestSubmitCLI.java        | 170 ++++++-
 .../cms/servlet/csadmin/ConfigurationUtils.java    | 101 ++++
 .../dogtagpki/server/rest/SystemConfigService.java |  38 +-
 base/server/etc/default.cfg                        |  10 +-
 base/server/python/pki/server/__init__.py          |   7 +
 base/server/python/pki/server/cli/subsystem.py     | 126 +++++
 .../python/pki/server/deployment/pkihelper.py      |  77 +--
 .../server/deployment/scriptlets/configuration.py  | 132 ++++-
 .../server/deployment/scriptlets/finalization.py   |  12 +-
 base/server/sbin/pkispawn                          |  39 +-
 13 files changed, 1211 insertions(+), 68 deletions(-)
 create mode 100644 base/common/python/pki/nssdb.py

diff --git a/base/common/python/pki/nssdb.py b/base/common/python/pki/nssdb.py
new file mode 100644
index 0000000000000000000000000000000000000000..44e286853c252675bff9e25c32377aaa86f8cf18
--- /dev/null
+++ b/base/common/python/pki/nssdb.py
@@ -0,0 +1,532 @@
+# Authors:
+#     Endi S. Dewata <edewata@redhat.com>
+#
+# 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.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+
+import base64
+import os
+import shutil
+import subprocess
+import tempfile
+
+
+CSR_HEADER = '-----BEGIN NEW CERTIFICATE REQUEST-----'
+CSR_FOOTER = '-----END NEW CERTIFICATE REQUEST-----'
+
+CERT_HEADER = '-----BEGIN CERTIFICATE-----'
+CERT_FOOTER = '-----END CERTIFICATE-----'
+
+PKCS7_HEADER = '-----BEGIN PKCS7-----'
+PKCS7_FOOTER = '-----END PKCS7-----'
+
+
+def convert_data(data, input_format, output_format, header=None, footer=None):
+
+    if input_format == output_format:
+        return data
+
+    if input_format == 'base64' and output_format == 'pem':
+
+        # join base-64 data into a single line
+        data = data.replace('\r', '').replace('\n', '')
+
+        # re-split the line into fixed-length lines
+        lines = [data[i:i+64] for i in range(0, len(data), 64)]
+
+        # add header and footer
+        return '%s\n%s\n%s\n' % (header, '\n'.join(lines), footer)
+
+    if input_format == 'pem' and output_format == 'base64':
+
+        # join multiple lines into a single line
+        lines = []
+        for line in data.splitlines():
+            line = line.rstrip('\r\n')
+            if line == header:
+                continue
+            if line == footer:
+                continue
+            lines.append(line)
+
+        return ''.join(lines)
+
+    raise Exception('Unable to convert data from %s to %s' % (input_format, output_format))
+
+def convert_csr(csr_data, input_format, output_format):
+
+    return convert_data(csr_data, input_format, output_format, CSR_HEADER, CSR_FOOTER)
+
+def convert_cert(cert_data, input_format, output_format):
+
+    return convert_data(cert_data, input_format, output_format, CERT_HEADER, CERT_FOOTER)
+
+def convert_pkcs7(pkcs7_data, input_format, output_format):
+
+    return convert_data(pkcs7_data, input_format, output_format, PKCS7_HEADER, PKCS7_FOOTER)
+
+def get_file_type(filename):
+
+    with open(filename, 'r') as f:
+        data = f.read()
+
+    if data.startswith(CSR_HEADER):
+        return 'csr'
+
+    if data.startswith(CERT_HEADER):
+        return 'cert'
+
+    if data.startswith(PKCS7_HEADER):
+        return 'pkcs7'
+
+    return None
+
+
+class NSSDatabase(object):
+
+    def __init__(self, directory, token='internal', password=None, password_file=None):
+        self.directory = directory
+        self.token = token
+
+        self.tmpdir = tempfile.mkdtemp()
+
+        if password:
+            self.password_file = os.path.join(self.tmpdir, 'password.txt')
+            with open(self.password_file, 'w') as f:
+                f.write(password)
+
+        elif password_file:
+            self.password_file = password_file
+
+        else:
+            raise Exception('Missing NSS database password')
+
+    def close(self):
+        shutil.rmtree(self.tmpdir)
+
+    def add_cert(self,
+        nickname,
+        cert_file,
+        trust_attributes=',,'):
+
+        cmd = [
+            'certutil',
+            '-A',
+            '-d', self.directory,
+            '-h', self.token,
+            '-f', self.password_file,
+            '-n', nickname,
+            '-i', cert_file,
+            '-t', trust_attributes
+        ]
+
+        subprocess.check_call(cmd)
+
+    def modify_cert(self,
+        nickname,
+        trust_attributes):
+
+        cmd = [
+            'certutil',
+            '-M',
+            '-d', self.directory,
+            '-h', self.token,
+            '-f', self.password_file,
+            '-n', nickname,
+            '-t', trust_attributes
+        ]
+
+        subprocess.check_call(cmd)
+
+    def create_noise(self, noise_file, size=2048):
+
+        subprocess.check_call([
+            'openssl',
+            'rand',
+            '-out', noise_file,
+            str(size)
+        ])
+
+    def create_request(self,
+        subject_dn,
+        request_file,
+        noise_file=None,
+        key_type=None,
+        key_size=None,
+        curve=None,
+        hash_alg=None):
+
+        tmpdir = tempfile.mkdtemp()
+
+        try:
+            if not noise_file:
+                noise_file = os.path.join(tmpdir, 'noise.bin')
+                if key_size:
+                    size = key_size
+                else:
+                    size = 2048
+                self.create_noise(
+                    noise_file=noise_file,
+                    size=size)
+
+            binary_request_file = os.path.join(tmpdir, 'request.bin')
+
+            cmd = [
+                'certutil',
+                '-R',
+                '-d', self.directory,
+                '-h', self.token,
+                '-f', self.password_file,
+                '-s', subject_dn,
+                '-o', binary_request_file,
+                '-z', noise_file
+            ]
+
+            if key_type:
+                cmd.extend(['-k', key_type])
+
+            if key_size:
+                cmd.extend(['-g', str(key_size)])
+
+            if curve:
+                cmd.extend(['-q', curve])
+
+            if hash_alg:
+                cmd.extend(['-Z', hash_alg])
+
+            # generate binary request
+            subprocess.check_call(cmd)
+
+            # encode binary request in base-64
+            b64_request_file = os.path.join(tmpdir, 'request.b64')
+            subprocess.check_call([
+                'BtoA', binary_request_file, b64_request_file])
+
+            # read base-64 request
+            with open(b64_request_file, 'r') as f:
+                b64_request = f.read()
+
+            # add header and footer
+            with open(request_file, 'w') as f:
+                f.write('-----BEGIN NEW CERTIFICATE REQUEST-----\n')
+                f.write(b64_request)
+                f.write('-----END NEW CERTIFICATE REQUEST-----\n')
+
+        finally:
+            shutil.rmtree(tmpdir)
+
+    def create_self_signed_ca_cert(self,
+        subject_dn,
+        request_file,
+        cert_file,
+        serial='1',
+        validity=240):
+
+        cmd = [
+            'certutil',
+            '-C',
+            '-x',
+            '-d', self.directory,
+            '-h', self.token,
+            '-f', self.password_file,
+            '-c', subject_dn,
+            '-a',
+            '-i', request_file,
+            '-o', cert_file,
+            '-m', serial,
+            '-v', str(validity),
+            '--keyUsage', 'digitalSignature,nonRepudiation,certSigning,crlSigning,critical',
+            '-2',
+            '-3',
+            '--extSKID',
+            '--extAIA'
+        ]
+
+        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+        keystroke = ''
+
+        # Is this a CA certificate [y/N]?
+        keystroke += 'y\n'
+
+        # Enter the path length constraint, enter to skip [<0 for unlimited path]:
+        keystroke += '\n'
+
+        # Is this a critical extension [y/N]?
+        keystroke += 'y\n'
+
+        # Enter value for the authKeyID extension [y/N]?
+        keystroke += 'y\n'
+
+        # TODO: generate SHA1 ID (see APolicyRule.formSHA1KeyId())
+        # Enter value for the key identifier fields,enter to omit:
+        keystroke += '2d:7e:83:37:75:5a:fd:0e:8d:52:a3:70:16:93:36:b8:4a:d6:84:9f\n'
+
+        # Select one of the following general name type:
+        keystroke += '0\n'
+
+        # Enter value for the authCertSerial field, enter to omit:
+        keystroke += '\n'
+
+        # Is this a critical extension [y/N]?
+        keystroke += '\n'
+
+        # TODO: generate SHA1 ID (see APolicyRule.formSHA1KeyId())
+        # Adding Subject Key ID extension.
+        # Enter value for the key identifier fields,enter to omit:
+        keystroke += '2d:7e:83:37:75:5a:fd:0e:8d:52:a3:70:16:93:36:b8:4a:d6:84:9f\n'
+
+        # Is this a critical extension [y/N]?
+        keystroke += '\n'
+
+        # Enter access method type for Authority Information Access extension:
+        keystroke += '2\n'
+
+        # Select one of the following general name type:
+        keystroke += '7\n'
+
+        # TODO: replace with actual hostname name and port number
+        # Enter data:
+        keystroke += 'http://server.example.com:8080/ca/ocsp\n'
+
+        # Select one of the following general name type:
+        keystroke += '0\n'
+
+        # Add another location to the Authority Information Access extension [y/N]
+        keystroke += '\n'
+
+        # Is this a critical extension [y/N]?
+        keystroke += '\n'
+
+        p.communicate(keystroke)
+
+        rc = p.wait()
+
+        if rc:
+            raise Exception('Failed to generate self-signed CA certificate. RC: %d' % rc)
+
+    def get_cert(self, nickname, output_format='pem'):
+
+        if output_format == 'pem':
+            output_format_option = '-a'
+
+        elif output_format == 'base64':
+            output_format_option = '-r'
+
+        else:
+            raise Exception('Unsupported output format: %s' % output_format)
+
+        cmd = [
+            'certutil',
+            '-L',
+            '-d', self.directory,
+            '-h', self.token,
+            '-f', self.password_file,
+            '-n', nickname,
+            output_format_option
+        ]
+
+        cert_data = subprocess.check_output(cmd)
+
+        if output_format == 'base64':
+            cert_data = base64.b64encode(cert_data)
+
+        return cert_data
+
+    def remove_cert(self, nickname):
+
+        cmd = [
+            'certutil',
+            '-D',
+            '-d', self.directory,
+            '-h', self.token,
+            '-f', self.password_file,
+            '-n', nickname
+        ]
+
+        subprocess.check_call(cmd)
+
+    def import_cert_chain(self, nickname, cert_chain_file, trust_attributes=None):
+
+        tmpdir = tempfile.mkdtemp()
+
+        try:
+            file_type = get_file_type(cert_chain_file)
+
+            if file_type == 'cert': # import single PEM cert
+                self.add_cert(
+                    nickname=nickname,
+                    cert_file=cert_chain_file,
+                    trust_attributes=trust_attributes)
+                return self.get_cert(
+                    nickname=nickname,
+                    output_format='base64')
+
+            elif file_type == 'pkcs7': # import PKCS #7 cert chain
+                return self.import_pkcs7(
+                    pkcs7_file=cert_chain_file,
+                    nickname=nickname,
+                    trust_attributes=trust_attributes,
+                    output_format='base64')
+
+            else: # import PKCS #7 data without header/footer
+                with open(cert_chain_file, 'r') as f:
+                    base64_data = f.read()
+                pkcs7_data = convert_pkcs7(base64_data, 'base64', 'pem')
+
+                tmp_cert_chain_file = os.path.join(tmpdir, 'cert_chain.p7b')
+                with open(tmp_cert_chain_file, 'w') as f:
+                    f.write(pkcs7_data)
+
+                self.import_pkcs7(
+                    pkcs7_file=tmp_cert_chain_file,
+                    nickname=nickname,
+                    trust_attributes=trust_attributes)
+
+                return base64_data
+
+        finally:
+            shutil.rmtree(tmpdir)
+
+    def import_pkcs7(self, pkcs7_file, nickname, trust_attributes=None, output_format='pem'):
+
+        tmpdir = tempfile.mkdtemp()
+
+        try:
+            # export certs from PKCS #7 into PEM output
+            output = subprocess.check_output([
+                'openssl',
+                'pkcs7',
+                '-print_certs',
+                '-in', pkcs7_file
+            ])
+
+            # parse PEM output into separate PEM certificates
+            certs = []
+            lines = []
+            state = 'header'
+
+            for line in output.splitlines():
+
+                if state == 'header':
+                    if line != CERT_HEADER:
+                        # ignore header lines
+                        pass
+                    else:
+                        # save cert header
+                        lines.append(line)
+                        state = 'body'
+
+                elif state == 'body':
+                    if line != CERT_FOOTER:
+                        # save cert body
+                        lines.append(line)
+                    else:
+                        # save cert footer
+                        lines.append(line)
+
+                        # construct PEM cert
+                        cert = '\n'.join(lines)
+                        certs.append(cert)
+                        lines = []
+                        state = 'header'
+
+            # import PEM certs into NSS database
+            counter = 1
+            for cert in certs:
+
+                cert_file = os.path.join(tmpdir, 'cert%d.pem' % counter)
+                with open(cert_file, 'w') as f:
+                    f.write(cert)
+
+                if counter == 1:
+                    n = nickname
+                else:
+                    n = '%s #%d' % (nickname, counter)
+
+                self.add_cert(n, cert_file, trust_attributes)
+
+                counter += 1
+
+            # convert PKCS #7 data to the requested format
+            with open(pkcs7_file, 'r') as f:
+                data = f.read()
+
+            return convert_pkcs7(data, 'pem', output_format)
+
+        finally:
+            shutil.rmtree(tmpdir)
+
+    def import_pkcs12(self, pkcs12_file, pkcs12_password=None, pkcs12_password_file=None):
+
+        tmpdir = tempfile.mkdtemp()
+
+        try:
+            if pkcs12_password:
+                password_file = os.path.join(tmpdir, 'password.txt')
+                with open(password_file, 'w') as f:
+                    f.write(pkcs12_password)
+
+            elif pkcs12_password_file:
+                password_file = pkcs12_password_file
+
+            else:
+                raise Exception('Missing PKCS #12 password')
+
+            cmd = [
+                'pk12util',
+                '-d', self.directory,
+                '-h', self.token,
+                '-k', self.password_file,
+                '-i', pkcs12_file,
+                '-w', password_file
+            ]
+
+            subprocess.check_call(cmd)
+
+        finally:
+            shutil.rmtree(tmpdir)
+
+    def export_pkcs12(self, pkcs12_file, nickname, pkcs12_password=None, pkcs12_password_file=None):
+
+        tmpdir = tempfile.mkdtemp()
+
+        try:
+            if pkcs12_password:
+                password_file = os.path.join(tmpdir, 'password.txt')
+                with open(password_file, 'w') as f:
+                    f.write(pkcs12_password)
+
+            elif pkcs12_password_file:
+                password_file = pkcs12_password_file
+
+            else:
+                raise Exception('Missing PKCS #12 password')
+
+            cmd = [
+                'pk12util',
+                '-d', self.directory,
+                '-k', self.password_file,
+                '-o', pkcs12_file,
+                '-w', password_file,
+                '-n', nickname
+            ]
+
+            subprocess.check_call(cmd)
+
+        finally:
+            shutil.rmtree(tmpdir)
diff --git a/base/common/src/com/netscape/certsrv/system/ConfigurationRequest.java b/base/common/src/com/netscape/certsrv/system/ConfigurationRequest.java
index 0682ac98f151f5405764636e77971974a91eed8c..f3f0f5072208ffd5dd5983b4b122c3e909c48db3 100644
--- a/base/common/src/com/netscape/certsrv/system/ConfigurationRequest.java
+++ b/base/common/src/com/netscape/certsrv/system/ConfigurationRequest.java
@@ -175,6 +175,9 @@ public class ConfigurationRequest {
     protected String adminCert;
 
     @XmlElement
+    protected Boolean external;
+
+    @XmlElement
     protected String standAlone;
 
     @XmlElement
@@ -739,6 +742,14 @@ public class ConfigurationRequest {
         this.adminCert = adminCert;
     }
 
+    public Boolean isExternal() {
+        return external;
+    }
+
+    public void setExternal(Boolean external) {
+        this.external = external;
+    }
+
     public boolean getStandAlone() {
         return (standAlone != null && standAlone.equalsIgnoreCase("true"));
     }
@@ -930,6 +941,7 @@ public class ConfigurationRequest {
                ", adminCert=" + adminCert +
                ", importAdminCert=" + importAdminCert +
                ", generateServerCert=" + generateServerCert +
+               ", external=" + external +
                ", standAlone=" + standAlone +
                ", stepTwo=" + stepTwo +
                ", authdbBaseDN=" + authdbBaseDN +
diff --git a/base/java-tools/man/man1/pki-cert.1 b/base/java-tools/man/man1/pki-cert.1
index ffa1fea5d0a29a5b425b1fcaa497e8b831251f7f..7ece1ad7bfc277a4093acdee9592d8671b00b6bd 100644
--- a/base/java-tools/man/man1/pki-cert.1
+++ b/base/java-tools/man/man1/pki-cert.1
@@ -191,23 +191,32 @@ To release a certificate that has been placed on hold:
 .B pki <agent authentication> ca-cert-release-hold <certificate ID>
 
 .SS Certificate Requests
-To request a certificate, first generate a certificate request in PKCS #10 or CRMF, and store this request in the XML template file, of the profile type the request relates to.
 
-The list of profiles can be viewed using the CLI command:
+To request a certificate, first generate a certificate signing request (CSR),
+then submit it with a certificate profile. The list of available profiles can
+be viewed using the following command:
 
 .B pki ca-cert-request-profile-find
 
-The XML template file for a profile type can be created by calling the ca-cert-request-profile-show CLI command. For example:
+To generate a CSR, use the certutil, PKCS10Client, or
+CRMFPopClient, and store it into a file.
 
-\fBpki ca-cert-request-profile-show <profileID> \-\-output <file to store the XML template>\fP
+Basic requests can be submitted using the following command:
 
-will store the XML template of the request in the specified output file.
+.B pki ca-cert-request-submit --profile <profile ID> --request-type <type> --csr-file <CSR file> --subject <subject DN>
 
-Then, fill in the values in the XML file and submit the request for review.  This can be done without authentication.
+To submit more advanced requests, download a template of the request file for
+a particular profile using the following command:
+
+.B pki ca-cert-request-profile-show <profile ID> \-\-output <request file>
+
+Then, edit the request file, fill in the input attributes required by the
+profile, and submit the request using the following command:
 
 .B pki ca-cert-request-submit <request file>
 
-Then, an agent needs to review the request by running the following command:
+Depending on the profile, an agent may need to review the request by running
+the following command:
 
 .B pki <agent authentication> ca-cert-request-review <request ID> --file <file to store the certificate 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..7a1f08ff3d51e785bef8b625c17082ed81efbdfd 100644
--- a/base/java-tools/src/com/netscape/cmstools/cert/CertRequestSubmitCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/cert/CertRequestSubmitCLI.java
@@ -3,17 +3,22 @@ package com.netscape.cmstools.cert;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Scanner;
-
-import javax.xml.bind.JAXBException;
+import java.util.Vector;
 
 import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
 import org.apache.commons.cli.ParseException;
 
 import com.netscape.certsrv.cert.CertEnrollmentRequest;
-import com.netscape.certsrv.cert.CertRequestInfos;
+import com.netscape.certsrv.profile.ProfileAttribute;
+import com.netscape.certsrv.profile.ProfileInput;
 import com.netscape.cmstools.cli.CLI;
-import com.netscape.cmstools.cli.MainCLI;
+
+import netscape.ldap.util.DN;
+import netscape.ldap.util.RDN;
 
 public class CertRequestSubmitCLI extends CLI {
 
@@ -22,6 +27,37 @@ public class CertRequestSubmitCLI extends CLI {
     public CertRequestSubmitCLI(CertCLI certCLI) {
         super("request-submit", "Submit certificate request", certCLI);
         this.certCLI = certCLI;
+
+        Option option = new Option(null, "issuer-id", true, "Authority ID (host authority if omitted)");
+        option.setArgName("ID");
+        options.addOption(option);
+
+        option = new Option(null, "issuer-dn", true, "Authority DN (host authority if omitted)");
+        option.setArgName("DN");
+        options.addOption(option);
+
+        option = new Option(null, "username", true, "Username for request authentication");
+        option.setArgName("username");
+        options.addOption(option);
+
+        option = new Option(null, "password", false, "Prompt password for request authentication");
+        options.addOption(option);
+
+        option = new Option(null, "profile", true, "Certificate profile");
+        option.setArgName("profile");
+        options.addOption(option);
+
+        option = new Option(null, "request-type", true, "Request type (default: pkcs10)");
+        option.setArgName("type");
+        options.addOption(option);
+
+        option = new Option(null, "csr-file", true, "File containing the CSR");
+        option.setArgName("path");
+        options.addOption(option);
+
+        option = new Option(null, "subject", true, "Subject DN");
+        option.setArgName("DN");
+        options.addOption(option);
     }
 
     public void printHelp() {
@@ -29,7 +65,7 @@ public class CertRequestSubmitCLI extends CLI {
     }
 
     @Override
-    public void execute(String[] args) {
+    public void execute(String[] args) throws Exception {
         // Always check for "--help" prior to parsing
         if (Arrays.asList(args).contains("--help")) {
             // Display usage
@@ -49,32 +85,124 @@ public class CertRequestSubmitCLI extends CLI {
 
         String[] cmdArgs = cmd.getArgs();
 
-        if (cmdArgs.length < 1) {
-            System.err.println("Error: No filename specified.");
+        String requestFilename = cmdArgs.length > 0 ? cmdArgs[0] : null;
+        String profileID = cmd.getOptionValue("profile");
+
+        if (requestFilename == null && profileID == null) {
+            System.err.println("Error: Missing request file or profile ID.");
             printHelp();
             System.exit(-1);
         }
 
-        try {
-            CertEnrollmentRequest erd = getEnrollmentRequest(cmdArgs[0]);
-            CertRequestInfos cri = certCLI.certClient.enrollRequest(erd);
-            MainCLI.printMessage("Submitted certificate request");
-            CertCLI.printCertRequestInfos(cri);
-
-        } catch (FileNotFoundException e) {
-            System.err.println("Error: " + e.getMessage());
+        if (requestFilename != null && profileID != null) {
+            System.err.println("Error: Request file and profile ID are mutually exclusive.");
+            printHelp();
             System.exit(-1);
+        }
 
-        } catch (JAXBException e) {
-            System.err.println("Error: " + e.getMessage());
-            System.exit(-1);
+        String requestType = cmd.getOptionValue("request-type");
+
+        CertEnrollmentRequest request;
+        if (requestFilename == null) { // if no request file specified, generate new request from profile
+
+            if (verbose) {
+                System.out.println("Retrieving " + profileID + " profile.");
+            }
+
+            request = certCLI.certClient.getEnrollmentTemplate(profileID);
+
+            // set default request type for new request
+            if (requestType == null) requestType = "pkcs10";
+
+        } else { // otherwise, load request from file
+
+            if (verbose) {
+                System.out.println("Loading request from " + requestFilename + ".");
+            }
+
+            String xml = loadFile(requestFilename);
+            request = CertEnrollmentRequest.fromXML(xml);
+        }
+
+        if (requestType != null) {
+
+            if (verbose) {
+                System.out.println("Request type: " + requestType);
+            }
+
+            for (ProfileInput input : request.getInputs()) {
+                ProfileAttribute typeAttr = input.getAttribute("cert_request_type");
+                if (typeAttr != null) {
+                    typeAttr.setValue(requestType);
+                }
+            }
+        }
+
+        String csrFilename = cmd.getOptionValue("csr-file");
+        if (csrFilename != null) {
+
+            String csr = loadFile(csrFilename);
+
+            if (verbose) {
+                System.out.println("CSR:");
+                System.out.println(csr);
+            }
+
+            for (ProfileInput input : request.getInputs()) {
+                ProfileAttribute csrAttr = input.getAttribute("cert_request");
+                if (csrAttr != null) {
+                    csrAttr.setValue(csr);
+                }
+            }
+        }
+
+        String subjectDN = cmd.getOptionValue("subject");
+        if (subjectDN != null) {
+            DN dn = new DN(subjectDN);
+            Vector<?> rdns = dn.getRDNs();
+
+            Map<String, String> subjectAttributes = new HashMap<String, String>();
+            for (int i=0; i< rdns.size(); i++) {
+                RDN rdn = (RDN)rdns.elementAt(i);
+                String type = rdn.getTypes()[0].toLowerCase();
+                String value = rdn.getValues()[0];
+                subjectAttributes.put(type, value);
+            }
+
+            ProfileInput sn = request.getInput("Subject Name");
+            if (sn != null) {
+                if (verbose) System.out.println("Subject Name:");
+
+                for (ProfileAttribute attribute : sn.getAttributes()) {
+                    String name = attribute.getName();
+                    String value = null;
+
+                    if (name.equals("subject")) {
+                        // get the whole subject DN
+                        value = subjectDN;
+
+                    } else if (name.startsWith("sn_")) {
+                        // get value from subject DN
+                        value = subjectAttributes.get(name.substring(3));
+
+                    } else {
+                        // unknown attribute, ignore
+                        if (verbose) System.out.println(" - " + name);
+                        continue;
+                    }
+
+                    if (value == null) continue;
+
+                    if (verbose) System.out.println(" - " + name + ": " + value);
+                    attribute.setValue(value);
+                }
+            }
         }
     }
 
-    private CertEnrollmentRequest getEnrollmentRequest(String fileName) throws JAXBException, FileNotFoundException {
+    private String loadFile(String fileName) throws FileNotFoundException {
         try (Scanner scanner = new Scanner(new File(fileName))) {
-            String xml = scanner.useDelimiter("\\A").next();
-            return CertEnrollmentRequest.fromXML(xml);
+            return scanner.useDelimiter("\\A").next();
         }
     }
 }
diff --git a/base/server/cms/src/com/netscape/cms/servlet/csadmin/ConfigurationUtils.java b/base/server/cms/src/com/netscape/cms/servlet/csadmin/ConfigurationUtils.java
index d99929f20996f1aa7ddbf21d29dfe7b54905354d..f092eacb1b5d0b340dcac06b20793334bf9baa70 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/csadmin/ConfigurationUtils.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/csadmin/ConfigurationUtils.java
@@ -127,6 +127,7 @@ import com.netscape.certsrv.base.EBaseException;
 import com.netscape.certsrv.base.EPropertyNotFound;
 import com.netscape.certsrv.base.IConfigStore;
 import com.netscape.certsrv.base.ISubsystem;
+import com.netscape.certsrv.base.MetaInfo;
 import com.netscape.certsrv.base.PKIException;
 import com.netscape.certsrv.base.ResourceNotFoundException;
 import com.netscape.certsrv.ca.ICertificateAuthority;
@@ -134,6 +135,8 @@ import com.netscape.certsrv.client.ClientConfig;
 import com.netscape.certsrv.client.PKIClient;
 import com.netscape.certsrv.client.PKIConnection;
 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.ICRLIssuingPointRecord;
 import com.netscape.certsrv.key.KeyData;
 import com.netscape.certsrv.ldap.ILdapConnFactory;
@@ -2270,6 +2273,54 @@ public class ConfigurationUtils {
         certObj.setCertChain(certChainStr);
     }
 
+    public static KeyPair loadKeyPair(String nickname) throws Exception {
+
+        CMS.debug("ConfigurationUtils: loadKeyPair(" + nickname + ")");
+
+        CryptoManager cm = CryptoManager.getInstance();
+
+        X509Certificate cert = cm.findCertByNickname(nickname);
+        PublicKey publicKey = cert.getPublicKey();
+        PrivateKey privateKey = cm.findPrivKeyByCert(cert);
+
+        return new KeyPair(publicKey, privateKey);
+    }
+
+    public static void storeKeyPair(IConfigStore config, String tag, KeyPair pair)
+            throws TokenException, EBaseException {
+
+        CMS.debug("ConfigurationUtils: storeKeyPair(" + tag + ")");
+
+        PublicKey publicKey = pair.getPublic();
+
+        if (publicKey instanceof RSAPublicKey) {
+
+            RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
+
+            byte modulus[] = rsaPublicKey.getModulus().toByteArray();
+            config.putString(PCERT_PREFIX + tag + ".pubkey.modulus",
+                    CryptoUtil.byte2string(modulus));
+
+            byte exponent[] = rsaPublicKey.getPublicExponent().toByteArray();
+            config.putString(PCERT_PREFIX + tag + ".pubkey.exponent",
+                    CryptoUtil.byte2string(exponent));
+
+        } else { // ECC
+
+            CMS.debug("ConfigurationUtils: Public key class: " + publicKey.getClass().getName());
+            byte encoded[] = publicKey.getEncoded();
+            config.putString(PCERT_PREFIX + tag + ".pubkey.encoded", CryptoUtil.byte2string(encoded));
+        }
+
+        PrivateKey privateKey = (PrivateKey) pair.getPrivate();
+        byte id[] = privateKey.getUniqueID();
+        String kid = CryptoUtil.byte2string(id);
+        config.putString(PCERT_PREFIX + tag + ".privkey.id", kid);
+
+        String keyAlgo = config.getString(PCERT_PREFIX + tag + ".signingalgorithm");
+        setSigningAlgorithm(tag, keyAlgo, config);
+    }
+
     public static void createECCKeyPair(String token, String curveName, IConfigStore config, String ct)
             throws NoSuchAlgorithmException, NoSuchTokenException, TokenException,
             CryptoManager.NotInitializedException, EPropertyNotFound, EBaseException {
@@ -2832,6 +2883,20 @@ public class ConfigurationUtils {
         }
     }
 
+    public static void loadCertRequest(IConfigStore config, String tag, Cert cert) throws Exception {
+
+        CMS.debug("ConfigurationUtils.loadCertRequest(" + tag + ")");
+
+        String subjectDN = config.getString(PCERT_PREFIX + tag + ".dn");
+        cert.setDN(subjectDN);
+
+        String subsystem = config.getString(PCERT_PREFIX + tag + ".subsystem");
+        String certreq = config.getString(subsystem + "." + tag + ".certreq");
+        String formattedCertreq = CryptoUtil.reqFormat(certreq);
+
+        cert.setRequest(formattedCertreq);
+    }
+
     public static void handleCertRequest(IConfigStore config, String certTag, Cert cert) throws EPropertyNotFound,
             EBaseException, InvalidKeyException, NotInitializedException, TokenException, NoSuchAlgorithmException,
             NoSuchProviderException, CertificateException, SignatureException, IOException {
@@ -2973,6 +3038,42 @@ public class ConfigurationUtils {
         return pubk;
     }
 
+    public static void loadCert(IConfigStore config, Cert cert) throws Exception {
+
+        String tag = cert.getCertTag();
+        CMS.debug("ConfigurationUtils: loadCert(" + tag + ")");
+
+        CryptoManager cm = CryptoManager.getInstance();
+        X509Certificate x509Cert = cm.findCertByNickname(cert.getNickname());
+
+        if (!x509Cert.getSubjectDN().equals(x509Cert.getIssuerDN())) {
+            CMS.debug("ConfigurationUtils: " + tag + " cert is not self-signed");
+
+            String subsystem = config.getString(PCERT_PREFIX + tag + ".subsystem");
+            String certChain = config.getString(subsystem + ".external_ca_chain.cert");
+            cert.setCertChain(certChain);
+
+            return;
+        }
+
+        CMS.debug("ConfigurationUtils: " + tag + " cert is self-signed");
+
+        // When importing existing self-signed CA certificate, create a
+        // certificate record to reserve the serial number. Otherwise it
+        // might conflict with system certificates to be created later.
+
+        X509CertImpl x509CertImpl = new X509CertImpl(x509Cert.getEncoded());
+
+        ICertificateAuthority ca = (ICertificateAuthority) CMS.getSubsystem(ICertificateAuthority.ID);
+        ICertificateRepository cr = ca.getCertificateRepository();
+
+        BigInteger serialNo = x509Cert.getSerialNumber();
+        MetaInfo meta = new MetaInfo();
+
+        ICertRecord record = cr.createCertRecord(serialNo, x509CertImpl, meta);
+        cr.addCertificateRecord(record);
+    }
+
     public static int handleCerts(Cert cert) throws IOException, EBaseException, CertificateException,
             NotInitializedException, TokenException, InvalidKeyException {
         String certTag = cert.getCertTag();
diff --git a/base/server/cms/src/org/dogtagpki/server/rest/SystemConfigService.java b/base/server/cms/src/org/dogtagpki/server/rest/SystemConfigService.java
index e7a99601b473aca6a466873d4a776044d5e16e22..45ab5feaeb00c727157b3ac69a93059eee50c83b 100644
--- a/base/server/cms/src/org/dogtagpki/server/rest/SystemConfigService.java
+++ b/base/server/cms/src/org/dogtagpki/server/rest/SystemConfigService.java
@@ -20,6 +20,7 @@ package org.dogtagpki.server.rest;
 import java.math.BigInteger;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.KeyPair;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.util.ArrayList;
@@ -421,7 +422,13 @@ public class SystemConfigService extends PKIService implements SystemConfigResou
                 }
                 cs.commit(false);
 
-                if (!request.getStepTwo()) {
+                if (request.isExternal() && tag.equals("signing")) { // external/existing CA
+                    // load key pair for existing and externally-signed signing cert
+                    CMS.debug("SystemConfigService: loading signing cert key pair");
+                    KeyPair pair = ConfigurationUtils.loadKeyPair(certData.getNickname());
+                    ConfigurationUtils.storeKeyPair(cs, tag, pair);
+
+                } else if (!request.getStepTwo()) {
                     if (keytype.equals("ecc")) {
                         String curvename = certData.getKeyCurveName() != null ?
                                 certData.getKeyCurveName() : cs.getString("keys.ecc.curve.default");
@@ -444,7 +451,15 @@ public class SystemConfigService extends PKIService implements SystemConfigResou
                 cert.setSubsystem(cs.getString("preop.cert." + tag + ".subsystem"));
                 cert.setType(cs.getString("preop.cert." + tag + ".type"));
 
-                if (!request.getStepTwo()) {
+                if (request.isExternal() && tag.equals("signing")) { // external/existing CA
+
+                    // update configuration for existing or externally-signed signing certificate
+                    String certStr = cs.getString("ca." + tag + ".cert" );
+                    cert.setCert(certStr);
+                    CMS.debug("SystemConfigService: certificate " + tag + ": " + certStr);
+                    ConfigurationUtils.updateConfig(cs, tag);
+
+                } else if (!request.getStepTwo()) {
                     ConfigurationUtils.configCert(null, null, null, cert, null);
 
                 } else {
@@ -466,8 +481,16 @@ public class SystemConfigService extends PKIService implements SystemConfigResou
                     CMS.debug("Step 2:  certStr for '" + tag + "' is " + certStr);
                 }
 
-                // Handle Cert Requests for everything EXCEPT Stand-alone PKI (Step 2)
-                if (request.getStandAlone()) {
+                if (request.isExternal() && tag.equals("signing")) { // external/existing CA
+
+                    CMS.debug("SystemConfigService: Loading cert request for " + tag + " cert");
+                    ConfigurationUtils.loadCertRequest(cs, tag, cert);
+
+                    CMS.debug("SystemConfigService: Loading cert " + tag);
+                    ConfigurationUtils.loadCert(cs, cert);
+
+                } else if (request.getStandAlone()) {
+                    // Handle Cert Requests for everything EXCEPT Stand-alone PKI (Step 2)
                     if (!request.getStepTwo()) {
                         // Stand-alone PKI (Step 1)
                         ConfigurationUtils.handleCertRequest(cs, tag, cert);
@@ -490,6 +513,13 @@ public class SystemConfigService extends PKIService implements SystemConfigResou
                     ConfigurationUtils.updateCloneConfig();
                 }
 
+                if (request.isExternal() && tag.equals("signing")) { // external/existing CA
+                    CMS.debug("SystemConfigService: External CA has signing cert");
+                    hasSigningCert.setValue(true);
+                    certs.add(cert);
+                    continue;
+                }
+
                 // to determine if we have the signing cert when using an external ca
                 // this will only execute on a ca or stand-alone pki
                 String b64 = certData.getCert();
diff --git a/base/server/etc/default.cfg b/base/server/etc/default.cfg
index 58f338692ac4f1c2637b575e27db2bec6254905b..c6b9686db9b2e0245f99db36c34188716e827d83 100644
--- a/base/server/etc/default.cfg
+++ b/base/server/etc/default.cfg
@@ -22,6 +22,7 @@ sensitive_parameters=
     pki_client_pkcs12_password
     pki_clone_pkcs12_password
     pki_ds_password
+    pki_external_pkcs12_password
     pki_one_time_pin
     pki_pin
     pki_replication_password
@@ -363,10 +364,13 @@ pki_req_ext_add=False
 pki_req_ext_oid=1.3.6.1.4.1.311.20.2
 pki_req_ext_critical=False
 pki_req_ext_data=1E0A00530075006200430041
-pki_external_csr_path=%(pki_instance_configuration_path)s/ca_signing.csr
+pki_external_csr_path=
 pki_external_step_two=False
-pki_external_ca_cert_chain_path=%(pki_instance_configuration_path)s/external_ca_chain.cert
-pki_external_ca_cert_path=%(pki_instance_configuration_path)s/external_ca.cert
+pki_external_ca_cert_chain_path=
+pki_external_ca_cert_chain_nickname=caSigningCert External CA
+pki_external_ca_cert_path=
+pki_external_pkcs12_path=
+pki_external_pkcs12_password=
 pki_import_admin_cert=False
 pki_ocsp_signing_key_algorithm=SHA256withRSA
 pki_ocsp_signing_key_size=2048
diff --git a/base/server/python/pki/server/__init__.py b/base/server/python/pki/server/__init__.py
index 89d4acfd5a36d5e58b6e1bbacb4d267d6f1e20c8..4493b59df00adc0dad9b6808fb10c1769634335c 100644
--- a/base/server/python/pki/server/__init__.py
+++ b/base/server/python/pki/server/__init__.py
@@ -33,6 +33,7 @@ import subprocess
 import tempfile
 
 import pki
+import pki.nssdb
 
 INSTANCE_BASE_DIR = '/var/lib/pki'
 REGISTRY_DIR = '/etc/sysconfig/pki'
@@ -303,6 +304,12 @@ class PKIInstance(object):
 
         return password
 
+    def open_nssdb(self, token='internal'):
+        return pki.nssdb.NSSDatabase(
+            directory=self.nssdb_dir,
+            token=token,
+            password=self.get_password(token))
+
     def get_subsystem(self, name):
         for subsystem in self.subsystems:
             if name == subsystem.name:
diff --git a/base/server/python/pki/server/cli/subsystem.py b/base/server/python/pki/server/cli/subsystem.py
index 19db203c0d8a09817131abaabd4e3ac26e288e95..9f82dff756bf98b0a14783fdd48c25d9d406f50b 100644
--- a/base/server/python/pki/server/cli/subsystem.py
+++ b/base/server/python/pki/server/cli/subsystem.py
@@ -23,11 +23,13 @@ from __future__ import absolute_import
 from __future__ import print_function
 import base64
 import getopt
+import getpass
 import nss.nss as nss
 import string
 import sys
 
 import pki.cli
+import pki.nssdb
 import pki.server
 
 
@@ -294,6 +296,7 @@ class SubsystemCertCLI(pki.cli.CLI):
 
         self.add_module(SubsystemCertFindCLI())
         self.add_module(SubsystemCertShowCLI())
+        self.add_module(SubsystemCertExportCLI())
         self.add_module(SubsystemCertUpdateCLI())
 
     @staticmethod
@@ -438,6 +441,129 @@ class SubsystemCertShowCLI(pki.cli.CLI):
         SubsystemCertCLI.print_subsystem_cert(subsystem_cert)
 
 
+class SubsystemCertExportCLI(pki.cli.CLI):
+
+    def __init__(self):
+        super(SubsystemCertExportCLI, self).__init__(
+            'export', 'Export subsystem certificate')
+
+    def usage(self):
+        print('Usage: pki-server subsystem-cert-export [OPTIONS] <subsystem ID> <cert ID>')
+        print()
+        print('  -i, --instance <instance ID>       Instance ID (default: pki-tomcat).')
+        print('      --cert-file <path>             Output file to store the exported certificate in PEM format.')
+        print('      --csr-file <path>              Output file to store the exported CSR in PEM format.')
+        print('      --pkcs12-file <path>           Output file to store the exported certificate and key in PKCS #12 format.')
+        print('      --pkcs12-password <password>   Password for the PKCS #12 file.')
+        print('      --pkcs12-password-file <path>  Input file containing the password for the PKCS #12 file.')
+        print('  -v, --verbose                      Run in verbose mode.')
+        print('      --help                         Show help message.')
+        print()
+
+    def execute(self, argv):
+
+        try:
+            opts, args = getopt.gnu_getopt(argv, 'i:v', [
+                'instance=', 'cert-file=', 'csr-file=',
+                'pkcs12-file=', 'pkcs12-password=', 'pkcs12-password-file=',
+                'verbose', 'help'])
+
+        except getopt.GetoptError as e:
+            print('ERROR: ' + str(e))
+            self.usage()
+            sys.exit(1)
+
+        if len(args) < 1:
+            print('ERROR: missing subsystem ID')
+            self.usage()
+            sys.exit(1)
+
+        if len(args) < 2:
+            print('ERROR: missing cert ID')
+            self.usage()
+            sys.exit(1)
+
+        subsystem_name = args[0]
+        cert_id = args[1]
+        instance_name = 'pki-tomcat'
+        cert_file = None
+        csr_file = None
+        pkcs12_file = None
+        pkcs12_password = None
+        pkcs12_password_file = None
+
+        for o, a in opts:
+            if o in ('-i', '--instance'):
+                instance_name = a
+
+            elif o == '--cert-file':
+                cert_file = a
+
+            elif o == '--csr-file':
+                csr_file = a
+
+            elif o == '--pkcs12-file':
+                pkcs12_file = a
+
+            elif o == '--pkcs12-password':
+                pkcs12_password = a
+
+            elif o == '--pkcs12-password-file':
+                pkcs12_password_file = a
+
+            elif o in ('-v', '--verbose'):
+                self.set_verbose(True)
+
+            elif o == '--help':
+                self.print_help()
+                sys.exit()
+
+            else:
+                print('ERROR: unknown option ' + o)
+                self.usage()
+                sys.exit(1)
+
+        if not cert_file and not csr_file and not pkcs12_file:
+            print('ERROR: missing output file')
+            self.usage()
+            sys.exit(1)
+
+        instance = pki.server.PKIInstance(instance_name)
+        instance.load()
+
+        subsystem = instance.get_subsystem(subsystem_name)
+        subsystem_cert = subsystem.get_subsystem_cert(cert_id)
+
+        if cert_file:
+
+            cert_data = pki.nssdb.convert_cert(subsystem_cert['data'], 'base64', 'pem')
+            with open(cert_file, 'w') as f:
+                f.write(cert_data)
+
+        if csr_file:
+
+            csr_data = pki.nssdb.convert_csr(subsystem_cert['request'], 'base64', 'pem')
+            with open(csr_file, 'w') as f:
+                f.write(csr_data)
+
+        if pkcs12_file:
+
+            if not pkcs12_password and not pkcs12_password_file:
+                pkcs12_password = getpass.getpass(prompt='Enter password for PKCS #12 file: ')
+
+            nssdb = instance.open_nssdb()
+            try:
+                nssdb.export_pkcs12(
+                    pkcs12_file=pkcs12_file,
+                    nickname=subsystem_cert['nickname'],
+                    pkcs12_password=pkcs12_password,
+                    pkcs12_password_file=pkcs12_password_file)
+            finally:
+                nssdb.close()
+
+        self.print_message('Exported %s certificate' % cert_id)
+
+
 class SubsystemCertUpdateCLI(pki.cli.CLI):
 
     def __init__(self):
diff --git a/base/server/python/pki/server/deployment/pkihelper.py b/base/server/python/pki/server/deployment/pkihelper.py
index 5bc4ffab814891aadf0508d0ae20bb8cc315bc90..fcef5a48bbd72eecf5e04975fc7c2401ab41b22c 100644
--- a/base/server/python/pki/server/deployment/pkihelper.py
+++ b/base/server/python/pki/server/deployment/pkihelper.py
@@ -490,15 +490,18 @@ class ConfigurationFile:
         # generic extension support in CSR - for external CA
         self.add_req_ext = config.str2bool(
             self.mdict['pki_req_ext_add'])
+
         self.external = config.str2bool(self.mdict['pki_external'])
+        self.external_step_one = not config.str2bool(self.mdict['pki_external_step_two'])
+        self.external_step_two = not self.external_step_one
+
         if self.external:
             # generic extension support in CSR - for external CA
             if self.add_req_ext:
                 self.req_ext_oid = self.mdict['pki_req_ext_oid']
                 self.req_ext_critical = self.mdict['pki_req_ext_critical']
                 self.req_ext_data = self.mdict['pki_req_ext_data']
-        self.external_step_two = config.str2bool(
-            self.mdict['pki_external_step_two'])
+
         self.skip_configuration = config.str2bool(
             self.mdict['pki_skip_configuration'])
         self.standalone = config.str2bool(self.mdict['pki_standalone'])
@@ -744,8 +747,7 @@ class ConfigurationFile:
             # External CA
             if not self.external_step_two:
                 # External CA (Step 1)
-                self.confirm_data_exists("pki_external_csr_path")
-                self.confirm_missing_file("pki_external_csr_path")
+                # The pki_external_csr_path is optional.
                 # generic extension support in CSR - for external CA
                 if self.add_req_ext:
                     self.confirm_data_exists("pki_req_ext_oid")
@@ -753,10 +755,9 @@ class ConfigurationFile:
                     self.confirm_data_exists("pki_req_ext_data")
             else:
                 # External CA (Step 2)
-                self.confirm_data_exists("pki_external_ca_cert_chain_path")
-                self.confirm_file_exists("pki_external_ca_cert_chain_path")
-                self.confirm_data_exists("pki_external_ca_cert_path")
-                self.confirm_file_exists("pki_external_ca_cert_path")
+                # The pki_external_ca_cert_chain_path and
+                # pki_external_ca_cert_path are optional.
+                pass
         elif not self.skip_configuration and self.standalone:
             if not self.external_step_two:
                 # Stand-alone PKI Admin CSR (Step 1)
@@ -3813,17 +3814,7 @@ class ConfigClient:
             if not isinstance(certs, types.ListType):
                 certs = [certs]
             for cdata in certs:
-                if (self.subsystem == "CA" and self.external and
-                        not self.external_step_two):
-                    # External CA (Step 1)
-                    if cdata['tag'].lower() == "signing":
-                        # Save 'External CA Signing Certificate' CSR (Step 1)
-                        self.save_system_csr(
-                            cdata['request'],
-                            log.PKI_CONFIG_EXTERNAL_CSR_SAVE,
-                            self.mdict['pki_external_csr_path'])
-                        return
-                elif self.standalone and not self.external_step_two:
+                if self.standalone and not self.external_step_two:
                     # Stand-alone PKI (Step 1)
                     if cdata['tag'].lower() == "audit_signing":
                         # Save Stand-alone PKI 'Audit Signing Certificate' CSR
@@ -3990,8 +3981,17 @@ class ConfigClient:
             data.token = self.mdict['pki_token_name']
             data.tokenPassword = self.mdict['pki_token_password']
         data.subsystemName = self.mdict['pki_subsystem_name']
+
+        data.external = self.external
         data.standAlone = self.standalone
-        data.stepTwo = self.external_step_two
+
+        if self.standalone:
+            # standalone installation uses two-step process (ticket #1698)
+            data.stepTwo = self.external_step_two
+
+        else:
+            # other installations use only one step in the configuration servlet
+            data.stepTwo = False
 
         # Cloning parameters
         if self.mdict['pki_instance_type'] == "Tomcat":
@@ -4119,25 +4119,46 @@ class ConfigClient:
                             self.mdict['pki_req_ext_critical']
                         cert1.req_ext_data = \
                             self.mdict['pki_req_ext_data']
-                if self.external_step_two:
-                    # External CA (Step 2) or Stand-alone PKI (Step 2)
-                    if not self.subsystem == "CA":
-                        # Stand-alone PKI (Step 2)
-                        cert1 = pki.system.SystemCertData()
-                        cert1.tag = self.mdict['pki_ca_signing_tag']
-                    # Load the External CA or Stand-alone PKI
+
+                if self.external and self.external_step_two: # external/existing CA step 2
+
+                    # If specified, load the externally-signed CA cert
+                    if self.mdict['pki_external_ca_cert_path']:
+                        self.load_system_cert(
+                            cert1,
+                            log.PKI_CONFIG_EXTERNAL_CA_LOAD,
+                            self.mdict['pki_external_ca_cert_path'])
+
+                    # If specified, load the external CA cert chain
+                    if self.mdict['pki_external_ca_cert_chain_path']:
+                        self.load_system_cert_chain(
+                            cert1,
+                            log.PKI_CONFIG_EXTERNAL_CA_CHAIN_LOAD,
+                            self.mdict['pki_external_ca_cert_chain_path'])
+
+                    systemCerts.append(cert1)
+
+                elif self.standalone and self.external_step_two: # standalone KRA/OCSP step 2
+
+                    cert1 = pki.system.SystemCertData()
+                    cert1.tag = self.mdict['pki_ca_signing_tag']
+
+                    # Load the stand-alone PKI
                     # 'External CA Signing Certificate' (Step 2)
                     self.load_system_cert(
                         cert1,
                         log.PKI_CONFIG_EXTERNAL_CA_LOAD,
                         self.mdict['pki_external_ca_cert_path'])
-                    # Load the External CA or Stand-alone PKI
+
+                    # Load the stand-alone PKI
                     # 'External CA Signing Certificate Chain' (Step 2)
                     self.load_system_cert_chain(
                         cert1,
                         log.PKI_CONFIG_EXTERNAL_CA_CHAIN_LOAD,
                         self.mdict['pki_external_ca_cert_chain_path'])
+
                     systemCerts.append(cert1)
+
                 elif self.subsystem == "CA":
                     # PKI CA or Subordinate CA
                     systemCerts.append(cert1)
diff --git a/base/server/python/pki/server/deployment/scriptlets/configuration.py b/base/server/python/pki/server/deployment/scriptlets/configuration.py
index fbcb1ccaa21001ec57f0fd9e7ffb00e062349ecc..e7b257f7d3eb1178827ed2ef1e211e4a21455d1b 100644
--- a/base/server/python/pki/server/deployment/scriptlets/configuration.py
+++ b/base/server/python/pki/server/deployment/scriptlets/configuration.py
@@ -20,13 +20,18 @@
 #
 
 import json
+import re
 
 # PKI Deployment Imports
 from .. import pkiconfig as config
 from .. import pkimessages as log
 from .. import pkiscriptlet
-import pki.system
+
 import pki.encoder
+import pki.nssdb
+import pki.server
+import pki.system
+import pki.util
 
 
 # PKI Deployment Configuration Scriptlet
@@ -80,6 +85,131 @@ class PkiScriptlet(pkiscriptlet.AbstractBasePkiScriptlet):
             deployer.mdict['pki_client_secmod_database'],
             password_file=deployer.mdict['pki_client_password_conf'])
 
+        instance = pki.server.PKIInstance(deployer.mdict['pki_instance_name'])
+        instance.load()
+
+        subsystem = instance.get_subsystem(deployer.mdict['pki_subsystem'].lower())
+
+        token = deployer.mdict['pki_token_name']
+        nssdb = instance.open_nssdb(token)
+
+        external = deployer.configuration_file.external
+        step_one = deployer.configuration_file.external_step_one
+        step_two = deployer.configuration_file.external_step_two
+
+        try:
+            if external and step_one: # external/existing CA step 1
+
+                key_type = deployer.mdict['pki_ca_signing_key_type']
+                key_alg = deployer.mdict['pki_ca_signing_key_algorithm']
+
+                if key_type == 'rsa':
+                    key_size = int(deployer.mdict['pki_ca_signing_key_size'])
+                    curve = None
+
+                    m = re.match(r'(.*)withRSA', key_alg)
+                    if not m:
+                        raise Exception('Invalid key algorithm: %s' % key_alg)
+                    hash_alg = m.group(1)
+
+                elif key_type == 'ec' or key_type == 'ecc':
+                    key_type = 'ec'
+                    key_size = None
+                    curve = deployer.mdict['pki_ca_signing_key_size']
+
+                    m = re.match(r'(.*)withEC', key_alg)
+                    if not m:
+                        raise Exception('Invalid key algorithm: %s' % key_alg)
+                    hash_alg = m.group(1)
+
+                else:
+                    raise Exception('Invalid key type: %s' % key_type)
+
+                # If filename specified, generate CA cert request and
+                # import it into CS.cfg.
+                external_csr_path = deployer.mdict['pki_external_csr_path']
+                if external_csr_path:
+                    nssdb.create_request(
+                        subject_dn=deployer.mdict['pki_ca_signing_subject_dn'],
+                        request_file=external_csr_path,
+                        key_type=key_type,
+                        key_size=key_size,
+                        curve=curve,
+                        hash_alg=hash_alg)
+                    with open(external_csr_path) as f:
+                        signing_csr = f.read()
+                    signing_csr = pki.nssdb.convert_csr(signing_csr, 'pem', 'base64')
+                    subsystem.config['ca.signing.certreq'] = signing_csr
+
+                # This is needed by IPA to detect step 1 completion.
+                # See is_step_one_done() in ipaserver/install/cainstance.py.
+                subsystem.config['preop.ca.type'] = 'otherca'
+
+                subsystem.save()
+
+            elif external and step_two: # external/existing CA step 2
+
+                # If specified, import existing CA cert request into CS.cfg.
+                external_csr_path = deployer.mdict['pki_external_csr_path']
+                if external_csr_path:
+                    with open(external_csr_path) as f:
+                        signing_csr = f.read()
+                    signing_csr = pki.nssdb.convert_csr(signing_csr, 'pem', 'base64')
+                    subsystem.config['ca.signing.certreq'] = signing_csr
+
+                # If specified, import external CA cert into NSS database.
+                external_ca_cert_chain_nickname = deployer.mdict['pki_external_ca_cert_chain_nickname']
+                external_ca_cert_chain_file = deployer.mdict['pki_external_ca_cert_chain_path']
+                if external_ca_cert_chain_file:
+                    cert_chain = nssdb.import_cert_chain(
+                        nickname=external_ca_cert_chain_nickname,
+                        cert_chain_file=external_ca_cert_chain_file,
+                        trust_attributes='CT,C,C')
+                    subsystem.config['ca.external_ca_chain.cert'] = cert_chain
+
+                # If specified, import externally-signed CA cert into NSS database.
+                signing_nickname = deployer.mdict['pki_ca_signing_nickname']
+                signing_cert_file = deployer.mdict['pki_external_ca_cert_path']
+                if signing_cert_file:
+                    nssdb.add_cert(
+                        nickname=signing_nickname,
+                        cert_file=signing_cert_file,
+                        trust_attributes='CT,C,C')
+
+                # If specified, import CA cert and key from PKCS #12 file into NSS database.
+                pkcs12_file = deployer.mdict['pki_external_pkcs12_path']
+                if pkcs12_file:
+                    pkcs12_password = deployer.mdict['pki_external_pkcs12_password']
+                    nssdb.import_pkcs12(pkcs12_file, pkcs12_password)
+
+                # Export CA cert from NSS database and import it into CS.cfg.
+                signing_cert_data = nssdb.get_cert(
+                    nickname=signing_nickname,
+                    output_format='base64')
+                subsystem.config['ca.signing.nickname'] = signing_nickname
+                subsystem.config['ca.signing.tokenname'] = deployer.mdict['pki_ca_signing_token']
+                subsystem.config['ca.signing.cert'] = signing_cert_data
+                subsystem.config['ca.signing.cacertnickname'] = signing_nickname
+                subsystem.config['ca.signing.defaultSigningAlgorithm'] = deployer.mdict['pki_ca_signing_signing_algorithm']
+
+                subsystem.save()
+
+            else: # self-signed CA
+
+                # To be implemented in ticket #1692.
+
+                # Generate CA cert request.
+                # Self sign CA cert.
+                # Import self-signed CA cert into NSS database.
+
+                pass
+
+        finally:
+            nssdb.close()
+
+        if external and step_one:
+            return self.rv
+
         # Start/Restart this Tomcat PKI Process
         # Optionally prepare to enable a java debugger
         # (e. g. - 'eclipse'):
diff --git a/base/server/python/pki/server/deployment/scriptlets/finalization.py b/base/server/python/pki/server/deployment/scriptlets/finalization.py
index b92965929ffa845ce10a667ff412e4dbcaf2393c..4c98cc4996e74083d1d240913dbaea7951cc593f 100644
--- a/base/server/python/pki/server/deployment/scriptlets/finalization.py
+++ b/base/server/python/pki/server/deployment/scriptlets/finalization.py
@@ -65,9 +65,15 @@ class PkiScriptlet(pkiscriptlet.AbstractBasePkiScriptlet):
         if len(deployer.instance.tomcat_instance_subsystems()) == 1:
             # Modify contents of 'serverCertNick.conf' (if necessary)
             deployer.servercertnick_conf.modify()
-        # Optionally, programmatically 'restart' the configured PKI instance
-        if config.str2bool(deployer.mdict['pki_restart_configured_instance']):
-            deployer.systemd.restart()
+
+        external = config.str2bool(deployer.mdict['pki_external'])
+        step_one = not config.str2bool(deployer.mdict['pki_external_step_two'])
+
+        if not (external and step_one):
+            # Optionally, programmatically 'restart' the configured PKI instance
+            if config.str2bool(deployer.mdict['pki_restart_configured_instance']):
+                deployer.systemd.restart()
+
         # Optionally, 'purge' the entire temporary client infrastructure
         # including the client NSS security databases and password files
         #
diff --git a/base/server/sbin/pkispawn b/base/server/sbin/pkispawn
index bebbf0b771a8a5b24347ffe5f97e0ce713a53e98..6b51acf920ec93c49c69dad8cc34e26ed352fb70 100755
--- a/base/server/sbin/pkispawn
+++ b/base/server/sbin/pkispawn
@@ -611,7 +611,17 @@ def main(argv):
     config.pki_log.debug(pkilogging.log_format(parser.mdict),
                          extra=config.PKI_INDENTATION_LEVEL_0)
 
-    print_install_information(parser.mdict)
+    external = deployer.configuration_file.external
+    step_one = deployer.configuration_file.external_step_one
+
+    if external and step_one:
+        external_csr_path = deployer.mdict['pki_external_csr_path']
+        if external_csr_path:
+            print_external_ca_step_one_information(parser.mdict)
+        else:
+            print_existing_ca_step_one_information(parser.mdict)
+    else:
+        print_install_information(parser.mdict)
 
 
 def set_port(parser, tag, prompt, existing_data):
@@ -621,6 +631,33 @@ def set_port(parser, tag, prompt, existing_data):
         parser.read_text(prompt, config.pki_subsystem, tag)
 
 
+def print_external_ca_step_one_information(mdict):
+
+    print(log.PKI_SPAWN_INFORMATION_HEADER)
+    print("      The %s subsystem of the '%s' instance is still incomplete." %
+          (config.pki_subsystem, mdict['pki_instance_name']))
+    print()
+    print("      A CSR for the CA certificate has been generated at:\n"
+          "            %s"
+          % mdict['pki_external_csr_path'])
+    print()
+    print("      Submit the CSR to an external CA to generate a CA certificate\n"
+          "      for this subsystem. Import the CA certificate and the certificate\n"
+          "      chain, then continue the installation.")
+    print(log.PKI_SPAWN_INFORMATION_FOOTER)
+
+
+def print_existing_ca_step_one_information(mdict):
+
+    print(log.PKI_SPAWN_INFORMATION_HEADER)
+    print("      The %s subsystem of the '%s' instance is still incomplete." %
+          (config.pki_subsystem, mdict['pki_instance_name']))
+    print()
+    print("      Import an existing CA certificate with the key and the CSR, and\n"
+          "      the certificate chain if available, then continue the installation.")
+    print(log.PKI_SPAWN_INFORMATION_FOOTER)
+
+
 def print_install_information(mdict):
 
     skip_configuration = config.str2bool(mdict['pki_skip_configuration'])
-- 
2.4.3