summaryrefslogtreecommitdiffstats
path: root/daemons
diff options
context:
space:
mode:
authorPetr Spacek <pspacek@redhat.com>2014-10-19 17:04:40 +0200
committerMartin Kosek <mkosek@redhat.com>2014-10-21 12:23:03 +0200
commit276e69de874f269f6e9089aebb650a5e0814a626 (patch)
tree829b68e2044ba4fd102b8eedf304f9b036f4c583 /daemons
parent5556b7f50e2939d0c61d852f2b0dcd82ba2fcf9c (diff)
downloadfreeipa-276e69de874f269f6e9089aebb650a5e0814a626.tar.gz
freeipa-276e69de874f269f6e9089aebb650a5e0814a626.tar.xz
freeipa-276e69de874f269f6e9089aebb650a5e0814a626.zip
DNSSEC: add ipa dnssec daemons
Tickets: https://fedorahosted.org/freeipa/ticket/3801 https://fedorahosted.org/freeipa/ticket/4417 Design: https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC Reviewed-By: Jan Cholasta <jcholast@redhat.com> Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'daemons')
-rwxr-xr-xdaemons/dnssec/ipa-dnskeysync-replica164
-rwxr-xr-xdaemons/dnssec/ipa-dnskeysyncd106
-rw-r--r--daemons/dnssec/ipa-dnskeysyncd.service15
-rwxr-xr-xdaemons/dnssec/ipa-ods-exporter501
-rw-r--r--daemons/dnssec/ipa-ods-exporter.service15
-rw-r--r--daemons/dnssec/ipa-ods-exporter.socket5
6 files changed, 806 insertions, 0 deletions
diff --git a/daemons/dnssec/ipa-dnskeysync-replica b/daemons/dnssec/ipa-dnskeysync-replica
new file mode 100755
index 000000000..4d3e660d7
--- /dev/null
+++ b/daemons/dnssec/ipa-dnskeysync-replica
@@ -0,0 +1,164 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+"""
+Download keys from LDAP to local HSM.
+
+This program should be run only on replicas, not on DNSSEC masters.
+"""
+
+from binascii import hexlify
+from datetime import datetime
+import dns.dnssec
+import fcntl
+import logging
+import os
+from pprint import pprint
+import subprocess
+import socket
+import sys
+import systemd.daemon
+import systemd.journal
+import time
+
+import ipalib
+from ipapython.dn import DN
+from ipapython.ipa_log_manager import root_logger, standard_logging_setup
+from ipapython import ipaldap
+from ipapython import ipautil
+from ipaserver.plugins.ldap2 import ldap2
+from ipaplatform.paths import paths
+
+from ipapython.dnssec.abshsm import sync_pkcs11_metadata, ldap2p11helper_api_params, wrappingmech_name2id
+from ipapython.dnssec.ldapkeydb import LdapKeyDB
+from ipapython.dnssec.localhsm import LocalHSM
+import _ipap11helper
+
+DAEMONNAME = 'ipa-dnskeysyncd'
+PRINCIPAL = None # not initialized yet
+WORKDIR = '/tmp'
+
+def hex_set(s):
+ out = set()
+ for i in s:
+ out.add("0x%s" % hexlify(i))
+ return out
+
+def update_metadata_set(log, source_set, target_set):
+ """sync metadata from source key set to target key set
+
+ Keys not present in both sets are left intact."""
+ log = log.getChild('sync_metadata')
+ matching_keys = set(source_set.keys()).intersection(set(target_set.keys()))
+ log.info("keys in local HSM & LDAP: %s", hex_set(matching_keys))
+ for key_id in matching_keys:
+ sync_pkcs11_metadata(log, source_set[key_id], target_set[key_id])
+
+
+def find_unwrapping_key(log, localhsm, wrapping_key_uri):
+ wrap_keys = localhsm.find_keys(uri=wrapping_key_uri)
+ # find usable unwrapping key with matching ID
+ for key_id, key in wrap_keys.iteritems():
+ unwrap_keys = localhsm.find_keys(id=key_id, cka_unwrap=True)
+ if len(unwrap_keys) > 0:
+ return unwrap_keys.popitem()[1]
+
+def ldap2replica_master_keys_sync(log, ldapkeydb, localhsm):
+ ## LDAP -> replica master key synchronization
+ # import new master keys from LDAP
+ new_keys = set(ldapkeydb.master_keys.keys()) \
+ - set(localhsm.master_keys.keys())
+ log.debug("master keys in local HSM: %s", hex_set(localhsm.master_keys.keys()))
+ log.debug("master keys in LDAP HSM: %s", hex_set(ldapkeydb.master_keys.keys()))
+ log.debug("new master keys in LDAP HSM: %s", hex_set(new_keys))
+ for mkey_id in new_keys:
+ mkey_ldap = ldapkeydb.master_keys[mkey_id]
+ for wrapped_ldap in mkey_ldap.wrapped_entries:
+ unwrapping_key = find_unwrapping_key(log, localhsm,
+ wrapped_ldap.single_value['ipaWrappingKey'])
+ if unwrapping_key:
+ break
+
+ # TODO: Could it happen in normal cases?
+ assert unwrapping_key is not None, "Local HSM does not contain suitable unwrapping key for master key 0x%s" % hexlify(mkey_id)
+
+ params = ldap2p11helper_api_params(mkey_ldap)
+ params['data'] = wrapped_ldap.single_value['ipaSecretKey']
+ params['unwrapping_key'] = unwrapping_key.handle
+ params['wrapping_mech'] = wrappingmech_name2id[wrapped_ldap.single_value['ipaWrappingMech']]
+ log.debug('Importing new master key: 0x%s %s', hexlify(mkey_id), params)
+ localhsm.p11.import_wrapped_secret_key(**params)
+
+ # synchronize metadata about master keys in LDAP
+ update_metadata_set(log, ldapkeydb.master_keys, localhsm.master_keys)
+
+def ldap2replica_zone_keys_sync(log, ldapkeydb, localhsm):
+ ## LDAP -> replica zone key synchronization
+ # import new zone keys from LDAP
+ new_keys = set(ldapkeydb.zone_keypairs.keys()) \
+ - set(localhsm.zone_privkeys.keys())
+
+ log.debug("zone keys in local HSM: %s", hex_set(localhsm.master_keys.keys()))
+ log.debug("zone keys in LDAP HSM: %s", hex_set(ldapkeydb.master_keys.keys()))
+ log.debug("new zone keys in LDAP HSM: %s", hex_set(new_keys))
+ for zkey_id in new_keys:
+ zkey_ldap = ldapkeydb.zone_keypairs[zkey_id]
+ log.debug('Looking for unwrapping key "%s" for zone key 0x%s',
+ zkey_ldap['ipaWrappingKey'], hexlify(zkey_id))
+ unwrapping_key = find_unwrapping_key(log, localhsm,
+ zkey_ldap['ipaWrappingKey'])
+ assert unwrapping_key is not None, \
+ "Local HSM does not contain suitable unwrapping key for ' \
+ 'zone key 0x%s" % hexlify(zkey_id)
+
+ log.debug('Importing zone key pair 0x%s', hexlify(zkey_id))
+ localhsm.import_private_key(zkey_ldap, zkey_ldap['ipaPrivateKey'],
+ unwrapping_key)
+ localhsm.import_public_key(zkey_ldap, zkey_ldap['ipaPublicKey'])
+
+ # synchronize metadata about zone keys in LDAP & local HSM
+ update_metadata_set(log, ldapkeydb.master_keys, localhsm.master_keys)
+
+ # delete keys removed from LDAP
+ deleted_keys = set(localhsm.zone_privkeys.keys()) \
+ - set(ldapkeydb.zone_keypairs.keys())
+
+ for zkey_id in deleted_keys:
+ localhsm.p11.delete_key(localhsm.zone_pubkeys[zkey_id].handle)
+ localhsm.p11.delete_key(localhsm.zone_privkeys[zkey_id].handle)
+
+
+# IPA framework initialization
+ipalib.api.bootstrap()
+ipalib.api.finalize()
+standard_logging_setup(verbose=True, debug = True) # debug=ipalib.api.env.debug)
+log = root_logger
+log.setLevel(level=logging.DEBUG)
+
+# Kerberos initialization
+PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host))
+log.debug('Kerberos principal: %s', PRINCIPAL)
+ipautil.kinit_hostprincipal(paths.IPA_DNSKEYSYNCD_KEYTAB, WORKDIR, PRINCIPAL)
+log.debug('Got TGT')
+
+# LDAP initialization
+ldap = ipalib.api.Backend[ldap2]
+# fixme
+log.debug('Connecting to LDAP')
+ldap.connect(ccache="%s/ccache" % WORKDIR)
+log.debug('Connected')
+
+
+### DNSSEC master: key synchronization
+ldapkeydb = LdapKeyDB(log, ldap,
+ DN(ipalib.api.env.container_dnssec_keys, ipalib.api.env.basedn))
+
+# TODO: slot number could be configurable
+localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, 0,
+ open(paths.DNSSEC_SOFTHSM_PIN).read())
+
+ldap2replica_master_keys_sync(log, ldapkeydb, localhsm)
+ldap2replica_zone_keys_sync(log, ldapkeydb, localhsm)
+
+sys.exit(0)
diff --git a/daemons/dnssec/ipa-dnskeysyncd b/daemons/dnssec/ipa-dnskeysyncd
new file mode 100755
index 000000000..c7475bd65
--- /dev/null
+++ b/daemons/dnssec/ipa-dnskeysyncd
@@ -0,0 +1,106 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+import sys
+import ldap
+import ldapurl
+import logging
+import os
+import signal
+import systemd.journal
+import time
+
+from ipalib import api
+from ipapython.dn import DN
+from ipapython.ipa_log_manager import root_logger, standard_logging_setup
+from ipapython import ipaldap
+from ipapython import ipautil
+from ipaserver.plugins.ldap2 import ldap2
+from ipaplatform.paths import paths
+
+from ipapython.dnssec.keysyncer import KeySyncer
+
+DAEMONNAME = 'ipa-dnskeysyncd'
+PRINCIPAL = None # not initialized yet
+WORKDIR = '/tmp' # private temp
+KEYTAB_FB = paths.IPA_DNSKEYSYNCD_KEYTAB
+
+# Shutdown handler
+def commenceShutdown(signum, stack):
+ # Declare the needed global variables
+ global watcher_running, ldap_connection, log
+ log.info('Signal %s received: Shutting down!', signum)
+
+ # We are no longer running
+ watcher_running = False
+
+ # Tear down the server connection
+ if ldap_connection:
+ ldap_connection.close_db()
+ del ldap_connection
+
+ # Shutdown
+ sys.exit(0)
+
+
+os.umask(007)
+
+# Global state
+watcher_running = True
+ldap_connection = False
+
+# Signal handlers
+signal.signal(signal.SIGTERM, commenceShutdown)
+signal.signal(signal.SIGINT, commenceShutdown)
+
+# IPA framework initialization
+api.bootstrap()
+api.finalize()
+standard_logging_setup(verbose=True, debug=api.env.debug)
+log = root_logger
+#log.addHandler(systemd.journal.JournalHandler())
+
+# Kerberos initialization
+PRINCIPAL = str('%s/%s' % (DAEMONNAME, api.env.host))
+log.debug('Kerberos principal: %s', PRINCIPAL)
+ipautil.kinit_hostprincipal(KEYTAB_FB, WORKDIR, PRINCIPAL)
+
+# LDAP initialization
+basedn = DN(api.env.container_dns, api.env.basedn)
+ldap_url = ldapurl.LDAPUrl(api.env.ldap_uri)
+ldap_url.dn = str(basedn)
+ldap_url.scope = ldapurl.LDAP_SCOPE_SUBTREE
+ldap_url.filterstr = '(|(objectClass=idnsZone)(objectClass=idnsSecKey)(objectClass=ipk11PublicKey))'
+log.debug('LDAP URL: %s', ldap_url.unparse())
+
+# Real work
+while watcher_running:
+ # Prepare the LDAP server connection (triggers the connection as well)
+ ldap_connection = KeySyncer(ldap_url.initializeUrl(), ipa_api=api)
+
+ # Now we login to the LDAP server
+ try:
+ log.info('LDAP bind...')
+ ldap_connection.sasl_interactive_bind_s("", ipaldap.SASL_GSSAPI)
+ except ldap.INVALID_CREDENTIALS, e:
+ log.exception('Login to LDAP server failed: %s', e)
+ sys.exit(1)
+ except ldap.SERVER_DOWN, e:
+ log.exception('LDAP server is down, going to retry: %s', e)
+ time.sleep(5)
+ continue
+
+ # Commence the syncing
+ log.info('Commencing sync process')
+ ldap_search = ldap_connection.syncrepl_search(
+ ldap_url.dn,
+ ldap_url.scope,
+ mode='refreshAndPersist',
+ attrlist=ldap_url.attrs,
+ filterstr=ldap_url.filterstr
+ )
+
+ while ldap_connection.syncrepl_poll(all=1, msgid=ldap_search):
+ pass
diff --git a/daemons/dnssec/ipa-dnskeysyncd.service b/daemons/dnssec/ipa-dnskeysyncd.service
new file mode 100644
index 000000000..ecd38a593
--- /dev/null
+++ b/daemons/dnssec/ipa-dnskeysyncd.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=IPA key daemon
+
+[Service]
+EnvironmentFile=/etc/sysconfig/ipa-dnskeysyncd
+ExecStart=/usr/libexec/ipa/ipa-dnskeysyncd
+User=ods
+Group=named
+SupplementaryGroups=ods
+PrivateTmp=yes
+Restart=on-failure
+RestartSec=60s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
new file mode 100755
index 000000000..4ae0d99b5
--- /dev/null
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -0,0 +1,501 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+"""
+This is FreeIPA's replacement for signerd from OpenDNSSEC suite version 1.4.x.
+
+This program uses the same socket and protocol as original signerd and should
+be activated via systemd socket activation using "ods-signer" command line
+utility.
+
+Purpose of this replacement is to upload keys generated by OpenDNSSEC to LDAP.
+"""
+
+from binascii import hexlify
+from datetime import datetime
+import dns.dnssec
+import fcntl
+import logging
+import os
+import subprocess
+import socket
+import sys
+import systemd.daemon
+import systemd.journal
+import sqlite3
+import time
+
+import ipalib
+from ipapython.dn import DN
+from ipapython.ipa_log_manager import root_logger, standard_logging_setup
+from ipapython import ipaldap
+from ipapython import ipautil
+from ipaserver.plugins.ldap2 import ldap2
+from ipaplatform.paths import paths
+
+from ipapython.dnssec.abshsm import sync_pkcs11_metadata, wrappingmech_name2id
+from ipapython.dnssec.ldapkeydb import LdapKeyDB
+from ipapython.dnssec.localhsm import LocalHSM
+import _ipap11helper
+
+DAEMONNAME = 'ipa-ods-exporter'
+PRINCIPAL = None # not initialized yet
+WORKDIR = os.path.join(paths.VAR_OPENDNSSEC_DIR ,'tmp')
+KEYTAB_FB = paths.IPA_ODS_EXPORTER_KEYTAB
+
+ODS_SE_MAXLINE = 1024 # from ODS common/config.h
+ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock')
+
+# TODO: MECH_RSA_OAEP
+SECRETKEY_WRAPPING_MECH = 'rsaPkcs'
+PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad'
+
+# DNSKEY flag constants
+dnskey_flag_by_value = {
+ 0x0001: 'SEP',
+ 0x0080: 'REVOKE',
+ 0x0100: 'ZONE'
+}
+
+def dnskey_flags_to_text_set(flags):
+ """Convert a DNSKEY flags value to set texts
+ @rtype: set([string])"""
+
+ flags_set = set()
+ mask = 0x1
+ while mask <= 0x8000:
+ if flags & mask:
+ text = dnskey_flag_by_value.get(mask)
+ if not text:
+ text = hex(mask)
+ flags_set.add(text)
+ mask <<= 1
+ return flags_set
+
+def datetime2ldap(dt):
+ return dt.strftime(ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT)
+
+def sql2datetime(sql_time):
+ return datetime.strptime(sql_time, "%Y-%m-%d %H:%M:%S")
+
+def sql2datetimes(row):
+ row2key_map = {'generate': 'idnsSecKeyCreated',
+ 'publish': 'idnsSecKeyPublish',
+ 'active': 'idnsSecKeyActivate',
+ 'retire': 'idnsSecKeyInactive',
+ 'dead': 'idnsSecKeyDelete'}
+ times = {}
+ for column, key in row2key_map.iteritems():
+ if row[column] is not None:
+ times[key] = sql2datetime(row[column])
+ return times
+
+def sql2ldap_algorithm(sql_algorithm):
+ return {"idnsSecAlgorithm": dns.dnssec.algorithm_to_text(sql_algorithm)}
+
+def sql2ldap_flags(sql_flags):
+ dns_flags = dnskey_flags_to_text_set(sql_flags)
+ ldap_flags = {}
+ for flag in dns_flags:
+ attr = 'idnsSecKey%s' % flag
+ ldap_flags[attr] = 'TRUE'
+ return ldap_flags
+
+def sql2ldap_keyid(sql_keyid):
+ assert len(sql_keyid) % 2 == 0
+ assert len(sql_keyid) > 0
+ # TODO: this is huge hack. BIND has some problems with % notation in URIs.
+ # Workaround: OpenDNSSEC uses same value for ID also for label (but in hex).
+ uri = "pkcs11:object=%s" % sql_keyid
+ #uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2))
+ return {"idnsSecKeyRef": uri}
+
+class ods_db_lock(object):
+ def __enter__(self):
+ self.f = open(ODS_DB_LOCK_PATH, 'w')
+ fcntl.lockf(self.f, fcntl.LOCK_EX)
+
+ def __exit__(self, *args):
+ fcntl.lockf(self.f, fcntl.LOCK_UN)
+ self.f.close()
+
+def get_ldap_zone(ldap, dns_base, name):
+ zone_names = ["%s." % name, name]
+
+ # find zone object: name can optionally end with period
+ ldap_zone = None
+ for zone_name in zone_names:
+ zone_base = DN("idnsname=%s" % zone_name, dns_base)
+ try:
+ ldap_zone = ldap.get_entry(dn=zone_base,
+ attrs_list=["idnsname"])
+ break
+ except ipalib.errors.NotFound:
+ continue
+
+ assert ldap_zone is not None, 'DNS zone "%s" should exist in LDAP' % name
+
+ return ldap_zone
+
+def get_ldap_keys_dn(zone_dn):
+ """Container DN"""
+ return DN("cn=keys", zone_dn)
+
+def get_ldap_keys(ldap, zone_dn):
+ """Keys objects"""
+ keys_dn = get_ldap_keys_dn(zone_dn)
+ ldap_filter = ldap.make_filter_from_attr('objectClass', 'idnsSecKey')
+ ldap_keys = ldap.get_entries(base_dn=keys_dn, filter=ldap_filter)
+
+ return ldap_keys
+
+def get_ods_keys(zone_name):
+ # Open DB directly and read key timestamps etc.
+ with ods_db_lock():
+ db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB,
+ isolation_level="EXCLUSIVE")
+ db.row_factory = sqlite3.Row
+ db.execute('BEGIN')
+
+ # get zone ID
+ cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)",
+ (zone_name,))
+ rows = cur.fetchall()
+ 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.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']))
+ log.debug("%s", key_data)
+ assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
+ 'unexpected key type 0x%x' % row['keytype']
+ if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
+ key_type = 'KSK'
+ else:
+ key_type = 'ZSK'
+
+ key_data.update(sql2ldap_algorithm(row['algorithm']))
+ key_id = "%s-%s-%s" % (key_type,
+ datetime2ldap(key_data['idnsSecKeyCreated']),
+ row['HSMkey_id'])
+
+ key_data.update(sql2ldap_keyid(row['HSMkey_id']))
+ keys[key_id] = key_data
+
+ return keys
+
+def sync_set_metadata_2ldap(log, source_set, target_set):
+ """sync metadata from source key set to target key set in LDAP
+
+ Keys not present in both sets are left intact."""
+ log = log.getChild('sync_set_metadata_2ldap')
+ matching_keys = set(source_set.keys()).intersection(set(target_set.keys()))
+ log.info("keys in local HSM & LDAP: %s", hex_set(matching_keys))
+ for key_id in matching_keys:
+ sync_pkcs11_metadata(log, source_set[key_id], target_set[key_id])
+
+def ldap2master_replica_keys_sync(log, ldapkeydb, localhsm):
+ """LDAP=>master's local HSM replica key synchronization"""
+ # import new replica keys from LDAP
+ log = log.getChild('ldap2master_replica')
+ log.debug("replica pub keys in LDAP: %s", hex_set(ldapkeydb.replica_pubkeys_wrap))
+ log.debug("replica pub keys in SoftHSM: %s", hex_set(localhsm.replica_pubkeys_wrap))
+ new_replica_keys = set(ldapkeydb.replica_pubkeys_wrap.keys()) \
+ - set(localhsm.replica_pubkeys_wrap.keys())
+ log.info("new replica keys in LDAP: %s", hex_set(new_replica_keys))
+ for key_id in new_replica_keys:
+ new_key_ldap = ldapkeydb.replica_pubkeys_wrap[key_id]
+ log.error('label=%s, id=%s, data=%s',
+ new_key_ldap['ipk11label'],
+ hexlify(new_key_ldap['ipk11id']),
+ hexlify(new_key_ldap['ipapublickey']))
+ localhsm.import_public_key(new_key_ldap, new_key_ldap['ipapublickey'])
+
+ # set CKA_WRAP = FALSE for all replica keys removed from LDAP
+ removed_replica_keys = set(localhsm.replica_pubkeys_wrap.keys()) \
+ - set(ldapkeydb.replica_pubkeys_wrap.keys())
+ log.info("obsolete replica keys in local HSM: %s",
+ hex_set(removed_replica_keys))
+ for key_id in removed_replica_keys:
+ localhsm.replica_pubkeys_wrap[key_id]['ipk11wrap'] = False
+
+ # synchronize replica key attributes from LDAP to local HSM
+ sync_set_metadata_2ldap(log, localhsm.replica_pubkeys_wrap,
+ ldapkeydb.replica_pubkeys_wrap)
+
+def master2ldap_master_keys_sync(log, ldapkeydb, localhsm):
+ ## master key -> LDAP synchronization
+ # export new master keys to LDAP
+ new_master_keys = set(localhsm.master_keys.keys()) \
+ - set(ldapkeydb.master_keys.keys())
+ log.debug("master keys in local HSM: %s", hex_set(localhsm.master_keys.keys()))
+ log.debug("master keys in LDAP HSM: %s", hex_set(ldapkeydb.master_keys.keys()))
+ log.debug("new master keys in local HSM: %s", hex_set(new_master_keys))
+ for mkey_id in new_master_keys:
+ mkey = localhsm.master_keys[mkey_id]
+ ldapkeydb.import_master_key(mkey)
+
+ # re-fill cache with keys we just added
+ ldapkeydb.flush()
+ log.debug('master keys in LDAP after flush: %s', hex_set(ldapkeydb.master_keys))
+
+ # synchronize master key metadata to LDAP
+ for mkey_id, mkey_local in localhsm.master_keys.iteritems():
+ log.debug('synchronizing master key metadata: 0x%s', hexlify(mkey_id))
+ sync_pkcs11_metadata(log, mkey_local, ldapkeydb.master_keys[mkey_id])
+
+ # re-wrap all master keys in LDAP with new replica keys (as necessary)
+ enabled_replica_key_ids = set(localhsm.replica_pubkeys_wrap.keys())
+ log.debug('enabled replica key ids: %s', hex_set(enabled_replica_key_ids))
+
+ for mkey_id, mkey_ldap in ldapkeydb.master_keys.iteritems():
+ log.debug('processing master key data: 0x%s', hexlify(mkey_id))
+
+ # check that all active replicas have own copy of master key
+ used_replica_keys = set()
+ for wrapped_entry in mkey_ldap.wrapped_entries:
+ matching_keys = localhsm.find_keys(
+ uri=wrapped_entry.single_value['ipaWrappingKey'])
+ for matching_key in matching_keys.itervalues():
+ assert matching_key['ipk11label'].startswith(u'dnssec-replica:'), \
+ 'Wrapped key "%s" refers to PKCS#11 URI "%s" which is ' \
+ 'not a know DNSSEC replica key: label "%s" does not start ' \
+ 'with "dnssec-replica:" prefix' % (wrapped_entry.dn,
+ wrapped_entry['ipaWrappingKey'],
+ matching_key['ipk11label'])
+ used_replica_keys.add(matching_key['ipk11id'])
+
+ new_replica_keys = enabled_replica_key_ids - used_replica_keys
+ log.debug('master key 0x%s is not wrapped with replica keys %s',
+ hexlify(mkey_id), hex_set(new_replica_keys))
+
+ # wrap master key with new replica keys
+ mkey_local = localhsm.find_keys(id=mkey_id).popitem()[1]
+ for replica_key_id in new_replica_keys:
+ log.info('adding master key 0x%s wrapped with replica key 0x%s' % (
+ hexlify(mkey_id), hexlify(replica_key_id)))
+ replica_key = localhsm.replica_pubkeys_wrap[replica_key_id]
+ keydata = localhsm.p11.export_wrapped_key(mkey_local.handle,
+ replica_key.handle, _ipap11helper.MECH_RSA_PKCS)
+ mkey_ldap.add_wrapped_data(keydata, SECRETKEY_WRAPPING_MECH,
+ replica_key_id)
+
+ ldapkeydb.flush()
+
+def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm):
+ # synchroniza zone keys
+ log = log.getChild('master2ldap_zone_keys')
+ keypairs_ldap = ldapkeydb.zone_keypairs
+ log.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
+
+ pubkeys_local = localhsm.zone_pubkeys
+ privkeys_local = localhsm.zone_privkeys
+ log.debug("zone keys in local HSM: %s", hex_set(privkeys_local))
+
+ assert set(pubkeys_local) == set(privkeys_local), \
+ "IDs of private and public keys for DNS zones in local HSM does " \
+ "not match to key pairs: %s vs. %s" % \
+ (hex_set(pubkeys_local), hex_set(privkeys_local))
+
+ new_keys = set(pubkeys_local) - set(keypairs_ldap)
+ log.debug("new zone keys in local HSM: %s", hex_set(new_keys))
+ mkey = localhsm.active_master_key
+ # wrap each new zone key pair with selected master key
+ for zkey_id in new_keys:
+ pubkey = pubkeys_local[zkey_id]
+ pubkey_data = localhsm.p11.export_public_key(pubkey.handle)
+
+ privkey = privkeys_local[zkey_id]
+ privkey_data = localhsm.p11.export_wrapped_key(privkey.handle,
+ wrapping_key=mkey.handle,
+ wrapping_mech=wrappingmech_name2id[PRIVKEY_WRAPPING_MECH])
+ ldapkeydb.import_zone_key(pubkey, pubkey_data, privkey, privkey_data,
+ PRIVKEY_WRAPPING_MECH, mkey['ipk11id'])
+
+ sync_set_metadata_2ldap(log, pubkeys_local, keypairs_ldap)
+ sync_set_metadata_2ldap(log, privkeys_local, keypairs_ldap)
+ ldapkeydb.flush()
+
+
+def hex_set(s):
+ out = set()
+ for i in s:
+ out.add("0x%s" % hexlify(i))
+ return out
+
+def receive_zone_name(log):
+ fds = systemd.daemon.listen_fds()
+ if len(fds) != 1:
+ raise KeyError('Exactly one socket is expected.')
+
+ sck = socket.fromfd(fds[0], socket.AF_UNIX, socket.SOCK_STREAM)
+
+ conn, addr = sck.accept()
+ log.debug('accepted new connection %s', repr(conn))
+
+ # this implements cmdhandler_handle_cmd() logic
+ cmd = conn.recv(ODS_SE_MAXLINE)
+ cmd = cmd.strip()
+
+ try:
+ if cmd == 'ipa-hsm-update':
+ msg = 'HSM synchronization finished, exiting.'
+ conn.send('%s\n' % msg)
+ log.info(msg)
+ sys.exit(0)
+
+ elif not cmd.startswith('update '):
+ conn.send('Command "%s" is not supported by IPA; ' \
+ 'HSM synchronization was finished and the command ' \
+ 'will be ignored.\n' % cmd)
+ log.info('Ignoring unsupported command "%s".', cmd)
+ sys.exit(0)
+
+ else:
+ zone_name = cmd2ods_zone_name(cmd)
+ conn.send('Update request for zone "%s" queued.\n' % zone_name)
+ log.info('Processing command: "%s"', cmd)
+
+ finally:
+ # Reply & close connection early.
+ # This is necessary to let Enforcer to unlock the ODS DB.
+ conn.shutdown(socket.SHUT_RDWR)
+ conn.close()
+
+ return zone_name
+
+def cmd2ods_zone_name(cmd):
+ # ODS stores zone name without trailing period
+ zone_name = cmd[7:].strip()
+ if len(zone_name) > 1 and zone_name[-1] == '.':
+ zone_name = zone_name[:-1]
+
+ return zone_name
+
+log = logging.getLogger('root')
+# this service is socket-activated
+log.addHandler(systemd.journal.JournalHandler())
+log.setLevel(level=logging.DEBUG)
+
+if len(sys.argv) != 1:
+ print __doc__
+ sys.exit(1)
+
+# IPA framework initialization
+ipalib.api.bootstrap()
+ipalib.api.finalize()
+
+# Kerberos initialization
+PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host))
+log.debug('Kerberos principal: %s', PRINCIPAL)
+ipautil.kinit_hostprincipal(paths.IPA_ODS_EXPORTER_KEYTAB, WORKDIR, PRINCIPAL)
+log.debug('Got TGT')
+
+# LDAP initialization
+dns_dn = DN(ipalib.api.env.container_dns, ipalib.api.env.basedn)
+ldap = ipalib.api.Backend[ldap2]
+# fixme
+log.debug('Connecting to LDAP')
+ldap.connect(ccache="%s/ccache" % WORKDIR)
+log.debug('Connected')
+
+
+### DNSSEC master: key synchronization
+ldapkeydb = LdapKeyDB(log, ldap, DN(ipalib.api.env.container_dnssec_keys,
+ ipalib.api.env.basedn))
+localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, 0,
+ open(paths.DNSSEC_SOFTHSM_PIN).read())
+
+ldap2master_replica_keys_sync(log, ldapkeydb, localhsm)
+master2ldap_master_keys_sync(log, ldapkeydb, localhsm)
+master2ldap_zone_keys_sync(log, ldapkeydb, localhsm)
+
+
+### DNSSEC master: DNSSEC key metadata upload
+# command receive is delayed so the command will stay in socket queue until
+# the problem with LDAP server or HSM is fixed
+try:
+ zone_name = receive_zone_name(log)
+
+# Handle cases where somebody ran the program without systemd.
+except KeyError as e:
+ print 'HSM (key material) sychronization is finished but ' \
+ 'this program should be socket-activated by OpenDNSSEC.'
+ print 'Use "ods-signer" command line utility to synchronize ' \
+ 'DNS zone keys and metadata.'
+ print 'Error: %s' % e
+ sys.exit(0)
+
+ods_keys = get_ods_keys(zone_name)
+ods_keys_id = set(ods_keys.keys())
+
+ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name)
+zone_dn = ldap_zone.dn
+
+keys_dn = get_ldap_keys_dn(zone_dn)
+try:
+ ldap_keys = get_ldap_keys(ldap, zone_dn)
+except ipalib.errors.NotFound:
+ # cn=keys container does not exist, create it
+ ldap_keys = []
+ ldap_keys_container = ldap.make_entry(keys_dn,
+ objectClass=['nsContainer'])
+ try:
+ ldap.add_entry(ldap_keys_container)
+ except ipalib.errors.DuplicateEntry:
+ # ldap.get_entries() does not distinguish non-existent base DN
+ # from empty result set so addition can fail because container
+ # itself exists already
+ pass
+
+ldap_keys_dict = {}
+for ldap_key in ldap_keys:
+ cn = ldap_key['cn'][0]
+ ldap_keys_dict[cn] = ldap_key
+
+ldap_keys = ldap_keys_dict # shorthand
+ldap_keys_id = set(ldap_keys.keys())
+
+new_keys_id = ods_keys_id - ldap_keys_id
+log.info('new keys from ODS: %s', new_keys_id)
+for key_id in new_keys_id:
+ cn = "cn=%s" % key_id
+ key_dn = DN(cn, keys_dn)
+ log.debug('adding key "%s" to LDAP', key_dn)
+ ldap_key = ldap.make_entry(key_dn,
+ objectClass=['idnsSecKey'],
+ **ods_keys[key_id])
+ ldap.add_entry(ldap_key)
+
+deleted_keys_id = ldap_keys_id - ods_keys_id
+log.info('deleted keys in LDAP: %s', deleted_keys_id)
+for key_id in deleted_keys_id:
+ cn = "cn=%s" % key_id
+ key_dn = DN(cn, keys_dn)
+ log.debug('deleting key "%s" from LDAP', key_dn)
+ ldap.delete_entry(key_dn)
+
+update_keys_id = ldap_keys_id.intersection(ods_keys_id)
+log.info('keys in LDAP & ODS: %s', update_keys_id)
+for key_id in update_keys_id:
+ ldap_key = ldap_keys[key_id]
+ ods_key = ods_keys[key_id]
+ log.debug('updating key "%s" in LDAP', ldap_key.dn)
+ ldap_key.update(ods_key)
+ try:
+ ldap.update_entry(ldap_key)
+ except ipalib.errors.EmptyModlist:
+ continue
+
+log.debug('Done')
diff --git a/daemons/dnssec/ipa-ods-exporter.service b/daemons/dnssec/ipa-ods-exporter.service
new file mode 100644
index 000000000..0d917b8d3
--- /dev/null
+++ b/daemons/dnssec/ipa-ods-exporter.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=IPA OpenDNSSEC Signer replacement
+Wants=ipa-ods-exporter.socket
+After=ipa-ods-exporter.socket
+
+[Service]
+EnvironmentFile=/etc/sysconfig/ipa-ods-exporter
+ExecStart=/usr/libexec/ipa/ipa-ods-exporter
+User=ods
+PrivateTmp=yes
+Restart=on-failure
+RestartSec=60s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/daemons/dnssec/ipa-ods-exporter.socket b/daemons/dnssec/ipa-ods-exporter.socket
new file mode 100644
index 000000000..1499f1823
--- /dev/null
+++ b/daemons/dnssec/ipa-ods-exporter.socket
@@ -0,0 +1,5 @@
+[Socket]
+ListenStream=/var/run/opendnssec/engine.sock
+
+[Install]
+WantedBy=sockets.target