[Freeipa-devel] [PATCH 0014 v3] Support multiple user and host certificates

Fraser Tweedale ftweedal at redhat.com
Tue Jun 2 09:42:28 UTC 2015


On Mon, Jun 01, 2015 at 02:50:45PM +0200, Martin Basti wrote:
> On 01/06/15 06:40, Fraser Tweedale wrote:
> >New version of patch; ``{host,service}-show --out=FILE`` now writes
> >all certs to FILE.  Rebased on latest master.
> >
> >Thanks,
> >Fraser
> >
> >On Thu, May 28, 2015 at 09:18:04PM +1000, Fraser Tweedale wrote:
> >>Updated patch attached.  Notably restores/adds revocation behaviour
> >>to host-mod and service-mod.
> >>
> >>Thanks,
> >>Fraser
> >>
> >>On Wed, May 27, 2015 at 06:12:50PM +0200, Martin Basti wrote:
> >>>On 27/05/15 15:53, Fraser Tweedale wrote:
> >>>>This patch adds supports for multiple user / host certificates.  No
> >>>>schema change is needed ('usercertificate' attribute is already
> >>>>multi-value).  The revoke-previous-cert behaviour of host-mod and
> >>>>user-mod has been removed but revocation behaviour of -del and
> >>>>-disable is preserved.
> >>>>
> >>>>The latest profiles/caacl patchset (0001..0013 v5) depends on this
> >>>>patch for correct cert-request behaviour.
> >>>>
> >>>>There is one design question (or maybe more, let me know): the
> >>>>`--out=FILENAME' option to {host,service} show saves ONE certificate
> >>>>to the named file.  I propose to either:
> >>>>
> >>>>a) write all certs, suffixing suggested filename with either a
> >>>>    sequential numerical index, e.g. "cert.pem" becomes
> >>>>    "cert.pem.1", "cert.pem.2", and so on; or
> >>>>
> >>>>b) as above, but suffix with serial number and, if there are
> >>>>    different issues, some issuer-identifying information.
> >>>>
> >>>>Let me know your thoughts.
> >>>>
> >>>>Thanks,
> >>>>Fraser
> >>>>
> >>>>
> >>>Is there a possible way how to store certificates into one file?
> >>>I read about possibilities to have multiple certs in one .pem file, but I'm
> >>>not cert guru :)
> >>>
> >>>I personally vote for serial number in case there are multiple certificates,
> >>>if ^ is no possible.
> >>>
> >>>
> >>>1)
> >>>+            if len(certs) > 0:
> >>>
> >>>please use only,
> >>>if certs:
> >>>
> >>>2)
> >>>You need to re-generate API/ACI.txt in this patch
> >>>
> >>>3)
> >>>syntax error:
> >>>+        for dercert in certs_der
> >>>
> >>>
> >>>4)
> >>>command
> >>>ipa user-mod ca_user --certificate=<ceritifcate>
> >>>
> >>>removes the current certificate from the LDAP, by design.
> >>>Should be the old certificate(s) revoked? You removed that part in the code.
> >>>
> >>>only the --addattr='usercertificate=<cert>' appends new value there
> >>>
> >>>-- 
> >>>Martin Basti
> >>>
> My objections/proposed solutions in attached patch.
> 
> * VERSION
> * In the previous version normalized values was stored in LDAP, so I added
> it back.  (I dont know why there is no normalization in param settings, but
> normalization for every certificate is done in callback. I will file a
> ticket for this)
> * IMO only normalized certificates should be compared in the old
> certificates detection
> 
I incorporated your suggested changes in new patch (attached).

There were no proposed changes to the other patchset (0001..0013)
since rebase.

Thanks,
Fraser
-------------- next part --------------
From a1381dcb849d24f9d77fed4cc92655075d0a5a35 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Wed, 27 May 2015 08:02:08 -0400
Subject: [PATCH] Support multiple host and service certificates

Update the framework to support multiple host and service
certificates.

host-mod and service-mod revoke existing certificates that are not
included in the modified entry.  Using addattr=certificate=... will
result in no certificates being revoked.

The existing behaviour of host-disable, host-del, service-disable
and service-del (revoke existing certificate) is preserved but now
applies to all certificates in the host or service entry.

Also update host-show and service-show to write all the principal's
certificates to the file given by the ``--out=FILE`` option.

Part of: http://www.freeipa.org/page/V4/User_Certificates
---
 API.txt                   |  10 ++---
 VERSION                   |   4 +-
 ipalib/plugins/host.py    | 107 +++++++++++++++++++++++++---------------------
 ipalib/plugins/service.py |  94 +++++++++++++++++++++++++---------------
 4 files changed, 124 insertions(+), 91 deletions(-)

diff --git a/API.txt b/API.txt
index da69f32de5c12c0d85a7d61d9027385aa3c0ee05..3cfcf34939a58d6888de8f0a7a6ef0c7779c993e 100644
--- a/API.txt
+++ b/API.txt
@@ -1812,7 +1812,7 @@ option: Str('nsosversion', attribute=True, cli_name='os', multivalue=False, requ
 option: Flag('random', attribute=False, autofill=True, cli_name='random', default=False, multivalue=False, required=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('setattr*', cli_name='setattr', exclude='webui')
-option: Bytes('usercertificate', attribute=True, cli_name='certificate', multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, cli_name='certificate', multivalue=True, required=False)
 option: Str('userclass', attribute=True, cli_name='class', multivalue=True, required=False)
 option: Str('userpassword', attribute=True, cli_name='password', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
@@ -1935,7 +1935,7 @@ option: Flag('pkey_only?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Int('sizelimit?', autofill=False, minvalue=0)
 option: Int('timelimit?', autofill=False, minvalue=0)
-option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=False, query=True, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, query=True, required=False)
 option: Str('userclass', attribute=True, autofill=False, cli_name='class', multivalue=True, query=True, required=False)
 option: Str('userpassword', attribute=True, autofill=False, cli_name='password', multivalue=False, query=True, required=False)
 option: Str('version?', exclude='webui')
@@ -1966,7 +1966,7 @@ option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui
 option: Flag('rights', autofill=True, default=False)
 option: Str('setattr*', cli_name='setattr', exclude='webui')
 option: Flag('updatedns?', autofill=True, default=False)
-option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, required=False)
 option: Str('userclass', attribute=True, autofill=False, cli_name='class', multivalue=True, required=False)
 option: Str('userpassword', attribute=True, autofill=False, cli_name='password', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
@@ -3584,7 +3584,7 @@ option: Bool('ipakrbrequirespreauth', attribute=False, cli_name='requires_pre_au
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('setattr*', cli_name='setattr', exclude='webui')
-option: Bytes('usercertificate', attribute=True, cli_name='certificate', multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, cli_name='certificate', multivalue=True, required=False)
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
@@ -3702,7 +3702,7 @@ option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
 option: Str('setattr*', cli_name='setattr', exclude='webui')
-option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, required=False)
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
diff --git a/VERSION b/VERSION
index 07c00d000064a7687497b09524aa821dbcecc88a..24a2913226961a807da49076184a1053c897e748 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=121
-# Last change: pvoborni - added server-find and server-show
+IPA_API_VERSION_MINOR=122
+# Last change: ftweedal - allow multiple host/service certificates
diff --git a/ipalib/plugins/host.py b/ipalib/plugins/host.py
index c47439743da45b8629d1b2afbd210d87591784ce..9ad087e26250d86b15fbe723a98cca278ef29adf 100644
--- a/ipalib/plugins/host.py
+++ b/ipalib/plugins/host.py
@@ -493,7 +493,7 @@ class host(LDAPObject):
             label=_('Random password'),
             flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'),
         ),
-        Bytes('usercertificate?', validate_certificate,
+        Bytes('usercertificate*', validate_certificate,
             cli_name='certificate',
             label=_('Certificate'),
             doc=_('Base-64 encoded server certificate'),
@@ -640,11 +640,11 @@ class host_add(LDAPCreate):
             entry_attrs['userpassword'] = ipa_generate_password(characters=host_pwd_chars)
             # save the password so it can be displayed in post_callback
             setattr(context, 'randompassword', entry_attrs['userpassword'])
-        cert = options.get('usercertificate')
-        if cert:
-            cert = x509.normalize_certificate(cert)
+        certs = options.get('usercertificate', [])
+        certs_der = map(x509.normalize_certificate, certs)
+        for cert in certs_der:
             x509.verify_cert_subject(ldap, keys[-1], cert)
-            entry_attrs['usercertificate'] = cert
+        entry_attrs['usercertificate'] = certs_der
         entry_attrs['managedby'] = dn
         entry_attrs['objectclass'].append('ieee802device')
         entry_attrs['objectclass'].append('ipasshhost')
@@ -786,8 +786,7 @@ class host_del(LDAPDelete):
                 entry_attrs = ldap.get_entry(dn, ['usercertificate'])
             except errors.NotFound:
                 self.obj.handle_not_found(*keys)
-            cert = entry_attrs.single_value.get('usercertificate')
-            if cert:
+            for cert in entry_attrs.get('usercertificate', []):
                 cert = x509.normalize_certificate(cert)
                 try:
                     serial = unicode(x509.get_serial_number(cert, x509.DER))
@@ -864,39 +863,43 @@ class host_mod(LDAPUpdate):
             if 'krbprincipalaux' not in obj_classes:
                 obj_classes.append('krbprincipalaux')
                 entry_attrs['objectclass'] = obj_classes
-        cert = x509.normalize_certificate(entry_attrs.get('usercertificate'))
-        if cert:
-            if self.api.Command.ca_is_enabled()['result']:
-                x509.verify_cert_subject(ldap, keys[-1], cert)
-                entry_attrs_old = ldap.get_entry(dn, ['usercertificate'])
-                oldcert = entry_attrs_old.single_value.get('usercertificate')
-                if oldcert:
-                    oldcert = x509.normalize_certificate(oldcert)
+
+        # verify certificates
+        certs = entry_attrs.get('usercertificate') or []
+        certs_der = map(x509.normalize_certificate, certs)
+        for cert in certs_der:
+            x509.verify_cert_subject(ldap, keys[-1], cert)
+
+        # revoke removed certificates
+        if self.api.Command.ca_is_enabled()['result']:
+            entry_attrs_old = ldap.get_entry(dn, ['usercertificate'])
+            old_certs = entry_attrs_old.get('usercertificate', [])
+            old_certs_der = map(x509.normalize_certificate, old_certs)
+            removed_certs_der = set(old_certs_der) - set(certs_der)
+            for cert in removed_certs_der:
+                try:
+                    serial = unicode(x509.get_serial_number(cert, x509.DER))
                     try:
-                        serial = x509.get_serial_number(oldcert, x509.DER)
-                        serial = unicode(serial)
-                        try:
-                            result = api.Command['cert_show'](serial)['result']
-                            if 'revocation_reason' not in result:
-                                try:
-                                    api.Command['cert_revoke'](
-                                        serial, revocation_reason=4)
-                                except errors.NotImplementedError:
-                                    # some CA's might not implement revoke
-                                    pass
-                        except errors.NotImplementedError:
-                            # some CA's might not implement revoke
-                            pass
-                    except NSPRError, nsprerr:
-                        if nsprerr.errno == -8183:
-                            # If we can't decode the cert them proceed with
-                            # modifying the host.
-                            self.log.info("Problem decoding certificate %s" %
-                                          nsprerr.args[1])
-                        else:
-                            raise nsprerr
-
-            entry_attrs['usercertificate'] = cert
+                        result = api.Command['cert_show'](serial)['result']
+                        if 'revocation_reason' not in result:
+                            try:
+                                api.Command['cert_revoke'](
+                                    serial, revocation_reason=4)
+                            except errors.NotImplementedError:
+                                # some CA's might not implement revoke
+                                pass
+                    except errors.NotImplementedError:
+                        # some CA's might not implement revoke
+                        pass
+                except NSPRError, nsprerr:
+                    if nsprerr.errno == -8183:
+                        # If we can't decode the cert them proceed with
+                        # modifying the host.
+                        self.log.info("Problem decoding certificate %s" %
+                                      nsprerr.args[1])
+                    else:
+                        raise nsprerr
+        entry_attrs['usercertificate'] = certs_der
 
         if options.get('random'):
             entry_attrs['userpassword'] = ipa_generate_password(characters=host_pwd_chars)
@@ -1093,8 +1096,14 @@ class host_show(LDAPRetrieve):
             util.check_writable_file(options['out'])
             result = super(host_show, self).forward(*keys, **options)
             if 'usercertificate' in result['result']:
-                x509.write_certificate(result['result']['usercertificate'][0], options['out'])
-                result['summary'] = _('Certificate stored in file \'%(file)s\'') % dict(file=options['out'])
+                x509.write_certificate_list(
+                    result['result']['usercertificate'],
+                    options['out']
+                )
+                result['summary'] = (
+                    _('Certificate(s) stored in file \'%(file)s\'')
+                    % dict(file=options['out'])
+                )
                 return result
             else:
                 raise errors.NoCertificateError(entry=keys[-1])
@@ -1148,10 +1157,9 @@ class host_disable(LDAPQuery):
             entry_attrs = ldap.get_entry(dn, ['usercertificate'])
         except errors.NotFound:
             self.obj.handle_not_found(*keys)
-        cert = entry_attrs.single_value.get('usercertificate')
-        if cert:
-            if self.api.Command.ca_is_enabled()['result']:
-                cert = x509.normalize_certificate(cert)
+        if self.api.Command.ca_is_enabled()['result']:
+            certs = entry_attrs.get('usercertificate', [])
+            for cert in map(x509.normalize_certificate, certs):
                 try:
                     serial = unicode(x509.get_serial_number(cert, x509.DER))
                     try:
@@ -1175,10 +1183,11 @@ class host_disable(LDAPQuery):
                     else:
                         raise nsprerr
 
-            # Remove the usercertificate altogether
-            entry_attrs['usercertificate'] = None
-            ldap.update_entry(entry_attrs)
-            done_work = True
+            if certs:
+                # Remove the usercertificate altogether
+                entry_attrs['usercertificate'] = None
+                ldap.update_entry(entry_attrs)
+                done_work = True
 
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         if entry_attrs['has_keytab']:
diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py
index b37dc7b4bf56b69df204fd29e9487f1390197bbe..c290344cf6c14155ec1b103525ff8642a7a8e2af 100644
--- a/ipalib/plugins/service.py
+++ b/ipalib/plugins/service.py
@@ -437,7 +437,7 @@ class service(LDAPObject):
             primary_key=True,
             normalizer=lambda value: normalize_principal(value),
         ),
-        Bytes('usercertificate?', validate_certificate,
+        Bytes('usercertificate*', validate_certificate,
             cli_name='certificate',
             label=_('Certificate'),
             doc=_('Base-64 encoded server certificate'),
@@ -503,11 +503,11 @@ class service_add(LDAPCreate):
 
         self.obj.validate_ipakrbauthzdata(entry_attrs)
 
-        cert = options.get('usercertificate')
-        if cert:
-            dercert = x509.normalize_certificate(cert)
+        certs = options.get('usercertificate', [])
+        certs_der = map(x509.normalize_certificate, certs)
+        for dercert in certs_der:
             x509.verify_cert_subject(ldap, hostname, dercert)
-            entry_attrs['usercertificate'] = dercert
+        entry_attrs['usercertificate'] = certs_der
 
         if not options.get('force', False):
              # We know the host exists if we've gotten this far but we
@@ -555,9 +555,7 @@ class service_del(LDAPDelete):
                 entry_attrs = ldap.get_entry(dn, ['usercertificate'])
             except errors.NotFound:
                 self.obj.handle_not_found(*keys)
-            cert = entry_attrs.get('usercertificate')
-            if cert:
-                cert = cert[0]
+            for cert in entry_attrs.get('usercertificate', []):
                 try:
                     serial = unicode(x509.get_serial_number(cert, x509.DER))
                     try:
@@ -597,25 +595,44 @@ class service_mod(LDAPUpdate):
 
         self.obj.validate_ipakrbauthzdata(entry_attrs)
 
-        if 'usercertificate' in options:
-            (service, hostname, realm) = split_principal(keys[-1])
-            cert = options.get('usercertificate')
-            if cert:
-                dercert = x509.normalize_certificate(cert)
-                x509.verify_cert_subject(ldap, hostname, dercert)
+        (service, hostname, realm) = split_principal(keys[-1])
+
+        # verify certificates
+        certs = options.get('usercertificate') or []
+        certs_der = map(x509.normalize_certificate, certs)
+        for dercert in certs_der:
+            x509.verify_cert_subject(ldap, hostname, dercert)
+
+        # revoke removed certificates
+        if self.api.Command.ca_is_enabled()['result']:
+            entry_attrs_old = ldap.get_entry(dn, ['usercertificate'])
+            old_certs = entry_attrs_old.get('usercertificate', [])
+            old_certs_der = map(x509.normalize_certificate, old_certs)
+            removed_certs_der = set(old_certs_der) - set(certs_der)
+            for cert in removed_certs_der:
                 try:
-                    entry_attrs_old = ldap.get_entry(dn, ['usercertificate'])
-                except errors.NotFound:
-                    self.obj.handle_not_found(*keys)
-                if 'usercertificate' in entry_attrs_old:
-                    # FIXME: what to do here? do we revoke the old cert?
-                    fmt = 'entry already has a certificate, serial number: %s' % (
-                        x509.get_serial_number(entry_attrs_old['usercertificate'][0], x509.DER)
-                    )
-                    raise errors.GenericError(format=fmt)
-                entry_attrs['usercertificate'] = dercert
-            else:
-                entry_attrs['usercertificate'] = None
+                    serial = unicode(x509.get_serial_number(cert, x509.DER))
+                    try:
+                        result = api.Command['cert_show'](serial)['result']
+                        if 'revocation_reason' not in result:
+                            try:
+                                api.Command['cert_revoke'](
+                                    serial, revocation_reason=4)
+                            except errors.NotImplementedError:
+                                # some CA's might not implement revoke
+                                pass
+                    except errors.NotImplementedError:
+                        # some CA's might not implement revoke
+                        pass
+                except NSPRError, nsprerr:
+                    if nsprerr.errno == -8183:
+                        # If we can't decode the cert them proceed with
+                        # modifying the host.
+                        self.log.info("Problem decoding certificate %s" %
+                                      nsprerr.args[1])
+                    else:
+                        raise nsprerr
+        entry_attrs['usercertificate'] = certs_der
 
         update_krbticketflags(ldap, entry_attrs, attrs_list, options, True)
 
@@ -695,8 +712,14 @@ class service_show(LDAPRetrieve):
             util.check_writable_file(options['out'])
             result = super(service_show, self).forward(*keys, **options)
             if 'usercertificate' in result['result']:
-                x509.write_certificate(result['result']['usercertificate'][0], options['out'])
-                result['summary'] = _('Certificate stored in file \'%(file)s\'') % dict(file=options['out'])
+                x509.write_certificate_list(
+                    result['result']['usercertificate'],
+                    options['out']
+                )
+                result['summary'] = (
+                    _('Certificate(s) stored in file \'%(file)s\'')
+                    % dict(file=options['out'])
+                )
                 return result
             else:
                 raise errors.NoCertificateError(entry=keys[-1])
@@ -815,9 +838,9 @@ class service_disable(LDAPQuery):
         # See if we do any work at all here and if not raise an exception
         done_work = False
 
-        if 'usercertificate' in entry_attrs:
-            if self.api.Command.ca_is_enabled()['result']:
-                cert = x509.normalize_certificate(entry_attrs.get('usercertificate')[0])
+        if self.api.Command.ca_is_enabled()['result']:
+            certs = entry_attrs.get('usercertificate', [])
+            for cert in map(x509.normalize_certificate, certs):
                 try:
                     serial = unicode(x509.get_serial_number(cert, x509.DER))
                     try:
@@ -839,10 +862,11 @@ class service_disable(LDAPQuery):
                     else:
                         raise nsprerr
 
-            # Remove the usercertificate altogether
-            entry_attrs['usercertificate'] = None
-            ldap.update_entry(entry_attrs)
-            done_work = True
+            if len(certs) > 0:
+                # Remove the usercertificate altogether
+                entry_attrs['usercertificate'] = None
+                ldap.update_entry(entry_attrs)
+                done_work = True
 
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         if entry_attrs['has_keytab']:
-- 
2.1.0



More information about the Freeipa-devel mailing list