DNSSEC: Make sure that current state in OpenDNSSEC matches key state in LDAP

Previously we published timestamps of planned state changes in LDAP.
This led to situations where state transition in OpenDNSSEC was blocked
by an additional condition (or unavailability of OpenDNSSEC) but BIND
actually did the transition as planned.

Additionally key state mapping was incorrect for KSK so sometimes KSK
was not used for signing when it should.

Example (for code without this fix):
- Add a zone and let OpenDNSSEC to generate keys.
- Wait until keys are in state "published" and next state is "inactive".
- Shutdown OpenDNSSEC or break replication from DNSSEC key master.
- See that keys on DNS replicas will transition to state "inactive" even
  though it should not happen because OpenDNSSEC is not available
  (i.e. new keys may not be available).
- End result is that affected zone will not be signed anymore, even
  though it should stay signed with the old keys.

https://fedorahosted.org/freeipa/ticket/5348

Reviewed-By: Martin Basti <mbasti@redhat.com>
This commit is contained in:
Petr Spacek 2015-11-24 12:49:40 +01:00 committed by Martin Basti
parent 9bcb9887ea
commit 9ff1c0ac29

View File

@ -53,6 +53,14 @@ ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock')
SECRETKEY_WRAPPING_MECH = 'rsaPkcsOaep'
PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad'
# Constants from OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h
KSM_STATE_PUBLISH = 2
KSM_STATE_READY = 3
KSM_STATE_ACTIVE = 4
KSM_STATE_RETIRE = 5
KSM_STATE_DEAD = 6
KSM_STATE_KEYPUBLISH = 10
# DNSKEY flag constants
dnskey_flag_by_value = {
0x0001: 'SEP',
@ -118,6 +126,77 @@ def sql2ldap_keyid(sql_keyid):
#uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2))
return {"idnsSecKeyRef": uri}
def ods2bind_timestamps(key_state, key_type, ods_times):
"""Transform (timestamps and key states) from ODS to set of BIND timestamps
with equivalent meaning. At the same time, remove timestamps
for future/planned state transitions to prevent ODS & BIND
from desynchronizing.
OpenDNSSEC database may contain timestamps for state transitions planned
in the future, but timestamp itself is not sufficient information because
there could be some additional condition which is guaded by OpenDNSSEC
itself.
BIND works directly with timestamps without any additional conditions.
This difference causes problem when state transition planned in OpenDNSSEC
does not happen as originally planned for some reason.
At the same time, this difference causes problem when OpenDNSSEC on DNSSEC
key master and BIND instances on replicas are not synchronized. This
happens when DNSSEC key master is down, or a replication is down. Even
a temporary desynchronization could cause DNSSEC validation failures
which could have huge impact.
To prevent this problem, this function removes all timestamps corresponding
to future state transitions. As a result, BIND will not do state transition
until it happens in OpenDNSSEC first and until the change is replicated.
Also, timestamp mapping depends on key type and is not 1:1.
For detailed description of the mapping please see
https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC/OpenDNSSEC2BINDKeyStates
"""
bind_times = {}
# idnsSecKeyCreated is equivalent to SQL column 'created'
bind_times['idnsSecKeyCreated'] = ods_times['idnsSecKeyCreated']
# set of key states where publishing in DNS zone is desired is taken from
# opendnssec/enforcer/ksm/ksm_request.c:KsmRequestIssueKeys()
# TODO: support for RFC 5011, requires OpenDNSSEC v1.4.8+
if ('idnsSecKeyPublish' in ods_times and
key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE,
KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}):
bind_times['idnsSecKeyPublish'] = ods_times['idnsSecKeyPublish']
# ZSK and KSK handling differs in enforcerd, see
# opendnssec/enforcer/enforcerd/enforcer.c:commKeyConfig()
if key_type == 'ZSK':
# idnsSecKeyActivate cannot be set before the key reaches ACTIVE state
if ('idnsSecKeyActivate' in ods_times and
key_state in {KSM_STATE_ACTIVE, KSM_STATE_RETIRE, KSM_STATE_DEAD}):
bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyActivate']
# idnsSecKeyInactive cannot be set before the key reaches RETIRE state
if ('idnsSecKeyInactive' in ods_times and
key_state in {KSM_STATE_RETIRE, KSM_STATE_DEAD}):
bind_times['idnsSecKeyInactive'] = ods_times['idnsSecKeyInactive']
elif key_type == 'KSK':
# KSK is special: it is used for signing as long as it is in zone
if ('idnsSecKeyPublish' in ods_times and
key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE,
KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}):
bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyPublish']
# idnsSecKeyInactive is ignored for KSK on purpose
else:
assert False, "unsupported key type %s" % key_type
# idnsSecKeyDelete is relevant only in DEAD state
if 'idnsSecKeyDelete' in ods_times and key_state == KSM_STATE_DEAD:
bind_times['idnsSecKeyDelete'] = ods_times['idnsSecKeyDelete']
return bind_times
class ods_db_lock(object):
def __enter__(self):
self.f = open(ODS_DB_LOCK_PATH, 'w')
@ -168,18 +247,20 @@ def get_ods_keys(zone_name):
assert len(rows) == 1, "exactly one DNS zone should exist in ODS DB"
zone_id = rows[0][0]
# get all keys for given zone ID
cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, dnsk.keytype "
"FROM keypairs AS kp JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id "
"WHERE dnsk.zone_id = ?", (zone_id,))
# get relevant keys for given zone ID:
# ignore keys which were generated but not used yet
# key state check is using constants from
# OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h
# WARNING! OpenDNSSEC version 1 and 2 are using different constants!
cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, "
"dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, "
"dnsk.keytype, dnsk.state "
"FROM keypairs AS kp "
"JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id "
"WHERE dnsk.zone_id = ?", (zone_id,))
keys = {}
for row in cur:
key_data = sql2datetimes(row)
if 'idnsSecKeyDelete' in key_data \
and key_data['idnsSecKeyDelete'] > datetime.now():
continue # ignore deleted keys
key_data.update(sql2ldap_flags(row['keytype']))
key_data = sql2ldap_flags(row['keytype'])
assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
'unexpected key type 0x%x' % row['keytype']
if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
@ -187,6 +268,10 @@ def get_ods_keys(zone_name):
else:
key_type = 'ZSK'
# transform key state to timestamps for BIND with equivalent semantics
ods_times = sql2datetimes(row)
key_data.update(ods2bind_timestamps(row['state'], key_type, ods_times))
key_data.update(sql2ldap_algorithm(row['algorithm']))
key_id = "%s-%s-%s" % (key_type,
datetime2ldap(key_data['idnsSecKeyCreated']),