mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
parent
1e98f310f6
commit
d27f01b2fb
@ -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()
|
@ -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()
|
@ -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)
|
@ -1 +0,0 @@
|
||||
# Copyright (C) 2016 Custodia Project Contributors - see LICENSE 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()
|
@ -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")
|
@ -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()
|
@ -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)
|
@ -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)
|
@ -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,)
|
@ -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
|
Loading…
Reference in New Issue
Block a user