summaryrefslogtreecommitdiffstats
path: root/ipapython/dnssec
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 /ipapython/dnssec
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 'ipapython/dnssec')
-rw-r--r--ipapython/dnssec/__init__.py0
-rw-r--r--ipapython/dnssec/abshsm.py187
-rw-r--r--ipapython/dnssec/bindmgr.py176
-rw-r--r--ipapython/dnssec/keysyncer.py181
-rw-r--r--ipapython/dnssec/ldapkeydb.py351
-rwxr-xr-xipapython/dnssec/localhsm.py229
-rw-r--r--ipapython/dnssec/odsmgr.py194
-rw-r--r--ipapython/dnssec/syncrepl.py123
-rw-r--r--ipapython/dnssec/temp.py23
9 files changed, 1464 insertions, 0 deletions
diff --git a/ipapython/dnssec/__init__.py b/ipapython/dnssec/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ipapython/dnssec/__init__.py
diff --git a/ipapython/dnssec/abshsm.py b/ipapython/dnssec/abshsm.py
new file mode 100644
index 000000000..cc8119865
--- /dev/null
+++ b/ipapython/dnssec/abshsm.py
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+import _ipap11helper
+
+attrs_id2name = {
+ #_ipap11helper.CKA_ALLOWED_MECHANISMS: 'ipk11allowedmechanisms',
+ _ipap11helper.CKA_ALWAYS_AUTHENTICATE: 'ipk11alwaysauthenticate',
+ _ipap11helper.CKA_ALWAYS_SENSITIVE: 'ipk11alwayssensitive',
+ #_ipap11helper.CKA_CHECK_VALUE: 'ipk11checkvalue',
+ _ipap11helper.CKA_COPYABLE: 'ipk11copyable',
+ _ipap11helper.CKA_DECRYPT: 'ipk11decrypt',
+ _ipap11helper.CKA_DERIVE: 'ipk11derive',
+ #_ipap11helper.CKA_DESTROYABLE: 'ipk11destroyable',
+ _ipap11helper.CKA_ENCRYPT: 'ipk11encrypt',
+ #_ipap11helper.CKA_END_DATE: 'ipk11enddate',
+ _ipap11helper.CKA_EXTRACTABLE: 'ipk11extractable',
+ _ipap11helper.CKA_ID: 'ipk11id',
+ #_ipap11helper.CKA_KEY_GEN_MECHANISM: 'ipk11keygenmechanism',
+ _ipap11helper.CKA_KEY_TYPE: 'ipk11keytype',
+ _ipap11helper.CKA_LABEL: 'ipk11label',
+ _ipap11helper.CKA_LOCAL: 'ipk11local',
+ _ipap11helper.CKA_MODIFIABLE: 'ipk11modifiable',
+ _ipap11helper.CKA_NEVER_EXTRACTABLE: 'ipk11neverextractable',
+ _ipap11helper.CKA_PRIVATE: 'ipk11private',
+ #_ipap11helper.CKA_PUBLIC_KEY_INFO: 'ipapublickey',
+ #_ipap11helper.CKA_PUBLIC_KEY_INFO: 'ipk11publickeyinfo',
+ _ipap11helper.CKA_SENSITIVE: 'ipk11sensitive',
+ _ipap11helper.CKA_SIGN: 'ipk11sign',
+ _ipap11helper.CKA_SIGN_RECOVER: 'ipk11signrecover',
+ #_ipap11helper.CKA_START_DATE: 'ipk11startdate',
+ #_ipap11helper.CKA_SUBJECT: 'ipk11subject',
+ _ipap11helper.CKA_TRUSTED: 'ipk11trusted',
+ _ipap11helper.CKA_UNWRAP: 'ipk11unwrap',
+ #_ipap11helper.CKA_UNWRAP_TEMPLATE: 'ipk11unwraptemplate',
+ _ipap11helper.CKA_VERIFY: 'ipk11verify',
+ _ipap11helper.CKA_VERIFY_RECOVER: 'ipk11verifyrecover',
+ _ipap11helper.CKA_WRAP: 'ipk11wrap',
+ #_ipap11helper.CKA_WRAP_TEMPLATE: 'ipk11wraptemplate',
+ _ipap11helper.CKA_WRAP_WITH_TRUSTED: 'ipk11wrapwithtrusted',
+}
+
+attrs_name2id = dict(zip(attrs_id2name.values(), attrs_id2name.keys()))
+
+# attribute:
+# http://www.freeipa.org/page/V4/PKCS11_in_LDAP/Schema#ipk11KeyType
+#
+# mapping table:
+# http://www.freeipa.org/page/V4/PKCS11_in_LDAP/Schema#CK_MECHANISM_TYPE
+keytype_name2id = {
+ "rsa": _ipap11helper.KEY_TYPE_RSA,
+ "aes": _ipap11helper.KEY_TYPE_AES,
+ }
+
+keytype_id2name = dict(zip(keytype_name2id.values(), keytype_name2id.keys()))
+
+wrappingmech_name2id = {
+ "rsaPkcs": _ipap11helper.MECH_RSA_PKCS,
+ "rsaPkcsOaep": _ipap11helper.MECH_RSA_PKCS_OAEP,
+ "aesKeyWrap": _ipap11helper.MECH_AES_KEY_WRAP,
+ "aesKeyWrapPad": _ipap11helper.MECH_AES_KEY_WRAP_PAD
+ }
+
+wrappingmech_id2name = dict(zip(wrappingmech_name2id.values(),
+ wrappingmech_name2id.keys()))
+
+
+bool_attr_names = set([
+ 'ipk11alwaysauthenticate',
+ 'ipk11alwayssensitive',
+ 'ipk11copyable',
+ 'ipk11decrypt',
+ 'ipk11derive',
+ 'ipk11encrypt',
+ 'ipk11extractable',
+ 'ipk11local',
+ 'ipk11modifiable',
+ 'ipk11neverextractable',
+ 'ipk11private',
+ 'ipk11sensitive',
+ 'ipk11sign',
+ 'ipk11signrecover',
+ 'ipk11trusted',
+ 'ipk11unwrap',
+ 'ipk11verify',
+ 'ipk11verifyrecover',
+ 'ipk11wrap',
+ 'ipk11wrapwithtrusted',
+])
+
+modifiable_attrs_id2name = {
+ _ipap11helper.CKA_DECRYPT: 'ipk11decrypt',
+ _ipap11helper.CKA_DERIVE: 'ipk11derive',
+ _ipap11helper.CKA_ENCRYPT: 'ipk11encrypt',
+ _ipap11helper.CKA_EXTRACTABLE: 'ipk11extractable',
+ _ipap11helper.CKA_ID: 'ipk11id',
+ _ipap11helper.CKA_LABEL: 'ipk11label',
+ _ipap11helper.CKA_SENSITIVE: 'ipk11sensitive',
+ _ipap11helper.CKA_SIGN: 'ipk11sign',
+ _ipap11helper.CKA_SIGN_RECOVER: 'ipk11signrecover',
+ _ipap11helper.CKA_UNWRAP: 'ipk11unwrap',
+ _ipap11helper.CKA_VERIFY: 'ipk11verify',
+ _ipap11helper.CKA_VERIFY_RECOVER: 'ipk11verifyrecover',
+ _ipap11helper.CKA_WRAP: 'ipk11wrap',
+}
+
+modifiable_attrs_name2id = dict(zip(modifiable_attrs_id2name.values(),
+ modifiable_attrs_id2name.keys()))
+
+def sync_pkcs11_metadata(log, source, target):
+ """sync ipk11 metadata from source object to target object"""
+
+ # iterate over list of modifiable PKCS#11 attributes - this prevents us
+ # from attempting to set read-only attributes like CKA_LOCAL
+ for attr in modifiable_attrs_name2id:
+ if attr in source:
+ if source[attr] != target[attr]:
+ log.debug('Updating attribute %s from "%s" to "%s"', attr, repr(source[attr]), repr(target[attr]))
+ target[attr] = source[attr]
+
+def populate_pkcs11_metadata(source, target):
+ """populate all ipk11 metadata attributes in target object from source object"""
+ for attr in attrs_name2id:
+ if attr in source:
+ target[attr] = source[attr]
+
+def ldap2p11helper_api_params(ldap_key):
+ """prepare dict with metadata parameters suitable for key unwrapping"""
+ unwrap_params = {}
+
+ # some attributes are just renamed
+ direct_param_map = {
+ "ipk11label": "label",
+ "ipk11id": "id",
+ "ipk11copyable": "cka_copyable",
+ "ipk11decrypt": "cka_decrypt",
+ "ipk11derive": "cka_derive",
+ "ipk11encrypt": "cka_encrypt",
+ "ipk11extractable": "cka_extractable",
+ "ipk11modifiable": "cka_modifiable",
+ "ipk11private": "cka_private",
+ "ipk11sensitive": "cka_sensitive",
+ "ipk11sign": "cka_sign",
+ "ipk11unwrap": "cka_unwrap",
+ "ipk11verify": "cka_verify",
+ "ipk11wrap": "cka_wrap",
+ "ipk11wrapwithtrusted": "cka_wrap_with_trusted"
+ }
+
+ for ldap_name, p11h_name in direct_param_map.iteritems():
+ if ldap_name in ldap_key:
+ unwrap_params[p11h_name] = ldap_key[ldap_name]
+
+ # and some others needs conversion
+
+ indirect_param_map = {
+ "ipk11keytype": ("key_type", keytype_name2id),
+ "ipawrappingmech": ("wrapping_mech", wrappingmech_name2id),
+ }
+
+ for ldap_name, rules in indirect_param_map.iteritems():
+ p11h_name, mapping = rules
+ if ldap_name in ldap_key:
+ unwrap_params[p11h_name] = mapping[ldap_key[ldap_name]]
+
+ return unwrap_params
+
+
+class AbstractHSM(object):
+ def _filter_replica_keys(self, all_keys):
+ replica_keys = {}
+ for key_id, key in all_keys.iteritems():
+ if not key['ipk11label'].startswith('dnssec-replica:'):
+ continue
+ replica_keys[key_id] = key
+ return replica_keys
+
+ def _filter_zone_keys(self, all_keys):
+ zone_keys = {}
+ for key_id, key in all_keys.iteritems():
+ if key['ipk11label'] == u'dnssec-master' \
+ or key['ipk11label'].startswith('dnssec-replica:'):
+ continue
+ zone_keys[key_id] = key
+ return zone_keys
diff --git a/ipapython/dnssec/bindmgr.py b/ipapython/dnssec/bindmgr.py
new file mode 100644
index 000000000..55765e16b
--- /dev/null
+++ b/ipapython/dnssec/bindmgr.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+from datetime import datetime
+import dns.name
+import errno
+import os
+import logging
+import shutil
+import stat
+import subprocess
+
+from ipalib import api
+import ipalib.constants
+from ipapython.dn import DN
+from ipapython import ipa_log_manager, ipautil
+from ipaplatform.paths import paths
+
+from temp import TemporaryDirectory
+
+time_bindfmt = '%Y%m%d%H%M%S'
+
+# this daemon should run under ods:named user:group
+# user has to be ods because ODSMgr.py sends signal to ods-enforcerd
+FILE_PERM = (stat.S_IRUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IWUSR)
+DIR_PERM = (stat.S_IRWXU | stat.S_IRWXG)
+
+class BINDMgr(object):
+ """BIND key manager. It does LDAP->BIND key files synchronization.
+
+ One LDAP object with idnsSecKey object class will produce
+ single pair of BIND key files.
+ """
+ def __init__(self, api):
+ self.api = api
+ self.log = ipa_log_manager.log_mgr.get_logger(self)
+ self.ldap_keys = {}
+ self.modified_zones = set()
+
+ def notify_zone(self, zone):
+ cmd = ['rndc', 'sign', zone.to_text()]
+ output = ipautil.run(cmd)[0]
+ self.log.info(output)
+
+ def dn2zone_name(self, dn):
+ """cn=KSK-20140813162153Z-cede9e182fc4af76c4bddbc19123a565,cn=keys,idnsname=test,cn=dns,dc=ipa,dc=example"""
+ # verify that metadata object is under DNS sub-tree
+ dn = DN(dn)
+ container = DN(self.api.env.container_dns, self.api.env.basedn)
+ idx = dn.rfind(container)
+ assert idx != -1, 'Metadata object %s is not inside %s' % (dn, container)
+ assert len(dn[idx - 1]) == 1, 'Multi-valued RDN as zone name is not supported'
+ return dns.name.from_text(dn[idx - 1]['idnsname'])
+
+ def time_ldap2bindfmt(self, str_val):
+ dt = datetime.strptime(str_val, ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT)
+ return dt.strftime(time_bindfmt)
+
+ def dates2params(self, ldap_attrs):
+ attr2param = {'idnsseckeypublish': '-P',
+ 'idnsseckeyactivate': '-A',
+ 'idnsseckeyinactive': '-I',
+ 'idnsseckeydelete': '-D'}
+
+ params = []
+ for attr, param in attr2param.items():
+ if attr in ldap_attrs:
+ params.append(param)
+ assert len(ldap_attrs[attr]) == 1, 'Timestamp %s is expected to be single-valued' % attr
+ params.append(self.time_ldap2bindfmt(ldap_attrs[attr][0]))
+
+ return params
+
+ def ldap_event(self, op, uuid, attrs):
+ """Record single LDAP event - key addition, deletion or modification.
+
+ Change is only recorded to memory.
+ self.sync() has to be called to synchronize change to BIND."""
+ assert op == 'add' or op == 'del' or op == 'mod'
+ zone = self.dn2zone_name(attrs['dn'])
+ self.modified_zones.add(zone)
+ zone_keys = self.ldap_keys.setdefault(zone, {})
+ if op == 'add':
+ self.log.info('Key metadata %s added to zone %s' % (attrs['dn'], zone))
+ zone_keys[uuid] = attrs
+
+ elif op == 'del':
+ self.log.info('Key metadata %s deleted from zone %s' % (attrs['dn'], zone))
+ zone_keys.pop(uuid)
+
+ elif op == 'mod':
+ self.log.info('Key metadata %s updated in zone %s' % (attrs['dn'], zone))
+ zone_keys[uuid] = attrs
+
+ def install_key(self, zone, uuid, attrs, workdir):
+ """Run dnssec-keyfromlabel on given LDAP object.
+ :returns: base file name of output files, e.g. Kaaa.test.+008+19719"""
+ self.log.info('attrs: %s', attrs)
+ assert attrs.get('idnsseckeyzone', ['FALSE'])[0] == 'TRUE', \
+ 'object %s is not a DNS zone key' % attrs['dn']
+
+ uri = "%s;pin-source=%s" % (attrs['idnsSecKeyRef'][0], paths.DNSSEC_SOFTHSM_PIN)
+ cmd = [paths.DNSSEC_KEYFROMLABEL, '-K', workdir, '-a', attrs['idnsSecAlgorithm'][0], '-l', uri]
+ cmd += self.dates2params(attrs)
+ if attrs.get('idnsSecKeySep', ['FALSE'])[0].upper() == 'TRUE':
+ cmd += ['-f', 'KSK']
+ if attrs.get('idnsSecKeyRevoke', ['FALSE'])[0].upper() == 'TRUE':
+ cmd += ['-R', datetime.now().strftime(time_bindfmt)]
+ cmd.append(zone.to_text())
+
+ # keys has to be readable by ODS & named
+ basename = ipautil.run(cmd)[0].strip()
+ private_fn = "%s/%s.private" % (workdir, basename)
+ os.chmod(private_fn, FILE_PERM)
+ # this is useful mainly for debugging
+ with open("%s/%s.uuid" % (workdir, basename), 'w') as uuid_file:
+ uuid_file.write(uuid)
+ with open("%s/%s.dn" % (workdir, basename), 'w') as dn_file:
+ dn_file.write(attrs['dn'])
+
+ def sync_zone(self, zone):
+ self.log.info('Synchronizing zone %s' % zone)
+ zone_path = os.path.join(paths.BIND_LDAP_DNS_ZONE_WORKDIR,
+ zone.to_text(omit_final_dot=True))
+ try:
+ os.makedirs(zone_path)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise e
+
+ # fix HSM permissions
+ # TODO: move out
+ for prefix, dirs, files in os.walk(paths.DNSSEC_TOKENS_DIR, topdown=True):
+ for name in dirs:
+ fpath = os.path.join(prefix, name)
+ self.log.debug('Fixing directory permissions: %s', fpath)
+ os.chmod(fpath, DIR_PERM | stat.S_ISGID)
+ for name in files:
+ fpath = os.path.join(prefix, name)
+ self.log.debug('Fixing file permissions: %s', fpath)
+ os.chmod(fpath, FILE_PERM)
+ # TODO: move out
+
+ with TemporaryDirectory(zone_path) as tempdir:
+ for uuid, attrs in self.ldap_keys[zone].items():
+ self.install_key(zone, uuid, attrs, tempdir)
+ # keys were generated in a temporary directory, swap directories
+ target_dir = "%s/keys" % zone_path
+ try:
+ shutil.rmtree(target_dir)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise e
+ shutil.move(tempdir, target_dir)
+ os.chmod(target_dir, DIR_PERM)
+
+ self.notify_zone(zone)
+
+ def sync(self):
+ """Synchronize list of zones in LDAP with BIND."""
+ self.log.debug('Key metadata in LDAP: %s' % self.ldap_keys)
+ for zone in self.modified_zones:
+ self.sync_zone(zone)
+
+ self.modified_zones = set()
+
+ def diff_zl(self, s1, s2):
+ """Compute zones present in s1 but not present in s2.
+
+ Returns: List of (uuid, name) tuples with zones present only in s1."""
+ s1_extra = s1.uuids - s2.uuids
+ removed = [(uuid, name) for (uuid, name) in s1.mapping.items()
+ if uuid in s1_extra]
+ return removed
diff --git a/ipapython/dnssec/keysyncer.py b/ipapython/dnssec/keysyncer.py
new file mode 100644
index 000000000..1b2757326
--- /dev/null
+++ b/ipapython/dnssec/keysyncer.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+import logging
+import ldap.dn
+import os
+
+from ipaplatform.paths import paths
+from ipapython import ipautil
+
+from syncrepl import SyncReplConsumer
+from odsmgr import ODSMgr
+from bindmgr import BINDMgr
+
+SIGNING_ATTR = 'idnsSecInlineSigning'
+OBJCLASS_ATTR = 'objectClass'
+
+
+class KeySyncer(SyncReplConsumer):
+ def __init__(self, *args, **kwargs):
+ # hack
+ self.api = kwargs['ipa_api']
+ del kwargs['ipa_api']
+
+ # DNSSEC master should have OpenDNSSEC installed
+ # TODO: Is this the best way?
+ if os.environ.get('ISMASTER', '0') == '1':
+ self.ismaster = True
+ self.odsmgr = ODSMgr()
+ else:
+ self.ismaster = False
+
+ self.bindmgr = BINDMgr(self.api)
+ self.init_done = False
+ SyncReplConsumer.__init__(self, *args, **kwargs)
+
+ def _get_objclass(self, attrs):
+ """Get object class.
+
+ Given set of attributes has to have exactly one supported object class.
+ """
+ supported_objclasses = set(['idnszone', 'idnsseckey', 'ipk11publickey'])
+ present_objclasses = set([o.lower() for o in attrs[OBJCLASS_ATTR]]).intersection(supported_objclasses)
+ assert len(present_objclasses) == 1, attrs[OBJCLASS_ATTR]
+ return present_objclasses.pop()
+
+ def __get_signing_attr(self, attrs):
+ """Get SIGNING_ATTR from dictionary with LDAP zone attributes.
+
+ Returned value is normalized to TRUE or FALSE, defaults to FALSE."""
+ values = attrs.get(SIGNING_ATTR, ['FALSE'])
+ assert len(values) == 1, '%s is expected to be single-valued' \
+ % SIGNING_ATTR
+ return values[0].upper()
+
+ def __is_dnssec_enabled(self, attrs):
+ """Test if LDAP DNS zone with given attributes is DNSSEC enabled."""
+ return self.__get_signing_attr(attrs) == 'TRUE'
+
+ def __is_replica_pubkey(self, attrs):
+ vals = attrs.get('ipk11label', [])
+ if len(vals) != 1:
+ return False
+ return vals[0].startswith('dnssec-replica:')
+
+ def application_add(self, uuid, dn, newattrs):
+ objclass = self._get_objclass(newattrs)
+ if objclass == 'idnszone':
+ self.zone_add(uuid, dn, newattrs)
+ elif objclass == 'idnsseckey':
+ self.key_meta_add(uuid, dn, newattrs)
+ elif objclass == 'ipk11publickey' and \
+ self.__is_replica_pubkey(newattrs):
+ self.hsm_master_sync()
+
+ def application_del(self, uuid, dn, oldattrs):
+ objclass = self._get_objclass(oldattrs)
+ if objclass == 'idnszone':
+ self.zone_del(uuid, dn, oldattrs)
+ elif objclass == 'idnsseckey':
+ self.key_meta_del(uuid, dn, oldattrs)
+ elif objclass == 'ipk11publickey' and \
+ self.__is_replica_pubkey(oldattrs):
+ self.hsm_master_sync()
+
+ def application_sync(self, uuid, dn, newattrs, oldattrs):
+ objclass = self._get_objclass(oldattrs)
+ if objclass == 'idnszone':
+ olddn = ldap.dn.str2dn(oldattrs['dn'])
+ newdn = ldap.dn.str2dn(newattrs['dn'])
+ assert olddn == newdn, 'modrdn operation is not supported'
+
+ oldval = self.__get_signing_attr(oldattrs)
+ newval = self.__get_signing_attr(newattrs)
+ if oldval != newval:
+ if self.__is_dnssec_enabled(newattrs):
+ self.zone_add(uuid, olddn, newattrs)
+ else:
+ self.zone_del(uuid, olddn, oldattrs)
+
+ elif objclass == 'idnsseckey':
+ self.key_metadata_sync(uuid, dn, oldattrs, newattrs)
+
+ elif objclass == 'ipk11publickey' and \
+ self.__is_replica_pubkey(newattrs):
+ self.hsm_master_sync()
+
+ def syncrepl_refreshdone(self):
+ self.log.info('Initial LDAP dump is done, sychronizing with ODS and BIND')
+ self.init_done = True
+ self.ods_sync()
+ self.hsm_replica_sync()
+ self.hsm_master_sync()
+ self.bindmgr.sync()
+
+ # idnsSecKey wrapper
+ # Assumption: metadata points to the same key blob all the time,
+ # i.e. it is not necessary to re-download blobs because of change in DNSSEC
+ # metadata - DNSSEC flags or timestamps.
+ def key_meta_add(self, uuid, dn, newattrs):
+ self.hsm_replica_sync()
+ self.bindmgr.ldap_event('add', uuid, newattrs)
+ self.bindmgr_sync()
+
+ def key_meta_del(self, uuid, dn, oldattrs):
+ self.bindmgr.ldap_event('del', uuid, oldattrs)
+ self.bindmgr_sync()
+ self.hsm_replica_sync()
+
+ def key_metadata_sync(self, uuid, dn, oldattrs, newattrs):
+ self.bindmgr.ldap_event('mod', uuid, newattrs)
+ self.bindmgr_sync()
+
+ def bindmgr_sync(self):
+ if self.init_done:
+ self.bindmgr.sync()
+
+ # idnsZone wrapper
+ def zone_add(self, uuid, dn, newattrs):
+ if not self.ismaster:
+ return
+
+ if self.__is_dnssec_enabled(newattrs):
+ self.odsmgr.ldap_event('add', uuid, newattrs)
+ self.ods_sync()
+
+ def zone_del(self, uuid, dn, oldattrs):
+ if not self.ismaster:
+ return
+
+ if self.__is_dnssec_enabled(oldattrs):
+ self.odsmgr.ldap_event('del', uuid, oldattrs)
+ self.ods_sync()
+
+ def ods_sync(self):
+ if not self.ismaster:
+ return
+
+ if self.init_done:
+ self.odsmgr.sync()
+
+ # triggered by modification to idnsSecKey objects
+ def hsm_replica_sync(self):
+ """Download keys from LDAP to local HSM."""
+ if self.ismaster:
+ return
+ if not self.init_done:
+ return
+ ipautil.run([paths.IPA_DNSKEYSYNCD_REPLICA])
+
+ # triggered by modification to ipk11PublicKey objects
+ def hsm_master_sync(self):
+ """Download replica keys from LDAP to local HSM
+ & upload master and zone keys to LDAP."""
+ if not self.ismaster:
+ return
+ if not self.init_done:
+ return
+ ipautil.run([paths.ODS_SIGNER])
diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py
new file mode 100644
index 000000000..e2e58f880
--- /dev/null
+++ b/ipapython/dnssec/ldapkeydb.py
@@ -0,0 +1,351 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+from binascii import hexlify
+import collections
+import sys
+import time
+
+import ipalib
+from ipapython.dn import DN
+from ipapython import ipaldap
+from ipapython import ipautil
+from ipaserver.plugins.ldap2 import ldap2
+from ipaplatform.paths import paths
+
+from abshsm import attrs_name2id, attrs_id2name, bool_attr_names, populate_pkcs11_metadata, AbstractHSM
+import _ipap11helper
+import uuid
+
+def uri_escape(val):
+ """convert val to %-notation suitable for ID component in URI"""
+ assert len(val) > 0, "zero-length URI component detected"
+ hexval = hexlify(val)
+ out = '%'
+ out += '%'.join(hexval[i:i+2] for i in range(0, len(hexval), 2))
+ return out
+
+def ldap_bool(val):
+ if val == 'TRUE' or val is True:
+ return True
+ elif val == 'FALSE' or val is False:
+ return False
+ else:
+ raise AssertionError('invalid LDAP boolean "%s"' % val)
+
+def get_default_attrs(object_classes):
+ # object class -> default attribute values mapping
+ defaults = {
+ u'ipk11publickey': {
+ 'ipk11copyable': True,
+ 'ipk11derive': False,
+ 'ipk11encrypt': False,
+ 'ipk11local': True,
+ 'ipk11modifiable': True,
+ 'ipk11private': True,
+ 'ipk11trusted': False,
+ 'ipk11verify': True,
+ 'ipk11verifyrecover': True,
+ 'ipk11wrap': False
+ },
+ u'ipk11privatekey': {
+ 'ipk11alwaysauthenticate': False,
+ 'ipk11alwayssensitive': True,
+ 'ipk11copyable': True,
+ 'ipk11decrypt': False,
+ 'ipk11derive': False,
+ 'ipk11extractable': True,
+ 'ipk11local': True,
+ 'ipk11modifiable': True,
+ 'ipk11neverextractable': False,
+ 'ipk11private': True,
+ 'ipk11sensitive': True,
+ 'ipk11sign': True,
+ 'ipk11signrecover': True,
+ 'ipk11unwrap': False,
+ 'ipk11wrapwithtrusted': False
+ },
+ u'ipk11secretkey': {
+ 'ipk11alwaysauthenticate': False,
+ 'ipk11alwayssensitive': True,
+ 'ipk11copyable': True,
+ 'ipk11decrypt': False,
+ 'ipk11derive': False,
+ 'ipk11encrypt': False,
+ 'ipk11extractable': True,
+ 'ipk11local': True,
+ 'ipk11modifiable': True,
+ 'ipk11neverextractable': False,
+ 'ipk11private': True,
+ 'ipk11sensitive': True,
+ 'ipk11sign': False,
+ 'ipk11trusted': False,
+ 'ipk11unwrap': True,
+ 'ipk11verify': False,
+ 'ipk11wrap': True,
+ 'ipk11wrapwithtrusted': False
+ }
+ }
+
+ # get set of supported object classes
+ present_clss = set()
+ for cls in object_classes:
+ present_clss.add(cls.lower())
+ present_clss.intersection_update(set(defaults.keys()))
+ if len(present_clss) <= 0:
+ raise AssertionError('none of "%s" object classes are supported' %
+ object_classes)
+
+ result = {}
+ for cls in present_clss:
+ result.update(defaults[cls])
+ return result
+
+class Key(collections.MutableMapping):
+ """abstraction to hide LDAP entry weirdnesses:
+ - non-normalized attribute names
+ - boolean attributes returned as strings
+ """
+ def __init__(self, entry, ldap, ldapkeydb):
+ self.entry = entry
+ self.ldap = ldap
+ self.ldapkeydb = ldapkeydb
+ self.log = ldap.log.getChild(__name__)
+
+ def __getitem__(self, key):
+ val = self.entry.single_value[key]
+ if key.lower() in bool_attr_names:
+ val = ldap_bool(val)
+ return val
+
+ def __setitem__(self, key, value):
+ self.entry[key] = value
+
+ def __delitem__(self, key):
+ del self.entry[key]
+
+ def __iter__(self):
+ """generates list of ipa names of all PKCS#11 attributes present in the object"""
+ for ipa_name in self.entry.keys():
+ lowercase = ipa_name.lower()
+ if lowercase in attrs_name2id:
+ yield lowercase
+
+ def __len__(self):
+ return len(self.entry)
+
+ def __str__(self):
+ return str(self.entry)
+
+ def _cleanup_key(self):
+ """remove default values from LDAP entry"""
+ default_attrs = get_default_attrs(self.entry['objectclass'])
+ empty = object()
+ for attr in default_attrs:
+ if self.get(attr, empty) == default_attrs[attr]:
+ del self[attr]
+
+class ReplicaKey(Key):
+ # TODO: object class assert
+ def __init__(self, entry, ldap, ldapkeydb):
+ super(ReplicaKey, self).__init__(entry, ldap, ldapkeydb)
+
+class MasterKey(Key):
+ # TODO: object class assert
+ def __init__(self, entry, ldap, ldapkeydb):
+ super(MasterKey, self).__init__(entry, ldap, ldapkeydb)
+
+ @property
+ def wrapped_entries(self):
+ """LDAP entires with wrapped data
+
+ One entry = one blob + ipaWrappingKey pointer to unwrapping key"""
+
+ keys = []
+ if 'ipaSecretKeyRef' not in self.entry:
+ return keys
+
+ for dn in self.entry['ipaSecretKeyRef']:
+ try:
+ obj = self.ldap.get_entry(dn)
+ keys.append(obj)
+ except ipalib.errors.NotFound:
+ continue
+
+ return keys
+
+ def add_wrapped_data(self, data, wrapping_mech, replica_key_id):
+ wrapping_key_uri = 'pkcs11:id=%s;type=public' \
+ % uri_escape(replica_key_id)
+ # TODO: replace this with 'autogenerate' to prevent collisions
+ uuid_rdn = DN('ipk11UniqueId=%s' % uuid.uuid1())
+ entry_dn = DN(uuid_rdn, self.ldapkeydb.base_dn)
+ # TODO: add ipaWrappingMech attribute
+ entry = self.ldap.make_entry(entry_dn,
+ objectClass=['ipaSecretKeyObject', 'ipk11Object'],
+ ipaSecretKey=data,
+ ipaWrappingKey=wrapping_key_uri,
+ ipaWrappingMech=wrapping_mech)
+
+ self.log.info('adding master key 0x%s wrapped with replica key 0x%s to %s',
+ hexlify(self['ipk11id']),
+ hexlify(replica_key_id),
+ entry_dn)
+ self.ldap.add_entry(entry)
+ if 'ipaSecretKeyRef' not in self.entry:
+ self.entry['objectClass'] += ['ipaSecretKeyRefObject']
+ self.entry.setdefault('ipaSecretKeyRef', []).append(entry_dn)
+
+
+class LdapKeyDB(AbstractHSM):
+ def __init__(self, log, ldap, base_dn):
+ self.ldap = ldap
+ self.base_dn = base_dn
+ self.log = log
+ self.cache_replica_pubkeys_wrap = None
+ self.cache_masterkeys = None
+ self.cache_zone_keypairs = None
+
+ def _get_key_dict(self, key_type, ldap_filter):
+ try:
+ objs = self.ldap.get_entries(base_dn=self.base_dn,
+ filter=ldap_filter)
+ except ipalib.errors.NotFound:
+ return {}
+
+ keys = {}
+ for o in objs:
+ # add default values not present in LDAP
+ key = key_type(o, self.ldap, self)
+ default_attrs = get_default_attrs(key.entry['objectclass'])
+ for attr in default_attrs:
+ key.setdefault(attr, default_attrs[attr])
+
+ assert 'ipk11id' in o, 'key is missing ipk11Id in %s' % key.entry.dn
+ key_id = key['ipk11id']
+ assert key_id not in keys, 'duplicate ipk11Id=0x%s in "%s" and "%s"' % (hexlify(key_id), key.entry.dn, keys[key_id].entry.dn)
+ assert 'ipk11label' in key, 'key "%s" is missing ipk11Label' % key.entry.dn
+ assert 'objectclass' in key.entry, 'key "%s" is missing objectClass attribute' % key.entry.dn
+
+ keys[key_id] = key
+
+ self._update_keys()
+ return keys
+
+ def _update_key(self, key):
+ """remove default values from LDAP entry and write back changes"""
+ key._cleanup_key()
+
+ try:
+ self.ldap.update_entry(key.entry)
+ except ipalib.errors.EmptyModlist:
+ pass
+
+ def _update_keys(self):
+ for cache in [self.cache_masterkeys, self.cache_replica_pubkeys_wrap,
+ self.cache_zone_keypairs]:
+ if cache:
+ for key in cache.itervalues():
+ self._update_key(key)
+
+ def flush(self):
+ """write back content of caches to LDAP"""
+ self._update_keys()
+ self.cache_masterkeys = None
+ self.cache_replica_pubkeys_wrap = None
+ self.cache_zone_keypairs = None
+
+ def _import_keys_metadata(self, source_keys):
+ """import key metadata from Key-compatible objects
+
+ metadata from multiple source keys can be imported into single LDAP
+ object
+
+ :param: source_keys is iterable of (Key object, PKCS#11 object class)"""
+
+ entry_dn = DN('ipk11UniqueId=autogenerate', self.base_dn)
+ entry = self.ldap.make_entry(entry_dn, objectClass=['ipk11Object'])
+ new_key = Key(entry, self.ldap, self)
+
+ for source_key, pkcs11_class in source_keys:
+ if pkcs11_class == _ipap11helper.KEY_CLASS_SECRET_KEY:
+ entry['objectClass'].append('ipk11SecretKey')
+ elif pkcs11_class == _ipap11helper.KEY_CLASS_PUBLIC_KEY:
+ entry['objectClass'].append('ipk11PublicKey')
+ elif pkcs11_class == _ipap11helper.KEY_CLASS_PRIVATE_KEY:
+ entry['objectClass'].append('ipk11PrivateKey')
+ else:
+ raise AssertionError('unsupported object class %s' % pkcs11_class)
+
+ populate_pkcs11_metadata(source_key, new_key)
+ new_key._cleanup_key()
+ return new_key
+
+ def import_master_key(self, mkey):
+ new_key = self._import_keys_metadata(
+ [(mkey, _ipap11helper.KEY_CLASS_SECRET_KEY)])
+ self.ldap.add_entry(new_key.entry)
+ self.log.debug('imported master key metadata: %s', new_key.entry)
+
+ def import_zone_key(self, pubkey, pubkey_data, privkey,
+ privkey_wrapped_data, wrapping_mech, master_key_id):
+ new_key = self._import_keys_metadata(
+ [(pubkey, _ipap11helper.KEY_CLASS_PUBLIC_KEY),
+ (privkey, _ipap11helper.KEY_CLASS_PRIVATE_KEY)])
+
+ new_key.entry['objectClass'].append('ipaPrivateKeyObject')
+ new_key.entry['ipaPrivateKey'] = privkey_wrapped_data
+ new_key.entry['ipaWrappingKey'] = 'pkcs11:id=%s;type=secret-key' \
+ % uri_escape(master_key_id)
+ new_key.entry['ipaWrappingMech'] = wrapping_mech
+
+ new_key.entry['objectClass'].append('ipaPublicKeyObject')
+ new_key.entry['ipaPublicKey'] = pubkey_data
+
+ self.ldap.add_entry(new_key.entry)
+ self.log.debug('imported zone key id: 0x%s', hexlify(new_key['ipk11id']))
+
+ @property
+ def replica_pubkeys_wrap(self):
+ if self.cache_replica_pubkeys_wrap:
+ return self.cache_replica_pubkeys_wrap
+
+ keys = self._filter_replica_keys(
+ self._get_key_dict(ReplicaKey,
+ '(&(objectClass=ipk11PublicKey)(ipk11Wrap=TRUE)(objectClass=ipaPublicKeyObject))'))
+
+ self.cache_replica_pubkeys_wrap = keys
+ return keys
+
+ @property
+ def master_keys(self):
+ if self.cache_masterkeys:
+ return self.cache_masterkeys
+
+ keys = self._get_key_dict(MasterKey,
+ '(&(objectClass=ipk11SecretKey)(|(ipk11UnWrap=TRUE)(!(ipk11UnWrap=*)))(ipk11Label=dnssec-master))')
+ for key in keys.itervalues():
+ prefix = 'dnssec-master'
+ assert key['ipk11label'] == prefix, \
+ 'secret key dn="%s" ipk11id=0x%s ipk11label="%s" with ipk11UnWrap = TRUE does not have '\
+ '"%s" key label' % (
+ key.entry.dn,
+ hexlify(key['ipk11id']),
+ str(key['ipk11label']),
+ prefix)
+
+ self.cache_masterkeys = keys
+ return keys
+
+ @property
+ def zone_keypairs(self):
+ if self.cache_zone_keypairs:
+ return self.cache_zone_keypairs
+
+ self.cache_zone_keypairs = self._filter_zone_keys(
+ self._get_key_dict(Key,
+ '(&(objectClass=ipk11PrivateKey)(objectClass=ipaPrivateKeyObject)(objectClass=ipk11PublicKey)(objectClass=ipaPublicKeyObject))'))
+
+ return self.cache_zone_keypairs
diff --git a/ipapython/dnssec/localhsm.py b/ipapython/dnssec/localhsm.py
new file mode 100755
index 000000000..de49641b0
--- /dev/null
+++ b/ipapython/dnssec/localhsm.py
@@ -0,0 +1,229 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+from binascii import hexlify
+import collections
+import logging
+import os
+from pprint import pprint
+import sys
+import time
+
+from ipaplatform.paths import paths
+
+import _ipap11helper
+from abshsm import attrs_name2id, attrs_id2name, AbstractHSM, keytype_id2name, keytype_name2id, ldap2p11helper_api_params
+
+private_key_api_params = set(["label", "id", "data", "unwrapping_key",
+ "wrapping_mech", "key_type", "cka_always_authenticate", "cka_copyable",
+ "cka_decrypt", "cka_derive", "cka_extractable", "cka_modifiable",
+ "cka_private", "cka_sensitive", "cka_sign", "cka_sign_recover",
+ "cka_unwrap", "cka_wrap_with_trusted"])
+
+public_key_api_params = set(["label", "id", "data", "cka_copyable",
+ "cka_derive", "cka_encrypt", "cka_modifiable", "cka_private",
+ "cka_trusted", "cka_verify", "cka_verify_recover", "cka_wrap"])
+
+class Key(collections.MutableMapping):
+ def __init__(self, p11, handle):
+ self.p11 = p11
+ self.handle = handle
+ # sanity check CKA_ID and CKA_LABEL
+ try:
+ cka_id = self.p11.get_attribute(handle, _ipap11helper.CKA_ID)
+ assert len(cka_id) != 0, 'ipk11id length should not be 0'
+ except _ipap11helper.NotFound:
+ raise _ipap11helper.NotFound('key without ipk11id: handle %s' % handle)
+
+ try:
+ cka_label = self.p11.get_attribute(handle, _ipap11helper.CKA_LABEL)
+ assert len(cka_label) != 0, 'ipk11label length should not be 0'
+
+ except _ipap11helper.NotFound:
+ raise _ipap11helper.NotFound('key without ipk11label: id 0x%s'
+ % hexlify(cka_id))
+
+ def __getitem__(self, key):
+ key = key.lower()
+ try:
+ value = self.p11.get_attribute(self.handle, attrs_name2id[key])
+ if key == 'ipk11keytype':
+ value = keytype_id2name[value]
+ return value
+ except _ipap11helper.NotFound:
+ raise KeyError()
+
+ def __setitem__(self, key, value):
+ key = key.lower()
+ if key == 'ipk11keytype':
+ value = keytype_name2id[value]
+
+ return self.p11.set_attribute(self.handle, attrs_name2id[key], value)
+
+ def __delitem__(self, key):
+ raise _ipap11helper.Exception('__delitem__ is not supported')
+
+ def __iter__(self):
+ """generates list of ipa names of all attributes present in the object"""
+ for pkcs11_id, ipa_name in attrs_id2name.iteritems():
+ try:
+ self.p11.get_attribute(self.handle, pkcs11_id)
+ except _ipap11helper.NotFound:
+ continue
+
+ yield ipa_name
+
+ def __len__(self):
+ cnt = 0
+ for attr in self:
+ cnt += 1
+ return cnt
+
+ def __str__(self):
+ d = {}
+ for ipa_name, value in self.iteritems():
+ d[ipa_name] = value
+
+ return str(d)
+
+ def __repr__(self):
+ return self.__str__()
+
+class LocalHSM(AbstractHSM):
+ def __init__(self, library, slot, pin):
+ self.cache_replica_pubkeys = None
+ self.p11 = _ipap11helper.P11_Helper(slot, pin, library)
+ self.log = logging.getLogger()
+
+ def __del__(self):
+ self.p11.finalize()
+
+ def find_keys(self, **kwargs):
+ """Return dict with Key objects matching given criteria.
+
+ CKA_ID is used as key so all matching objects have to have unique ID."""
+
+ # this is a hack for old p11-kit URI parser
+ # see https://bugs.freedesktop.org/show_bug.cgi?id=85057
+ if 'uri' in kwargs:
+ kwargs['uri'] = kwargs['uri'].replace('type=', 'object-type=')
+
+ handles = self.p11.find_keys(**kwargs)
+ keys = {}
+ for h in handles:
+ key = Key(self.p11, h)
+ o_id = key['ipk11id']
+ assert o_id not in keys, 'duplicate ipk11Id = 0x%s; keys = %s' % (
+ hexlify(o_id), keys)
+ keys[o_id] = key
+
+ return keys
+
+ @property
+ def replica_pubkeys(self):
+ return self._filter_replica_keys(
+ self.find_keys(objclass=_ipap11helper.KEY_CLASS_PUBLIC_KEY))
+
+ @property
+ def replica_pubkeys_wrap(self):
+ return self._filter_replica_keys(
+ self.find_keys(objclass=_ipap11helper.KEY_CLASS_PUBLIC_KEY,
+ cka_wrap=True))
+
+ @property
+ def master_keys(self):
+ """Get all usable DNSSEC master keys"""
+ keys = self.find_keys(objclass=_ipap11helper.KEY_CLASS_SECRET_KEY, label=u'dnssec-master', cka_unwrap=True)
+
+ for key in keys.itervalues():
+ prefix = 'dnssec-master'
+ assert key['ipk11label'] == prefix, \
+ 'secret key ipk11id=0x%s ipk11label="%s" with ipk11UnWrap = TRUE does not have '\
+ '"%s" key label' % (hexlify(key['ipk11id']),
+ str(key['ipk11label']), prefix)
+
+ return keys
+
+ @property
+ def active_master_key(self):
+ """Get one active DNSSEC master key suitable for key wrapping"""
+ keys = self.find_keys(objclass=_ipap11helper.KEY_CLASS_SECRET_KEY,
+ label=u'dnssec-master', cka_wrap=True, cka_unwrap=True)
+ assert len(keys) > 0, "DNSSEC master key with UN/WRAP = TRUE not found"
+ return keys.popitem()[1]
+
+ @property
+ def zone_pubkeys(self):
+ return self._filter_zone_keys(
+ self.find_keys(objclass=_ipap11helper.KEY_CLASS_PUBLIC_KEY))
+
+ @property
+ def zone_privkeys(self):
+ return self._filter_zone_keys(
+ self.find_keys(objclass=_ipap11helper.KEY_CLASS_PRIVATE_KEY))
+
+
+ def import_public_key(self, source, data):
+ params = ldap2p11helper_api_params(source)
+ # filter out params inappropriate for public keys
+ for par in set(params.keys()).difference(public_key_api_params):
+ del params[par]
+ params['data'] = data
+
+ h = self.p11.import_public_key(**params)
+ return Key(self.p11, h)
+
+ def import_private_key(self, source, data, unwrapping_key):
+ params = ldap2p11helper_api_params(source)
+ # filter out params inappropriate for private keys
+ for par in set(params.keys()).difference(private_key_api_params):
+ del params[par]
+ params['data'] = data
+ params['unwrapping_key'] = unwrapping_key.handle
+
+ h = self.p11.import_wrapped_private_key(**params)
+ return Key(self.p11, h)
+
+
+
+if __name__ == '__main__':
+ if 'SOFTHSM2_CONF' not in os.environ:
+ os.environ['SOFTHSM2_CONF'] = paths.DNSSEC_SOFTHSM2_CONF
+ localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, 0,
+ open(paths.DNSSEC_SOFTHSM_PIN).read())
+
+ print 'replica public keys: CKA_WRAP = TRUE'
+ print '===================================='
+ for pubkey_id, pubkey in localhsm.replica_pubkeys_wrap.iteritems():
+ print hexlify(pubkey_id)
+ pprint(pubkey)
+
+ print ''
+ print 'replica public keys: all'
+ print '========================'
+ for pubkey_id, pubkey in localhsm.replica_pubkeys.iteritems():
+ print hexlify(pubkey_id)
+ pprint(pubkey)
+
+ print ''
+ print 'master keys'
+ print '==========='
+ for mkey_id, mkey in localhsm.master_keys.iteritems():
+ print hexlify(mkey_id)
+ pprint(mkey)
+
+ print ''
+ print 'zone public keys'
+ print '================'
+ for key_id, key in localhsm.zone_pubkeys.iteritems():
+ print hexlify(key_id)
+ pprint(key)
+
+ print ''
+ print 'zone private keys'
+ print '================='
+ for key_id, key in localhsm.zone_privkeys.iteritems():
+ print hexlify(key_id)
+ pprint(key)
diff --git a/ipapython/dnssec/odsmgr.py b/ipapython/dnssec/odsmgr.py
new file mode 100644
index 000000000..a91b6c553
--- /dev/null
+++ b/ipapython/dnssec/odsmgr.py
@@ -0,0 +1,194 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+import logging
+from lxml import etree
+import dns.name
+import subprocess
+
+from ipapython import ipa_log_manager, ipautil
+
+# hack: zone object UUID is stored as path to imaginary zone file
+ENTRYUUID_PREFIX = "/var/lib/ipa/dns/zone/entryUUID/"
+ENTRYUUID_PREFIX_LEN = len(ENTRYUUID_PREFIX)
+
+
+class ZoneListReader(object):
+ def __init__(self):
+ self.names = set() # dns.name
+ self.uuids = set() # UUID strings
+ self.mapping = dict() # {UUID: dns.name}
+ self.log = ipa_log_manager.log_mgr.get_logger(self)
+
+ def _add_zone(self, name, zid):
+ """Add zone & UUID to internal structures.
+
+ Zone with given name and UUID must not exist."""
+ # detect duplicate zone names
+ name = dns.name.from_text(name)
+ assert name not in self.names, \
+ 'duplicate name (%s, %s) vs. %s' % (name, zid, self.mapping)
+ # duplicate non-None zid is not allowed
+ assert not zid or zid not in self.uuids, \
+ 'duplicate UUID (%s, %s) vs. %s' % (name, zid, self.mapping)
+
+ self.names.add(name)
+ self.uuids.add(zid)
+ self.mapping[zid] = name
+
+ def _del_zone(self, name, zid):
+ """Remove zone & UUID from internal structures.
+
+ Zone with given name and UUID must exist.
+ """
+ name = dns.name.from_text(name)
+ assert zid is not None
+ assert name in self.names, \
+ 'name (%s, %s) does not exist in %s' % (name, zid, self.mapping)
+ assert zid in self.uuids, \
+ 'UUID (%s, %s) does not exist in %s' % (name, zid, self.mapping)
+ assert zid in self.mapping and name == self.mapping[zid], \
+ 'pair {%s: %s} does not exist in %s' % (zid, name, self.mapping)
+
+ self.names.remove(name)
+ self.uuids.remove(zid)
+ del self.mapping[zid]
+
+
+class ODSZoneListReader(ZoneListReader):
+ """One-shot parser for ODS zonelist.xml."""
+ def __init__(self, zonelist_text):
+ super(ODSZoneListReader, self).__init__()
+ xml = etree.fromstring(zonelist_text)
+ self._parse_zonelist(xml)
+
+ def _parse_zonelist(self, xml):
+ """iterate over Zone elements with attribute 'name' and
+ add IPA zones to self.zones"""
+ for zone_xml in xml.xpath('/ZoneList/Zone[@name]'):
+ name, zid = self._parse_ipa_zone(zone_xml)
+ self._add_zone(name, zid)
+
+ def _parse_ipa_zone(self, zone_xml):
+ """Extract zone name, input adapter and detect IPA zones.
+
+ IPA zones have contains Adapters/Input/Adapter element with
+ attribute type = "File" and with value prefixed with ENTRYUUID_PREFIX.
+
+ Returns:
+ tuple (zone name, ID)
+ """
+ name = zone_xml.get('name')
+ in_adapters = zone_xml.xpath(
+ 'Adapters/Input/Adapter[@type="File" '
+ 'and starts-with(text(), "%s")]' % ENTRYUUID_PREFIX)
+ assert len(in_adapters) == 1, 'only IPA zones are supported: %s' \
+ % etree.tostring(zone_xml)
+
+ path = in_adapters[0].text
+ # strip prefix from path
+ zid = path[ENTRYUUID_PREFIX_LEN:]
+ return (name, zid)
+
+
+class LDAPZoneListReader(ZoneListReader):
+ def __init__(self):
+ super(LDAPZoneListReader, self).__init__()
+
+ def process_ipa_zone(self, op, uuid, zone_ldap):
+ assert (op == 'add' or op == 'del'), 'unsupported op %s' % op
+ assert uuid is not None
+ assert 'idnsname' in zone_ldap, \
+ 'LDAP zone UUID %s without idnsName' % uuid
+ assert len(zone_ldap['idnsname']) == 1, \
+ 'LDAP zone UUID %s with len(idnsname) != 1' % uuid
+
+ if op == 'add':
+ self._add_zone(zone_ldap['idnsname'][0], uuid)
+ elif op == 'del':
+ self._del_zone(zone_ldap['idnsname'][0], uuid)
+
+
+class ODSMgr(object):
+ """OpenDNSSEC zone manager. It does LDAP->ODS synchronization.
+
+ Zones with idnsSecInlineSigning attribute = TRUE in LDAP are added
+ or deleted from ODS as necessary. ODS->LDAP key synchronization
+ has to be solved seperatelly.
+ """
+ def __init__(self):
+ self.log = ipa_log_manager.log_mgr.get_logger(self)
+ self.zl_ldap = LDAPZoneListReader()
+
+ def ksmutil(self, params):
+ """Call ods-ksmutil with given parameters and return stdout.
+
+ Raises CalledProcessError if returncode != 0.
+ """
+ cmd = ['ods-ksmutil'] + params
+ return ipautil.run(cmd)[0]
+
+ def get_ods_zonelist(self):
+ stdout = self.ksmutil(['zonelist', 'export'])
+ reader = ODSZoneListReader(stdout)
+ return reader
+
+ def add_ods_zone(self, uuid, name):
+ zone_path = '%s%s' % (ENTRYUUID_PREFIX, uuid)
+ cmd = ['zone', 'add', '--zone', str(name), '--input', zone_path]
+ output = self.ksmutil(cmd)
+ self.log.info(output)
+ self.notify_enforcer()
+
+ def del_ods_zone(self, name):
+ # ods-ksmutil blows up if zone name has period at the end
+ name = name.relativize(dns.name.root)
+ cmd = ['zone', 'delete', '--zone', str(name)]
+ output = self.ksmutil(cmd)
+ self.log.info(output)
+ self.notify_enforcer()
+
+ def notify_enforcer(self):
+ cmd = ['notify']
+ output = self.ksmutil(cmd)
+ self.log.info(output)
+
+ def ldap_event(self, op, uuid, attrs):
+ """Record single LDAP event - zone addition or deletion.
+
+ Change is only recorded to memory.
+ self.sync() have to be called to synchronize change to ODS."""
+ assert op == 'add' or op == 'del'
+ self.zl_ldap.process_ipa_zone(op, uuid, attrs)
+ self.log.debug("LDAP zones: %s", self.zl_ldap.mapping)
+
+ def sync(self):
+ """Synchronize list of zones in LDAP with ODS."""
+ zl_ods = self.get_ods_zonelist()
+ self.log.debug("ODS zones: %s", zl_ods.mapping)
+ removed = self.diff_zl(zl_ods, self.zl_ldap)
+ self.log.info("Zones removed from LDAP: %s", removed)
+ added = self.diff_zl(self.zl_ldap, zl_ods)
+ self.log.info("Zones added to LDAP: %s", added)
+ for (uuid, name) in removed:
+ self.del_ods_zone(name)
+ for (uuid, name) in added:
+ self.add_ods_zone(uuid, name)
+
+ def diff_zl(self, s1, s2):
+ """Compute zones present in s1 but not present in s2.
+
+ Returns: List of (uuid, name) tuples with zones present only in s1."""
+ s1_extra = s1.uuids - s2.uuids
+ removed = [(uuid, name) for (uuid, name) in s1.mapping.items()
+ if uuid in s1_extra]
+ return removed
+
+
+if __name__ == '__main__':
+ ipa_log_manager.standard_logging_setup(debug=True)
+ ods = ODSMgr()
+ reader = ods.get_ods_zonelist()
+ ipa_log_manager.root_logger.info('ODS zones: %s', reader.mapping)
diff --git a/ipapython/dnssec/syncrepl.py b/ipapython/dnssec/syncrepl.py
new file mode 100644
index 000000000..2f657f599
--- /dev/null
+++ b/ipapython/dnssec/syncrepl.py
@@ -0,0 +1,123 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+"""
+This script implements a syncrepl consumer which syncs data from server
+to a local dict.
+"""
+
+# Import the python-ldap modules
+import ldap
+import ldapurl
+# Import specific classes from python-ldap
+from ldap.cidict import cidict
+from ldap.ldapobject import ReconnectLDAPObject
+from ldap.syncrepl import SyncreplConsumer
+
+# Import modules from Python standard lib
+import signal
+import time
+import sys
+import logging
+
+from ipapython import ipa_log_manager
+
+
+class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer):
+ """
+ Syncrepl Consumer interface
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.log = ipa_log_manager.log_mgr.get_logger(self)
+ # Initialise the LDAP Connection first
+ ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs)
+ # Now prepare the data store
+ self.__data = cidict()
+ self.__data['uuids'] = cidict()
+ # We need this for later internal use
+ self.__presentUUIDs = cidict()
+
+ def close_db(self):
+ # This is useless for dict
+ pass
+
+ def syncrepl_get_cookie(self):
+ if 'cookie' in self.__data:
+ cookie = self.__data['cookie']
+ self.log.debug('Current cookie is: %s', cookie)
+ return cookie
+ else:
+ self.log.debug('Current cookie is: None (not received yet)')
+
+ def syncrepl_set_cookie(self, cookie):
+ self.log.debug('New cookie is: %s', cookie)
+ self.__data['cookie'] = cookie
+
+ def syncrepl_entry(self, dn, attributes, uuid):
+ attributes = cidict(attributes)
+ # First we determine the type of change we have here
+ # (and store away the previous data for later if needed)
+ previous_attributes = cidict()
+ if uuid in self.__data['uuids']:
+ change_type = 'modify'
+ previous_attributes = self.__data['uuids'][uuid]
+ else:
+ change_type = 'add'
+ # Now we store our knowledge of the existence of this entry
+ # (including the DN as an attribute for convenience)
+ attributes['dn'] = dn
+ self.__data['uuids'][uuid] = attributes
+ # Debugging
+ self.log.debug('Detected %s of entry: %s %s', change_type, dn, uuid)
+ if change_type == 'modify':
+ self.application_sync(uuid, dn, attributes, previous_attributes)
+ else:
+ self.application_add(uuid, dn, attributes)
+
+ def syncrepl_delete(self, uuids):
+ # Make sure we know about the UUID being deleted, just in case...
+ uuids = [uuid for uuid in uuids if uuid in self.__data['uuids']]
+ # Delete all the UUID values we know of
+ for uuid in uuids:
+ attributes = self.__data['uuids'][uuid]
+ dn = attributes['dn']
+ self.log.debug('Detected deletion of entry: %s %s', dn, uuid)
+ self.application_del(uuid, dn, attributes)
+ del self.__data['uuids'][uuid]
+
+ def syncrepl_present(self, uuids, refreshDeletes=False):
+ # If we have not been given any UUID values,
+ # then we have recieved all the present controls...
+ if uuids is None:
+ # We only do things if refreshDeletes is false
+ # as the syncrepl extension will call syncrepl_delete instead
+ # when it detects a delete notice
+ if refreshDeletes is False:
+ deletedEntries = [uuid for uuid in self.__data['uuids'].keys()
+ if uuid not in self.__presentUUIDs]
+ self.syncrepl_delete(deletedEntries)
+ # Phase is now completed, reset the list
+ self.__presentUUIDs = {}
+ else:
+ # Note down all the UUIDs we have been sent
+ for uuid in uuids:
+ self.__presentUUIDs[uuid] = True
+
+ def application_add(self, uuid, dn, attributes):
+ self.log.info('Performing application add for: %s %s', dn, uuid)
+ self.log.debug('New attributes: %s', attributes)
+ return True
+
+ def application_sync(self, uuid, dn, attributes, previous_attributes):
+ self.log.info('Performing application sync for: %s %s', dn, uuid)
+ self.log.debug('Old attributes: %s', previous_attributes)
+ self.log.debug('New attributes: %s', attributes)
+ return True
+
+ def application_del(self, uuid, dn, previous_attributes):
+ self.log.info('Performing application delete for: %s %s', dn, uuid)
+ self.log.debug('Old attributes: %s', previous_attributes)
+ return True
diff --git a/ipapython/dnssec/temp.py b/ipapython/dnssec/temp.py
new file mode 100644
index 000000000..23ee377be
--- /dev/null
+++ b/ipapython/dnssec/temp.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+import errno
+import shutil
+import tempfile
+
+class TemporaryDirectory(object):
+ def __init__(self, root):
+ self.root = root
+
+ def __enter__(self):
+ self.name = tempfile.mkdtemp(dir=self.root)
+ return self.name
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ try:
+ shutil.rmtree(self.name)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise