URL: https://github.com/freeipa/freeipa/pull/930
Author: frasertweedale
 Title: #930: install: allow specifying external CA template
Action: opened

PR body:
"""
This PR allow an admin to specify an AD-CS target certificate template
by name or OID, via the new option --external-ca-profile.
The profile may be specified by name (string) or OID + version + optional minor 
version.

https://pagure.io/freeipa/issue/6858

The approach is:

1. preliminary refactor to the IPAOptionParser to allow easily specifying a 
custom
data constructor (this is used for the data type that holds the template 
specifier).

2. refactor to reduce duplication of external CA type enum values.

3. the main thing:
   - add data type for template specifier
   - add ipa-server-install `--external-ca-profile` CLI option
   - update CA installation to add the appropriate *pkispawn* config based on 
the template
    specifier
   - update *ipa-server-install* man page

4. add the `external-ca-profile` option to *ipa-ca-install* and update man page.

**NOTE FOR TESTERS**

*python-cryptography* has a bug parsing long OIDs.  It is fixed as of v1.9 
(f27).
AD-CS creates and uses OIDs long enough to trigger the bug as a matter of 
course.
Apply the following small diff to your *python-cryptography* lib to avoid the 
bug:

https://github.com/frasertweedale/cryptography/blob/effeb600057a93f7cb95df1b609f149201a712ba/src/cryptography/hazmat/backends/openssl/decode_asn1.py#L40-L43

**HOW TO TEST**

1. Install AD-CS in a Windows machine and create a custom profile by copying 
the *SubCA*
profile.

2. Two-step external CA ipa-server-install:

```
$ ipa-server-install \
    --external-ca --external-ca-type=ms-cs \
    
--external-ca-profile=1.3.6.1.4.1.311.21.8.8950086.10656446.2706058.12775672.480128.147.7130143.4405632:1
```

(Use the actual OID of the custom profile).  If everything works, hooray!

3. Start over with ca-less deployment.  Then add CA via ``ipa-ca-install 
--external-ca-... # as before``.
  If everything works, hooray.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/930/head:pr930
git checkout pr930
From 2229e1257b12dab8577f97351b21b5f52fca3c65 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 23 May 2017 19:32:24 +1000
Subject: [PATCH 1/4] cli: simplify parsing of arbitrary types

Add the 'constructor' type to IPAOption to allow parsing arbitrary
types.

When using this type, supply the 'constructor' attribute with the
constructor of the type.  The checker for the 'constructor' type
attempts to construct the data, returning if successful else raising
OptionValueError.

The 'knob' interface remains unchanged but now accepts arbitrary
constructors.

This feature subsumes the '_option_callback' mechanism, which has
been refactored away.

This feature also subsumes the "dn" type in IPAOption, but this
refactor is deferred.

Part of: https://pagure.io/freeipa/issue/6858
---
 ipapython/config.py      | 16 ++++++++++++++--
 ipapython/install/cli.py | 28 ++++------------------------
 2 files changed, 18 insertions(+), 26 deletions(-)

diff --git a/ipapython/config.py b/ipapython/config.py
index 6e53472e08..8393e0d5d5 100644
--- a/ipapython/config.py
+++ b/ipapython/config.py
@@ -79,16 +79,28 @@ def check_dn_option(option, opt, value):
     except Exception as e:
         raise OptionValueError("option %s: invalid DN: %s" % (opt, e))
 
+
+def check_constructor(option, opt, value):
+    con = option.constructor
+    assert con is not None, "Oops! Developer forgot to set 'constructor' kwarg"
+    try:
+        return con(value)
+    except Exception as e:
+        raise OptionValueError("option {} invalid: {}".format(opt, e))
+
+
 class IPAOption(Option):
     """
     optparse.Option subclass with support of options labeled as
     security-sensitive such as passwords.
     """
-    ATTRS = Option.ATTRS + ["sensitive"]
-    TYPES = Option.TYPES + ("ip", "dn")
+    ATTRS = Option.ATTRS + ["sensitive", "constructor"]
+    TYPES = Option.TYPES + ("ip", "dn", "constructor")
     TYPE_CHECKER = copy(Option.TYPE_CHECKER)
     TYPE_CHECKER["ip"] = check_ip_option
     TYPE_CHECKER["dn"] = check_dn_option
+    TYPE_CHECKER["constructor"] = check_constructor
+
 
 class IPAOptionParser(OptionParser):
     """
diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py
index 9dff308d2f..1cac24d50e 100644
--- a/ipapython/install/cli.py
+++ b/ipapython/install/cli.py
@@ -8,7 +8,6 @@
 
 import collections
 import enum
-import functools
 import logging
 import optparse  # pylint: disable=deprecated-module
 import signal
@@ -105,17 +104,6 @@ def uninstall_tool(configurable_class, command_name, log_file_name,
     )
 
 
-def _option_callback(action, option, opt_str, value, parser, opt_type):
-    try:
-        value = opt_type(value)
-    except ValueError as e:
-        raise optparse.OptionValueError(
-            "option {0}: {1}".format(opt_str, e))
-
-    option.take_action(
-        action, option.dest, opt_str, value, parser.values, parser)
-
-
 class ConfigureTool(admintool.AdminTool):
     configurable_class = None
     debug_option = False
@@ -186,24 +174,16 @@ def add_options(cls, parser, positional=False):
                 kwargs['metavar'] = "{{{0}}}".format(
                                                 ",".join(kwargs['choices']))
             else:
-                kwargs['nargs'] = 1
-                kwargs['callback_args'] = (knob_scalar_type,)
+                kwargs['type'] = 'constructor'
+                kwargs['constructor'] = knob_scalar_type
             kwargs['dest'] = name
             if issubclass(knob_type, list):
-                if 'type' not in kwargs:
-                    kwargs['action'] = 'callback'
-                    kwargs['callback'] = (
-                        functools.partial(_option_callback, 'append'))
-                elif kwargs['type'] is None:
+                if kwargs['type'] is None:
                     kwargs['action'] = 'append_const'
                 else:
                     kwargs['action'] = 'append'
             else:
-                if 'type' not in kwargs:
-                    kwargs['action'] = 'callback'
-                    kwargs['callback'] = (
-                        functools.partial(_option_callback, 'store'))
-                elif kwargs['type'] is None:
+                if kwargs['type'] is None:
                     kwargs['action'] = 'store_const'
                 else:
                     kwargs['action'] = 'store'

From 34f08c0429f4a4f05cc2d77643fde04f6c28b99c Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Thu, 18 May 2017 10:18:20 +1000
Subject: [PATCH 2/4] Remove duplicate references to external CA type

Part of: https://pagure.io/freeipa/issue/6858
---
 install/tools/ipa-ca-install           |  2 +-
 ipaserver/install/ca.py                |  7 +------
 ipaserver/install/cainstance.py        | 10 ++++++++--
 ipaserver/install/ipa_cacert_manage.py |  5 +++--
 4 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/install/tools/ipa-ca-install b/install/tools/ipa-ca-install
index fc485c5955..4579492891 100755
--- a/install/tools/ipa-ca-install
+++ b/install/tools/ipa-ca-install
@@ -64,7 +64,7 @@ def parse_options():
                       default=False, help="unattended installation never prompts the user")
     parser.add_option("--external-ca", dest="external_ca", action="store_true",
                       default=False, help="Generate a CSR to be signed by an external CA")
-    ext_cas = ("generic", "ms-cs")
+    ext_cas = tuple(x.value for x in cainstance.ExternalCAType)
     parser.add_option("--external-ca-type", dest="external_ca_type",
                       type="choice", choices=ext_cas,
                       metavar="{{{0}}}".format(",".join(ext_cas)),
diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py
index 6b43af66be..07b1d14352 100644
--- a/ipaserver/install/ca.py
+++ b/ipaserver/install/ca.py
@@ -360,11 +360,6 @@ def uninstall():
         ca_instance.uninstall()
 
 
-class ExternalCAType(enum.Enum):
-    GENERIC = 'generic'
-    MS_CS = 'ms-cs'
-
-
 class CASigningAlgorithm(enum.Enum):
     SHA1_WITH_RSA = 'SHA1withRSA'
     SHA_256_WITH_RSA = 'SHA256withRSA'
@@ -410,7 +405,7 @@ class CAInstallInterface(dogtag.DogtagInstallInterface,
     external_ca = master_install_only(external_ca)
 
     external_ca_type = knob(
-        ExternalCAType, None,
+        cainstance.ExternalCAType, None,
         description="Type of the external CA",
     )
     external_ca_type = master_install_only(external_ca_type)
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index a646ee68ce..eef61e3155 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -25,6 +25,7 @@
 import logging
 
 import dbus
+import enum
 import ldap
 import os
 import pwd
@@ -96,6 +97,11 @@
 ]
 
 
+class ExternalCAType(enum.Enum):
+    GENERIC = 'generic'
+    MS_CS = 'ms-cs'
+
+
 def check_port():
     """
     Check that dogtag port (8443) is available.
@@ -354,7 +360,7 @@ def configure_instance(self, host_name, dm_password, admin_password,
         if ca_type is not None:
             self.ca_type = ca_type
         else:
-            self.ca_type = 'generic'
+            self.ca_type = ExternalCAType.GENERIC.value
         self.no_db_setup = promote
         self.use_ldaps = use_ldaps
 
@@ -572,7 +578,7 @@ def __spawn_instance(self):
             config.set("CA", "pki_external", "True")
             config.set("CA", "pki_external_csr_path", self.csr_file)
 
-            if self.ca_type == 'ms-cs':
+            if self.ca_type == ExternalCAType.MS_CS.value:
                 # Include MS template name extension in the CSR
                 config.set("CA", "pki_req_ext_add", "True")
                 config.set("CA", "pki_req_ext_oid", "1.3.6.1.4.1.311.20.2")
diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py
index 626968d849..e79f5afc2e 100644
--- a/ipaserver/install/ipa_cacert_manage.py
+++ b/ipaserver/install/ipa_cacert_manage.py
@@ -61,7 +61,7 @@ def add_options(cls, parser):
             "--self-signed", dest='self_signed',
             action='store_true',
             help="Sign the renewed certificate by itself")
-        ext_cas = ("generic", "ms-cs")
+        ext_cas = tuple(x.value for x in cainstance.ExternalCAType)
         renew_group.add_option(
             "--external-ca-type", dest="external_ca_type",
             type="choice", choices=ext_cas,
@@ -192,7 +192,8 @@ def renew_self_signed(self, ca):
     def renew_external_step_1(self, ca):
         print("Exporting CA certificate signing request, please wait")
 
-        if self.options.external_ca_type == 'ms-cs':
+        if self.options.external_ca_type \
+                == cainstance.ExternalCAType.MS_CS.value:
             profile = 'SubCA'
         else:
             profile = ''

From 36b0c772d2adeda186ec3d4b28939c3100aadad4 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Wed, 24 May 2017 17:59:46 +1000
Subject: [PATCH 3/4] install: allow specifying external CA template

Allow the MS/AD-CS target certificate template to be specified by
name or OID, via the new option --external-ca-profile.

Part of: https://pagure.io/freeipa/issue/6858
---
 install/tools/man/ipa-server-install.1 |  21 +++-
 ipaserver/install/ca.py                |  28 +++++
 ipaserver/install/cainstance.py        | 186 ++++++++++++++++++++++++++++++++-
 ipaserver/install/server/__init__.py   |   5 +
 4 files changed, 236 insertions(+), 4 deletions(-)

diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1
index 3f46eba0f1..ca1857d1dc 100644
--- a/install/tools/man/ipa-server-install.1
+++ b/install/tools/man/ipa-server-install.1
@@ -87,7 +87,26 @@ The path to LDIF file that will be used to modify configuration of dse.ldif duri
 Generate a CSR for the IPA CA certificate to be signed by an external CA.
 .TP
 \fB\-\-external\-ca\-type\fR=\fITYPE\fR
-Type of the external CA. Possible values are "generic", "ms-cs". Default value is "generic". Use "ms-cs" to include template name required by Microsoft Certificate Services (MS CS) in the generated CSR.
+Type of the external CA. Possible values are "generic", "ms-cs". Default value is "generic". Use "ms-cs" to include the template name required by Microsoft Certificate Services (MS CS) in the generated CSR (see \fB\-\-external\-ca\-profile\fR for full details).
+
+.TP
+\fB\-\-external\-ca\-profile\fR=\fIPROFILE_SPEC\fR
+Specify the certificate profile or template to use at the external CA.
+
+When \fB\-\-external\-ca\-type\fR is "ms-cs" the following specifiers may be used:
+
+.RS
+.TP
+\fB<oid>:<majorVersion>[:<minorVersion>]\fR
+Specify a certificate template by OID and major version, optionally also specifying minor version.
+.TP
+\fB<name>\fR
+Specify a certificate template by name.  The name cannot contain any \fI:\fR characters and cannot be an OID (otherwise the OID-based template specifier syntax takes precedence).
+.TP
+\fBdefault\fR
+If no template is specified, the template name "SubCA" is used.
+.RE
+
 .TP
 \fB\-\-external\-cert\-file\fR=\fIFILE\fR
 File containing the IPA CA certificate and the external CA certificate chain. The file is accepted in PEM and DER certificate and PKCS#7 certificate chain formats. This option may be used multiple times.
diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py
index 07b1d14352..7b989e5ea3 100644
--- a/ipaserver/install/ca.py
+++ b/ipaserver/install/ca.py
@@ -171,6 +171,22 @@ def install_check(standalone, replica_config, options):
                 "remove the file and run the installer again." %
                 paths.ROOT_IPA_CSR)
 
+        if not options.external_ca_type:
+            options.external_ca_type = \
+                cainstance.ExternalCAType.GENERIC.value
+
+        if options.external_ca_profile is not None:
+            # check that profile is valid for the external ca type
+            if options.external_ca_type \
+                    not in options.external_ca_profile.valid_for:
+                raise ScriptError(
+                    "External CA profile specification '{}' "
+                    "cannot be used with external CA type '{}'."
+                    .format(
+                        options.external_ca_profile.unparsed_input,
+                        options.external_ca_type)
+                    )
+
     if not options.external_cert_files:
         if not cainstance.check_port():
             print("IPA requires port 8443 for PKI but it is currently in use.")
@@ -214,11 +230,13 @@ def install_step_0(standalone, replica_config, options):
     host_name = options.host_name
     ca_subject = options._ca_subject
     subject_base = options._subject_base
+    external_ca_profile = None
 
     if replica_config is None:
         ca_signing_algorithm = options.ca_signing_algorithm
         if options.external_ca:
             ca_type = options.external_ca_type
+            external_ca_profile = options.external_ca_profile
             csr_file = paths.ROOT_IPA_CSR
         else:
             ca_type = None
@@ -274,6 +292,7 @@ def install_step_0(standalone, replica_config, options):
                           ca_subject=ca_subject,
                           ca_signing_algorithm=ca_signing_algorithm,
                           ca_type=ca_type,
+                          external_ca_profile=external_ca_profile,
                           csr_file=csr_file,
                           cert_file=cert_file,
                           cert_chain_file=cert_chain_file,
@@ -410,6 +429,15 @@ class CAInstallInterface(dogtag.DogtagInstallInterface,
     )
     external_ca_type = master_install_only(external_ca_type)
 
+    external_ca_profile = knob(
+        type=cainstance.ExternalCAProfile,
+        default=None,
+        description=(
+            "Specify the certificate profile/template to use at the "
+            "external CA"),
+    )
+    external_ca_profile = master_install_only(external_ca_profile)
+
     external_cert_files = knob(
         # pylint: disable=invalid-sequence-index
         typing.List[str], None,
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index eef61e3155..cca4a190cd 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -42,6 +42,9 @@
 from six.moves.configparser import RawConfigParser
 # pylint: enable=import-error
 from cryptography.hazmat.primitives import serialization
+from pyasn1.codec.der import encoder
+from pyasn1.type import char, univ, namedtype
+import pyasn1.error
 
 from ipalib import api
 from ipalib import x509
@@ -325,7 +328,8 @@ def configure_instance(self, host_name, dm_password, admin_password,
                            master_replication_port=None,
                            subject_base=None, ca_subject=None,
                            ca_signing_algorithm=None,
-                           ca_type=None, ra_p12=None, ra_only=False,
+                           ca_type=None, external_ca_profile=None,
+                           ra_p12=None, ra_only=False,
                            promote=False, use_ldaps=False):
         """Create a CA instance.
 
@@ -361,6 +365,8 @@ def configure_instance(self, host_name, dm_password, admin_password,
             self.ca_type = ca_type
         else:
             self.ca_type = ExternalCAType.GENERIC.value
+        self.external_ca_profile = external_ca_profile
+
         self.no_db_setup = promote
         self.use_ldaps = use_ldaps
 
@@ -580,10 +586,16 @@ def __spawn_instance(self):
 
             if self.ca_type == ExternalCAType.MS_CS.value:
                 # Include MS template name extension in the CSR
+                template = self.external_ca_profile
+                if template is None:
+                    # default template name
+                    template = MSCSTemplateV1(u"SubCA")
+
+                ext_data = template.get_ext_data().encode('hex')
                 config.set("CA", "pki_req_ext_add", "True")
-                config.set("CA", "pki_req_ext_oid", "1.3.6.1.4.1.311.20.2")
+                config.set("CA", "pki_req_ext_oid", template.ext_oid)
                 config.set("CA", "pki_req_ext_critical", "False")
-                config.set("CA", "pki_req_ext_data", "1E0A00530075006200430041")
+                config.set("CA", "pki_req_ext_data", ext_data)
 
         elif self.external == 2:
             cert_file = tempfile.NamedTemporaryFile()
@@ -1860,6 +1872,174 @@ def update_ipa_conf():
         parser.write(f)
 
 
+class ExternalCAProfile(object):
+    """
+    An external CA profile configuration.  Currently the only
+    subclasses are for Microsoft CAs, for providing data in the
+    "Certificate Template" extension.
+
+    Constructing this class will actually return an instance of a
+    subclass.
+
+    Subclasses MUST set ``valid_for``.
+
+    """
+    def __init__(self, s=None):
+        self.unparsed_input = s
+
+    # Which external CA types is the data valid for?
+    # A set of VALUES of the ExternalCAType enum.
+    valid_for = set()
+
+    def __new__(cls, s):
+        """Construct the ExternalCAProfile value.
+
+        Return an instance of a subclass determined by
+        the format of the argument.
+
+        """
+        if cls is not ExternalCAProfile:
+            return super(ExternalCAProfile, cls).__new__(cls)
+
+        parts = s.split(':')
+
+        try:
+            # Is the first part on OID?
+            oid = univ.ObjectIdentifier(parts[0])
+
+            # It is; construct a V2 template
+            return MSCSTemplateV2.__new__(MSCSTemplateV2, s)
+
+        except pyasn1.error.PyAsn1Error:
+            # It is not an OID; treat as a template name
+            return MSCSTemplateV1.__new__(MSCSTemplateV1, s)
+
+
+class MSCSTemplate(ExternalCAProfile):
+    """
+    An Microsoft AD-CS Template specifier.
+
+    Subclasses MUST set ext_oid.
+
+    Subclass constructors MUST set asn1obj.
+
+    """
+    valid_for = set([ExternalCAType.MS_CS.value])
+
+    ext_oid = None  # extension OID, as a Python str
+    asn1obj = None  # unencoded extension data
+
+    def get_ext_data(self):
+        """Return DER-encoded extension data."""
+        return encoder.encode(self.asn1obj)
+
+
+class MSCSTemplateV1(MSCSTemplate):
+    """
+    A v1 template specifier, per
+    https://msdn.microsoft.com/en-us/library/cc250011.aspx.
+
+    ::
+
+        CertificateTemplateName ::= SEQUENCE {
+           Name            UTF8String
+        }
+
+    But note that a bare BMPString is used in practice.
+
+    """
+    ext_oid = "1.3.6.1.4.1.311.20.2"
+
+    def __init__(self, s):
+        super(MSCSTemplateV1, self).__init__(s)
+        parts = s.split(':')
+        if len(parts) > 1:
+            raise ValueError(
+                "Cannot specify certificate template version when using name.")
+        self.asn1obj = char.BMPString(six.text_type(parts[0]))
+
+
+class MSCSTemplateV2(MSCSTemplate):
+    """
+    A v2 template specifier, per
+    https://msdn.microsoft.com/en-us/library/windows/desktop/aa378274(v=vs.85).aspx
+
+    ::
+
+        CertificateTemplate ::= SEQUENCE {
+            templateID              EncodedObjectID,
+            templateMajorVersion    TemplateVersion,
+            templateMinorVersion    TemplateVersion OPTIONAL
+        }
+
+        TemplateVersion ::= INTEGER (0..4294967295)
+
+    """
+    ext_oid = "1.3.6.1.4.1.311.21.7"
+
+    @staticmethod
+    def check_version_in_range(desc, n):
+        if n < 0 or n >= 2**32:
+            raise ValueError(
+                "Template {} version must be in range 0..4294967295"
+                .format(desc))
+
+    def __init__(self, s):
+        super(MSCSTemplateV2, self).__init__(s)
+
+        parts = s.split(':')
+
+        obj = CertificateTemplateV2()
+        if len(parts) < 2 or len(parts) > 3:
+            raise ValueError(
+                "Incorrect template specification; required format is: "
+                "<oid>:<majorVersion>[:<minorVersion>]")
+        try:
+            obj['templateID'] = univ.ObjectIdentifier(parts[0])
+
+            major = int(parts[1])
+            self.check_version_in_range("major", major)
+            obj['templateMajorVersion'] = major
+
+            if len(parts) > 2:
+                minor = int(parts[2])
+                self.check_version_in_range("minor", minor)
+                obj['templateMinorVersion'] = int(parts[2])
+
+        except pyasn1.error.PyAsn1Error:
+            raise ValueError("Could not parse certificate template specifier.")
+        self.asn1obj = obj
+
+    # pickle doesn't like CertificateTemplateV2
+    # so implement the pickle protocol directly
+    #
+    def __getstate__(self):
+        state = (
+            self.asn1obj['templateID'],
+            self.asn1obj['templateMajorVersion'],
+            self.asn1obj['templateMinorVersion'],
+            self.unparsed_input,
+        )
+        return state
+
+    def __setstate__(self, state):
+        obj = CertificateTemplateV2()
+        obj['templateID'] = state[0]
+        obj['templateMajorVersion'] = state[1]
+        if state[2] is not None:
+            obj['templateMinorVersion'] = state[2]
+        self.asn1obj = obj
+        self.unparsed_input = state[3]
+
+
+class CertificateTemplateV2(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('templateID', univ.ObjectIdentifier()),
+        namedtype.NamedType('templateMajorVersion', univ.Integer()),
+        namedtype.OptionalNamedType('templateMinorVersion', univ.Integer())
+    )
+
+
 if __name__ == "__main__":
     standard_logging_setup("install.log")
     ds = dsinstance.DsInstance()
diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py
index 028a4aa60f..fe5555b037 100644
--- a/ipaserver/install/server/__init__.py
+++ b/ipaserver/install/server/__init__.py
@@ -437,6 +437,11 @@ def __init__(self, **kwargs):
                     "You cannot specify --external-ca-type without "
                     "--external-ca")
 
+            if self.external_ca_profile and not self.external_ca:
+                raise RuntimeError(
+                    "You cannot specify --external-ca-profile without "
+                    "--external-ca")
+
             if self.uninstalling:
                 if (self.realm_name or self.admin_password or
                         self.master_password):

From e588ebbc4bf2b6711d366e722428ad855620b866 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 25 Jul 2017 17:03:36 +1000
Subject: [PATCH 4/4] ipa-ca-install: add --external-ca-profile option

Fixes: https://pagure.io/freeipa/issue/6858
---
 install/tools/ipa-ca-install       | 10 ++++++++++
 install/tools/man/ipa-ca-install.1 | 21 ++++++++++++++++++++-
 2 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/install/tools/ipa-ca-install b/install/tools/ipa-ca-install
index 4579492891..3bdd7634dc 100755
--- a/install/tools/ipa-ca-install
+++ b/install/tools/ipa-ca-install
@@ -69,6 +69,11 @@ def parse_options():
                       type="choice", choices=ext_cas,
                       metavar="{{{0}}}".format(",".join(ext_cas)),
                       help="Type of the external CA. Default: generic")
+    parser.add_option("--external-ca-profile", dest="external_ca_profile",
+                      type='constructor', constructor=cainstance.ExternalCAProfile,
+                      default=None, metavar="PROFILE-SPEC",
+                      help="Specify the certificate profile/template to use "
+                           "at the external CA")
     parser.add_option("--external-cert-file", dest="external_cert_files",
                       action="append", metavar="FILE",
                       help="File containing the IPA CA certificate and the external CA certificate chain")
@@ -116,6 +121,11 @@ def parse_options():
             parser.error(
                 "You cannot specify --external-ca-type without --external-ca")
 
+        if options.external_ca_profile and not options.external_ca:
+            parser.error(
+                "You cannot specify --external-ca-profile "
+                "without --external-ca")
+
     return safe_options, options, filename
 
 
diff --git a/install/tools/man/ipa-ca-install.1 b/install/tools/man/ipa-ca-install.1
index 79703a47c0..99ff918789 100644
--- a/install/tools/man/ipa-ca-install.1
+++ b/install/tools/man/ipa-ca-install.1
@@ -48,7 +48,26 @@ Admin user Kerberos password used for connection check
 Generate a CSR for the IPA CA certificate to be signed by an external CA.
 .TP
 \fB\-\-external\-ca\-type\fR=\fITYPE\fR
-Type of the external CA. Possible values are "generic", "ms-cs". Default value is "generic". Use "ms-cs" to include template name required by Microsoft Certificate Services (MS CS) in the generated CSR.
+Type of the external CA. Possible values are "generic", "ms-cs". Default value is "generic". Use "ms-cs" to include the template name required by Microsoft Certificate Services (MS CS) in the generated CSR (see \fB\-\-external\-ca\-profile\fR for full details).
+
+.TP
+\fB\-\-external\-ca\-profile\fR=\fIPROFILE_SPEC\fR
+Specify the certificate profile or template to use at the external CA.
+
+When \fB\-\-external\-ca\-type\fR is "ms-cs" the following specifiers may be used:
+
+.RS
+.TP
+\fB<oid>:<majorVersion>[:<minorVersion>]\fR
+Specify a certificate template by OID and major version, optionally also specifying minor version.
+.TP
+\fB<name>\fR
+Specify a certificate template by name.  The name cannot contain any \fI:\fR characters and cannot be an OID (otherwise the OID-based template specifier syntax takes precedence).
+.TP
+\fBdefault\fR
+If no template is specified, the template name "SubCA" is used.
+.RE
+
 .TP
 \fB\-\-external\-cert\-file\fR=\fIFILE\fR
 File containing the IPA CA certificate and the external CA certificate chain. The file is accepted in PEM and DER certificate and PKCS#7 certificate chain formats. This option may be used multiple times.
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org

Reply via email to