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 <cheimes@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Christian Heimes 2021-06-11 08:32:04 +02:00 committed by Rob Crittenden
parent 1e98f310f6
commit d27f01b2fb
12 changed files with 0 additions and 1458 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -1 +0,0 @@
# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file

View File

@ -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()

View File

@ -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")

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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,)

View File

@ -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