Add ipa-custodia service

Add a customized Custodia daemon and enable it after installation.
Generates server keys and loads them in LDAP autonomously on install
or update.
Provides client code classes too.

Signed-off-by: Simo Sorce <simo@redhat.com>
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
This commit is contained in:
Simo Sorce 2015-05-08 13:39:29 -04:00 committed by Jan Cholasta
parent ba22999cef
commit 463dda3067
21 changed files with 763 additions and 4 deletions

View File

@ -97,6 +97,8 @@ BuildRequires: python-pytest-multihost >= 0.5
BuildRequires: python-pytest-sourceorder
BuildRequires: python-kdcproxy >= 0.3
BuildRequires: python-six
BuildRequires: python-jwcrypto
BuildRequires: custodia
%description
IPA is an integrated solution to provide centrally managed Identity (users,
@ -158,6 +160,8 @@ Requires: p11-kit
Requires: systemd-python
Requires: %{etc_systemd_dir}
Requires: gzip
Requires: python-gssapi >= 1.1.0
Requires: custodia
Conflicts: %{alt_name}-server
Obsoletes: %{alt_name}-server < %{version}
@ -322,6 +326,7 @@ Requires: wget
Requires: dbus-python
Requires: python-setuptools
Requires: python-six
Requires: python-jwcrypto
Conflicts: %{alt_name}-python
Obsoletes: %{alt_name}-python < %{version}
@ -512,6 +517,7 @@ mkdir -p %{buildroot}%{etc_systemd_dir}
install -m 644 init/systemd/ipa.service %{buildroot}%{_unitdir}/ipa.service
install -m 644 init/systemd/ipa_memcached.service %{buildroot}%{_unitdir}/ipa_memcached.service
install -m 644 init/systemd/httpd.service %{buildroot}%{etc_systemd_dir}/httpd.service
install -m 644 init/systemd/ipa-custodia.service %{buildroot}%{_unitdir}/ipa-custodia.service
# END
mkdir -p %{buildroot}/%{_localstatedir}/lib/ipa/backup
%endif # ONLY_CLIENT
@ -536,6 +542,10 @@ mkdir -p %{buildroot}%{_sysconfdir}/cron.d
(cd %{buildroot}/%{python_sitelib}/ipatests && find . -type f | \
sed -e 's,\.py.*$,.*,g' | sort -u | \
sed -e 's,\./,%%{python_sitelib}/ipatests/,g' ) >tests-python.list
mkdir -p %{buildroot}%{_sysconfdir}/ipa/custodia
%endif # ONLY_CLIENT
%clean
@ -739,6 +749,7 @@ fi
%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
%attr(644,root,root) %{_unitdir}/ipa-custodia.service
%attr(644,root,root) %{etc_systemd_dir}/httpd.service
# END
%dir %{python_sitelib}/ipaserver
@ -856,6 +867,7 @@ fi
%ghost %{_localstatedir}/lib/ipa/pki-ca/publish
%ghost %{_localstatedir}/named/dyndb-ldap/ipa
%attr(755,root,root) %{_libdir}/krb5/plugins/kdb/ipadb.so
%dir %attr(0700,root,root) %{_sysconfdir}/ipa/custodia
%{_mandir}/man1/ipa-replica-conncheck.1.gz
%{_mandir}/man1/ipa-replica-install.1.gz
%{_mandir}/man1/ipa-replica-manage.1.gz
@ -938,6 +950,8 @@ fi
%{python_sitelib}/ipapython/dnssec/*.py*
%dir %{python_sitelib}/ipapython/install
%{python_sitelib}/ipapython/install/*.py*
%dir %{python_sitelib}/ipapython/secrets
%{python_sitelib}/ipapython/secrets/*.py*
%dir %{python_sitelib}/ipalib
%{python_sitelib}/ipalib/*
%dir %{python_sitelib}/ipaplatform

View File

@ -0,0 +1,13 @@
[Unit]
Description=IPA Custodia Service
[Service]
Type=simple
ExecStart=/usr/sbin/custodia /etc/ipa/custodia/custodia.conf
PrivateTmp=yes
Restart=on-failure
RestartSec=60s
[Install]
WantedBy=multi-user.target

View File

@ -1,5 +1,5 @@
#
# VERSION 18 - DO NOT REMOVE THIS LINE
# VERSION 19 - DO NOT REMOVE THIS LINE
#
# This file may be overwritten on upgrades.
#
@ -103,6 +103,14 @@ WSGIScriptReloading Off
Allow from all
</Location>
# Custodia stuff is redirected to the custodia daemon
# after authentication
<Location "/ipa/keys/">
ProxyPass "unix:/run/httpd/ipa-custodia.sock|http://localhost/keys/"
RequestHeader set GSS_NAME %{GSS_NAME}s
RequestHeader set REMOTE_USER %{REMOTE_USER}s
</Location>
# This is where we redirect on failed auth
Alias /ipa/errors "/usr/share/ipa/html"

View File

@ -28,6 +28,7 @@ app_DATA = \
anonymous-vlv.ldif \
bootstrap-template.ldif \
caJarSigningCert.cfg.template \
custodia.conf.template \
default-aci.ldif \
default-caacl.ldif \
default-hbac.ldif \

View File

@ -167,6 +167,12 @@ objectClass: nsContainer
objectClass: top
cn: certificates
dn: cn=custodia,cn=ipa,cn=etc,$SUFFIX
changetype: add
objectClass: nsContainer
objectClass: top
cn: custodia
dn: cn=s4u2proxy,cn=etc,$SUFFIX
changetype: add
objectClass: nsContainer

View File

@ -0,0 +1,28 @@
[global]
server_version = "IPAKeys/0.0.1"
server_socket = $IPA_CUSTODIA_SOCKET
auditlog = $IPA_CUSTODIA_AUDIT_LOG
[auth:simple]
handler = custodia.httpd.authenticators.SimpleCredsAuth
uid = 48
gid = 48
[auth:header]
handler = custodia.httpd.authenticators.SimpleHeaderAuth
header = GSS_NAME
[authz:kemkeys]
handler = ipapython.secrets.kem.IPAKEMKeys
paths = /keys
store = ipa
server_keys = $IPA_CUSTODIA_CONF_DIR/server.keys
[store:ipa]
handler = ipapython.secrets.store.iSecStore
ldap_uri = $LDAP_URI
[/keys]
handler = custodia.secrets.Secrets
allowed_keytypes = kem
store = ipa

View File

@ -0,0 +1,4 @@
dn: cn=custodia,cn=ipa,cn=etc,$SUFFIX
default: objectClass: top
default: objectClass: nsContainer
default: cn: custodia

View File

@ -356,5 +356,9 @@ class BasePathNamespace(object):
KDCPROXY_CONFIG = '/etc/ipa/kdcproxy/kdcproxy.conf'
CERTMONGER = '/usr/sbin/certmonger'
NETWORK_MANAGER_CONFIG_DIR = '/etc/NetworkManager/conf.d'
IPA_CUSTODIA_CONF_DIR = '/etc/ipa/custodia'
IPA_CUSTODIA_CONF = '/etc/ipa/custodia/custodia.conf'
IPA_CUSTODIA_SOCKET = '/run/httpd/ipa-custodia.sock'
IPA_CUSTODIA_AUDIT_LOG = '/var/log/ipa-custodia.audit.log'
path_namespace = BasePathNamespace

View File

View File

@ -0,0 +1,99 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function
from custodia.message.kem import KEMClient, KEY_USAGE_SIG, KEY_USAGE_ENC
from jwcrypto.common import json_decode
from jwcrypto.jwk import JWK
from ipapython.secrets.kem import IPAKEMKeys
from ipapython.secrets.store import iSecStore
from ipaplatform.paths import paths
from base64 import b64encode
import ldapurl
import gssapi
import os
import requests
class CustodiaClient(object):
def _client_keys(self):
return self.ikk.server_keys
def _server_keys(self, server, realm):
principal = 'host/%s@%s' % (server, realm)
sk = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_SIG)))
ek = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_ENC)))
return (sk, ek)
def _ldap_uri(self, realm):
dashrealm = '-'.join(realm.split('.'))
socketpath = paths.SLAPD_INSTANCE_SOCKET_TEMPLATE % (dashrealm,)
return 'ldapi://' + ldapurl.ldapUrlEscape(socketpath)
def _keystore(self, realm, ldap_uri, auth_type):
config = dict()
if ldap_uri is None:
config['ldap_uri'] = self._ldap_uri(realm)
else:
config['ldap_uri'] = ldap_uri
if auth_type is not None:
config['auth_type'] = auth_type
return iSecStore(config)
def __init__(self, client, server, realm, ldap_uri=None, auth_type=None):
self.client = client
self.creds = None
self.service_name = gssapi.Name('HTTP@%s' % (server,),
gssapi.NameType.hostbased_service)
self.server = server
keyfile = os.path.join(paths.IPA_CUSTODIA_CONF_DIR, 'server.keys')
self.ikk = IPAKEMKeys({'server_keys': keyfile})
self.kemcli = KEMClient(self._server_keys(server, realm),
self._client_keys())
self.keystore = self._keystore(realm, ldap_uri, auth_type)
# FIXME: Remove warnings about missig subjAltName
requests.packages.urllib3.disable_warnings()
def init_creds(self):
name = gssapi.Name('host@%s' % (self.client,),
gssapi.NameType.hostbased_service)
store = {'client_keytab': paths.KRB5_KEYTAB,
'ccache': 'MEMORY:Custodia_%s' % b64encode(os.urandom(8))}
return gssapi.Credentials(name=name, store=store, usage='initiate')
def _auth_header(self):
if not self.creds or self.creds.lifetime < 300:
self.creds = self.init_creds()
ctx = gssapi.SecurityContext(name=self.service_name, creds=self.creds)
authtok = ctx.step()
return {'Authorization': 'Negotiate %s' % b64encode(authtok)}
def fetch_key(self, keyname):
# Prepare URL
url = 'https://%s/ipa/keys/%s' % (self.server, keyname)
# Prepare signed/encrypted request
encalg = ('RSA1_5', 'A256CBC-HS512')
request = self.kemcli.make_request(keyname, encalg=encalg)
# Prepare Authentication header
headers = self._auth_header()
# Perform request
r = requests.get(url, headers=headers,
params={'type': 'kem', 'value': request})
r.raise_for_status()
reply = r.json()
if 'type' not in reply or reply['type'] != 'kem':
raise RuntimeError('Invlid JSON response type')
value = self.kemcli.parse_reply(keyname, reply['value'])
self.keystore.set('keys/%s' % keyname, value)

View File

@ -0,0 +1,45 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function
import ldap
import ldap.sasl
import ldap.filter
class iSecLdap(object):
def __init__(self, uri, auth_type=None):
self.uri = uri
if auth_type is not None:
self.auth_type = auth_type
else:
if uri.startswith('ldapi'):
self.auth_type = 'EXTERNAL'
else:
self.auth_type = 'GSSAPI'
self._basedn = None
@property
def basedn(self):
if self._basedn is None:
conn = self.connect()
r = conn.search_s('', ldap.SCOPE_BASE)
self._basedn = r[0][1]['defaultnamingcontext'][0]
return self._basedn
def connect(self):
conn = ldap.initialize(self.uri)
if self.auth_type == 'EXTERNAL':
auth_tokens = ldap.sasl.external(None)
elif self.auth_type == 'GSSAPI':
auth_tokens = ldap.sasl.sasl({}, 'GSSAPI')
else:
raise ValueError(
'Invalid authentication type: %s' % self.auth_type)
conn.sasl_interactive_bind_s('', auth_tokens)
return conn
def build_filter(self, formatstr, args):
escaped_args = dict()
for key, value in args.iteritems():
escaped_args[key] = ldap.filter.escape_filter_chars(value)
return formatstr.format(**escaped_args)

205
ipapython/secrets/kem.py Normal file
View File

@ -0,0 +1,205 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function
from ipaplatform.paths import paths
import ConfigParser
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from custodia.message.kem import KEMKeysStore
from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP
from jwcrypto.common import json_decode, json_encode
from jwcrypto.common import base64url_encode
from jwcrypto.jwk import JWK
from ipapython.secrets.common import iSecLdap
from binascii import unhexlify
import ldap
IPA_REL_BASE_DN = 'cn=custodia,cn=ipa,cn=etc'
IPA_KEYS_QUERY = '(&(ipaKeyUsage={usage:s})(memberPrincipal={princ:s}))'
RFC5280_USAGE_MAP = {KEY_USAGE_SIG: 'digitalSignature',
KEY_USAGE_ENC: 'dataEncipherment'}
class KEMLdap(iSecLdap):
@property
def keysbase(self):
return '%s,%s' % (IPA_REL_BASE_DN, self.basedn)
def _encode_int(self, i):
I = hex(i).rstrip("L").lstrip("0x")
return base64url_encode(unhexlify((len(I) % 2) * '0' + I))
def _parse_public_key(self, ipa_public_key):
public_key = serialization.load_der_public_key(ipa_public_key,
default_backend())
num = public_key.public_numbers()
if isinstance(num, rsa.RSAPublicNumbers):
return {'kty': 'RSA',
'e': self._encode_int(num.e),
'n': self._encode_int(num.n)}
elif isinstance(num, ec.EllipticCurvePublicNumbers):
if num.curve.name == 'secp256r1':
curve = 'P-256'
elif num.curve.name == 'secp384r1':
curve = 'P-384'
elif num.curve.name == 'secp521r1':
curve = 'P-521'
else:
raise TypeError('Unsupported Elliptic Curve')
return {'kty': 'EC',
'crv': curve,
'x': self._encode_int(num.x),
'y': self._encode_int(num.y)}
else:
raise TypeError('Unknown Public Key type')
def get_key(self, usage, principal):
conn = self.connect()
scope = ldap.SCOPE_SUBTREE
ldap_filter = self.build_filter(IPA_KEYS_QUERY,
{'usage': RFC5280_USAGE_MAP[usage],
'princ': principal})
r = conn.search_s(self.keysbase, scope, ldap_filter)
if len(r) != 1:
raise ValueError("Incorrect number of results (%d) searching for"
"public key for %s" % (len(r), principal))
ipa_public_key = r[0][1]['ipaPublicKey'][0]
jwk = self._parse_public_key(ipa_public_key)
jwk['use'] = KEY_USAGE_MAP[usage]
return json_encode(jwk)
def _format_public_key(self, key):
if isinstance(key, str):
jwkey = json_decode(key)
if 'kty' not in jwkey:
raise ValueError('Invalid key, missing "kty" attribute')
if jwkey['kty'] == 'RSA':
pubnum = rsa.RSAPublicNumbers(jwkey['e'], jwkey['n'])
pubkey = pubnum.public_key(default_backend())
elif jwkey['kty'] == 'EC':
if jwkey['crv'] == 'P-256':
curve = ec.SECP256R1
elif jwkey['crv'] == 'P-384':
curve = ec.SECP384R1
elif jwkey['crv'] == 'P-521':
curve = ec.SECP521R1
else:
raise TypeError('Unsupported Elliptic Curve')
pubnum = ec.EllipticCurvePublicNumbers(
jwkey['x'], jwkey['y'], curve)
pubkey = pubnum.public_key(default_backend())
else:
raise ValueError('Unknown key type: %s' % jwkey['kty'])
elif isinstance(key, rsa.RSAPublicKey):
pubkey = key
elif isinstance(key, ec.EllipticCurvePublicKey):
pubkey = key
else:
raise TypeError('Unknown key type: %s' % type(key))
return pubkey.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def set_key(self, usage, host, principal, key):
public_key = self._format_public_key(key)
conn = self.connect()
name = '%s/%s' % (KEY_USAGE_MAP[usage], host)
dn = 'cn=%s,%s' % (name, self.keysbase)
try:
mods = [('objectClass', ['nsContainer',
'ipaKeyPolicy',
'ipaPublicKeyObject',
'groupOfPrincipals']),
('cn', name),
('ipaKeyUsage', RFC5280_USAGE_MAP[usage]),
('memberPrincipal', principal),
('ipaPublicKey', public_key)]
conn.add_s(dn, mods)
except Exception: # pylint: disable=broad-except
# This may fail if the entry already exists
mods = [(ldap.MOD_REPLACE, 'memberPrincipal', principal),
(ldap.MOD_REPLACE, 'ipaPublicKey', public_key)]
conn.modify_s(dn, mods)
def newServerKeys(path, keyid):
skey = JWK(generate='RSA', use='sig', kid=keyid)
ekey = JWK(generate='RSA', use='enc', kid=keyid)
with open(path, 'w+') as f:
f.write('[%s,%s]' % (skey.export(), ekey.export()))
return [skey.get_op_key('verify'), ekey.get_op_key('encrypt')]
class IPAKEMKeys(KEMKeysStore):
"""A KEM Keys Store.
This is a store that holds public keys of registered
clients allowed to use KEM messages. It takes the form
of an authorizer merely for the purpose of attaching
itself to a 'request' so that later on the KEM Parser
can fetch the appropariate key to verify/decrypt an
incoming request and make the payload available.
The KEM Parser will actually perform additional
authorization checks in this case.
SimplePathAuthz is extended here as we want to attach the
store only to requests on paths we are configured to
manage.
"""
def __init__(self, config=None, ipaconf=paths.IPA_DEFAULT_CONF):
super(IPAKEMKeys, self).__init__(config)
conf = ConfigParser.ConfigParser()
conf.read(ipaconf)
self.host = conf.get('global', 'host')
self.realm = conf.get('global', 'realm')
self.ldap_uri = config.get('ldap_uri', None)
if self.ldap_uri is None:
self.ldap_uri = conf.get('global', 'ldap_uri', None)
self._server_keys = None
def find_key(self, kid, usage):
if kid is None:
raise TypeError('Key ID is None, should be a SPN')
conn = KEMLdap(self.ldap_uri)
return conn.get_key(usage, kid)
def generate_server_keys(self):
principal = 'host/%s@%s' % (self.host, self.realm)
# Neutralize the key with read if any
self._server_keys = None
# Generate private key and store it
pubkeys = newServerKeys(self.config['server_keys'], principal)
# Store public key in LDAP
ldapconn = KEMLdap(self.ldap_uri)
ldapconn.set_key(KEY_USAGE_SIG, self.host, principal, pubkeys[0])
ldapconn.set_key(KEY_USAGE_ENC, self.host, principal, pubkeys[1])
@property
def server_keys(self):
if self._server_keys is None:
with open(self.config['server_keys']) as f:
jsonkeys = f.read()
dictkeys = json_decode(jsonkeys)
self._server_keys = (JWK(**dictkeys[KEY_USAGE_SIG]),
JWK(**dictkeys[KEY_USAGE_ENC]))
return self._server_keys
# Manual testing
if __name__ == '__main__':
IKK = IPAKEMKeys({'paths': '/',
'server_keys': '/etc/ipa/custodia/server.keys'})
IKK.generate_server_keys()
print(('SIG', IKK.server_keys[0].export_public()))
print(('ENC', IKK.server_keys[1].export_public()))
print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm),
usage=KEY_USAGE_SIG))
print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm),
usage=KEY_USAGE_ENC))

199
ipapython/secrets/store.py Normal file
View File

@ -0,0 +1,199 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function
from base64 import b64encode, b64decode
from custodia.store.interface import CSStore
from jwcrypto.common import json_decode, json_encode
from ipaplatform.paths import paths
from ipapython import ipautil
from ipapython.secrets.common import iSecLdap
import ldap
import os
import shutil
import sys
import StringIO
import tempfile
class UnknownKeyName(Exception):
pass
class DBMAPHandler(object):
def __init__(self, config, dbmap, nickname):
raise NotImplementedError
def export_key(self):
raise NotImplementedError
def import_key(self, value):
raise NotImplementedError
def log_error(error):
print(error, file=sys.stderr)
def PKI_TOMCAT_password_callback():
password = None
with open(paths.PKI_TOMCAT_PASSWORD_CONF) as f:
for line in f.readlines():
key, value = line.strip().split('=')
if key == 'internal':
password = value
break
return password
def HTTPD_password_callback():
with open(paths.ALIAS_PWDFILE_TXT) as f:
password = f.read()
return password
class NSSCertDB(DBMAPHandler):
def __init__(self, config, dbmap, nickname):
if 'type' not in dbmap or dbmap['type'] != 'NSSDB':
raise ValueError('Invalid type "%s",'
' expected "NSSDB"' % (dbmap['type'],))
if 'path' not in dbmap:
raise ValueError('Configuration does not provide NSSDB path')
if 'pwcallback' not in dbmap:
raise ValueError('Configuration does not provide Password Calback')
self.nssdb_path = dbmap['path']
self.nickname = nickname
self.nssdb_password = dbmap['pwcallback']()
def export_key(self):
tdir = tempfile.mkdtemp(dir=paths.TMP)
try:
nsspwfile = os.path.join(tdir, 'nsspwfile')
with open(nsspwfile, 'w+') as f:
f.write(self.nssdb_password)
pk12pwfile = os.path.join(tdir, 'pk12pwfile')
password = b64encode(os.urandom(16))
with open(pk12pwfile, 'w+') as f:
f.write(password)
pk12file = os.path.join(tdir, 'pk12file')
ipautil.run([paths.PK12UTIL,
"-d", self.nssdb_path,
"-o", pk12file,
"-n", self.nickname,
"-k", nsspwfile,
"-w", pk12pwfile])
with open(pk12file, 'r') as f:
data = f.read()
finally:
shutil.rmtree(tdir)
return json_encode({'export password': password,
'pkcs12 data': b64encode(data)})
def import_key(self, value):
v = json_decode(value)
tdir = tempfile.mkdtemp(dir=paths.TMP)
try:
nsspwfile = os.path.join(tdir, 'nsspwfile')
with open(nsspwfile, 'w+') as f:
f.write(self.nssdb_password)
pk12pwfile = os.path.join(tdir, 'pk12pwfile')
with open(pk12pwfile, 'w+') as f:
f.write(v['export password'])
pk12file = os.path.join(tdir, 'pk12file')
with open(pk12file, 'w+') as f:
f.write(b64decode(v['pkcs12 data']))
ipautil.run([paths.PK12UTIL,
"-d", self.nssdb_path,
"-i", pk12file,
"-n", self.nickname,
"-k", nsspwfile,
"-w", pk12pwfile])
finally:
shutil.rmtree(tdir)
# Exfiltrate the DM password Hash so it can be set in replica's and this
# way let a replica be install without knowing the DM password and yet
# still keep the DM password synchronized across replicas
class DMLDAP(DBMAPHandler):
def __init__(self, config, dbmap, nickname):
if 'type' not in dbmap or dbmap['type'] != 'DMLDAP':
raise ValueError('Invalid type "%s",'
' expected "DMLDAP"' % (dbmap['type'],))
if nickname != 'DMHash':
raise UnknownKeyName("Unknown Key Named '%s'" % nickname)
self.ldap = iSecLdap(config['ldap_uri'],
config.get('auth_type', None))
def export_key(self):
conn = self.ldap.connect()
r = conn.search_s('cn=config', ldap.SCOPE_BASE,
attrlist=['nsslapd-rootpw'])
if len(r) != 1:
raise RuntimeError('DM Hash not found!')
return json_encode({'dmhash': r[0][1]['nsslapd-rootpw'][0]})
def import_key(self, value):
v = json_decode(value)
conn = self.ldap.connect()
mods = [(ldap.MOD_REPLACE, 'nsslapd-rootpw', str(v['dmhash']))]
conn.modify_s('cn=config', mods)
NAME_DB_MAP = {
'ca': {
'type': 'NSSDB',
'path': paths.PKI_TOMCAT_ALIAS_DIR,
'handler': NSSCertDB,
'pwcallback': PKI_TOMCAT_password_callback,
},
'ra': {
'type': 'NSSDB',
'path': paths.HTTPD_ALIAS_DIR,
'handler': NSSCertDB,
'pwcallback': HTTPD_password_callback,
},
'dm': {
'type': 'DMLDAP',
'handler': DMLDAP,
}
}
class iSecStore(CSStore):
def __init__(self, config=None):
self.config = config
def _get_handler(self, key):
path = key.split('/', 3)
if len(path) != 3 or path[0] != 'keys':
raise ValueError('Invalid name')
if path[1] not in NAME_DB_MAP:
raise UnknownKeyName("Unknown DB named '%s'" % path[1])
dbmap = NAME_DB_MAP[path[1]]
return dbmap['handler'](self.config, dbmap, path[2])
def get(self, key):
try:
key_handler = self._get_handler(key)
value = key_handler.export_key()
except Exception as e: # pylint: disable=broad-except
log_error('Error retrievieng key "%s": %s' % (key, str(e)))
value = None
return value
def set(self, key, value, replace=False):
try:
key_handler = self._get_handler(key)
key_handler.import_key(value)
except Exception as e: # pylint: disable=broad-except
log_error('Error storing key "%s": %s' % (key, str(e)))
def list(self, keyfilter=None):
raise NotImplementedError
def cut(self, key):
raise NotImplementedError

1
ipapython/setup.py.in Normal file → Executable file
View File

@ -67,6 +67,7 @@ def setup_package():
package_dir = {'ipapython': ''},
packages = ["ipapython",
"ipapython.dnssec",
"ipapython.secrets",
"ipapython.install"],
)
finally:

View File

@ -0,0 +1,51 @@
# Copyright (C) 2015 FreeIPa Project Contributors, see 'COPYING' for license.
from ipapython.secrets.kem import IPAKEMKeys
from ipaplatform.paths import paths
from service import SimpleServiceInstance
from ipapython import ipautil
from ipaserver.install import installutils
import os
class CustodiaInstance(SimpleServiceInstance):
def __init__(self):
super(CustodiaInstance, self).__init__("ipa-custodia")
self.config_file = paths.IPA_CUSTODIA_CONF
self.server_keys = os.path.join(paths.IPA_CUSTODIA_CONF_DIR,
'server.keys')
def __config_file(self):
template_file = os.path.basename(self.config_file) + '.template'
template = os.path.join(ipautil.SHARE_DIR, template_file)
sub_dict = dict(IPA_CUSTODIA_CONF_DIR=paths.IPA_CUSTODIA_CONF_DIR,
IPA_CUSTODIA_SOCKET=paths.IPA_CUSTODIA_SOCKET,
IPA_CUSTODIA_AUDIT_LOG=paths.IPA_CUSTODIA_AUDIT_LOG,
LDAP_URI=installutils.realm_to_ldapi_uri(self.realm))
conf = ipautil.template_file(template, sub_dict)
fd = open(self.config_file, "w+")
fd.write(conf)
fd.flush()
fd.close()
def create_instance(self, *args, **kwargs):
self.step("Generating ipa-custodia config file", self.__config_file)
self.step("Generating ipa-custodia keys", self.__gen_keys)
super(CustodiaInstance, self).create_instance(*args, **kwargs)
def __gen_keys(self):
KeyStore = IPAKEMKeys({'server_keys': self.server_keys})
KeyStore.generate_server_keys()
def upgrade_instance(self, realm):
self.realm = realm
if not os.path.exists(self.config_file):
self.__config_file()
if not os.path.exists(self.server_keys):
self.__gen_keys()
def __start(self):
super(CustodiaInstance, self).__start()
def __enable(self):
super(CustodiaInstance, self).__enable()

View File

@ -35,6 +35,7 @@ from contextlib import contextmanager
from dns import resolver, rdatatype
from dns.exception import DNSException
import ldap
import ldapurl
from nss.error import NSPRError
import six
from six.moves.configparser import SafeConfigParser, NoOptionError
@ -1097,6 +1098,13 @@ def check_version():
def realm_to_serverid(realm_name):
return "-".join(realm_name.split("."))
def realm_to_ldapi_uri(realm_name):
serverid = realm_to_serverid(realm_name)
socketname = paths.SLAPD_INSTANCE_SOCKET_TEMPLATE % (serverid,)
return 'ldapi://' + ldapurl.ldapUrlEscape(socketname)
def enable_and_start_oddjobd(sstore):
oddjobd = services.service('oddjobd')
sstore.backup_state('oddjobd', 'running', oddjobd.is_running())

View File

@ -33,7 +33,7 @@ import ipaclient.ntpconf
from ipaserver.install import (
bindinstance, ca, cainstance, certs, dns, dsinstance, httpinstance,
installutils, kra, krbinstance, memcacheinstance, ntpinstance,
otpdinstance, replication, service, sysupgrade)
otpdinstance, custodiainstance, replication, service, sysupgrade)
from ipaserver.install.installutils import (
IPA_MODULES, BadHostError, get_fqdn, get_server_ip_address,
is_ipa_configured, load_pkcs12, read_password, verify_fqdn,
@ -814,6 +814,11 @@ def install(installer):
otpd.create_instance('OTPD', host_name, dm_password,
ipautil.realm_to_suffix(realm_name))
custodia = custodiainstance.CustodiaInstance()
custodia.create_instance('KEYS', host_name, dm_password,
ipautil.realm_to_suffix(realm_name),
realm_name)
# Create a HTTP instance
http = httpinstance.HTTPInstance(fstore)
if options.http_cert_files:
@ -1078,6 +1083,7 @@ def uninstall(installer):
dsinstance.DsInstance(fstore=fstore).uninstall()
if _server_trust_ad_installed:
adtrustinstance.ADTRUSTInstance(fstore).uninstall()
custodiainstance.CustodiaInstance().uninstall()
memcacheinstance.MemcacheInstance().uninstall()
otpdinstance.OtpdInstance().uninstall()
tasks.restore_network_configuration(fstore, sstore)

View File

@ -28,7 +28,7 @@ import ipaclient.ntpconf
from ipaserver.install import (
bindinstance, ca, cainstance, certs, dns, dsinstance, httpinstance,
installutils, kra, krbinstance, memcacheinstance, ntpinstance,
otpdinstance, service)
otpdinstance, custodiainstance, service)
from ipaserver.install.installutils import create_replica_config
from ipaserver.install.replication import (
ReplicationManager, replica_conn_check)
@ -596,6 +596,13 @@ def install(installer):
CA.import_ra_cert(config.dir + "/ra.p12")
CA.fix_ra_perms()
# FIXME: must be done earlier in replica to fetch keys for CA/ldap server
# before they are configured
custodia = custodiainstance.CustodiaInstance()
custodia.create_instance('KEYS', config.host_name,
config.dirman_password,
ipautil.realm_to_suffix(config.realm_name))
# The DS instance is created before the keytab, add the SSL cert we
# generated
ds.add_cert_to_service()

View File

@ -36,6 +36,7 @@ from ipaserver.install import cainstance
from ipaserver.install import certs
from ipaserver.install import otpdinstance
from ipaserver.install import schemaupdate
from ipaserver.install import custodiainstance
from ipaserver.install import sysupgrade
from ipaserver.install import dnskeysyncinstance
from ipaserver.install import krainstance
@ -1490,7 +1491,7 @@ def upgrade_configuration():
service.ldapi = True
try:
if not service.is_configured():
# 389-ds needs to be running to create the memcache instance
# 389-ds needs to be running to create the instances
# because we record the new service in cn=masters.
ds.start()
service.create_instance(ldap_name, fqdn, None,
@ -1539,6 +1540,9 @@ def upgrade_configuration():
except ipautil.CalledProcessError as e:
root_logger.error("Failed to restart %s: %s", bind.service_name, e)
custodia = custodiainstance.CustodiaInstance()
custodia.upgrade_instance(api.env.realm)
ca_restart = any([
ca_restart,
ca_upgrade_schema(ca),

View File

@ -40,6 +40,7 @@ SERVICE_LIST = {
'DNS': ('named', 30),
'MEMCACHE': ('ipa_memcached', 39),
'HTTP': ('httpd', 40),
'KEYS': ('ipa-custodia', 41),
'CA': ('%sd' % dogtag.configured_constants().PKI_INSTANCE_NAME, 50),
'KRA': ('%sd' % dogtag.configured_constants().PKI_INSTANCE_NAME, 51),
'ADTRUST': ('smb', 60),

View File

@ -0,0 +1,55 @@
# Copyright (C) 2015 FreeIPA Project Contributors - see LICENSE file
from __future__ import print_function
from ipapython.secrets.store import iSecStore, NAME_DB_MAP, NSSCertDB
import os
import shutil
import subprocess
import unittest
def _test_password_callback():
with open('test-ipa-sec-store/pwfile') as f:
password = f.read()
return password
class TestiSecStore(unittest.TestCase):
@classmethod
def setUpClass(cls):
try:
shutil.rmtree('test-ipa-sec-store')
except Exception: # pylint: disable=broad-except
pass
testdir = 'test-ipa-sec-store'
pwfile = os.path.join(testdir, 'pwfile')
os.mkdir(testdir)
with open(pwfile, 'w') as f:
f.write('testpw')
cls.certdb = os.path.join(testdir, 'certdb')
os.mkdir(cls.certdb)
cls.cert2db = os.path.join(testdir, 'cert2db')
os.mkdir(cls.cert2db)
seedfile = os.path.join(testdir, 'seedfile')
with open(seedfile, 'w') as f:
seed = os.urandom(1024)
f.write(seed)
subprocess.call(['certutil', '-d', cls.certdb, '-N', '-f', pwfile])
subprocess.call(['certutil', '-d', cls.cert2db, '-N', '-f', pwfile])
subprocess.call(['certutil', '-d', cls.certdb, '-S', '-f', pwfile,
'-s', 'CN=testCA', '-n', 'testCACert', '-x',
'-t', 'CT,C,C', '-m', '1', '-z', seedfile])
def test_iSecStore(self):
iss = iSecStore({})
NAME_DB_MAP['test'] = {
'type': 'NSSDB',
'path': self.certdb,
'handler': NSSCertDB,
'pwcallback': _test_password_callback,
}
value = iss.get('keys/test/testCACert')
NAME_DB_MAP['test']['path'] = self.cert2db
iss.set('keys/test/testCACert', value)