e0ab38
From db24ab9357ea63deaf25e8b9c5b3ad2d08a0c82b Mon Sep 17 00:00:00 2001
e0ab38
From: Petr Spacek <pspacek@redhat.com>
e0ab38
Date: Tue, 15 Dec 2015 15:22:45 +0100
e0ab38
Subject: [PATCH] DNSSEC: remove keys purged by OpenDNSSEC from master HSM from
e0ab38
 LDAP
e0ab38
e0ab38
Key purging has to be only only after key metadata purging so
e0ab38
ipa-dnskeysyncd on replices does not fail while dereferencing
e0ab38
non-existing keys.
e0ab38
e0ab38
https://fedorahosted.org/freeipa/ticket/5334
e0ab38
e0ab38
Reviewed-By: Martin Basti <mbasti@redhat.com>
e0ab38
Reviewed-By: Martin Basti <mbasti@redhat.com>
e0ab38
---
e0ab38
 daemons/dnssec/ipa-ods-exporter | 45 ++++++++++++++++++++++----
e0ab38
 ipapython/dnssec/ldapkeydb.py   | 72 ++++++++++++++++++++++++++++++++++-------
e0ab38
 2 files changed, 99 insertions(+), 18 deletions(-)
e0ab38
e0ab38
diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
e0ab38
index 051fa53a950f7afbea5e9b1e541a9435aa02bc17..2a1cc4315355569b24ec6ef42a68f4d64fee9f4f 100755
e0ab38
--- a/daemons/dnssec/ipa-ods-exporter
e0ab38
+++ b/daemons/dnssec/ipa-ods-exporter
e0ab38
@@ -387,7 +387,10 @@ def master2ldap_master_keys_sync(log, ldapkeydb, localhsm):
e0ab38
     ldapkeydb.flush()
e0ab38
 
e0ab38
 def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm):
e0ab38
-    # synchroniza zone keys
e0ab38
+    """add and update zone key material from local HSM to LDAP
e0ab38
+
e0ab38
+    No key material will be removed, only new keys will be added or updated.
e0ab38
+    Key removal is hanled by master2ldap_zone_keys_purge()."""
e0ab38
     log = log.getChild('master2ldap_zone_keys')
e0ab38
     keypairs_ldap = ldapkeydb.zone_keypairs
e0ab38
     log.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
e0ab38
@@ -396,10 +399,10 @@ def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm):
e0ab38
     privkeys_local = localhsm.zone_privkeys
e0ab38
     log.debug("zone keys in local HSM: %s", hex_set(privkeys_local))
e0ab38
 
e0ab38
-    assert set(pubkeys_local) == set(privkeys_local), \
e0ab38
-            "IDs of private and public keys for DNS zones in local HSM does " \
e0ab38
-            "not match to key pairs: %s vs. %s" % \
e0ab38
-            (hex_set(pubkeys_local), hex_set(privkeys_local))
e0ab38
+    assert set(pubkeys_local) == set(privkeys_local), (
e0ab38
+            "IDs of private and public keys for DNS zones in local HSM does "
e0ab38
+            "not match to key pairs: %s vs. %s" %
e0ab38
+            (hex_set(pubkeys_local), hex_set(privkeys_local)))
e0ab38
 
e0ab38
     new_keys = set(pubkeys_local) - set(keypairs_ldap)
e0ab38
     log.debug("new zone keys in local HSM: %s", hex_set(new_keys))
e0ab38
@@ -420,6 +423,29 @@ def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm):
e0ab38
     sync_set_metadata_2ldap(log, privkeys_local, keypairs_ldap)
e0ab38
     ldapkeydb.flush()
e0ab38
 
e0ab38
+def master2ldap_zone_keys_purge(log, ldapkeydb, localhsm):
e0ab38
+    """purge removed key material from LDAP (but not metadata)
e0ab38
+
e0ab38
+    Keys which are present in LDAP but not in local HSM will be removed.
e0ab38
+    Key metadata must be removed first so references to removed key material
e0ab38
+    are removed before actually removing the keys."""
e0ab38
+    keypairs_ldap = ldapkeydb.zone_keypairs
e0ab38
+    log.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
e0ab38
+
e0ab38
+    pubkeys_local = localhsm.zone_pubkeys
e0ab38
+    privkeys_local = localhsm.zone_privkeys
e0ab38
+    log.debug("zone keys in local HSM: %s", hex_set(privkeys_local))
e0ab38
+    assert set(pubkeys_local) == set(privkeys_local), \
e0ab38
+            "IDs of private and public keys for DNS zones in local HSM does " \
e0ab38
+            "not match to key pairs: %s vs. %s" % \
e0ab38
+            (hex_set(pubkeys_local), hex_set(privkeys_local))
e0ab38
+
e0ab38
+    deleted_key_ids = set(keypairs_ldap) - set(pubkeys_local)
e0ab38
+    log.debug("zone keys deleted from local HSM but present in LDAP: %s",
e0ab38
+            hex_set(deleted_key_ids))
e0ab38
+    for zkey_id in deleted_key_ids:
e0ab38
+        keypairs_ldap[zkey_id].schedule_deletion()
e0ab38
+    ldapkeydb.flush()
e0ab38
 
e0ab38
 def hex_set(s):
e0ab38
     out = set()
e0ab38
@@ -600,7 +626,7 @@ ldap.connect(ccache=ccache_name)
e0ab38
 log.debug('Connected')
e0ab38
 
e0ab38
 
e0ab38
-### DNSSEC master: key synchronization
e0ab38
+### DNSSEC master: key material upload & synchronization (but not deletion)
e0ab38
 ldapkeydb = LdapKeyDB(log, ldap, DN(('cn', 'keys'), ('cn', 'sec'),
e0ab38
                                     ipalib.api.env.container_dns,
e0ab38
                                     ipalib.api.env.basedn))
e0ab38
@@ -612,7 +638,7 @@ master2ldap_master_keys_sync(log, ldapkeydb, localhsm)
e0ab38
 master2ldap_zone_keys_sync(log, ldapkeydb, localhsm)
e0ab38
 
e0ab38
 
e0ab38
-### DNSSEC master: DNSSEC key metadata upload
e0ab38
+### DNSSEC master: DNSSEC key metadata upload & synchronization & deletion
e0ab38
 # command receive is delayed so the command will stay in socket queue until
e0ab38
 # the problem with LDAP server or HSM is fixed
e0ab38
 try:
e0ab38
@@ -666,6 +692,11 @@ try:
e0ab38
         for zone_row in db.execute("SELECT name FROM zones"):
e0ab38
             sync_zone(log, ldap, dns_dn, zone_row['name'])
e0ab38
 
e0ab38
+    ### DNSSEC master: DNSSEC key material purging
e0ab38
+    # references to old key material were removed above in sync_zone()
e0ab38
+    # so now we can purge old key material from LDAP
e0ab38
+    master2ldap_zone_keys_purge(log, ldapkeydb, localhsm)
e0ab38
+
e0ab38
 except Exception as ex:
e0ab38
     msg = "ipa-ods-exporter exception: %s" % traceback.format_exc(ex)
e0ab38
     log.exception(ex)
e0ab38
diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py
e0ab38
index 54a1fba1d2db8f27c9c9b881ff42201365852587..2131508cc6779d1cc99c417a31da866b7fdcf2c2 100644
e0ab38
--- a/ipapython/dnssec/ldapkeydb.py
e0ab38
+++ b/ipapython/dnssec/ldapkeydb.py
e0ab38
@@ -105,40 +105,56 @@ def get_default_attrs(object_classes):
e0ab38
         result.update(defaults[cls])
e0ab38
     return result
e0ab38
 
e0ab38
+
e0ab38
 class Key(collections.MutableMapping):
e0ab38
     """abstraction to hide LDAP entry weirdnesses:
e0ab38
         - non-normalized attribute names
e0ab38
         - boolean attributes returned as strings
e0ab38
+        - planned entry deletion prevents subsequent use of the instance
e0ab38
     """
e0ab38
     def __init__(self, entry, ldap, ldapkeydb):
e0ab38
         self.entry = entry
e0ab38
+        self._delentry = None  # indicates that object was deleted
e0ab38
         self.ldap = ldap
e0ab38
         self.ldapkeydb = ldapkeydb
e0ab38
         self.log = ldap.log.getChild(__name__)
e0ab38
 
e0ab38
+    def __assert_not_deleted(self):
e0ab38
+        assert self.entry and not self._delentry, (
e0ab38
+            "attempt to use to-be-deleted entry %s detected"
e0ab38
+            % self._delentry.dn)
e0ab38
+
e0ab38
     def __getitem__(self, key):
e0ab38
+        self.__assert_not_deleted()
e0ab38
         val = self.entry.single_value[key]
e0ab38
         if key.lower() in bool_attr_names:
e0ab38
             val = ldap_bool(val)
e0ab38
         return val
e0ab38
 
e0ab38
     def __setitem__(self, key, value):
e0ab38
+        self.__assert_not_deleted()
e0ab38
         self.entry[key] = value
e0ab38
 
e0ab38
     def __delitem__(self, key):
e0ab38
+        self.__assert_not_deleted()
e0ab38
         del self.entry[key]
e0ab38
 
e0ab38
     def __iter__(self):
e0ab38
         """generates list of ipa names of all PKCS#11 attributes present in the object"""
e0ab38
+        self.__assert_not_deleted()
e0ab38
         for ipa_name in self.entry.keys():
e0ab38
             lowercase = ipa_name.lower()
e0ab38
             if lowercase in attrs_name2id:
e0ab38
                 yield lowercase
e0ab38
 
e0ab38
     def __len__(self):
e0ab38
+        self.__assert_not_deleted()
e0ab38
         return len(self.entry)
e0ab38
 
e0ab38
     def __repr__(self):
e0ab38
+        if self._delentry:
e0ab38
+            return 'deleted entry: %s' % repr(self._delentry)
e0ab38
+
e0ab38
         sanitized = dict(self.entry)
e0ab38
         for attr in ['ipaPrivateKey', 'ipaPublicKey', 'ipk11publickeyinfo']:
e0ab38
             if attr in sanitized:
e0ab38
@@ -153,6 +169,49 @@ class Key(collections.MutableMapping):
e0ab38
             if self.get(attr, empty) == default_attrs[attr]:
e0ab38
                 del self[attr]
e0ab38
 
e0ab38
+    def _update_key(self):
e0ab38
+        """remove default values from LDAP entry and write back changes"""
e0ab38
+        if self._delentry:
e0ab38
+            self._delete_key()
e0ab38
+            return
e0ab38
+
e0ab38
+        self._cleanup_key()
e0ab38
+
e0ab38
+        try:
e0ab38
+            self.ldap.update_entry(self.entry)
e0ab38
+        except ipalib.errors.EmptyModlist:
e0ab38
+            pass
e0ab38
+
e0ab38
+    def _delete_key(self):
e0ab38
+        """remove key metadata entry from LDAP
e0ab38
+
e0ab38
+        After calling this, the python object is no longer valid and all
e0ab38
+        subsequent method calls on it will fail.
e0ab38
+        """
e0ab38
+        assert not self.entry, (
e0ab38
+            "Key._delete_key() called before Key.schedule_deletion()")
e0ab38
+        assert self._delentry, "Key._delete_key() called more than once"
e0ab38
+        self.log.debug('deleting key id 0x%s DN %s from LDAP',
e0ab38
+                       hexlify(self._delentry.single_value['ipk11id']),
e0ab38
+                       self._delentry.dn)
e0ab38
+        self.ldap.delete_entry(self._delentry)
e0ab38
+        self._delentry = None
e0ab38
+        self.ldap = None
e0ab38
+        self.ldapkeydb = None
e0ab38
+
e0ab38
+    def schedule_deletion(self):
e0ab38
+        """schedule key deletion from LDAP
e0ab38
+
e0ab38
+        Calling schedule_deletion() will make this object incompatible with
e0ab38
+        normal Key. After that the object must not be read or modified.
e0ab38
+        Key metadata will be actually deleted when LdapKeyDB.flush() is called.
e0ab38
+        """
e0ab38
+        assert not self._delentry, (
e0ab38
+            "Key.schedule_deletion() called more than once")
e0ab38
+        self._delentry = self.entry
e0ab38
+        self.entry = None
e0ab38
+
e0ab38
+
e0ab38
 class ReplicaKey(Key):
e0ab38
     # TODO: object class assert
e0ab38
     def __init__(self, entry, ldap, ldapkeydb):
e0ab38
@@ -239,21 +298,12 @@ class LdapKeyDB(AbstractHSM):
e0ab38
         self._update_keys()
e0ab38
         return keys
e0ab38
 
e0ab38
-    def _update_key(self, key):
e0ab38
-        """remove default values from LDAP entry and write back changes"""
e0ab38
-        key._cleanup_key()
e0ab38
-
e0ab38
-        try:
e0ab38
-            self.ldap.update_entry(key.entry)
e0ab38
-        except ipalib.errors.EmptyModlist:
e0ab38
-            pass
e0ab38
-
e0ab38
     def _update_keys(self):
e0ab38
         for cache in [self.cache_masterkeys, self.cache_replica_pubkeys_wrap,
e0ab38
-                self.cache_zone_keypairs]:
e0ab38
+                      self.cache_zone_keypairs]:
e0ab38
             if cache:
e0ab38
                 for key in cache.itervalues():
e0ab38
-                    self._update_key(key)
e0ab38
+                    key._update_key()
e0ab38
 
e0ab38
     def flush(self):
e0ab38
         """write back content of caches to LDAP"""
e0ab38
-- 
e0ab38
2.4.3
e0ab38