mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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>
This commit is contained in:
committed by
Martin Kosek
parent
5556b7f50e
commit
276e69de87
164
daemons/dnssec/ipa-dnskeysync-replica
Executable file
164
daemons/dnssec/ipa-dnskeysync-replica
Executable file
@@ -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)
|
||||
106
daemons/dnssec/ipa-dnskeysyncd
Executable file
106
daemons/dnssec/ipa-dnskeysyncd
Executable file
@@ -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
|
||||
15
daemons/dnssec/ipa-dnskeysyncd.service
Normal file
15
daemons/dnssec/ipa-dnskeysyncd.service
Normal file
@@ -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
|
||||
501
daemons/dnssec/ipa-ods-exporter
Executable file
501
daemons/dnssec/ipa-ods-exporter
Executable file
@@ -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')
|
||||
15
daemons/dnssec/ipa-ods-exporter.service
Normal file
15
daemons/dnssec/ipa-ods-exporter.service
Normal file
@@ -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
|
||||
5
daemons/dnssec/ipa-ods-exporter.socket
Normal file
5
daemons/dnssec/ipa-ods-exporter.socket
Normal file
@@ -0,0 +1,5 @@
|
||||
[Socket]
|
||||
ListenStream=/var/run/opendnssec/engine.sock
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -423,6 +423,17 @@ mkdir -p %{buildroot}%{_usr}/share/ipa/html/
|
||||
mkdir -p %{buildroot}%{_initrddir}
|
||||
mkdir %{buildroot}%{_sysconfdir}/sysconfig/
|
||||
install -m 644 init/ipa_memcached.conf %{buildroot}%{_sysconfdir}/sysconfig/ipa_memcached
|
||||
install -m 644 init/ipa-dnskeysyncd.conf %{buildroot}%{_sysconfdir}/sysconfig/ipa-dnskeysyncd
|
||||
install -m 644 init/ipa-ods-exporter.conf %{buildroot}%{_sysconfdir}/sysconfig/ipa-ods-exporter
|
||||
install -m 644 daemons/dnssec/ipa-ods-exporter.socket %{buildroot}%{_unitdir}/ipa-ods-exporter.socket
|
||||
install -m 644 daemons/dnssec/ipa-ods-exporter.service %{buildroot}%{_unitdir}/ipa-ods-exporter.service
|
||||
install -m 644 daemons/dnssec/ipa-dnskeysyncd.service %{buildroot}%{_unitdir}/ipa-dnskeysyncd.service
|
||||
|
||||
# dnssec daemons
|
||||
mkdir -p %{buildroot}%{_libexecdir}/ipa/
|
||||
install daemons/dnssec/ipa-dnskeysyncd %{buildroot}%{_libexecdir}/ipa/ipa-dnskeysyncd
|
||||
install daemons/dnssec/ipa-dnskeysync-replica %{buildroot}%{_libexecdir}/ipa/ipa-dnskeysync-replica
|
||||
install daemons/dnssec/ipa-ods-exporter %{buildroot}%{_libexecdir}/ipa/ipa-ods-exporter
|
||||
|
||||
# Web UI plugin dir
|
||||
mkdir -p %{buildroot}%{_usr}/share/ipa/ui/js/plugins
|
||||
@@ -642,7 +653,13 @@ fi
|
||||
%{_sbindir}/ipa-cacert-manage
|
||||
%{_libexecdir}/certmonger/dogtag-ipa-ca-renew-agent-submit
|
||||
%{_libexecdir}/ipa-otpd
|
||||
%dir %{_libexecdir}/ipa
|
||||
%{_libexecdir}/ipa/ipa-dnskeysyncd
|
||||
%{_libexecdir}/ipa/ipa-dnskeysync-replica
|
||||
%{_libexecdir}/ipa/ipa-ods-exporter
|
||||
%config(noreplace) %{_sysconfdir}/sysconfig/ipa_memcached
|
||||
%config(noreplace) %{_sysconfdir}/sysconfig/ipa-dnskeysyncd
|
||||
%config(noreplace) %{_sysconfdir}/sysconfig/ipa-ods-exporter
|
||||
%dir %attr(0700,apache,apache) %{_localstatedir}/run/ipa_memcached/
|
||||
%dir %attr(0700,root,root) %{_localstatedir}/run/ipa/
|
||||
# NOTE: systemd specific section
|
||||
@@ -651,6 +668,9 @@ fi
|
||||
%attr(644,root,root) %{_unitdir}/ipa_memcached.service
|
||||
%attr(644,root,root) %{_unitdir}/ipa-otpd.socket
|
||||
%attr(644,root,root) %{_unitdir}/ipa-otpd@.service
|
||||
%attr(644,root,root) %{_unitdir}/ipa-dnskeysyncd.service
|
||||
%attr(644,root,root) %{_unitdir}/ipa-ods-exporter.socket
|
||||
%attr(644,root,root) %{_unitdir}/ipa-ods-exporter.service
|
||||
# END
|
||||
%dir %{python_sitelib}/ipaserver
|
||||
%dir %{python_sitelib}/ipaserver/install
|
||||
@@ -826,6 +846,8 @@ fi
|
||||
%doc COPYING README Contributors.txt
|
||||
%dir %{python_sitelib}/ipapython
|
||||
%{python_sitelib}/ipapython/*.py*
|
||||
%dir %{python_sitelib}/ipapython/dnssec
|
||||
%{python_sitelib}/ipapython/dnssec/*.py*
|
||||
%dir %{python_sitelib}/ipalib
|
||||
%{python_sitelib}/ipalib/*
|
||||
%dir %{python_sitelib}/ipaplatform
|
||||
|
||||
0
init/ipa-dnskeysyncd.conf
Normal file
0
init/ipa-dnskeysyncd.conf
Normal file
0
init/ipa-ods-exporter.conf
Normal file
0
init/ipa-ods-exporter.conf
Normal file
0
ipapython/dnssec/__init__.py
Normal file
0
ipapython/dnssec/__init__.py
Normal file
187
ipapython/dnssec/abshsm.py
Normal file
187
ipapython/dnssec/abshsm.py
Normal file
@@ -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
|
||||
176
ipapython/dnssec/bindmgr.py
Normal file
176
ipapython/dnssec/bindmgr.py
Normal file
@@ -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
|
||||
181
ipapython/dnssec/keysyncer.py
Normal file
181
ipapython/dnssec/keysyncer.py
Normal file
@@ -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])
|
||||
351
ipapython/dnssec/ldapkeydb.py
Normal file
351
ipapython/dnssec/ldapkeydb.py
Normal file
@@ -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
|
||||
229
ipapython/dnssec/localhsm.py
Executable file
229
ipapython/dnssec/localhsm.py
Executable file
@@ -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)
|
||||
194
ipapython/dnssec/odsmgr.py
Normal file
194
ipapython/dnssec/odsmgr.py
Normal file
@@ -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)
|
||||
123
ipapython/dnssec/syncrepl.py
Normal file
123
ipapython/dnssec/syncrepl.py
Normal file
@@ -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
|
||||
23
ipapython/dnssec/temp.py
Normal file
23
ipapython/dnssec/temp.py
Normal file
@@ -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
|
||||
@@ -65,7 +65,7 @@ def setup_package():
|
||||
classifiers=filter(None, CLASSIFIERS.split('\n')),
|
||||
platforms = ["Linux", "Solaris", "Unix"],
|
||||
package_dir = {'ipapython': ''},
|
||||
packages = [ "ipapython" ],
|
||||
packages = [ "ipapython", "ipapython.dnssec" ],
|
||||
)
|
||||
finally:
|
||||
del sys.path[0]
|
||||
|
||||
Reference in New Issue
Block a user