URL: https://github.com/freeipa/freeipa/pull/1126 Author: pvomacka Title: #1126: Backport PR 930 to ipa-4-6 Action: opened
PR body: """ This PR was opened automatically because PR #930 was pushed to master and backport to ipa-4-6 is required. """ To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/1126/head:pr1126 git checkout pr1126
From 56080e3568174616dd932358b3df0cced85025fd Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Tue, 23 May 2017 19:32:24 +1000 Subject: [PATCH 01/10] 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 c90aa74e4cf830d6bfdd6c84574c3ed44d7196ee Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Thu, 18 May 2017 10:18:20 +1000 Subject: [PATCH 02/10] 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 1f295f73a4..17b6befe27 100644 --- a/ipaserver/install/ca.py +++ b/ipaserver/install/ca.py @@ -363,11 +363,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' @@ -413,7 +408,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 6b3ad3fb1b..b6fbd08ecc 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 @@ -95,6 +96,11 @@ ] +class ExternalCAType(enum.Enum): + GENERIC = 'generic' + MS_CS = 'ms-cs' + + def check_port(): """ Check that dogtag port (8443) is available. @@ -353,7 +359,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 @@ -565,7 +571,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 2d499c1f9b..b227d318bd 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -60,7 +60,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, @@ -191,7 +191,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 08e51ee42f56ff9fc162e21e315dcbb8d5469126 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Wed, 24 May 2017 17:59:46 +1000 Subject: [PATCH 03/10] 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 | 181 ++++++++++++++++++++++++++++++++- ipaserver/install/server/__init__.py | 5 + 4 files changed, 231 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 17b6befe27..5647651701 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 b6fbd08ecc..4a6909a254 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 @@ # 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 @@ 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. @@ -360,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 @@ -573,10 +580,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 = 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: " + "<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 + + +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 cc98c771f5491325eaba3267612cd7005963586d Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Tue, 25 Jul 2017 17:03:36 +1000 Subject: [PATCH 04/10] 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. From c5247edd308f17519116ce3be0e414edff789072 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Tue, 22 Aug 2017 15:39:21 +1000 Subject: [PATCH 05/10] certmonger: refactor 'resubmit_request' and 'modify' certmonger.resubmit_request() and .modify() contain a redundant if statement that means more lines of code must be changed when adding or removing a function argument. Perform a small refactor to improve these functions. Part of: https://pagure.io/freeipa/issue/6858 --- ipalib/install/certmonger.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/ipalib/install/certmonger.py b/ipalib/install/certmonger.py index 7ceeee3f16..2c37899af7 100644 --- a/ipalib/install/certmonger.py +++ b/ipalib/install/certmonger.py @@ -508,14 +508,14 @@ def stop_tracking(secdir=None, request_id=None, nickname=None, certfile=None): def modify(request_id, ca=None, profile=None): - if ca or profile: + update = {} + if ca is not None: + cm = _certmonger() + update['CA'] = cm.obj_if.find_ca_by_nickname(ca) + if profile is not None: + update['template-profile'] = profile + if len(update) > 0: request = _get_request({'nickname': request_id}) - update = {} - if ca is not None: - cm = _certmonger() - update['CA'] = cm.obj_if.find_ca_by_nickname(ca) - if profile is not None: - update['template-profile'] = profile request.obj_if.modify(update) @@ -528,16 +528,17 @@ def resubmit_request(request_id, ca=None, profile=None, is_ca=False): """ request = _get_request({'nickname': request_id}) if request: - if ca or profile or is_ca: - update = {} - if ca is not None: - cm = _certmonger() - update['CA'] = cm.obj_if.find_ca_by_nickname(ca) - if profile is not None: - update['template-profile'] = profile - if is_ca: - update['template-is-ca'] = True - update['template-ca-path-length'] = -1 # no path length + update = {} + if ca is not None: + cm = _certmonger() + update['CA'] = cm.obj_if.find_ca_by_nickname(ca) + if profile is not None: + update['template-profile'] = profile + if is_ca: + update['template-is-ca'] = True + update['template-ca-path-length'] = -1 # no path length + + if len(update) > 0: request.obj_if.modify(update) request.obj_if.resubmit() From bef18583e0f5bf0ca207d26c36b0b1158b26d01e Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Tue, 22 Aug 2017 15:39:53 +1000 Subject: [PATCH 06/10] certmonger: add support for MS V2 template Update certmonger.resubmit_request() and .modify() to support specifying the Microsoft V2 certificate template extension. This feature was introduced in certmonger-0.79.5 so bump the minimum version in the spec file. Part of: https://pagure.io/freeipa/issue/6858 --- freeipa.spec.in | 6 ++---- ipalib/install/certmonger.py | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/freeipa.spec.in b/freeipa.spec.in index 6d992ba151..8b7f179da4 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -320,8 +320,7 @@ Requires(preun): python systemd-units Requires(postun): python systemd-units Requires: policycoreutils >= 2.1.12-5 Requires: tar -# certmonger-0.79.4-2 fixes newlines in PEM files -Requires(pre): certmonger >= 0.79.4-2 +Requires(pre): certmonger >= 0.79.5-1 Requires(pre): 389-ds-base >= 1.3.5.14 Requires: fontawesome-fonts Requires: open-sans-fonts @@ -540,8 +539,7 @@ Requires: libcurl >= 7.21.7-2 Requires: xmlrpc-c >= 1.27.4 Requires: sssd >= 1.14.0 Requires: python-sssdconfig -# certmonger-0.79.4-2 fixes newlines in PEM files -Requires: certmonger >= 0.79.4-2 +Requires: certmonger >= 0.79.5-1 Requires: nss-tools Requires: bind-utils Requires: oddjob-mkhomedir diff --git a/ipalib/install/certmonger.py b/ipalib/install/certmonger.py index 2c37899af7..e52005c2a6 100644 --- a/ipalib/install/certmonger.py +++ b/ipalib/install/certmonger.py @@ -507,23 +507,36 @@ def stop_tracking(secdir=None, request_id=None, nickname=None, certfile=None): request.parent.obj_if.remove_request(request.path) -def modify(request_id, ca=None, profile=None): +def modify(request_id, ca=None, profile=None, template_v2=None): update = {} if ca is not None: cm = _certmonger() update['CA'] = cm.obj_if.find_ca_by_nickname(ca) if profile is not None: update['template-profile'] = profile + if template_v2 is not None: + update['template-ms-certificate-template'] = template_v2 + if len(update) > 0: request = _get_request({'nickname': request_id}) request.obj_if.modify(update) -def resubmit_request(request_id, ca=None, profile=None, is_ca=False): +def resubmit_request( + request_id, + ca=None, + profile=None, + template_v2=None, + is_ca=False): """ :param request_id: the certmonger numeric request ID :param ca: the nickname for the certmonger CA, e.g. IPA or SelfSign - :param profile: the dogtag template profile to use, e.g. SubCA + :param profile: the profile to use, e.g. SubCA. For requests using the + Dogtag CA, this is the profile to use. This also causes + the Microsoft certificate tempalte name extension to the + CSR (for telling AD CS what template to use). + :param template_v2: Microsoft V2 template specifier extension value. + Format: <oid>:<major-version>[:<minor-version>] :param is_ca: boolean that if True adds the CA basic constraint """ request = _get_request({'nickname': request_id}) @@ -534,6 +547,8 @@ def resubmit_request(request_id, ca=None, profile=None, is_ca=False): update['CA'] = cm.obj_if.find_ca_by_nickname(ca) if profile is not None: update['template-profile'] = profile + if template_v2 is not None: + update['template-ms-certificate-template'] = template_v2 if is_ca: update['template-is-ca'] = True update['template-ca-path-length'] = -1 # no path length From 27da63d1196fc880d9798262514c388246511f71 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Tue, 22 Aug 2017 15:40:00 +1000 Subject: [PATCH 07/10] ipa-cacert-manage: support MS V2 template extension Update ipa-cacert-manage to support the MS V2 certificate template extension. Part of: https://pagure.io/freeipa/issue/6858 --- install/tools/man/ipa-cacert-manage.1 | 21 +++++++++++- ipaserver/install/ipa_cacert_manage.py | 63 +++++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/install/tools/man/ipa-cacert-manage.1 b/install/tools/man/ipa-cacert-manage.1 index 03172814ff..bacd56b5a8 100644 --- a/install/tools/man/ipa-cacert-manage.1 +++ b/install/tools/man/ipa-cacert-manage.1 @@ -79,7 +79,26 @@ Sign the renewed certificate by itself. Sign the renewed certificate by 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/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py index b227d318bd..bff1678b05 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -60,6 +60,10 @@ def add_options(cls, parser): "--self-signed", dest='self_signed', action='store_true', help="Sign the renewed certificate by itself") + renew_group.add_option( + "--external-ca", dest='self_signed', + action='store_false', + help="Sign the renewed certificate by external CA") ext_cas = tuple(x.value for x in cainstance.ExternalCAType) renew_group.add_option( "--external-ca-type", dest="external_ca_type", @@ -67,9 +71,11 @@ def add_options(cls, parser): metavar="{{{0}}}".format(",".join(ext_cas)), help="Type of the external CA. Default: generic") renew_group.add_option( - "--external-ca", dest='self_signed', - action='store_false', - help="Sign the renewed certificate by external CA") + "--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") renew_group.add_option( "--external-cert-file", dest="external_cert_files", action="append", metavar="FILE", @@ -179,6 +185,12 @@ def renew(self): def renew_self_signed(self, ca): print("Renewing CA certificate, please wait") + msg = "You cannot specify {} when renewing a self-signed CA" + if self.options.external_ca_type: + raise admintool.ScriptError(msg.format("--external-ca-type")) + if self.options.external_ca_profile: + raise admintool.ScriptError(msg.format("--external-ca-profile")) + try: ca.set_renewal_master() except errors.NotFound: @@ -191,13 +203,30 @@ 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 \ - == cainstance.ExternalCAType.MS_CS.value: - profile = 'SubCA' - else: - profile = '' + options = self.options + + if not options.external_ca_type: + options.external_ca_type = cainstance.ExternalCAType.GENERIC.value + + if options.external_ca_type == cainstance.ExternalCAType.MS_CS.value \ + and options.external_ca_profile is None: + options.external_ca_profile = cainstance.MSCSTemplateV1(u"SubCA") - self.resubmit_request('dogtag-ipa-ca-renew-agent-reuse', profile) + 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 admintool.ScriptError( + "External CA profile specification '{}' " + "cannot be used with external CA type '{}'." + .format( + options.external_ca_profile.unparsed_input, + options.external_ca_type) + ) + + self.resubmit_request( + 'dogtag-ipa-ca-renew-agent-reuse', + profile=options.external_ca_profile) print(("The next step is to get %s signed by your CA and re-run " "ipa-cacert-manage as:" % paths.IPA_CA_CSR)) @@ -299,12 +328,20 @@ def renew_external_step_2(self, ca, old_cert): print("CA certificate successfully renewed") - def resubmit_request(self, ca='dogtag-ipa-ca-renew-agent', profile=''): + def resubmit_request(self, ca='dogtag-ipa-ca-renew-agent', profile=None): timeout = api.env.startup_timeout + 60 + cm_profile = None + if isinstance(profile, cainstance.MSCSTemplateV1): + cm_profile = profile.unparsed_input + + cm_template = None + if isinstance(profile, cainstance.MSCSTemplateV2): + cm_template = profile.unparsed_input + logger.debug("resubmitting certmonger request '%s'", self.request_id) - certmonger.resubmit_request(self.request_id, ca=ca, profile=profile, - is_ca=True) + certmonger.resubmit_request(self.request_id, ca=ca, profile=cm_profile, + template_v2=cm_template, is_ca=True) try: state = certmonger.wait_for_request(self.request_id, timeout) except RuntimeError: @@ -320,7 +357,7 @@ def resubmit_request(self, ca='dogtag-ipa-ca-renew-agent', profile=''): logger.debug("modifying certmonger request '%s'", self.request_id) certmonger.modify(self.request_id, ca='dogtag-ipa-ca-renew-agent', - profile='') + profile='', template_v2='') def install(self): print("Installing CA certificate, please wait") From ba2feafb5d7049d5c8823465eb2a0ed553ea94eb Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Mon, 25 Sep 2017 11:39:51 +1000 Subject: [PATCH 08/10] Add tests for external CA profile specifiers Part of: https://pagure.io/freeipa/issue/6858 --- .../test_ipaserver/test_install/test_cainstance.py | 125 +++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 ipatests/test_ipaserver/test_install/test_cainstance.py diff --git a/ipatests/test_ipaserver/test_install/test_cainstance.py b/ipatests/test_ipaserver/test_install/test_cainstance.py new file mode 100644 index 0000000000..7af474df41 --- /dev/null +++ b/ipatests/test_ipaserver/test_install/test_cainstance.py @@ -0,0 +1,125 @@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# + +from binascii import hexlify +import pickle +# pylint: disable=import-error +from six.moves.configparser import RawConfigParser +# pylint: enable=import-error +from six import StringIO +import pytest +from ipaserver.install import cainstance + +pytestmark = pytest.mark.tier0 + + +class test_ExternalCAProfile(object): + def test_MSCSTemplateV1_good(self): + o = cainstance.MSCSTemplateV1("MySubCA") + assert hexlify(o.get_ext_data()) == b'1e0e004d007900530075006200430041' + + def test_MSCSTemplateV1_bad(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV1("MySubCA:1") + + def test_MSCSTemplateV1_pickle_roundtrip(self): + o = cainstance.MSCSTemplateV1("MySubCA") + s = pickle.dumps(o) + assert o.get_ext_data() == pickle.loads(s).get_ext_data() + + def test_MSCSTemplateV2_too_few_parts(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4") + + def test_MSCSTemplateV2_too_many_parts(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:100:200:300") + + def test_MSCSTemplateV2_bad_oid(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("not_an_oid:1") + + def test_MSCSTemplateV2_non_numeric_major_version(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:major:200") + + def test_MSCSTemplateV2_non_numeric_minor_version(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:100:minor") + + def test_MSCSTemplateV2_major_version_lt_zero(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:-1:200") + + def test_MSCSTemplateV2_minor_version_lt_zero(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:100:-1") + + def test_MSCSTemplateV2_major_version_gt_max(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:4294967296:200") + + def test_MSCSTemplateV2_minor_version_gt_max(self): + with pytest.raises(ValueError): + cainstance.MSCSTemplateV2("1.2.3.4:100:4294967296") + + def test_MSCSTemplateV2_good_major(self): + o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295") + assert hexlify(o.get_ext_data()) == b'300c06032a0304020500ffffffff' + + def test_MSCSTemplateV2_good_major_minor(self): + o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295:0") + assert hexlify(o.get_ext_data()) \ + == b'300f06032a0304020500ffffffff020100' + + def test_MSCSTemplateV2_pickle_roundtrip(self): + o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295:0") + s = pickle.dumps(o) + assert o.get_ext_data() == pickle.loads(s).get_ext_data() + + def test_ExternalCAProfile_dispatch(self): + """ + Test that constructing ExternalCAProfile actually returns an + instance of the appropriate subclass. + """ + assert isinstance( + cainstance.ExternalCAProfile("MySubCA"), + cainstance.MSCSTemplateV1) + assert isinstance( + cainstance.ExternalCAProfile("1.2.3.4:100"), + cainstance.MSCSTemplateV2) + + def test_write_pkispawn_config_file_MSCSTemplateV1(self): + template = cainstance.MSCSTemplateV1(u"SubCA") + expected = ( + '[CA]\n' + 'pki_req_ext_oid = 1.3.6.1.4.1.311.20.2\n' + 'pki_req_ext_data = 1e0a00530075006200430041\n\n' + ) + self._test_write_pkispawn_config_file(template, expected) + + def test_write_pkispawn_config_file_MSCSTemplateV2(self): + template = cainstance.MSCSTemplateV2(u"1.2.3.4:4294967295") + expected = ( + '[CA]\n' + 'pki_req_ext_oid = 1.3.6.1.4.1.311.21.7\n' + 'pki_req_ext_data = 300c06032a0304020500ffffffff\n\n' + ) + self._test_write_pkispawn_config_file(template, expected) + + def _test_write_pkispawn_config_file(self, template, expected): + """ + Test that the values we read from an ExternalCAProfile + object can be used to produce a reasonable-looking pkispawn + configuration. + """ + config = RawConfigParser() + config.optionxform = str + config.add_section("CA") + config.set("CA", "pki_req_ext_oid", template.ext_oid) + config.set("CA", "pki_req_ext_data", + hexlify(template.get_ext_data()).decode('ascii')) + out = StringIO() + config.write(out) + assert out.getvalue() == expected From af89a79a8368dcadb7738f23f0a625262adbeebf Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Mon, 25 Sep 2017 17:11:46 +1000 Subject: [PATCH 09/10] ipa-cacert-manage: handle alternative tracking request CA name For an externally-signed CA, if an earlier run of ipa-cacert-manage was interrupted, the CA name in the IPA CA tracking request may have been left as "dogtag-ipa-ca-renew-agent-reuse" (it gets reverted to "dogtag-ipa-ca-renew-agent" at the end of the CSR generation procedure). `ipa-cacert-manage renew` currently only looks for a tracking request with the "dogtag-ipa-ca-renew-agent" CA, so in this scenario the program fails with message "CA certificate is not tracked by certmonger". To handle this scenario, if the IPA CA tracking request is not found, try once again but with the "dogtag-ipa-ca-renew-agent-renew" CA name. Part of: https://pagure.io/freeipa/issue/6858 --- ipaserver/install/ipa_cacert_manage.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py index bff1678b05..f764638c71 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -148,20 +148,30 @@ def ldap_connect(self): api.Backend.ldap2.connect(bind_pw=password) + def _get_ca_request_id(self, ca_name): + """Lookup tracking request for IPA CA, using given ca-name.""" + criteria = { + 'cert-database': paths.PKI_TOMCAT_ALIAS_DIR, + 'cert-nickname': self.cert_nickname, + 'ca-name': ca_name, + } + return certmonger.get_request_id(criteria) + def renew(self): ca = cainstance.CAInstance(api.env.realm) if not ca.is_configured(): raise admintool.ScriptError("CA is not configured on this system") - criteria = { - 'cert-database': paths.PKI_TOMCAT_ALIAS_DIR, - 'cert-nickname': self.cert_nickname, - 'ca-name': 'dogtag-ipa-ca-renew-agent', - } - self.request_id = certmonger.get_request_id(criteria) + self.request_id = self._get_ca_request_id('dogtag-ipa-ca-renew-agent') if self.request_id is None: - raise admintool.ScriptError( - "CA certificate is not tracked by certmonger") + # if external CA renewal was interrupted, the request may have + # been left with the "dogtag-ipa-ca-renew-agent-reuse" CA; + # look for it too + self.request_id = \ + self._get_ca_request_id('dogtag-ipa-ca-renew-agent-reuse') + if self.request_id is None: + raise admintool.ScriptError( + "CA certificate is not tracked by certmonger") logger.debug( "Found certmonger request id %r", self.request_id) From 9f0d9ba71f3f5efc97bba2768b0a3f19c62ab71b Mon Sep 17 00:00:00 2001 From: Fraser Tweedale <ftwee...@redhat.com> Date: Mon, 25 Sep 2017 17:30:06 +1000 Subject: [PATCH 10/10] ipa-cacert-manage: avoid some duplicate string definitions Part of: https://pagure.io/freeipa/issue/6858 --- ipalib/constants.py | 1 + ipaserver/install/ipa_cacert_manage.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ipalib/constants.py b/ipalib/constants.py index bc511d9379..dce0b152ad 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -286,6 +286,7 @@ IPA_CA_RECORD = "ipa-ca" IPA_CA_NICKNAME = 'caSigningCert cert-pki-ca' RENEWAL_CA_NAME = 'dogtag-ipa-ca-renew-agent' +RENEWAL_REUSE_CA_NAME = 'dogtag-ipa-ca-renew-agent-reuse' # regexp definitions PATTERN_GROUPUSER_NAME = '^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$' diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py index f764638c71..0ac0c5c0a1 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -24,6 +24,7 @@ from optparse import OptionGroup # pylint: disable=deprecated-module import gssapi +from ipalib.constants import RENEWAL_CA_NAME, RENEWAL_REUSE_CA_NAME from ipalib.install import certmonger, certstore from ipapython import admintool, ipautil from ipapython.certdb import (EMPTY_TRUST_FLAGS, @@ -162,13 +163,12 @@ def renew(self): if not ca.is_configured(): raise admintool.ScriptError("CA is not configured on this system") - self.request_id = self._get_ca_request_id('dogtag-ipa-ca-renew-agent') + self.request_id = self._get_ca_request_id(RENEWAL_CA_NAME) if self.request_id is None: # if external CA renewal was interrupted, the request may have # been left with the "dogtag-ipa-ca-renew-agent-reuse" CA; # look for it too - self.request_id = \ - self._get_ca_request_id('dogtag-ipa-ca-renew-agent-reuse') + self.request_id = self._get_ca_request_id(RENEWAL_REUSE_CA_NAME) if self.request_id is None: raise admintool.ScriptError( "CA certificate is not tracked by certmonger") @@ -235,7 +235,7 @@ def renew_external_step_1(self, ca): ) self.resubmit_request( - 'dogtag-ipa-ca-renew-agent-reuse', + RENEWAL_REUSE_CA_NAME, profile=options.external_ca_profile) print(("The next step is to get %s signed by your CA and re-run " @@ -334,11 +334,11 @@ def renew_external_step_2(self, ca, old_cert): except errors.NotFound: raise admintool.ScriptError("CA renewal master not found") - self.resubmit_request('dogtag-ipa-ca-renew-agent-reuse') + self.resubmit_request(RENEWAL_REUSE_CA_NAME) print("CA certificate successfully renewed") - def resubmit_request(self, ca='dogtag-ipa-ca-renew-agent', profile=None): + def resubmit_request(self, ca=RENEWAL_CA_NAME, profile=None): timeout = api.env.startup_timeout + 60 cm_profile = None @@ -366,7 +366,7 @@ def resubmit_request(self, ca='dogtag-ipa-ca-renew-agent', profile=None): logger.debug("modifying certmonger request '%s'", self.request_id) certmonger.modify(self.request_id, - ca='dogtag-ipa-ca-renew-agent', + ca=RENEWAL_CA_NAME, profile='', template_v2='') def install(self):
_______________________________________________ FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org