Imported Upstream version 4.8.10

This commit is contained in:
Mario Fetka
2021-10-03 11:06:28 +02:00
parent 10dfc9587b
commit 03a8170b15
2361 changed files with 1883897 additions and 338759 deletions

View File

133
ipaserver/secrets/client.py Normal file
View File

@@ -0,0 +1,133 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function, absolute_import
import contextlib
import os
import secrets
from base64 import b64encode
# pylint: disable=relative-import
from custodia.message.kem import KEMClient, KEY_USAGE_SIG, KEY_USAGE_ENC
# pylint: enable=relative-import
from jwcrypto.common import json_decode
from jwcrypto.jwk import JWK
from ipalib.krb_utils import krb5_format_service_principal_name
from ipaserver.secrets.kem import IPAKEMKeys
from ipaserver.secrets.store import IPASecStore
from ipaplatform.paths import paths
import gssapi
import requests
@contextlib.contextmanager
def ccache_env(ccache):
"""Temporarily set KRB5CCNAME environment variable
"""
orig_ccache = os.environ.get('KRB5CCNAME')
os.environ['KRB5CCNAME'] = ccache
try:
yield
finally:
os.environ.pop('KRB5CCNAME', None)
if orig_ccache is not None:
os.environ['KRB5CCNAME'] = orig_ccache
class CustodiaClient:
def __init__(self, client_service, keyfile, keytab, server, realm,
ldap_uri=None, auth_type=None):
if client_service.endswith(realm) or "@" not in client_service:
raise ValueError(
"Client service name must be a GSS name (service@host), "
"not '{}'.".format(client_service)
)
self.client_service = client_service
self.keytab = keytab
self.server = server
self.realm = realm
self.ldap_uri = ldap_uri
self.auth_type = auth_type
self.service_name = gssapi.Name(
'HTTP@{}'.format(server), gssapi.NameType.hostbased_service
)
self.keystore = IPASecStore()
# use in-process MEMORY ccache. Handler process don't need a TGT.
self.ccache = 'MEMORY:Custodia_{}'.format(secrets.token_hex())
with ccache_env(self.ccache):
# Init creds immediately to make sure they are valid. Creds
# can also be re-inited by _auth_header to avoid expiry.
self.creds = self._init_creds()
self.ikk = IPAKEMKeys(
{'server_keys': keyfile, 'ldap_uri': ldap_uri}
)
self.kemcli = KEMClient(
self._server_keys(), self._client_keys()
)
def _client_keys(self):
return self.ikk.server_keys
def _server_keys(self):
principal = krb5_format_service_principal_name(
'host', self.server, self.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 _init_creds(self):
name = gssapi.Name(
self.client_service, gssapi.NameType.hostbased_service
)
store = {
'client_keytab': self.keytab,
'ccache': self.ccache
}
return gssapi.Credentials(name=name, store=store, usage='initiate')
def _auth_header(self):
if 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).decode('ascii')}
def fetch_key(self, keyname, store=True):
# Prepare URL
url = 'https://%s/ipa/keys/%s' % (self.server, keyname)
# Prepare signed/encrypted request
encalg = ('RSA-OAEP', '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,
verify=paths.IPA_CA_CRT,
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'])
if store:
self.keystore.set('keys/%s' % keyname, value)
else:
return value
return None

View File

@@ -0,0 +1,47 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function
import ldap
import ldap.sasl
import ldap.filter
from ipapython.ipaldap import ldap_initialize
class iSecLdap:
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].decode('utf-8')
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.items():
escaped_args[key] = ldap.filter.escape_filter_chars(value)
return formatstr.format(**escaped_args)

View File

@@ -0,0 +1,2 @@
"""Export / import handlers
"""

View File

@@ -0,0 +1,75 @@
#
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
#
"""Common helpers for handlers
"""
import argparse
import base64
import json
import shutil
import tempfile
def default_json(obj):
"""JSON encoder default handler
"""
if isinstance(obj, (bytes, bytearray)):
return base64.b64encode(obj).decode('ascii')
raise TypeError(
"Object of type {} is not JSON serializable".format(type(obj))
)
def json_dump(data, exportfile):
"""Dump JSON to file
"""
json.dump(
data,
exportfile,
default=default_json,
separators=(',', ':'),
sort_keys=True
)
def mkparser(supports_import=True, **kwargs):
"""Create default parser for handler with export / import args
All commands support export to file or stdout. Most commands can also
import from a file or stdin. Export and import are mutually exclusive
options.
"""
parser = argparse.ArgumentParser(**kwargs)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
'--export',
help='JSON export file ("-" for stdout)',
dest='exportfile',
type=argparse.FileType('w')
)
if supports_import:
group.add_argument(
'--import',
help='JSON import file ("-" for stdin)',
dest='importfile',
type=argparse.FileType('r')
)
return parser
def main(parser, export_func, import_func=None, **kwargs):
"""Common main function for handlers
"""
args = parser.parse_args()
if args.exportfile is not None:
func = export_func
else:
func = import_func
tmpdir = tempfile.mkdtemp()
try:
func(args, tmpdir, **kwargs)
finally:
shutil.rmtree(tmpdir)

View File

@@ -0,0 +1,63 @@
#
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
#
"""Export / import Directory Manager password hash
"""
import json
import os
from ipalib import api
from ipalib import errors
from ipaplatform.paths import paths
from ipapython.dn import DN
from ipapython.ipaldap import LDAPClient, realm_to_ldapi_uri
from . import common
CN_CONFIG = DN(('cn', 'config'))
ROOTPW = 'nsslapd-rootpw'
def export_key(args, tmpdir, conn):
entry = conn.get_entry(CN_CONFIG, [ROOTPW])
data = {
'dmhash': entry.single_value[ROOTPW],
}
common.json_dump(data, args.exportfile)
def import_key(args, tmpdir, conn):
data = json.load(args.importfile)
dmhash = data['dmhash'].encode('ascii')
entry = conn.get_entry(CN_CONFIG, [ROOTPW])
entry.single_value[ROOTPW] = dmhash
try:
conn.update_entry(entry)
except errors.EmptyModlist:
pass
def main():
parser = common.mkparser(
description='ipa-custodia LDAP DM hash handler'
)
if os.getegid() != 0:
parser.error("Must be run as root user.\n")
# create LDAP connection using LDAPI and EXTERNAL bind as root
if not api.isdone('bootstrap'):
api.bootstrap(confdir=paths.ETC_IPA, log=None)
realm = api.env.realm
ldap_uri = realm_to_ldapi_uri(realm)
conn = LDAPClient(ldap_uri=ldap_uri, no_schema=True)
try:
conn.external_bind()
except Exception as e:
parser.error("Failed to connect to {}: {}\n".format(ldap_uri, e))
with conn:
common.main(parser, export_key, import_key, conn=conn)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,122 @@
#
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
#
"""Export / import cert and key from NSS DB as PKCS#12 data
"""
import base64
import json
import os
from ipaplatform.paths import paths
from ipapython import ipautil
from ipapython.certdb import NSSDatabase
from . import common
def export_key(args, tmpdir):
"""Export key and certificate from the NSS DB to a PKCS#12 file.
The PKCS#12 file is encrypted with a password.
"""
pk12file = os.path.join(tmpdir, 'export.p12')
password = ipautil.ipa_generate_password()
pk12pk12pwfile = os.path.join(tmpdir, 'passwd')
with open(pk12pk12pwfile, 'w') as f:
f.write(password)
nssdb = NSSDatabase(args.nssdb_path)
nssdb.run_pk12util([
"-o", pk12file,
"-n", args.nickname,
"-k", args.nssdb_pwdfile,
"-w", pk12pk12pwfile,
])
with open(pk12file, 'rb') as f:
p12data = f.read()
data = {
'export password': password,
'pkcs12 data': p12data,
}
common.json_dump(data, args.exportfile)
def import_key(args, tmpdir):
"""Import key and certificate from a PKCS#12 file to a NSS DB.
"""
data = json.load(args.importfile)
password = data['export password']
p12data = base64.b64decode(data['pkcs12 data'])
pk12pwfile = os.path.join(tmpdir, 'passwd')
with open(pk12pwfile, 'w') as f:
f.write(password)
pk12file = os.path.join(tmpdir, 'import.p12')
with open(pk12file, 'wb') as f:
f.write(p12data)
nssdb = NSSDatabase(args.nssdb_path)
nssdb.run_pk12util([
"-i", pk12file,
"-n", args.nickname,
"-k", args.nssdb_pwdfile,
"-w", pk12pwfile,
])
def default_parser():
"""Generic interface
"""
parser = common.mkparser(
description='ipa-custodia NSS cert handler'
)
parser.add_argument(
'--nssdb',
dest='nssdb_path',
help='path to NSS DB',
required=True
)
parser.add_argument(
'--pwdfile',
dest='nssdb_pwdfile',
help='path to password file for NSS DB',
required=True
)
parser.add_argument(
'--nickname',
help='nick name of certificate',
required=True
)
return parser
def pki_tomcat_parser():
"""Hard-code Dogtag's NSSDB and its password file
"""
parser = common.mkparser(
description='ipa-custodia pki-tomcat NSS cert handler'
)
parser.add_argument(
'--nickname',
help='nick name of certificate',
required=True
)
parser.set_defaults(
nssdb_path=paths.PKI_TOMCAT_ALIAS_DIR,
nssdb_pwdfile=paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT,
)
return parser
def main(parser=None):
if parser is None:
parser = default_parser()
common.main(parser, export_key, import_key)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,126 @@
#
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
#
"""Export and wrap key from NSS DB
"""
import os
from ipaplatform.paths import paths
from ipapython import ipautil
from ipapython.certdb import NSSDatabase
from . import common
def export_key(args, tmpdir):
"""Export key and certificate from the NSS DB
The private key is encrypted using key wrapping.
"""
wrapped_key_file = os.path.join(tmpdir, 'wrapped_key')
certificate_file = os.path.join(tmpdir, 'certificate')
ipautil.run([
paths.PKI,
'-d', args.nssdb_path,
'-C', args.nssdb_pwdfile,
'ca-authority-key-export',
'--wrap-nickname', args.wrap_nickname,
'--target-nickname', args.nickname,
'--algorithm', args.algorithm,
'-o', wrapped_key_file
])
nssdb = NSSDatabase(args.nssdb_path)
nssdb.run_certutil([
'-L',
'-n', args.nickname,
'-a',
'-o', certificate_file,
])
with open(wrapped_key_file, 'rb') as f:
wrapped_key = f.read()
with open(certificate_file, 'r') as f:
certificate = f.read()
data = {
'wrapped_key': wrapped_key,
'certificate': certificate
}
common.json_dump(data, args.exportfile)
def default_parser():
"""Generic interface
"""
parser = common.mkparser(
supports_import=False,
description='ipa-custodia NSS wrapped cert handler',
)
parser.add_argument(
'--nssdb',
dest='nssdb_path',
help='path to NSS DB',
required=True
)
parser.add_argument(
'--pwdfile',
dest='nssdb_pwdfile',
help='path to password file for NSS DB',
required=True
)
parser.add_argument(
'--wrap-nickname',
dest='wrap_nickname',
help='nick name of wrapping key',
required=True
)
parser.add_argument(
'--nickname',
dest='nickname',
help='nick name of target key',
required=True
)
return parser
def pki_tomcat_parser():
"""Hard-code Dogtag's NSS DB, its password file, and CA key for wrapping
"""
parser = common.mkparser(
supports_import=False,
description='ipa-custodia pki-tomcat NSS wrapped cert handler',
)
parser.add_argument(
'--nickname',
dest='nickname',
help='nick name of target key',
required=True
)
# Caller must specify a cipher. This gets passed on to
# the 'pki ca-authority-key-export' command (part of
# Dogtag) via its own --algorithm option.
parser.add_argument(
'--algorithm',
dest='algorithm',
help='OID of symmetric wrap algorithm',
required=True
)
parser.set_defaults(
nssdb_path=paths.PKI_TOMCAT_ALIAS_DIR,
nssdb_pwdfile=paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT,
wrap_nickname='caSigningCert cert-pki-ca',
)
return parser
def main(parser=None):
if parser is None:
parser = default_parser()
common.main(parser, export_key, None)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,118 @@
#
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
#
"""Export / import PEM cert and key file as PKCS#12 data
"""
import base64
import json
import os
from ipaplatform.paths import paths
from ipapython import ipautil
from . import common
def export_key(args, tmpdir):
"""Export cert and private from PEM files as PKCS#12 file.
The PKCS#12 file is encrypted with a password.
"""
pk12file = os.path.join(tmpdir, 'export.p12')
password = ipautil.ipa_generate_password()
pk12pwfile = os.path.join(tmpdir, 'passwd')
with open(pk12pwfile, 'w') as f:
f.write(password)
# OpenSSL does not support pkcs12 export of a cert without key
ipautil.run([
paths.OPENSSL, 'pkcs12', '-export',
'-in', args.certfile,
'-out', pk12file,
'-inkey', args.keyfile,
'-password', 'file:{pk12pwfile}'.format(pk12pwfile=pk12pwfile),
])
with open(pk12file, 'rb') as f:
p12data = f.read()
data = {
'export password': password,
'pkcs12 data': p12data,
}
common.json_dump(data, args.exportfile)
def import_key(args, tmpdir):
"""Export key and certificate from a PKCS#12 file to key and cert files.
"""
data = json.load(args.importfile)
password = data['export password']
p12data = base64.b64decode(data['pkcs12 data'])
pk12pwfile = os.path.join(tmpdir, 'passwd')
with open(pk12pwfile, 'w') as f:
f.write(password)
pk12file = os.path.join(tmpdir, 'import.p12')
with open(pk12file, 'wb') as f:
f.write(p12data)
# get the certificate from the file
cmd = [
paths.OPENSSL, 'pkcs12',
'-in', pk12file,
'-clcerts', '-nokeys',
'-out', args.certfile,
'-password', 'file:{pk12pwfile}'.format(pk12pwfile=pk12pwfile),
]
ipautil.run(cmd, umask=0o027)
# get the private key from the file
cmd = [
paths.OPENSSL, 'pkcs12',
'-in', pk12file,
'-nocerts', '-nodes',
'-out', args.keyfile,
'-password', 'file:{pk12pwfile}'.format(pk12pwfile=pk12pwfile),
]
ipautil.run(cmd, umask=0o027)
def default_parser():
parser = common.mkparser(
description='ipa-custodia PEM file handler'
)
parser.add_argument(
'--certfile',
help='path to PEM encoded cert file',
required=True
)
parser.add_argument(
'keyfile',
help='path to PEM encoded key file',
required=True
)
return parser
def ra_agent_parser():
parser = common.mkparser(
description='ipa-custodia RA agent cert handler'
)
parser.set_defaults(
certfile=paths.RA_AGENT_PEM,
keyfile=paths.RA_AGENT_KEY
)
return parser
def main(parser=None):
if parser is None:
parser = default_parser()
common.main(parser, export_key, import_key)
if __name__ == '__main__':
main()

289
ipaserver/secrets/kem.py Normal file
View File

@@ -0,0 +1,289 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function, absolute_import
import errno
import os
from configparser import ConfigParser
from ipaplatform.paths import paths
from ipapython.dn import DN
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, ec
# pylint: disable=relative-import
from custodia.message.kem import KEMKeysStore
from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP
# pylint: enable=relative-import
from jwcrypto.common import json_decode, json_encode
from jwcrypto.common import base64url_encode
from jwcrypto.jwk import JWK
from ipaserver.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}))'
IPA_CHECK_QUERY = '(cn=enc/{host: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 check_host_keys(self, host):
conn = self.connect()
scope = ldap.SCOPE_SUBTREE
ldap_filter = self.build_filter(IPA_CHECK_QUERY, {'host': host})
r = conn.search_s(self.keysbase, scope, ldap_filter)
if not r:
raise ValueError("No public keys were found for %s" % host)
return True
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 _get_dn(self, usage, principal):
servicename, host = principal.split('@')[0].split('/')
name = '%s/%s' % (KEY_USAGE_MAP[usage], host)
service_rdn = ('cn', servicename) if servicename != 'host' else DN()
return DN(('cn', name), service_rdn, self.keysbase)
def set_key(self, usage, principal, key):
"""
Write key for the host or service.
Service keys are nested one level beneath the 'cn=custodia'
container, in the 'cn=<servicename>' container; this allows
fine-grained control over key management permissions for
specific services.
The container is assumed to exist.
"""
public_key = self._format_public_key(key)
dn = self._get_dn(usage, principal)
conn = self.connect()
try:
mods = [('objectClass', [b'nsContainer',
b'ipaKeyPolicy',
b'ipaPublicKeyObject',
b'groupOfPrincipals']),
('cn', dn[0].value.encode('utf-8')),
('ipaKeyUsage', RFC5280_USAGE_MAP[usage].encode('utf-8')),
('memberPrincipal', principal.encode('utf-8')),
('ipaPublicKey', public_key)]
conn.add_s(str(dn), mods)
except ldap.ALREADY_EXISTS:
mods = [(ldap.MOD_REPLACE, 'ipaPublicKey', public_key)]
conn.modify_s(str(dn), mods)
def del_key(self, usage, principal):
"""Delete key for host or service
:returns: DN of removed key or None when key was not found
"""
dn = self._get_dn(usage, principal)
conn = self.connect()
try:
conn.delete_s(str(dn))
except ldap.NO_SUCH_OBJECT:
return None
else:
return dn
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:
os.fchmod(f.fileno(), 0o600)
os.fchown(f.fileno(), 0, 0)
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()
self.host = None
self.realm = None
self.ldap_uri = config.get('ldap_uri', None)
if conf.read(ipaconf):
self.host = conf.get('global', 'host')
self.realm = conf.get('global', 'realm')
if self.ldap_uri is None:
self.ldap_uri = conf.get('global', 'ldap_uri', raw=True)
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):
self.generate_keys('host')
def generate_keys(self, servicename):
principal = '%s/%s@%s' % (servicename, 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, principal, pubkeys[0])
ldapconn.set_key(KEY_USAGE_ENC, principal, pubkeys[1])
def remove_server_keys_file(self):
"""Remove keys from disk
The method does not fail when the file is missing.
"""
try:
os.unlink(self.config['server_keys'])
except OSError as e:
if e.errno != errno.ENOENT:
raise
return False
else:
return True
def remove_server_keys(self):
"""Remove keys from LDAP and disk
"""
self.remove_keys('host')
def remove_keys(self, servicename):
"""Remove keys from LDAP and disk
"""
self.remove_server_keys_file()
principal = '%s/%s@%s' % (servicename, self.host, self.realm)
if self.ldap_uri is not None:
ldapconn = KEMLdap(self.ldap_uri)
ldapconn.del_key(KEY_USAGE_SIG, principal)
ldapconn.del_key(KEY_USAGE_ENC, principal)
@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))

View File

@@ -0,0 +1,30 @@
# Copyright (C) 2017 IPA Project Contributors, see COPYING for license
import argparse
import custodia.server # pylint: disable=relative-import
argparser = argparse.ArgumentParser(
prog='ipa-custodia',
description='IPA Custodia service'
)
argparser.add_argument(
'--debug',
action='store_true',
help='Debug mode'
)
argparser.add_argument(
'configfile',
nargs='?',
type=argparse.FileType('r'),
help="Path to IPA's custodia server config",
default='/etc/ipa/custodia/custodia.conf'
)
def main():
return custodia.server.main(argparser)
if __name__ == '__main__':
main()

233
ipaserver/secrets/store.py Normal file
View File

@@ -0,0 +1,233 @@
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function, absolute_import
import os
import sys
from custodia.plugin import CSStore
from ipaplatform.paths import paths
from ipaplatform.constants import constants
from ipapython import ipautil
class UnknownKeyName(Exception):
pass
class InvalidKeyArguments(Exception):
pass
class DBMAPHandler:
dbtype = None
supports_extra_args = False
def __init__(self, config, dbmap, nickname):
dbtype = dbmap.get('type')
if dbtype is None or dbtype != self.dbtype:
raise ValueError(
"Invalid type '{}', expected '{}'".format(
dbtype, self.dbtype
)
)
self.config = config
self.dbmap = dbmap
self.nickname = nickname
def export_key(self):
raise NotImplementedError
def import_key(self, value):
raise NotImplementedError
class DBMAPCommandHandler(DBMAPHandler):
def __init__(self, config, dbmap, nickname):
super().__init__(config, dbmap, nickname)
self.runas = dbmap.get('runas')
self.command = os.path.join(
paths.IPA_CUSTODIA_HANDLER,
dbmap['command']
)
def run_handler(self, extra_args=(), stdin=None):
"""Run handler script to export / import key material
"""
args = [self.command]
args.extend(extra_args)
kwargs = dict(
runas=self.runas,
encoding='utf-8',
)
if stdin:
args.extend(['--import', '-'])
kwargs.update(stdin=stdin)
else:
args.extend(['--export', '-'])
kwargs.update(capture_output=True)
result = ipautil.run(args, **kwargs)
if stdin is None:
return result.output
else:
return None
def log_error(error):
print(error, file=sys.stderr)
class NSSWrappedCertDB(DBMAPCommandHandler):
"""
Store that extracts private keys from an NSSDB, wrapped with the
private key of the primary CA.
"""
dbtype = 'NSSDB'
supports_extra_args = True
OID_DES_EDE3_CBC = '1.2.840.113549.3.7'
def __init__(self, config, dbmap, nickname, *extra_args):
super().__init__(config, dbmap, nickname)
# Extra args is either a single OID specifying desired wrap
# algorithm, or empty. If empty, we must assume that the
# client is an old version that only supports DES-EDE3-CBC.
#
# Using either the client's requested algorithm or the
# default of DES-EDE3-CBC, we pass it along to the handler
# via the --algorithm option. The handler, in turn, passes
# it along to the 'pki ca-authority-key-export' program
# (which is part of Dogtag).
#
if len(extra_args) > 1:
raise InvalidKeyArguments("Too many arguments")
if len(extra_args) == 1:
self.alg = extra_args[0]
else:
self.alg = self.OID_DES_EDE3_CBC
def export_key(self):
return self.run_handler([
'--nickname', self.nickname,
'--algorithm', self.alg,
])
class NSSCertDB(DBMAPCommandHandler):
dbtype = 'NSSDB'
def export_key(self):
return self.run_handler(['--nickname', self.nickname])
def import_key(self, value):
return self.run_handler(
['--nickname', self.nickname],
stdin=value
)
# 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(DBMAPCommandHandler):
dbtype = 'DMLDAP'
def __init__(self, config, dbmap, nickname):
super().__init__(config, dbmap, nickname)
if nickname != 'DMHash':
raise UnknownKeyName("Unknown Key Named '%s'" % nickname)
def export_key(self):
return self.run_handler()
def import_key(self, value):
self.run_handler(stdin=value)
class PEMFileHandler(DBMAPCommandHandler):
dbtype = 'PEM'
def export_key(self):
return self.run_handler()
def import_key(self, value):
return self.run_handler(stdin=value)
NAME_DB_MAP = {
'ca': {
'type': 'NSSDB',
'handler': NSSCertDB,
'command': 'ipa-custodia-pki-tomcat',
'runas': constants.PKI_USER,
},
'ca_wrapped': {
'type': 'NSSDB',
'handler': NSSWrappedCertDB,
'command': 'ipa-custodia-pki-tomcat-wrapped',
'runas': constants.PKI_USER,
},
'ra': {
'type': 'PEM',
'handler': PEMFileHandler,
'command': 'ipa-custodia-ra-agent',
'runas': None, # import needs root permission to write to directory
},
'dm': {
'type': 'DMLDAP',
'handler': DMLDAP,
'command': 'ipa-custodia-dmldap',
'runas': None, # root
}
}
class IPASecStore(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]]
handler = dbmap['handler']
if len(path) > 3 and not handler.supports_extra_args:
raise InvalidKeyArguments('Handler does not support extra args')
return handler(self.config, dbmap, path[2], *path[3:])
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 retrieving 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
def span(self, key):
raise NotImplementedError
# backwards compatibility with FreeIPA 4.3 and 4.4.
iSecStore = IPASecStore