e0ab38
From 6c012544655b3730ebb0a1551cdbce04ab686cfb Mon Sep 17 00:00:00 2001
e0ab38
From: Petr Spacek <pspacek@redhat.com>
e0ab38
Date: Tue, 24 Nov 2015 12:49:40 +0100
e0ab38
Subject: [PATCH] DNSSEC: Make sure that current state in OpenDNSSEC matches
e0ab38
 key state in LDAP
e0ab38
e0ab38
Previously we published timestamps of planned state changes in LDAP.
e0ab38
This led to situations where state transition in OpenDNSSEC was blocked
e0ab38
by an additional condition (or unavailability of OpenDNSSEC) but BIND
e0ab38
actually did the transition as planned.
e0ab38
e0ab38
Additionally key state mapping was incorrect for KSK so sometimes KSK
e0ab38
was not used for signing when it should.
e0ab38
e0ab38
Example (for code without this fix):
e0ab38
- Add a zone and let OpenDNSSEC to generate keys.
e0ab38
- Wait until keys are in state "published" and next state is "inactive".
e0ab38
- Shutdown OpenDNSSEC or break replication from DNSSEC key master.
e0ab38
- See that keys on DNS replicas will transition to state "inactive" even
e0ab38
  though it should not happen because OpenDNSSEC is not available
e0ab38
  (i.e. new keys may not be available).
e0ab38
- End result is that affected zone will not be signed anymore, even
e0ab38
  though it should stay signed with the old keys.
e0ab38
e0ab38
https://fedorahosted.org/freeipa/ticket/5348
e0ab38
e0ab38
Reviewed-By: Martin Basti <mbasti@redhat.com>
e0ab38
Reviewed-By: Martin Basti <mbasti@redhat.com>
e0ab38
---
e0ab38
 daemons/dnssec/ipa-ods-exporter | 105 ++++++++++++++++++++++++++++++++++++----
e0ab38
 1 file changed, 95 insertions(+), 10 deletions(-)
e0ab38
e0ab38
diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
e0ab38
index 12a9294ae05d2ce8d206a2bbf74cc00d81259efa..6ed7588847042e742abeef724940eec31f23ca8f 100755
e0ab38
--- a/daemons/dnssec/ipa-ods-exporter
e0ab38
+++ b/daemons/dnssec/ipa-ods-exporter
e0ab38
@@ -57,6 +57,14 @@ ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock')
e0ab38
 SECRETKEY_WRAPPING_MECH = 'rsaPkcsOaep'
e0ab38
 PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad'
e0ab38
 
e0ab38
+# Constants from OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h
e0ab38
+KSM_STATE_PUBLISH    = 2
e0ab38
+KSM_STATE_READY      = 3
e0ab38
+KSM_STATE_ACTIVE     = 4
e0ab38
+KSM_STATE_RETIRE     = 5
e0ab38
+KSM_STATE_DEAD       = 6
e0ab38
+KSM_STATE_KEYPUBLISH = 10
e0ab38
+
e0ab38
 # DNSKEY flag constants
e0ab38
 dnskey_flag_by_value = {
e0ab38
     0x0001: 'SEP',
e0ab38
@@ -122,6 +130,77 @@ def sql2ldap_keyid(sql_keyid):
e0ab38
     #uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2))
e0ab38
     return {"idnsSecKeyRef": uri}
e0ab38
 
e0ab38
+def ods2bind_timestamps(key_state, key_type, ods_times):
e0ab38
+    """Transform (timestamps and key states) from ODS to set of BIND timestamps
e0ab38
+    with equivalent meaning. At the same time, remove timestamps
e0ab38
+    for future/planned state transitions to prevent ODS & BIND
e0ab38
+    from desynchronizing.
e0ab38
+
e0ab38
+    OpenDNSSEC database may contain timestamps for state transitions planned
e0ab38
+    in the future, but timestamp itself is not sufficient information because
e0ab38
+    there could be some additional condition which is guaded by OpenDNSSEC
e0ab38
+    itself.
e0ab38
+
e0ab38
+    BIND works directly with timestamps without any additional conditions.
e0ab38
+    This difference causes problem when state transition planned in OpenDNSSEC
e0ab38
+    does not happen as originally planned for some reason.
e0ab38
+
e0ab38
+    At the same time, this difference causes problem when OpenDNSSEC on DNSSEC
e0ab38
+    key master and BIND instances on replicas are not synchronized. This
e0ab38
+    happens when DNSSEC key master is down, or a replication is down. Even
e0ab38
+    a temporary desynchronization could cause DNSSEC validation failures
e0ab38
+    which could have huge impact.
e0ab38
+
e0ab38
+    To prevent this problem, this function removes all timestamps corresponding
e0ab38
+    to future state transitions. As a result, BIND will not do state transition
e0ab38
+    until it happens in OpenDNSSEC first and until the change is replicated.
e0ab38
+
e0ab38
+    Also, timestamp mapping depends on key type and is not 1:1.
e0ab38
+    For detailed description of the mapping please see
e0ab38
+    https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC/OpenDNSSEC2BINDKeyStates
e0ab38
+    """
e0ab38
+    bind_times = {}
e0ab38
+    # idnsSecKeyCreated is equivalent to SQL column 'created'
e0ab38
+    bind_times['idnsSecKeyCreated'] = ods_times['idnsSecKeyCreated']
e0ab38
+
e0ab38
+    # set of key states where publishing in DNS zone is desired is taken from
e0ab38
+    # opendnssec/enforcer/ksm/ksm_request.c:KsmRequestIssueKeys()
e0ab38
+    # TODO: support for RFC 5011, requires OpenDNSSEC v1.4.8+
e0ab38
+    if ('idnsSecKeyPublish' in ods_times and
e0ab38
+        key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE,
e0ab38
+                      KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}):
e0ab38
+        bind_times['idnsSecKeyPublish'] = ods_times['idnsSecKeyPublish']
e0ab38
+
e0ab38
+    # ZSK and KSK handling differs in enforcerd, see
e0ab38
+    # opendnssec/enforcer/enforcerd/enforcer.c:commKeyConfig()
e0ab38
+    if key_type == 'ZSK':
e0ab38
+        # idnsSecKeyActivate cannot be set before the key reaches ACTIVE state
e0ab38
+        if ('idnsSecKeyActivate' in ods_times and
e0ab38
+            key_state in {KSM_STATE_ACTIVE, KSM_STATE_RETIRE, KSM_STATE_DEAD}):
e0ab38
+                bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyActivate']
e0ab38
+
e0ab38
+        # idnsSecKeyInactive cannot be set before the key reaches RETIRE state
e0ab38
+        if ('idnsSecKeyInactive' in ods_times and
e0ab38
+            key_state in {KSM_STATE_RETIRE, KSM_STATE_DEAD}):
e0ab38
+                bind_times['idnsSecKeyInactive'] = ods_times['idnsSecKeyInactive']
e0ab38
+
e0ab38
+    elif key_type == 'KSK':
e0ab38
+        # KSK is special: it is used for signing as long as it is in zone
e0ab38
+        if ('idnsSecKeyPublish' in ods_times and
e0ab38
+            key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE,
e0ab38
+                          KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}):
e0ab38
+            bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyPublish']
e0ab38
+        # idnsSecKeyInactive is ignored for KSK on purpose
e0ab38
+
e0ab38
+    else:
e0ab38
+        assert False, "unsupported key type %s" % key_type
e0ab38
+
e0ab38
+    # idnsSecKeyDelete is relevant only in DEAD state
e0ab38
+    if 'idnsSecKeyDelete' in ods_times and key_state == KSM_STATE_DEAD:
e0ab38
+        bind_times['idnsSecKeyDelete'] = ods_times['idnsSecKeyDelete']
e0ab38
+
e0ab38
+    return bind_times
e0ab38
+
e0ab38
 class ods_db_lock(object):
e0ab38
     def __enter__(self):
e0ab38
         self.f = open(ODS_DB_LOCK_PATH, 'w')
e0ab38
@@ -172,18 +251,20 @@ def get_ods_keys(zone_name):
e0ab38
     assert len(rows) == 1, "exactly one DNS zone should exist in ODS DB"
e0ab38
     zone_id = rows[0][0]
e0ab38
 
e0ab38
-    # get all keys for given zone ID
e0ab38
-    cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, dnsk.keytype "
e0ab38
-             "FROM keypairs AS kp JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id "
e0ab38
-             "WHERE dnsk.zone_id = ?", (zone_id,))
e0ab38
+    # get relevant keys for given zone ID:
e0ab38
+    # ignore keys which were generated but not used yet
e0ab38
+    # key state check is using constants from
e0ab38
+    # OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h
e0ab38
+    # WARNING! OpenDNSSEC version 1 and 2 are using different constants!
e0ab38
+    cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, "
e0ab38
+                     "dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, "
e0ab38
+                     "dnsk.keytype, dnsk.state "
e0ab38
+                     "FROM keypairs AS kp "
e0ab38
+                     "JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id "
e0ab38
+                     "WHERE dnsk.zone_id = ?", (zone_id,))
e0ab38
     keys = {}
e0ab38
     for row in cur:
e0ab38
-        key_data = sql2datetimes(row)
e0ab38
-        if 'idnsSecKeyDelete' in key_data \
e0ab38
-            and key_data['idnsSecKeyDelete'] > datetime.now():
e0ab38
-                continue  # ignore deleted keys
e0ab38
-
e0ab38
-        key_data.update(sql2ldap_flags(row['keytype']))
e0ab38
+        key_data = sql2ldap_flags(row['keytype'])
e0ab38
         assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
e0ab38
                 'unexpected key type 0x%x' % row['keytype']
e0ab38
         if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
e0ab38
@@ -191,6 +272,10 @@ def get_ods_keys(zone_name):
e0ab38
         else:
e0ab38
             key_type = 'ZSK'
e0ab38
 
e0ab38
+        # transform key state to timestamps for BIND with equivalent semantics
e0ab38
+        ods_times = sql2datetimes(row)
e0ab38
+        key_data.update(ods2bind_timestamps(row['state'], key_type, ods_times))
e0ab38
+
e0ab38
         key_data.update(sql2ldap_algorithm(row['algorithm']))
e0ab38
         key_id = "%s-%s-%s" % (key_type,
e0ab38
                                datetime2ldap(key_data['idnsSecKeyCreated']),
e0ab38
-- 
e0ab38
2.4.3
e0ab38