From b4365e3a7fa2fb1d4e7ffc41f21c23e3d369ffd6 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Oct 04 2017 08:09:18 +0000 Subject: 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 Reviewed-By: Florence Blanc-Renaud --- diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1 index 3f46eba..ca1857d 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:[:]\fR +Specify a certificate template by OID and major version, optionally also specifying minor version. +.TP +\fB\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 17b6bef..5647651 100644 --- a/ipaserver/install/ca.py +++ b/ipaserver/install/ca.py @@ -174,6 +174,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.") @@ -217,11 +233,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 @@ -277,6 +295,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, @@ -413,6 +432,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 b6fbd08..4a6909a 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -22,6 +22,7 @@ from __future__ import print_function import base64 +import binascii import logging import dbus @@ -41,6 +42,9 @@ import six # pylint: disable=import-error from six.moves.configparser import RawConfigParser # pylint: enable=import-error +from pyasn1.codec.der import encoder +from pyasn1.type import char, univ, namedtype +import pyasn1.error from ipalib import api from ipalib import x509 @@ -324,7 +328,8 @@ class CAInstance(DogtagInstance): 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. @@ -360,6 +365,8 @@ class CAInstance(DogtagInstance): 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 @@ -573,10 +580,16 @@ class CAInstance(DogtagInstance): 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 = binascii.hexlify(template.get_ext_data()) 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.decode('ascii')) elif self.external == 2: cert_file = tempfile.NamedTemporaryFile() @@ -1879,6 +1892,168 @@ 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=None): + """Construct the ExternalCAProfile value. + + Return an instance of a subclass determined by + the format of the argument. + + """ + # we are directly constructing a subclass; instantiate + # it and be done + if cls is not ExternalCAProfile: + return super(ExternalCAProfile, cls).__new__(cls) + + # construction via the base class; therefore the string + # argument is required, and is used to determine which + # subclass to construct + if s is None: + raise ValueError('string argument is required') + + 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) + + def __getstate__(self): + return self.unparsed_input + + def __setstate__(self, state): + # explicitly call __init__ method to initialise object + self.__init__(state) + + +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: " + ":[:]") + 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 + + +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 028a4aa..fe5555b 100644 --- a/ipaserver/install/server/__init__.py +++ b/ipaserver/install/server/__init__.py @@ -437,6 +437,11 @@ class ServerInstallInterface(ServerCertificateInstallInterface, "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):