[Freeipa-devel] [PATCH] 0072..0075 Lightweight CA renewal

Fraser Tweedale ftweedal at redhat.com
Tue Jun 21 06:24:16 UTC 2016


The attached patches add lightweight CA renewal.  There are two
substantive aspects:

1. The renew_ca_cert updates the serial number in the lightweight
CA's entry in the Dogtag database.  This causes CA clones to observe
the renewal and update the certs in their own NSSDBs.

2. The ipa-certupdate command adds Certmonger tracking requests for
lightweight CAs (on the renewal master only).

Correct behaviour also depends on my patch 0069 (in-server API for
renew_ca_cert script).

Cheers,
Fraser
-------------- next part --------------
From c2333f0dbe0560a67059030e1a04eb96a52c20d8 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 17 Jun 2016 10:05:49 +1000
Subject: [PATCH 72/75] ipaldap: turn LDAP filter utility functions into class
 methods

The LDAP filter utilities do not use any instance attributes, so
collectively turn them into class methods to promote reuse.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ipapython/ipaldap.py | 35 +++++++++++++++++++----------------
 1 file changed, 19 insertions(+), 16 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 410ddae2c484f060abf2bbf3e897549a26b0ebc9..67a3c82f03bd89299ce12a35112e9cfda35d6fe6 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -1153,7 +1153,8 @@ class LDAPClient(object):
     # entry_attrs = {u'firstName': u'Pavel', u'lastName': u'Zuna'}
     # f = ldap2.make_filter(entry_attrs, rules=ldap2.MATCH_ALL)
 
-    def combine_filters(self, filters, rules='|'):
+    @classmethod
+    def combine_filters(cls, filters, rules='|'):
         """
         Combine filters into one for ldap2.find_entries.
 
@@ -1164,9 +1165,9 @@ class LDAPClient(object):
         assert isinstance(filters, (list, tuple))
 
         filters = [f for f in filters if f]
-        if filters and rules == self.MATCH_NONE:  # unary operator
-            return '(%s%s)' % (self.MATCH_NONE,
-                               self.combine_filters(filters, self.MATCH_ANY))
+        if filters and rules == cls.MATCH_NONE:  # unary operator
+            return '(%s%s)' % (cls.MATCH_NONE,
+                               cls.combine_filters(filters, cls.MATCH_ANY))
 
         if len(filters) > 1:
             flt = '(%s' % rules
@@ -1180,8 +1181,9 @@ class LDAPClient(object):
             flt = '%s)' % flt
         return flt
 
+    @classmethod
     def make_filter_from_attr(
-            self, attr, value, rules='|', exact=True,
+            cls, attr, value, rules='|', exact=True,
             leading_wildcard=True, trailing_wildcard=True):
         """
         Make filter for ldap2.find_entries from attribute.
@@ -1198,18 +1200,18 @@ class LDAPClient(object):
             False - forbid trailing filter wildcard when exact=False
         """
         if isinstance(value, (list, tuple)):
-            if rules == self.MATCH_NONE:
-                make_filter_rules = self.MATCH_ANY
+            if rules == cls.MATCH_NONE:
+                make_filter_rules = cls.MATCH_ANY
             else:
                 make_filter_rules = rules
             flts = [
-                self.make_filter_from_attr(
+                cls.make_filter_from_attr(
                     attr, v, exact=exact,
                     leading_wildcard=leading_wildcard,
                     trailing_wildcard=trailing_wildcard)
                 for v in value
             ]
-            return self.combine_filters(flts, rules)
+            return cls.combine_filters(flts, rules)
         elif value is not None:
             value = ldap.filter.escape_filter_chars(value_to_utf8(value))
             if not exact:
@@ -1219,13 +1221,14 @@ class LDAPClient(object):
                 if trailing_wildcard:
                     template = template + '*'
                 value = template % value
-            if rules == self.MATCH_NONE:
+            if rules == cls.MATCH_NONE:
                 return '(!(%s=%s))' % (attr, value)
             return '(%s=%s)' % (attr, value)
         return ''
 
+    @classmethod
     def make_filter(
-            self, entry_attrs, attrs_list=None, rules='|', exact=True,
+            cls, entry_attrs, attrs_list=None, rules='|', exact=True,
             leading_wildcard=True, trailing_wildcard=True):
         """
         Make filter for ldap2.find_entries from entry attributes.
@@ -1247,15 +1250,15 @@ class LDAPClient(object):
         ldap2.MATCH_ALL - match entries that match all attributes
         ldap2.MATCH_ANY - match entries that match any of attribute
         """
-        if rules == self.MATCH_NONE:
-            make_filter_rules = self.MATCH_ANY
+        if rules == cls.MATCH_NONE:
+            make_filter_rules = cls.MATCH_ANY
         else:
             make_filter_rules = rules
         flts = []
         if attrs_list is None:
             for (k, v) in entry_attrs.items():
                 flts.append(
-                    self.make_filter_from_attr(
+                    cls.make_filter_from_attr(
                         k, v, make_filter_rules, exact,
                         leading_wildcard, trailing_wildcard)
                 )
@@ -1264,11 +1267,11 @@ class LDAPClient(object):
                 value = entry_attrs.get(a, None)
                 if value is not None:
                     flts.append(
-                        self.make_filter_from_attr(
+                        cls.make_filter_from_attr(
                             a, value, make_filter_rules, exact,
                             leading_wildcard, trailing_wildcard)
                     )
-        return self.combine_filters(flts, rules)
+        return cls.combine_filters(flts, rules)
 
     def get_entries(self, base_dn, scope=ldap.SCOPE_SUBTREE, filter=None,
                     attrs_list=None, **kwargs):
-- 
2.5.5

-------------- next part --------------
From 7bc9cdbf661dc5cc62d0676f5489269c7c78e6d2 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 17 Jun 2016 13:33:26 +1000
Subject: [PATCH 73/75] Skip CS.cfg update if cert nickname not known

After CA certificate renewal, the ``renew_ca_cert`` helper updates
certificate data in CS.cfg.  An unrecognised nickname will raise
``KeyError``.  To allow the helper to be used for arbitrary
certificates (e.g. lightweight CAs), do not fail if the nickname is
unrecognised - just skip the update.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ipaserver/install/cainstance.py     | 5 +++--
 ipaserver/install/dogtaginstance.py | 7 +++----
 ipaserver/install/krainstance.py    | 5 +++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 8dfb71528d2dc020e05ccd7ff42199218a1c0839..2ef568605e3148b30a9a62cf66e7202412910740 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1240,8 +1240,9 @@ class CAInstance(DogtagInstance):
         except Exception as e:
             syslog.syslog(syslog.LOG_ERR, "Failed to backup CS.cfg: %s" % e)
 
-        DogtagInstance.update_cert_cs_cfg(
-            nickname, cert, directives, paths.CA_CS_CFG_PATH)
+        if nickname in directives:
+            DogtagInstance.update_cert_cs_cfg(
+                directives[nickname], cert, paths.CA_CS_CFG_PATH)
 
     def __create_ds_db(self):
         '''
diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py
index 9f094d83404d8d59c871cadca325a8b8a5a0c0bc..b65628277d9e361a3ab5674dfd2689e258b1887b 100644
--- a/ipaserver/install/dogtaginstance.py
+++ b/ipaserver/install/dogtaginstance.py
@@ -370,21 +370,20 @@ class DogtagInstance(service.Service):
             cmonger.stop()
 
     @staticmethod
-    def update_cert_cs_cfg(nickname, cert, directives, cs_cfg):
+    def update_cert_cs_cfg(directive, cert, cs_cfg):
         """
         When renewing a Dogtag subsystem certificate the configuration file
         needs to get the new certificate as well.
 
-        nickname is one of the known nicknames.
+        ``directive`` is the directive to update in CS.cfg
         cert is a DER-encoded certificate.
-        directives is the list of directives to be updated for the subsystem
         cs_cfg is the path to the CS.cfg file
         """
 
         with stopped_service('pki-tomcatd', 'pki-tomcat'):
             installutils.set_directive(
                 cs_cfg,
-                directives[nickname],
+                directive,
                 base64.b64encode(cert),
                 quotes=False,
                 separator='=')
diff --git a/ipaserver/install/krainstance.py b/ipaserver/install/krainstance.py
index 67ad6544c16734a0f0a9fcb1355499daeec48642..dc44726887916c7216564679c6ea8e9902177b64 100644
--- a/ipaserver/install/krainstance.py
+++ b/ipaserver/install/krainstance.py
@@ -348,8 +348,9 @@ class KRAInstance(DogtagInstance):
             'subsystemCert cert-pki-kra': 'kra.subsystem.cert',
             'Server-Cert cert-pki-ca': 'kra.sslserver.cert'}
 
-        DogtagInstance.update_cert_cs_cfg(
-            nickname, cert, directives, paths.KRA_CS_CFG_PATH)
+        if nickname in directives:
+            DogtagInstance.update_cert_cs_cfg(
+                directives[nickname], cert, paths.KRA_CS_CFG_PATH)
 
     def __enable_instance(self):
         self.ldap_enable('KRA', self.fqdn, None, self.suffix)
-- 
2.5.5

-------------- next part --------------
From d6088f223caca532e524355f82dbe05b27f91e02 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 17 Jun 2016 15:11:08 +1000
Subject: [PATCH 74/75] Update lightweight CA serial after renewal

For CA replicas to pick up renewed lightweight CA signing
certificates, the authoritySerial attribute can be updated with the
new serial number.

Update the renew_ca_cert script, which is executed by Certmonger
after writing a renewed CA certificate to the NSSDB, to update the
authoritySerial attribute if the certificate belongs to a
lightweight CA.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 install/restart_scripts/renew_ca_cert |  1 +
 ipaserver/install/cainstance.py       | 89 ++++++++++++++++++++++++++++-------
 2 files changed, 72 insertions(+), 18 deletions(-)

diff --git a/install/restart_scripts/renew_ca_cert b/install/restart_scripts/renew_ca_cert
index dc0f1117b366e3fdcf6d00f0e6d928e2e32b8f2b..186fb34f65ebce1aff9c852ff03e392c2324e28a 100644
--- a/install/restart_scripts/renew_ca_cert
+++ b/install/restart_scripts/renew_ca_cert
@@ -78,6 +78,7 @@ def _main():
         ca.update_cert_config(nickname, cert)
         if ca.is_renewal_master():
             cainstance.update_people_entry(cert)
+            cainstance.update_authority_entry(cert)
 
         if nickname == 'auditSigningCert cert-pki-ca':
             # Fix trust on the audit cert
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 2ef568605e3148b30a9a62cf66e7202412910740..ffdabcee32547214a3d7ae9575cb9deac5938781 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1544,17 +1544,31 @@ def backup_config():
             "Dogtag must be stopped when creating backup of %s" % path)
     shutil.copy(path, path + '.ipabkp')
 
-def update_people_entry(dercert):
+def __update_entry_from_cert(make_filter, make_entry, dercert):
     """
-    Update the userCerticate for an entry in the dogtag ou=People. This
-    is needed when a certificate is renewed.
+    Given a certificate and functions to make a filter based on the
+    cert, and make a new entry based on the cert, update database
+    accordingly.
 
-    dercert: An X509.3 certificate in DER format
+    ``make_filter``
+        function that takes a certificate in DER format and
+        returns an LDAP search filter
 
-    Logging is done via syslog
+    ``make_entry``
+        function that takes a certificate in DER format and an
+        LDAP entry, and returns the new state of the LDAP entry.
+        Return the input unchanged to skip an entry.
+
+    ``dercert``
+        An X509.3 certificate in DER format
+
+    Logging is done via syslog.
+
+    Return ``True`` if all updates were successful (zero updates is
+    vacuously successful) otherwise ``False``.
 
-    Returns True or False
     """
+
     base_dn = DN(('o', 'ipaca'))
     serial_number = x509.get_serial_number(dercert, datatype=x509.DER)
     subject = x509.get_subject(dercert, datatype=x509.DER)
@@ -1571,14 +1585,7 @@ def update_people_entry(dercert):
             conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
             conn.connect(autobind=True)
 
-            db_filter = conn.combine_filters(
-                [
-                    conn.make_filter({'objectClass': 'inetOrgPerson'}),
-                    conn.make_filter(
-                        {'description': ';%s;%s' % (issuer, subject)},
-                        exact=False, trailing_wildcard=False),
-                ],
-                conn.MATCH_ALL)
+            db_filter = make_filter(dercert)
             try:
                 entries = conn.get_entries(base_dn, conn.SCOPE_SUBTREE, db_filter)
             except errors.NotFound:
@@ -1591,10 +1598,7 @@ def update_people_entry(dercert):
                     syslog.LOG_NOTICE, 'Updating entry %s' % str(entry.dn))
 
                 try:
-                    entry['usercertificate'].append(dercert)
-                    entry['description'] = '2;%d;%s;%s' % (
-                        serial_number, issuer, subject)
-
+                    entry = make_entry(dercert, entry)
                     conn.update_entry(entry)
                 except errors.EmptyModlist:
                     pass
@@ -1624,6 +1628,55 @@ def update_people_entry(dercert):
 
     return True
 
+
+def update_people_entry(dercert):
+    """
+    Update the userCerticate for an entry in the dogtag ou=People. This
+    is needed when a certificate is renewed.
+    """
+    def make_filter(dercert):
+        subject = x509.get_subject(dercert, datatype=x509.DER)
+        issuer = x509.get_issuer(dercert, datatype=x509.DER)
+        return ldap2.ldap2.combine_filters(
+            [
+                ldap2.ldap2.make_filter({'objectClass': 'inetOrgPerson'}),
+                ldap2.ldap2.make_filter(
+                    {'description': ';%s;%s' % (issuer, subject)},
+                    exact=False, trailing_wildcard=False),
+            ],
+            ldap2.ldap2.MATCH_ALL)
+
+    def make_entry(dercert, entry):
+        serial_number = x509.get_serial_number(dercert, datatype=x509.DER)
+        subject = x509.get_subject(dercert, datatype=x509.DER)
+        issuer = x509.get_issuer(dercert, datatype=x509.DER)
+        entry['usercertificate'].append(dercert)
+        entry['description'] = '2;%d;%s;%s' % (serial_number, issuer, subject)
+        return entry
+
+    return __update_entry_from_cert(make_filter, make_entry, dercert)
+
+
+def update_authority_entry(dercert):
+    """
+    Find the authority entry for the given cert, and update the
+    serial number to match the given cert.
+    """
+    def make_filter(dercert):
+        subject = x509.get_subject(dercert, datatype=x509.DER)
+        return ldap2.ldap2.make_filter(
+            dict(objectclass='authority', authoritydn=subject),
+            rules=ldap2.ldap2.MATCH_ALL,
+        )
+
+    def make_entry(dercert, entry):
+        serial_number = x509.get_serial_number(dercert, datatype=x509.DER)
+        entry['authoritySerial'] = serial_number
+        return entry
+
+    return __update_entry_from_cert(make_filter, make_entry, dercert)
+
+
 def ensure_ldap_profiles_container():
     ensure_entry(
         DN(('ou', 'certificateProfiles'), ('ou', 'ca'), ('o', 'ipaca')),
-- 
2.5.5

-------------- next part --------------
From c27e67c513e5c85ff877febb24c9bc73e962f7e8 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Tue, 21 Jun 2016 15:01:41 +1000
Subject: [PATCH 75/75] ipa-certupdate: track lightweight CA certs on renewal
 master

Enhance the ipa-certupdate program to add Certmonger tracking
requests for lightweight CA certificates.  This operation is
performed on the renewal master only, because Dogtag clones observe
renewals and update their NSSDBs on their own.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ipaclient/ipa_certupdate.py | 52 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 48 insertions(+), 4 deletions(-)

diff --git a/ipaclient/ipa_certupdate.py b/ipaclient/ipa_certupdate.py
index b9572196c0eab3b35aa62790eed4ce8a21e3c130..b8f4f788e3ff87da8cdebce56cf18dda13a622d5 100644
--- a/ipaclient/ipa_certupdate.py
+++ b/ipaclient/ipa_certupdate.py
@@ -29,7 +29,10 @@ from ipaplatform import services
 from ipaplatform.paths import paths
 from ipaplatform.tasks import tasks
 from ipalib import api, errors, x509, certstore
+from ipalib.constants import IPA_CA_CN
 
+IPA_CA_NICKNAME = 'caSigningCert cert-pki-ca'
+RENEWAL_CA_NAME = 'dogtag-ipa-ca-renew-agent'
 
 class CertUpdate(admintool.AdminTool):
     command_name = 'ipa-certupdate'
@@ -76,18 +79,29 @@ class CertUpdate(admintool.AdminTool):
                     version=u'2.0',
                 )
                 ca_enabled = result['result']['enable_ra']
-            api.Backend.rpcclient.disconnect()
 
             ldap.do_sasl_gssapi_bind()
 
             certs = certstore.get_ca_certs(ldap, api.env.basedn,
                                            api.env.realm, ca_enabled)
+
+            # find lightweight CAs (on renewal master only)
+            lwcas = []
+            config = api.Command.config_show()['result']
+            if api.env.host in config.get('ca_renewal_master_server', []):
+                for ca_obj in api.Command.ca_find()['result']:
+                    if IPA_CA_CN not in ca_obj['cn']:
+                        lwcas.append(ca_obj)
+
+            api.Backend.rpcclient.disconnect()
         finally:
             shutil.rmtree(tmpdir)
 
         server_fstore = sysrestore.FileStore(paths.SYSRESTORE)
         if server_fstore.has_files():
             self.update_server(certs)
+            for entry in lwcas:
+                self.server_track_lightweight_ca(entry)
 
         self.update_client(certs)
 
@@ -122,11 +136,10 @@ class CertUpdate(admintool.AdminTool):
         if services.knownservices.httpd.is_running():
             services.knownservices.httpd.restart()
 
-        nickname = 'caSigningCert cert-pki-ca'
         criteria = {
             'cert-database': paths.PKI_TOMCAT_ALIAS_DIR,
-            'cert-nickname': nickname,
-            'ca-name': 'dogtag-ipa-ca-renew-agent',
+            'cert-nickname': IPA_CA_NICKNAME,
+            'ca-name': RENEWAL_CA_NAME
         }
         request_id = certmonger.get_request_id(criteria)
         if request_id is not None:
@@ -152,6 +165,37 @@ class CertUpdate(admintool.AdminTool):
 
         self.update_file(paths.CA_CRT, certs)
 
+    def server_track_lightweight_ca(self, entry):
+        nickname = "{} {}".format(IPA_CA_NICKNAME, entry['ipacaid'][0])
+        criteria = {
+            'cert-database': paths.PKI_TOMCAT_ALIAS_DIR,
+            'cert-nickname': nickname,
+            'ca-name': RENEWAL_CA_NAME,
+        }
+        request_id = certmonger.get_request_id(criteria)
+        if request_id is None:
+            try:
+                certmonger.dogtag_start_tracking(
+                    secdir=paths.PKI_TOMCAT_ALIAS_DIR,
+                    pin=certmonger.get_pin('internal'),
+                    pinfile=None,
+                    nickname=nickname,
+                    ca=RENEWAL_CA_NAME,
+                    pre_command='stop_pkicad',
+                    post_command='renew_ca_cert "%s"' % nickname,
+                )
+                self.log.debug(
+                    'Lightweight CA renewal: '
+                    'added tracking request for "%s"', nickname)
+            except RuntimeError as e:
+                self.log.error(
+                    'Lightweight CA renewal: Certmonger failed to '
+                    'start tracking certificate: %s', e)
+        else:
+            self.log.debug(
+                'Lightweight CA renewal: '
+                'already tracking certificate "%s"', nickname)
+
     def update_file(self, filename, certs, mode=0o444):
         certs = (c[0] for c in certs if c[2] is not False)
         try:
-- 
2.5.5



More information about the Freeipa-devel mailing list