From d804f1feeddd31957bc3d88dfb79e9bd119813cb Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 11 Jun 2021 08:32:04 +0200 Subject: [PATCH] Remove unused Custodia modules The CLI, IPA integration and storage backends are not used by IPA. See: https://pagure.io/freeipa/issue/8882 Signed-off-by: Christian Heimes Reviewed-By: Alexander Bokovoy Reviewed-By: Rob Crittenden --- ipaserver/custodia/cli/__init__.py | 373 -------------------------- ipaserver/custodia/cli/__main__.py | 7 - ipaserver/custodia/httpd/consumer.py | 13 - ipaserver/custodia/ipa/__init__.py | 1 - ipaserver/custodia/ipa/certrequest.py | 349 ------------------------ ipaserver/custodia/ipa/interface.py | 165 ------------ ipaserver/custodia/ipa/vault.py | 246 ----------------- ipaserver/custodia/store/__init__.py | 0 ipaserver/custodia/store/encgen.py | 107 -------- ipaserver/custodia/store/enclite.py | 40 --- ipaserver/custodia/store/interface.py | 12 - ipaserver/custodia/store/sqlite.py | 145 ---------- 12 files changed, 1458 deletions(-) delete mode 100644 ipaserver/custodia/cli/__init__.py delete mode 100644 ipaserver/custodia/cli/__main__.py delete mode 100644 ipaserver/custodia/httpd/consumer.py delete mode 100644 ipaserver/custodia/ipa/__init__.py delete mode 100644 ipaserver/custodia/ipa/certrequest.py delete mode 100644 ipaserver/custodia/ipa/interface.py delete mode 100644 ipaserver/custodia/ipa/vault.py delete mode 100644 ipaserver/custodia/store/__init__.py delete mode 100644 ipaserver/custodia/store/encgen.py delete mode 100644 ipaserver/custodia/store/enclite.py delete mode 100644 ipaserver/custodia/store/interface.py delete mode 100644 ipaserver/custodia/store/sqlite.py diff --git a/ipaserver/custodia/cli/__init__.py b/ipaserver/custodia/cli/__init__.py deleted file mode 100644 index cc4ab0d43..000000000 --- a/ipaserver/custodia/cli/__init__.py +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import, print_function - -import argparse -import operator -import os -import traceback - -import pkg_resources - -import requests.exceptions - -import six - -from custodia import log -from custodia.client import CustodiaSimpleClient, requests_gssapi -from custodia.compat import unquote, url_escape, urlparse - -if six.PY2: - from StringIO import StringIO # pylint: disable=import-error -else: - from io import StringIO - -try: - from json import JSONDecodeError -except ImportError: - # Python 2.7 has no JSONDecodeError - JSONDecodeError = ValueError - - -log.warn_provisional(__name__) - -# exit codes -E_HTTP_ERROR = 1 -E_CONNECTION_ERROR = 2 -E_JSON_ERROR = 3 -E_OTHER = 100 - - -main_parser = argparse.ArgumentParser( - prog='custodia-cli', - description='Custodia command line interface' -) - - -def server_check(arg): - """Check and format --server arg - """ - if arg.startswith(('http://', 'https://', 'http+unix://')): - return arg - if arg.startswith('./'): - arg = os.path.abspath(arg) - elif not arg.startswith('/'): - raise argparse.ArgumentTypeError( - 'Unix socket path must start with / or ./') - # assume it is a unix socket - return 'http+unix://{}'.format(url_escape(arg, '')) - - -def instance_check(arg): - if set(arg).intersection(':/@'): - raise argparse.ArgumentTypeError( - 'Instance name contains invalid characters') - return arg - - -def split_header(arg): - name, value = arg.split('=') - return name, value - - -def timeout(arg): - try: - arg = float(arg) - except (TypeError, ValueError): - raise argparse.ArgumentTypeError('Argument is not a float') - if arg < 0.0: - raise argparse.ArgumentTypeError('Argument is negative') - if arg == 0.0: - # no timeout - return None - return arg - - -group = main_parser.add_mutually_exclusive_group() -group.add_argument( - '--server', - type=server_check, - help=('Custodia server location, supports http://, https://, ' - 'or path to a unix socket.') -) -group.add_argument( - '--instance', - default=os.getenv('CUSTODIA_INSTANCE', 'custodia'), - type=instance_check, - help="Instance name (default: CUSTODIA_INSTANCE or 'custodia')", -) - -main_parser.add_argument( - '--uds-urlpath', type=str, default='/secrets/', - help='URL path for Unix Domain Socket' -) - -main_parser.add_argument( - '--header', type=split_header, action='append', - help='Extra headers' -) - -main_parser.add_argument( - '--verbose', action='store_true', -) -main_parser.add_argument( - '--debug', action='store_true', -) - -main_parser.add_argument( - '--timeout', type=timeout, default=10., - help='Connection timeout' -) - -# TLS -main_parser.add_argument( - '--cafile', type=str, default=None, - help='PEM encoded file with root CAs' -) - -# authentication mechanisms -# TLS client cert auth -tlsclient_group = main_parser.add_argument_group( - title="TLS client cert auth" -) -tlsclient_group.add_argument( - '--certfile', type=str, default=None, - help='PEM encoded file with certs for TLS client authentication' -) -tlsclient_group.add_argument( - '--keyfile', type=str, default=None, - help='PEM encoded key file (if not given, key is read from certfile)' -) - -# Use Negotiate / GSSAPI -gssapi_group = main_parser.add_argument_group( - title="GSSAPI auth" -) -gssapi_group.add_argument( - '--gssapi', action='store_true', - help='Use Negotiate / GSSAPI auth' -) - - -# handlers -def handle_name(args): - client = args.client_conn - func = getattr(client, args.command) - return func(args.name) - - -def handle_name_value(args): - client = args.client_conn - func = getattr(client, args.command) - return func(args.name, args.value) - - -# subparsers -subparsers = main_parser.add_subparsers() -subparsers.required = True - -parser_create_container = subparsers.add_parser( - 'mkdir', - help='Create a container') -parser_create_container.add_argument('name', type=str, help='key') -parser_create_container.set_defaults( - func=handle_name, - command='create_container', - sub='mkdir', -) - -parser_delete_container = subparsers.add_parser( - 'rmdir', - help='Delete a container') -parser_delete_container.add_argument('name', type=str, help='key') -parser_delete_container.set_defaults( - func=handle_name, - command='delete_container', - sub='rmdir', -) - -parser_list_container = subparsers.add_parser( - 'ls', help='List content of a container') -parser_list_container.add_argument('name', type=str, help='key') -parser_list_container.set_defaults( - func=handle_name, - command='list_container', - sub='ls', -) - -parser_get_secret = subparsers.add_parser( - 'get', help='Get secret') -parser_get_secret.add_argument('name', type=str, help='key') -parser_get_secret.set_defaults( - func=handle_name, - command='get_secret', - sub='get', -) - -parser_set_secret = subparsers.add_parser( - 'set', help='Set secret') -parser_set_secret.add_argument('name', type=str, help='key') -parser_set_secret.add_argument('value', type=str, help='value') -parser_set_secret.set_defaults( - command='set_secret', - func=handle_name_value, - sub='set' -) - -parser_del_secret = subparsers.add_parser( - 'del', help='Delete a secret') -parser_del_secret.add_argument('name', type=str, help='key') -parser_del_secret.set_defaults( - func=handle_name, - command='del_secret', - sub='del', -) - - -# plugins -PLUGINS = [ - 'custodia.authenticators', 'custodia.authorizers', 'custodia.clients', - 'custodia.consumers', 'custodia.stores' -] - - -def handle_plugins(args): - result = [] - errmsg = "**ERR** {0} ({1.__class__.__name__}: {1})" - for plugin in PLUGINS: - result.append('[{}]'.format(plugin)) - eps = pkg_resources.iter_entry_points(plugin) - eps = sorted(eps, key=operator.attrgetter('name')) - for ep in eps: - try: - if hasattr(ep, 'resolve'): - ep.resolve() - else: - ep.load(require=False) - except Exception as e: # pylint: disable=broad-except - if args.verbose: - result.append(errmsg.format(ep, e)) - else: - result.append(str(ep)) - result.append('') - return result[:-1] - - -parser_plugins = subparsers.add_parser( - 'plugins', help='List plugins') -parser_plugins.set_defaults( - func=handle_plugins, - command='plugins', - sub='plugins', - name=None, -) -parser_plugins.add_argument( - '--verbose', - action='store_true', - help="Verbose mode, show failing plugins." -) - - -def error_message(args, exc): - out = StringIO() - parts = urlparse(args.server) - - if args.debug: - traceback.print_exc(file=out) - out.write('\n') - - out.write("ERROR: Custodia command '{args.sub} {args.name}' failed.\n") - if args.verbose: - out.write("Custodia server '{args.server}'.\n") - - if isinstance(exc, requests.exceptions.HTTPError): - errcode = E_HTTP_ERROR - out.write("{exc.__class__.__name__}: {exc}\n") - elif isinstance(exc, requests.exceptions.ConnectionError): - errcode = E_CONNECTION_ERROR - if parts.scheme == 'http+unix': - out.write("Failed to connect to Unix socket '{unix_path}':\n") - else: - out.write("Failed to connect to '{parts.netloc}' " - "({parts.scheme}):\n") - # ConnectionError always contains an inner exception - out.write(" {exc.args[0]}\n") - elif isinstance(exc, JSONDecodeError): - errcode = E_JSON_ERROR - out.write("Server returned invalid JSON response:\n") - out.write(" {exc}\n") - else: - errcode = E_OTHER - out.write("{exc.__class__.__name__}: {exc}\n") - - msg = out.getvalue() - if not msg.endswith('\n'): - msg += '\n' - return errcode, msg.format(args=args, exc=exc, parts=parts, - unix_path=unquote(parts.netloc)) - - -def parse_args(arglist=None): - args = main_parser.parse_args(arglist) - - if args.keyfile and not args.certfile: - main_parser.error("keyfile without certfile is not supported\n") - # mutually exclusive groups don't supported nested subgroups - if args.gssapi and args.certfile: - main_parser.error("gssapi and certfile are mutually exclusive.\n") - if args.gssapi and requests_gssapi is None: - main_parser.error( - "'requests_gssapi' package is not available! You can install " - "it with: 'pip install custodia[gssapi]'.\n" - ) - - if args.debug: - args.verbose = True - - if not args.server: - instance_socket = '/var/run/custodia/{}.sock'.format(args.instance) - args.server = 'http+unix://{}'.format(url_escape(instance_socket, '')) - - if args.server.startswith('http+unix://'): - # append uds-path - if not args.server.endswith('/'): - udspath = args.uds_urlpath - if not udspath.startswith('/'): - udspath = '/' + udspath - args.server += udspath - - args.client_conn = CustodiaSimpleClient(args.server) - args.client_conn.timeout = args.timeout - if args.header is not None: - args.client_conn.headers.update(args.header) - if args.cafile: - args.client_conn.set_ca_cert(args.cafile) - # authentication - if args.certfile: - args.client_conn.set_client_cert(args.certfile, args.keyfile) - args.client_conn.headers['CUSTODIA_CERT_AUTH'] = 'true' - elif args.gssapi: - args.client_conn.set_gssapi_auth() - - return args - - -def main(): - args = parse_args() - - log.setup_logging(debug=args.debug, auditfile=None) - - try: - result = args.func(args) - except BaseException as e: - errcode, msg = error_message(args, e) - main_parser.exit(errcode, msg) - else: - if result is not None: - if isinstance(result, list): - print('\n'.join(result)) - else: - print(result) - - -if __name__ == '__main__': - main() diff --git a/ipaserver/custodia/cli/__main__.py b/ipaserver/custodia/cli/__main__.py deleted file mode 100644 index 2c4976032..000000000 --- a/ipaserver/custodia/cli/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import - -from custodia.cli import main - -if __name__ == '__main__': - main() diff --git a/ipaserver/custodia/httpd/consumer.py b/ipaserver/custodia/httpd/consumer.py deleted file mode 100644 index 60e7e93f3..000000000 --- a/ipaserver/custodia/httpd/consumer.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import - -import warnings - -from custodia.plugin import DEFAULT_CTYPE, HTTPConsumer, SUPPORTED_COMMANDS - - -__all__ = ('DEFAULT_CTYPE', 'SUPPORTED_COMMANDS', 'HTTPConsumer') - - -warnings.warn('custodia.httpd.consumer is deprecated, import from ' - 'custodia.plugin instead.', DeprecationWarning) diff --git a/ipaserver/custodia/ipa/__init__.py b/ipaserver/custodia/ipa/__init__.py deleted file mode 100644 index ef1bb9d5b..000000000 --- a/ipaserver/custodia/ipa/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file diff --git a/ipaserver/custodia/ipa/certrequest.py b/ipaserver/custodia/ipa/certrequest.py deleted file mode 100644 index 493a0ffd6..000000000 --- a/ipaserver/custodia/ipa/certrequest.py +++ /dev/null @@ -1,349 +0,0 @@ -# Copyright (C) 2017 Custodia Project Contributors - see LICENSE file -"""FreeIPA cert request store -""" -from __future__ import absolute_import - -import abc -import base64 -import datetime -import textwrap - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509 import oid - -from ipalib.errors import AuthorizationError, NotFound -from ipalib.krb_utils import krb5_format_service_principal_name - - -import six - -from custodia.plugin import CSStore, PluginOption, REQUIRED -from custodia.plugin import CSStoreDenied, CSStoreError - -from .interface import IPAInterface - - -TLS_SERVERAUTH = oid.ObjectIdentifier('2.5.29.37.1') - - -@six.add_metaclass(abc.ABCMeta) -class _CSRGenerator(object): - """Build and sign certificate signing request - """ - TEMPLATE = textwrap.dedent("""\ - Issuer: {issuer} - Subject: {subject} - Serial Number: {cert.serial_number} - Validity: - Not Before: {cert.not_valid_before} - Not After: {cert.not_valid_after} - {pem}\ - """) - - def __init__(self, plugin, backend=None): - if backend is None: - self.backend = default_backend() - else: - self.backend = backend - self.plugin = plugin - self._privkey = self._gen_private() - - def _gen_private(self): - """Generate private key - """ - return rsa.generate_private_key( - public_exponent=65537, - key_size=self.plugin.key_size, - backend=self.backend - ) - - @abc.abstractmethod - def build_csr(self, **kwargs): - """Generate a certificate signing request builder - """ - - def _sign_csr(self, builder): - return builder.sign(self._privkey, hashes.SHA256(), self.backend) - - @abc.abstractmethod - def _cert_request(self, csr_pem, **kwargs): - """Request certificate from IPA - """ - - def request_cert(self, builder, **kwargs): - """Send CSR and request certificate - """ - signed = self._sign_csr(builder) - csr_pem = signed.public_bytes(serialization.Encoding.PEM) - if not isinstance(csr_pem, six.text_type): - csr_pem = csr_pem.decode('ascii') - - response = self._cert_request(csr_pem, **kwargs) - - if self.plugin.chain: - certs = tuple( - x509.load_der_x509_certificate(cert, self.backend) - for cert in response[u'result'][u'certificate_chain'] - ) - else: - # certificate is just base64 without BEGIN/END certificate - cert = base64.b64decode(response[u'result'][u'certificate']) - certs = (x509.load_der_x509_certificate(cert, self.backend), ) - - pem = [self._dump_privkey(self._privkey)] - pem.extend(self._dump_cert(cert) for cert in certs) - return response, '\n'.join(pem) - - def _dump_cert(self, cert): - pem = cert.public_bytes(encoding=serialization.Encoding.PEM) - if six.PY3: - pem = pem.decode('ascii') - return self.TEMPLATE.format( - issuer=self._dump_x509name(cert.issuer), - subject=self._dump_x509name(cert.subject), - cert=cert, - pem=pem - ) - - def _dump_x509name(self, name): - # no quoting, just for debugging - out = [] - # pylint: disable=protected-access - for nameattr in list(name): - out.append("{}={}".format(nameattr.oid._name, nameattr.value)) - # pylint: enable=protected-access - return ', '.join(out) - - def _dump_privkey(self, privkey): - privkey = privkey.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption() - ) - if six.PY3: - privkey = privkey.decode('ascii') - return privkey - - -class _ServerCSRGenerator(_CSRGenerator): - # pylint: disable=arguments-differ - def build_csr(self, hostname, **kwargs): - realm = self.plugin.ipa.env.realm - builder = x509.CertificateSigningRequestBuilder() - builder = builder.subject_name( - x509.Name([ - x509.NameAttribute(oid.NameOID.COMMON_NAME, hostname), - x509.NameAttribute(oid.NameOID.ORGANIZATION_NAME, realm), - ]) - ) - build = builder.add_extension( - x509.BasicConstraints(ca=False, path_length=None), critical=True, - ) - build = builder.add_extension( - x509.ExtendedKeyUsage([TLS_SERVERAUTH]), critical=True - ) - builder = build.add_extension( - x509.SubjectAlternativeName([x509.DNSName(hostname)]), - critical=False - ) - return builder - - # pylint: disable=arguments-differ - def _cert_request(self, pem_req, principal, **kwargs): - # FreeIPA 4.4 has no chain option, only pass kwarg when enabled - if self.plugin.chain: - kwargs['chain'] = True - with self.plugin.ipa as ipa: - return ipa.Command.cert_request( - pem_req, - profile_id=self.plugin.cert_profile, - add=self.plugin.add_principal, - principal=principal, - **kwargs - ) - - -class IPACertRequest(CSStore): - """IPA cert request store - - The IPACertRequest store plugin generates or revokes certificates on the - fly. It uses a backing store to cache certs and private keys. - - The request ```GET /secrets/certs/HTTP/client1.ipa.example``` generates a - private key and CSR for the service ```HTTP/client1.ipa.example``` with - DNS subject alternative name ```client1.ipa.example```. - - A DELETE request removes the cert/key pair from the backing store and - revokes the cert at the same time. - """ - backing_store = PluginOption(str, REQUIRED, None) - - key_size = PluginOption(int, 2048, 'RSA key size') - cert_profile = PluginOption(str, 'caIPAserviceCert', 'IPA cert profile') - add_principal = PluginOption(bool, True, 'Add missing principal') - chain = PluginOption(bool, True, 'Return full cert chain') - allowed_services = PluginOption('str_set', {'HTTP'}, 'Service prefixes') - revocation_reason = PluginOption( - int, 4, 'Cert revocation reason (4: superseded)') - - def __init__(self, config, section=None): - super(IPACertRequest, self).__init__(config, section) - self.store_name = self.backing_store - self.store = None - self.ipa = None - if not isinstance(self.cert_profile, six.text_type): - self.cert_profile = self.cert_profile.decode('utf-8') - - def finalize_init(self, config, cfgparser, context=None): - super(IPACertRequest, self).finalize_init(config, cfgparser, context) - if self.ipa is not None: - return - self.ipa = IPAInterface.from_config(config) - self.ipa.finalize_init(config, cfgparser, context=self) - - def _parse_key(self, key): - if not isinstance(key, six.text_type): - key = key.decode('utf-8') - parts = key.split(u'/') - # XXX why is 'keys' added in in Secrets._db_key()? - if len(parts) != 3 or parts[0] != 'keys': - raise CSStoreDenied("Invalid cert request key '{}'".format(key)) - service, hostname = parts[1:3] - # pylint: disable=unsupported-membership-test - if service not in self.allowed_services: - raise CSStoreDenied("Invalid service '{}'".format(key)) - principal = krb5_format_service_principal_name( - service, hostname, self.ipa.env.realm - ) - # use cert prefix in storage key - key = u"cert/{}/{}".format(service, hostname) - return key, hostname, principal - - def get(self, key): - # check key first - key, hostname, principal = self._parse_key(key) - value = self.store.get(key) - if value is not None: - # TODO: validate certificate - self.logger.info("Found cached certificate for %s", principal) - return value - # found no cached key/cert pair, generate one - try: - data = self._request_cert(hostname, principal) - except AuthorizationError: - msg = "Unauthorized request for '{}' ({})".format( - hostname, principal - ) - self.logger.exception(msg) - raise CSStoreDenied(msg) - except NotFound: - msg = "Host '{}' or principal '{}' not found".format( - hostname, principal - ) - self.logger.exception(msg) - raise CSStoreDenied(msg) - except Exception: - msg = "Failed to request cert '{}' ({})".format( - hostname, principal - ) - self.logger.exception(msg) - raise CSStoreError(msg) - self.store.set(key, data, replace=True) - return data - - def set(self, key, value, replace=False): - key, hostname, principal = self._parse_key(key) - del hostname, principal - return self.store.set(key, value, replace) - - def span(self, key): - key, hostname, principal = self._parse_key(key) - del hostname, principal - return self.store.span(key) - - def list(self, keyfilter=''): - return self.store.list(keyfilter) - - def cut(self, key): - key, hostname, principal = self._parse_key(key) - certs = self._revoke_certs(hostname, principal) - return self.store.cut(key) or certs - - def _request_cert(self, hostname, principal): - self.logger.info("Requesting certificate for %s", hostname) - csrgen = _ServerCSRGenerator(plugin=self) - builder = csrgen.build_csr(hostname=hostname) - response, pem = csrgen.request_cert(builder, principal=principal) - self.logger.info( - "Got certificate for '%s', request id %s, serial number %s", - response[u'result'][u'subject'], - response[u'result'][u'request_id'], - response[u'result'][u'serial_number'], - ) - return pem - - def _revoke_certs(self, hostname, principal): - with self.ipa as ipa: - response = ipa.Command.cert_find( - service=principal, - validnotafter_from=datetime.datetime.utcnow(), - ) - # XXX cert_find has no filter for valid cert - certs = list( - cert for cert in response['result'] - if not cert[u'revoked'] - ) - for cert in certs: - self.logger.info( - 'Revoking cert %i (subject: %s, issuer: %s)', - cert[u'serial_number'], cert[u'subject'], - cert[u'issuer'] - ) - ipa.Command.cert_revoke( - cert[u'serial_number'], - revocation_reason=self.revocation_reason, - ) - return certs - - -def test(): - from custodia.compat import configparser - from custodia.log import setup_logging - from .interface import IPA_SECTIONNAME - from .vault import IPAVault - - parser = configparser.ConfigParser( - interpolation=configparser.ExtendedInterpolation() - ) - parser.read_string(u""" - [auth:ipa] - handler = IPAInterface - [store:ipa_vault] - handler = IPAVault - [store:ipa_certreq] - handler = IPAVault - backing_store = ipa_vault - """) - - setup_logging(debug=True, auditfile=None) - config = { - 'authenticators': { - 'ipa': IPAInterface(parser, IPA_SECTIONNAME) - } - } - vault = IPAVault(parser, 'store:ipa_vault') - vault.finalize_init(config, parser, None) - s = IPACertRequest(parser, 'store:ipa_certreq') - s.store = vault - s.finalize_init(config, parser, None) - print(s.get('keys/HTTP/client1.ipa.example')) - print(s.get('keys/HTTP/client1.ipa.example')) - print(s.cut('keys/HTTP/client1.ipa.example')) - - -if __name__ == '__main__': - test() diff --git a/ipaserver/custodia/ipa/interface.py b/ipaserver/custodia/ipa/interface.py deleted file mode 100644 index c73ef246c..000000000 --- a/ipaserver/custodia/ipa/interface.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (C) 2017 Custodia Project Contributors - see LICENSE file -"""IPA API wrapper and interface -""" -from __future__ import absolute_import - -import os -import sys - -import ipalib -import ipalib.constants -from ipalib.krb_utils import get_principal - -import six - -from custodia.plugin import HTTPAuthenticator, PluginOption - - -IPA_SECTIONNAME = 'auth:ipa' - - -class IPAInterface(HTTPAuthenticator): - """IPA interface authenticator - - Custodia uses a forking server model. We can bootstrap FreeIPA API in - the main process. Connections must be created in the client process. - """ - # Kerberos flags - krb5config = PluginOption(str, None, "Kerberos krb5.conf override") - keytab = PluginOption(str, None, "Kerberos keytab for auth") - ccache = PluginOption( - str, None, "Kerberos ccache, e,g. FILE:/path/to/ccache") - - # ipalib.api arguments - ipa_confdir = PluginOption(str, None, "IPA confdir override") - ipa_context = PluginOption(str, "cli", "IPA bootstrap context") - ipa_debug = PluginOption(bool, False, "debug mode for ipalib") - - # filled by gssapi() - principal = False - - def __init__(self, config, section=None, api=None): - super(IPAInterface, self).__init__(config, section) - # only one instance of this plugin is supported - if section != IPA_SECTIONNAME: - raise ValueError(section) - - if api is None: - self._api = ipalib.api - else: - self._api = api - - if self._api.isdone('bootstrap'): - raise RuntimeError("IPA API already initialized") - - self._ipa_config = dict( - context=self.ipa_context, - debug=self.ipa_debug, - log=None, # disable logging to file - ) - if self.ipa_confdir is not None: - self._ipa_config['confdir'] = self.ipa_confdir - - @classmethod - def from_config(cls, config): - return config['authenticators']['ipa'] - - def finalize_init(self, config, cfgparser, context=None): - super(IPAInterface, self).finalize_init(config, cfgparser, context) - - if self.principal: - # already initialized - return - - # get rundir from own section or DEFAULT - rundir = cfgparser.get(self.section, 'rundir', fallback=None) - if rundir: - self._ipa_config['dot_ipa'] = rundir - self._ipa_config['home'] = rundir - # workaround https://pagure.io/freeipa/issue/6761#comment-440329 - # monkey-patch ipalib.constants and all loaded ipa modules - ipalib.constants.USER_CACHE_PATH = rundir - for name, mod in six.iteritems(sys.modules): - if (name.startswith(('ipalib.', 'ipaclient.')) and - hasattr(mod, 'USER_CACHE_PATH')): - mod.USER_CACHE_PATH = rundir - - self._gssapi_config() - self._bootstrap() - with self: - self.logger.info("IPA server '%s': %s", - self.env.server, - self.Command.ping()[u'summary']) - - def handle(self, request): - request[IPA_SECTIONNAME] = self - return None - - # rest is interface and initialization - - def _gssapi_config(self): - # set client keytab env var for authentication - if self.keytab is not None: - os.environ['KRB5_CLIENT_KTNAME'] = self.keytab - if self.ccache is not None: - os.environ['KRB5CCNAME'] = self.ccache - if self.krb5config is not None: - os.environ['KRB5_CONFIG'] = self.krb5config - - self.principal = self._gssapi_cred() - self.logger.info(u"Kerberos principal '%s'", self.principal) - - def _gssapi_cred(self): - try: - return get_principal() - except Exception: - self.logger.exception( - "Unable to get principal from GSSAPI. Are you missing a " - "TGT or valid Kerberos keytab?" - ) - raise - - def _bootstrap(self): - # TODO: bandaid for "A PKCS #11 module returned CKR_DEVICE_ERROR" - # https://github.com/avocado-framework/avocado/issues/1112#issuecomment-206999400 - os.environ['NSS_STRICT_NOFORK'] = 'DISABLED' - self._api.bootstrap(**self._ipa_config) - self._api.finalize() - - @property - def Command(self): - return self._api.Command # pylint: disable=no-member - - @property - def env(self): - return self._api.env # pylint: disable=no-member - - def __enter__(self): - # pylint: disable=no-member - self._gssapi_cred() - if not self._api.Backend.rpcclient.isconnected(): - self._api.Backend.rpcclient.connect() - # pylint: enable=no-member - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - # pylint: disable=no-member - if self._api.Backend.rpcclient.isconnected(): - self._api.Backend.rpcclient.disconnect() - # pylint: enable=no-member - - -if __name__ == '__main__': - from custodia.compat import configparser - from custodia.log import setup_logging - - parser = configparser.ConfigParser( - interpolation=configparser.ExtendedInterpolation() - ) - parser.read_string(u""" - [auth:ipa] - handler = IPAInterface - """) - - setup_logging(debug=True, auditfile=None) - IPAInterface(parser, "auth:ipa") diff --git a/ipaserver/custodia/ipa/vault.py b/ipaserver/custodia/ipa/vault.py deleted file mode 100644 index dc95709c9..000000000 --- a/ipaserver/custodia/ipa/vault.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file -"""FreeIPA vault store (PoC) -""" -from __future__ import absolute_import - -from ipalib.errors import AuthorizationError, DuplicateEntry, NotFound - -import six - -from custodia.plugin import CSStore, PluginOption -from custodia.plugin import ( - CSStoreDenied, CSStoreError, CSStoreExists, CSStoreUnsupported -) - -from .interface import IPAInterface - - -def krb5_unparse_principal_name(name): - """Split a Kerberos principal name into parts - - Returns: - * ('host', hostname, realm) for a host principal - * (servicename, hostname, realm) for a service principal - * (None, username, realm) for a user principal - - :param text name: Kerberos principal name - :return: (service, host, realm) or (None, username, realm) - """ - prefix, realm = name.split(u'@') - if u'/' in prefix: - service, host = prefix.rsplit(u'/', 1) - return service, host, realm - else: - return None, prefix, realm - - -class IPAVault(CSStore): - # vault arguments - principal = PluginOption( - str, None, - "Service principal for service vault (auto-discovered from GSSAPI)" - ) - user = PluginOption( - str, None, - "User name for user vault (auto-discovered from GSSAPI)" - ) - vault_type = PluginOption( - str, None, - "vault type, one of 'user', 'service', 'shared', or " - "auto-discovered from GSSAPI" - ) - - def __init__(self, config, section=None, api=None): - super(IPAVault, self).__init__(config, section) - self._vault_args = None - self.ipa = None - - def finalize_init(self, config, cfgparser, context=None): - super(IPAVault, self).finalize_init(config, cfgparser, context) - - if self.ipa is not None: - return - self.ipa = IPAInterface.from_config(config) - self.ipa.finalize_init(config, cfgparser, context=self) - - # connect - with self.ipa: - # retrieve and cache KRA transport cert - response = self.ipa.Command.vaultconfig_show() - servers = response[u'result'].get(u'kra_server_server', ()) - if servers: - self.logger.info("KRA server(s) %s", ', '.join(servers)) - - service, user_host, realm = krb5_unparse_principal_name( - self.ipa.principal) - self._init_vault_args(service, user_host, realm) - - def _init_vault_args(self, service, user_host, realm): - if self.vault_type is None: - self.vault_type = 'user' if service is None else 'service' - self.logger.info("Setting vault type to '%s' from Kerberos", - self.vault_type) - - if self.vault_type == 'shared': - self._vault_args = {'shared': True} - elif self.vault_type == 'user': - if self.user is None: - if service is not None: - msg = "{!r}: User vault requires 'user' parameter" - raise ValueError(msg.format(self)) - else: - self.user = user_host - self.logger.info(u"Setting username '%s' from Kerberos", - self.user) - if six.PY2 and isinstance(self.user, str): - self.user = self.user.decode('utf-8') - self._vault_args = {'username': self.user} - elif self.vault_type == 'service': - if self.principal is None: - if service is None: - msg = "{!r}: Service vault requires 'principal' parameter" - raise ValueError(msg.format(self)) - else: - self.principal = u'/'.join((service, user_host)) - self.logger.info(u"Setting principal '%s' from Kerberos", - self.principal) - if six.PY2 and isinstance(self.principal, str): - self.principal = self.principal.decode('utf-8') - self._vault_args = {'service': self.principal} - else: - msg = '{!r}: Invalid vault type {}' - raise ValueError(msg.format(self, self.vault_type)) - - def _mangle_key(self, key): - if '__' in key: - raise ValueError - key = key.replace('/', '__') - if isinstance(key, bytes): - key = key.decode('utf-8') - return key - - def get(self, key): - key = self._mangle_key(key) - with self.ipa as ipa: - try: - result = ipa.Command.vault_retrieve( - key, **self._vault_args) - except NotFound as e: - self.logger.info("Key '%s' not found: %s", key, e) - return None - except Exception: - msg = "Failed to retrieve entry {}".format(key) - self.logger.exception(msg) - raise CSStoreError(msg) - else: - return result[u'result'][u'data'] - - def set(self, key, value, replace=False): - key = self._mangle_key(key) - if not isinstance(value, bytes): - value = value.encode('utf-8') - with self.ipa as ipa: - try: - ipa.Command.vault_add( - key, ipavaulttype=u"standard", **self._vault_args) - except DuplicateEntry as e: - self.logger.info("Vault '%s' already exists: %s", key, e) - if not replace: - raise CSStoreExists(key) - except AuthorizationError: - msg = "vault_add denied for entry {}".format(key) - self.logger.exception(msg) - raise CSStoreDenied(msg) - except Exception: - msg = "Failed to add entry {}".format(key) - self.logger.exception(msg) - raise CSStoreError(msg) - try: - ipa.Command.vault_archive( - key, data=value, **self._vault_args) - except AuthorizationError: - msg = "vault_archive denied for entry {}".format(key) - self.logger.exception(msg) - raise CSStoreDenied(msg) - except Exception: - msg = "Failed to archive entry {}".format(key) - self.logger.exception(msg) - raise CSStoreError(msg) - - def span(self, key): - raise CSStoreUnsupported("span is not implemented") - - def list(self, keyfilter=None): - with self.ipa as ipa: - try: - result = ipa.Command.vault_find( - ipavaulttype=u"standard", **self._vault_args) - except AuthorizationError: - msg = "vault_find denied" - self.logger.exception(msg) - raise CSStoreDenied(msg) - except Exception: - msg = "Failed to list entries" - self.logger.exception(msg) - raise CSStoreError(msg) - - names = [] - for entry in result[u'result']: - cn = entry[u'cn'][0] - key = cn.replace('__', '/') - if keyfilter is not None and not key.startswith(keyfilter): - continue - names.append(key.rsplit('/', 1)[-1]) - return names - - def cut(self, key): - key = self._mangle_key(key) - with self.ipa as ipa: - try: - ipa.Command.vault_del(key, **self._vault_args) - except NotFound: - return False - except AuthorizationError: - msg = "vault_del denied for entry {}".format(key) - self.logger.exception(msg) - raise CSStoreDenied(msg) - except Exception: - msg = "Failed to delete entry {}".format(key) - self.logger.exception(msg) - raise CSStoreError(msg) - else: - return True - - -def test(): - from custodia.compat import configparser - from custodia.log import setup_logging - from .interface import IPA_SECTIONNAME - - parser = configparser.ConfigParser( - interpolation=configparser.ExtendedInterpolation() - ) - parser.read_string(u""" - [auth:ipa] - handler = IPAInterface - [store:ipa_vault] - handler = IPAVault - """) - - setup_logging(debug=True, auditfile=None) - config = { - 'authenticators': { - 'ipa': IPAInterface(parser, IPA_SECTIONNAME) - } - } - v = IPAVault(parser, 'store:ipa_vault') - v.finalize_init(config, parser, None) - v.set('foo', 'bar', replace=True) - print(v.get('foo')) - print(v.list()) - v.cut('foo') - print(v.list()) - - -if __name__ == '__main__': - test() diff --git a/ipaserver/custodia/store/__init__.py b/ipaserver/custodia/store/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/ipaserver/custodia/store/encgen.py b/ipaserver/custodia/store/encgen.py deleted file mode 100644 index c46a08fc7..000000000 --- a/ipaserver/custodia/store/encgen.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import - -import os - -from jwcrypto.common import json_decode, json_encode -from jwcrypto.jwe import JWE -from jwcrypto.jwk import JWK - -from custodia.plugin import CSStore, CSStoreError -from custodia.plugin import PluginOption, REQUIRED - - -class EncryptedOverlay(CSStore): - """Encrypted overlay for storage backends - - Arguments: - backing_store (required): - name of backing storage - master_key (required) - path to master key (JWK JSON) - autogen_master_key (default: false) - auto-generate key file if missing? - master_enctype (default: A256CBC_HS512) - JWE algorithm name - secret_protection (default: 'encrypt'): - Determine the kind of protection used to save keys: - - 'encrypt': this is the classic method (backwards compatible) - - 'pinning': this adds a protected header with the key name as - add data, to prevent key swapping in the db - - 'migrate': as pinning, but on missing key information the - secret is updated instead of throwing an exception. - """ - key_sizes = { - 'A128CBC-HS256': 256, - 'A256CBC-HS512': 512, - } - - backing_store = PluginOption(str, REQUIRED, None) - master_enctype = PluginOption(str, 'A256CBC-HS512', None) - master_key = PluginOption(str, REQUIRED, None) - autogen_master_key = PluginOption(bool, False, None) - secret_protection = PluginOption(str, False, 'encrypt') - - def __init__(self, config, section): - super(EncryptedOverlay, self).__init__(config, section) - self.store_name = self.backing_store - self.store = None - self.protected_header = None - - if (not os.path.isfile(self.master_key) and - self.autogen_master_key): - # XXX https://github.com/latchset/jwcrypto/issues/50 - size = self.key_sizes.get(self.master_enctype, 512) - key = JWK(generate='oct', size=size) - with open(self.master_key, 'w') as f: - os.fchmod(f.fileno(), 0o600) - f.write(key.export()) - - with open(self.master_key) as f: - data = f.read() - key = json_decode(data) - self.mkey = JWK(**key) - - def get(self, key): - value = self.store.get(key) - if value is None: - return None - try: - jwe = JWE() - jwe.deserialize(value, self.mkey) - value = jwe.payload.decode('utf-8') - except Exception as err: - self.logger.error("Error parsing key %s: [%r]" % (key, repr(err))) - raise CSStoreError('Error occurred while trying to parse key') - if self.secret_protection == 'encrypt': - return value - if 'custodia.key' not in jwe.jose_header: - if self.secret_protection == 'migrate': - self.set(key, value, replace=True) - else: - raise CSStoreError('Secret Pinning check failed!' + - 'Missing custodia.key element') - elif jwe.jose_header['custodia.key'] != key: - raise CSStoreError( - 'Secret Pinning check failed! Expected {} got {}'.format( - key, jwe.jose_header['custodia.key'])) - return value - - def set(self, key, value, replace=False): - self.protected_header = {'alg': 'dir', 'enc': self.master_enctype} - if self.secret_protection != 'encrypt': - self.protected_header['custodia.key'] = key - protected = json_encode(self.protected_header) - jwe = JWE(value, protected) - jwe.add_recipient(self.mkey) - cvalue = jwe.serialize(compact=True) - return self.store.set(key, cvalue, replace) - - def span(self, key): - return self.store.span(key) - - def list(self, keyfilter=''): - return self.store.list(keyfilter) - - def cut(self, key): - return self.store.cut(key) diff --git a/ipaserver/custodia/store/enclite.py b/ipaserver/custodia/store/enclite.py deleted file mode 100644 index 325302e41..000000000 --- a/ipaserver/custodia/store/enclite.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import - -from jwcrypto.common import json_decode, json_encode -from jwcrypto.jwe import JWE -from jwcrypto.jwk import JWK - -from custodia.plugin import CSStoreError, PluginOption, REQUIRED -from custodia.store.sqlite import SqliteStore - - -class EncryptedStore(SqliteStore): - master_key = PluginOption(str, REQUIRED, None) - master_enctype = PluginOption(str, 'A256CBC-HS512', None) - - def __init__(self, config, section): - super(EncryptedStore, self).__init__(config, section) - with open(self.master_key) as f: - data = f.read() - key = json_decode(data) - self.mkey = JWK(**key) - - def get(self, key): - value = super(EncryptedStore, self).get(key) - if value is None: - return None - try: - jwe = JWE() - jwe.deserialize(value, self.mkey) - return jwe.payload.decode('utf-8') - except Exception: - self.logger.exception("Error parsing key %s", key) - raise CSStoreError('Error occurred while trying to parse key') - - def set(self, key, value, replace=False): - protected = json_encode({'alg': 'dir', 'enc': self.master_enctype}) - jwe = JWE(value, protected) - jwe.add_recipient(self.mkey) - cvalue = jwe.serialize(compact=True) - return super(EncryptedStore, self).set(key, cvalue, replace) diff --git a/ipaserver/custodia/store/interface.py b/ipaserver/custodia/store/interface.py deleted file mode 100644 index 508aa8ae1..000000000 --- a/ipaserver/custodia/store/interface.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import - -import warnings - -from custodia.plugin import CSStore, CSStoreError, CSStoreExists - -__all__ = ('CSStore', 'CSStoreError', 'CSStoreExists') - - -warnings.warn('custodia.store.interface is deprecated, import from ' - 'custodia.plugin instead.', DeprecationWarning,) diff --git a/ipaserver/custodia/store/sqlite.py b/ipaserver/custodia/store/sqlite.py deleted file mode 100644 index c3add933a..000000000 --- a/ipaserver/custodia/store/sqlite.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (C) 2015 Custodia Project Contributors - see LICENSE file -from __future__ import absolute_import, print_function - -import os -import sqlite3 - -from custodia.plugin import CSStore, CSStoreError, CSStoreExists -from custodia.plugin import PluginOption, REQUIRED - - -class SqliteStore(CSStore): - dburi = PluginOption(str, REQUIRED, None) - table = PluginOption(str, "CustodiaSecrets", None) - filemode = PluginOption(oct, '600', None) - - def __init__(self, config, section): - super(SqliteStore, self).__init__(config, section) - # Initialize the DB by trying to create the default table - try: - conn = sqlite3.connect(self.dburi) - os.chmod(self.dburi, self.filemode) - with conn: - c = conn.cursor() - self._create(c) - except sqlite3.Error: - self.logger.exception("Error creating table %s", self.table) - raise CSStoreError('Error occurred while trying to init db') - - def get(self, key): - self.logger.debug("Fetching key %s", key) - query = "SELECT value from %s WHERE key=?" % self.table - try: - conn = sqlite3.connect(self.dburi) - c = conn.cursor() - r = c.execute(query, (key,)) - value = r.fetchall() - except sqlite3.Error: - self.logger.exception("Error fetching key %s", key) - raise CSStoreError('Error occurred while trying to get key') - self.logger.debug("Fetched key %s got result: %r", key, value) - if len(value) > 0: - return value[0][0] - else: - return None - - def _create(self, cur): - create = "CREATE TABLE IF NOT EXISTS %s " \ - "(key PRIMARY KEY UNIQUE, value)" % self.table - cur.execute(create) - - def set(self, key, value, replace=False): - self.logger.debug("Setting key %s to value %s (replace=%s)", - key, value, replace) - if key.endswith('/'): - raise ValueError('Invalid Key name, cannot end in "/"') - if replace: - query = "INSERT OR REPLACE into %s VALUES (?, ?)" - else: - query = "INSERT into %s VALUES (?, ?)" - setdata = query % (self.table,) - try: - conn = sqlite3.connect(self.dburi) - with conn: - c = conn.cursor() - self._create(c) - c.execute(setdata, (key, value)) - except sqlite3.IntegrityError as err: - raise CSStoreExists(str(err)) - except sqlite3.Error as err: - self.logger.exception("Error storing key %s", key) - raise CSStoreError('Error occurred while trying to store key') - - def span(self, key): - name = key.rstrip('/') - self.logger.debug("Creating container %s", name) - query = "INSERT into %s VALUES (?, '')" - setdata = query % (self.table,) - try: - conn = sqlite3.connect(self.dburi) - with conn: - c = conn.cursor() - self._create(c) - c.execute(setdata, (name,)) - except sqlite3.IntegrityError as err: - raise CSStoreExists(str(err)) - except sqlite3.Error: - self.logger.exception("Error creating key %s", name) - raise CSStoreError('Error occurred while trying to span container') - - def list(self, keyfilter=''): - path = keyfilter.rstrip('/') - self.logger.debug("Listing keys matching %s", path) - child_prefix = path if path == '' else path + '/' - search = "SELECT key, value FROM %s WHERE key LIKE ?" % self.table - key = "%s%%" % (path,) - try: - conn = sqlite3.connect(self.dburi) - r = conn.execute(search, (key,)) - rows = r.fetchall() - except sqlite3.Error: - self.logger.exception("Error listing %s: [%r]", keyfilter) - raise CSStoreError('Error occurred while trying to list keys') - self.logger.debug("Searched for %s got result: %r", path, rows) - if len(rows) > 0: - parent_exists = False - result = list() - for key, value in rows: - if key == path or key == child_prefix: - parent_exists = True - continue - if not key.startswith(child_prefix): - continue - result_value = key[len(child_prefix):].lstrip('/') - if not value: - result.append(result_value + '/') - else: - result.append(result_value) - if result: - self.logger.debug("Returning sorted values %r", result) - return sorted(result) - elif parent_exists: - self.logger.debug("Returning empty list") - return [] - elif keyfilter == '': - self.logger.debug("Returning empty list") - return [] - self.logger.debug("Returning 'Not Found'") - return None - - def cut(self, key): - self.logger.debug("Removing key %s", key) - query = "DELETE from %s WHERE key=?" % self.table - try: - conn = sqlite3.connect(self.dburi) - with conn: - c = conn.cursor() - r = c.execute(query, (key,)) - except sqlite3.Error: - self.logger.error("Error removing key %s", key) - raise CSStoreError('Error occurred while trying to cut key') - self.logger.debug("Key %s %s", key, - "removed" if r.rowcount > 0 else "not found") - if r.rowcount > 0: - return True - return False