Imported Upstream version 4.8.10
This commit is contained in:
0
ipaserver/secrets/__init__.py
Normal file
0
ipaserver/secrets/__init__.py
Normal file
133
ipaserver/secrets/client.py
Normal file
133
ipaserver/secrets/client.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
|
||||
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import secrets
|
||||
from base64 import b64encode
|
||||
|
||||
|
||||
# pylint: disable=relative-import
|
||||
from custodia.message.kem import KEMClient, KEY_USAGE_SIG, KEY_USAGE_ENC
|
||||
# pylint: enable=relative-import
|
||||
from jwcrypto.common import json_decode
|
||||
from jwcrypto.jwk import JWK
|
||||
from ipalib.krb_utils import krb5_format_service_principal_name
|
||||
from ipaserver.secrets.kem import IPAKEMKeys
|
||||
from ipaserver.secrets.store import IPASecStore
|
||||
from ipaplatform.paths import paths
|
||||
import gssapi
|
||||
import requests
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ccache_env(ccache):
|
||||
"""Temporarily set KRB5CCNAME environment variable
|
||||
"""
|
||||
orig_ccache = os.environ.get('KRB5CCNAME')
|
||||
os.environ['KRB5CCNAME'] = ccache
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.environ.pop('KRB5CCNAME', None)
|
||||
if orig_ccache is not None:
|
||||
os.environ['KRB5CCNAME'] = orig_ccache
|
||||
|
||||
|
||||
class CustodiaClient:
|
||||
def __init__(self, client_service, keyfile, keytab, server, realm,
|
||||
ldap_uri=None, auth_type=None):
|
||||
if client_service.endswith(realm) or "@" not in client_service:
|
||||
raise ValueError(
|
||||
"Client service name must be a GSS name (service@host), "
|
||||
"not '{}'.".format(client_service)
|
||||
)
|
||||
self.client_service = client_service
|
||||
self.keytab = keytab
|
||||
self.server = server
|
||||
self.realm = realm
|
||||
self.ldap_uri = ldap_uri
|
||||
self.auth_type = auth_type
|
||||
self.service_name = gssapi.Name(
|
||||
'HTTP@{}'.format(server), gssapi.NameType.hostbased_service
|
||||
)
|
||||
self.keystore = IPASecStore()
|
||||
# use in-process MEMORY ccache. Handler process don't need a TGT.
|
||||
self.ccache = 'MEMORY:Custodia_{}'.format(secrets.token_hex())
|
||||
|
||||
with ccache_env(self.ccache):
|
||||
# Init creds immediately to make sure they are valid. Creds
|
||||
# can also be re-inited by _auth_header to avoid expiry.
|
||||
self.creds = self._init_creds()
|
||||
|
||||
self.ikk = IPAKEMKeys(
|
||||
{'server_keys': keyfile, 'ldap_uri': ldap_uri}
|
||||
)
|
||||
self.kemcli = KEMClient(
|
||||
self._server_keys(), self._client_keys()
|
||||
)
|
||||
|
||||
def _client_keys(self):
|
||||
return self.ikk.server_keys
|
||||
|
||||
def _server_keys(self):
|
||||
principal = krb5_format_service_principal_name(
|
||||
'host', self.server, self.realm
|
||||
)
|
||||
sk = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_SIG)))
|
||||
ek = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_ENC)))
|
||||
return sk, ek
|
||||
|
||||
def _init_creds(self):
|
||||
name = gssapi.Name(
|
||||
self.client_service, gssapi.NameType.hostbased_service
|
||||
)
|
||||
store = {
|
||||
'client_keytab': self.keytab,
|
||||
'ccache': self.ccache
|
||||
}
|
||||
return gssapi.Credentials(name=name, store=store, usage='initiate')
|
||||
|
||||
def _auth_header(self):
|
||||
if self.creds.lifetime < 300:
|
||||
self.creds = self._init_creds()
|
||||
ctx = gssapi.SecurityContext(
|
||||
name=self.service_name,
|
||||
creds=self.creds
|
||||
)
|
||||
authtok = ctx.step()
|
||||
return {'Authorization': 'Negotiate %s' % b64encode(
|
||||
authtok).decode('ascii')}
|
||||
|
||||
def fetch_key(self, keyname, store=True):
|
||||
# Prepare URL
|
||||
url = 'https://%s/ipa/keys/%s' % (self.server, keyname)
|
||||
|
||||
# Prepare signed/encrypted request
|
||||
encalg = ('RSA-OAEP', 'A256CBC-HS512')
|
||||
request = self.kemcli.make_request(keyname, encalg=encalg)
|
||||
|
||||
# Prepare Authentication header
|
||||
headers = self._auth_header()
|
||||
|
||||
# Perform request
|
||||
r = requests.get(
|
||||
url, headers=headers,
|
||||
verify=paths.IPA_CA_CRT,
|
||||
params={'type': 'kem', 'value': request}
|
||||
)
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
|
||||
if 'type' not in reply or reply['type'] != 'kem':
|
||||
raise RuntimeError('Invlid JSON response type')
|
||||
|
||||
value = self.kemcli.parse_reply(keyname, reply['value'])
|
||||
|
||||
if store:
|
||||
self.keystore.set('keys/%s' % keyname, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
return None
|
||||
47
ipaserver/secrets/common.py
Normal file
47
ipaserver/secrets/common.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
|
||||
from __future__ import print_function
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
import ldap.filter
|
||||
|
||||
from ipapython.ipaldap import ldap_initialize
|
||||
|
||||
|
||||
class iSecLdap:
|
||||
|
||||
def __init__(self, uri, auth_type=None):
|
||||
self.uri = uri
|
||||
if auth_type is not None:
|
||||
self.auth_type = auth_type
|
||||
else:
|
||||
if uri.startswith('ldapi'):
|
||||
self.auth_type = 'EXTERNAL'
|
||||
else:
|
||||
self.auth_type = 'GSSAPI'
|
||||
self._basedn = None
|
||||
|
||||
@property
|
||||
def basedn(self):
|
||||
if self._basedn is None:
|
||||
conn = self.connect()
|
||||
r = conn.search_s('', ldap.SCOPE_BASE)
|
||||
self._basedn = r[0][1]['defaultnamingcontext'][0].decode('utf-8')
|
||||
return self._basedn
|
||||
|
||||
def connect(self):
|
||||
conn = ldap_initialize(self.uri)
|
||||
if self.auth_type == 'EXTERNAL':
|
||||
auth_tokens = ldap.sasl.external(None)
|
||||
elif self.auth_type == 'GSSAPI':
|
||||
auth_tokens = ldap.sasl.sasl({}, 'GSSAPI')
|
||||
else:
|
||||
raise ValueError(
|
||||
'Invalid authentication type: %s' % self.auth_type)
|
||||
conn.sasl_interactive_bind_s('', auth_tokens)
|
||||
return conn
|
||||
|
||||
def build_filter(self, formatstr, args):
|
||||
escaped_args = dict()
|
||||
for key, value in args.items():
|
||||
escaped_args[key] = ldap.filter.escape_filter_chars(value)
|
||||
return formatstr.format(**escaped_args)
|
||||
2
ipaserver/secrets/handlers/__init__.py
Normal file
2
ipaserver/secrets/handlers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Export / import handlers
|
||||
"""
|
||||
75
ipaserver/secrets/handlers/common.py
Normal file
75
ipaserver/secrets/handlers/common.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#
|
||||
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
|
||||
#
|
||||
"""Common helpers for handlers
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
|
||||
def default_json(obj):
|
||||
"""JSON encoder default handler
|
||||
"""
|
||||
if isinstance(obj, (bytes, bytearray)):
|
||||
return base64.b64encode(obj).decode('ascii')
|
||||
raise TypeError(
|
||||
"Object of type {} is not JSON serializable".format(type(obj))
|
||||
)
|
||||
|
||||
|
||||
def json_dump(data, exportfile):
|
||||
"""Dump JSON to file
|
||||
"""
|
||||
json.dump(
|
||||
data,
|
||||
exportfile,
|
||||
default=default_json,
|
||||
separators=(',', ':'),
|
||||
sort_keys=True
|
||||
)
|
||||
|
||||
|
||||
def mkparser(supports_import=True, **kwargs):
|
||||
"""Create default parser for handler with export / import args
|
||||
|
||||
All commands support export to file or stdout. Most commands can also
|
||||
import from a file or stdin. Export and import are mutually exclusive
|
||||
options.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(**kwargs)
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'--export',
|
||||
help='JSON export file ("-" for stdout)',
|
||||
dest='exportfile',
|
||||
type=argparse.FileType('w')
|
||||
)
|
||||
if supports_import:
|
||||
group.add_argument(
|
||||
'--import',
|
||||
help='JSON import file ("-" for stdin)',
|
||||
dest='importfile',
|
||||
type=argparse.FileType('r')
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(parser, export_func, import_func=None, **kwargs):
|
||||
"""Common main function for handlers
|
||||
"""
|
||||
args = parser.parse_args()
|
||||
if args.exportfile is not None:
|
||||
func = export_func
|
||||
else:
|
||||
func = import_func
|
||||
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
func(args, tmpdir, **kwargs)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
63
ipaserver/secrets/handlers/dmldap.py
Normal file
63
ipaserver/secrets/handlers/dmldap.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
|
||||
#
|
||||
"""Export / import Directory Manager password hash
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from ipalib import api
|
||||
from ipalib import errors
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dn import DN
|
||||
from ipapython.ipaldap import LDAPClient, realm_to_ldapi_uri
|
||||
from . import common
|
||||
|
||||
CN_CONFIG = DN(('cn', 'config'))
|
||||
ROOTPW = 'nsslapd-rootpw'
|
||||
|
||||
|
||||
def export_key(args, tmpdir, conn):
|
||||
entry = conn.get_entry(CN_CONFIG, [ROOTPW])
|
||||
data = {
|
||||
'dmhash': entry.single_value[ROOTPW],
|
||||
}
|
||||
common.json_dump(data, args.exportfile)
|
||||
|
||||
|
||||
def import_key(args, tmpdir, conn):
|
||||
data = json.load(args.importfile)
|
||||
dmhash = data['dmhash'].encode('ascii')
|
||||
entry = conn.get_entry(CN_CONFIG, [ROOTPW])
|
||||
entry.single_value[ROOTPW] = dmhash
|
||||
try:
|
||||
conn.update_entry(entry)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
parser = common.mkparser(
|
||||
description='ipa-custodia LDAP DM hash handler'
|
||||
)
|
||||
|
||||
if os.getegid() != 0:
|
||||
parser.error("Must be run as root user.\n")
|
||||
|
||||
# create LDAP connection using LDAPI and EXTERNAL bind as root
|
||||
if not api.isdone('bootstrap'):
|
||||
api.bootstrap(confdir=paths.ETC_IPA, log=None)
|
||||
realm = api.env.realm
|
||||
ldap_uri = realm_to_ldapi_uri(realm)
|
||||
conn = LDAPClient(ldap_uri=ldap_uri, no_schema=True)
|
||||
try:
|
||||
conn.external_bind()
|
||||
except Exception as e:
|
||||
parser.error("Failed to connect to {}: {}\n".format(ldap_uri, e))
|
||||
|
||||
with conn:
|
||||
common.main(parser, export_key, import_key, conn=conn)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
122
ipaserver/secrets/handlers/nsscert.py
Normal file
122
ipaserver/secrets/handlers/nsscert.py
Normal file
@@ -0,0 +1,122 @@
|
||||
#
|
||||
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
|
||||
#
|
||||
"""Export / import cert and key from NSS DB as PKCS#12 data
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython import ipautil
|
||||
from ipapython.certdb import NSSDatabase
|
||||
from . import common
|
||||
|
||||
|
||||
def export_key(args, tmpdir):
|
||||
"""Export key and certificate from the NSS DB to a PKCS#12 file.
|
||||
|
||||
The PKCS#12 file is encrypted with a password.
|
||||
"""
|
||||
pk12file = os.path.join(tmpdir, 'export.p12')
|
||||
|
||||
password = ipautil.ipa_generate_password()
|
||||
pk12pk12pwfile = os.path.join(tmpdir, 'passwd')
|
||||
with open(pk12pk12pwfile, 'w') as f:
|
||||
f.write(password)
|
||||
|
||||
nssdb = NSSDatabase(args.nssdb_path)
|
||||
nssdb.run_pk12util([
|
||||
"-o", pk12file,
|
||||
"-n", args.nickname,
|
||||
"-k", args.nssdb_pwdfile,
|
||||
"-w", pk12pk12pwfile,
|
||||
])
|
||||
|
||||
with open(pk12file, 'rb') as f:
|
||||
p12data = f.read()
|
||||
|
||||
data = {
|
||||
'export password': password,
|
||||
'pkcs12 data': p12data,
|
||||
}
|
||||
common.json_dump(data, args.exportfile)
|
||||
|
||||
|
||||
def import_key(args, tmpdir):
|
||||
"""Import key and certificate from a PKCS#12 file to a NSS DB.
|
||||
"""
|
||||
data = json.load(args.importfile)
|
||||
password = data['export password']
|
||||
p12data = base64.b64decode(data['pkcs12 data'])
|
||||
|
||||
pk12pwfile = os.path.join(tmpdir, 'passwd')
|
||||
with open(pk12pwfile, 'w') as f:
|
||||
f.write(password)
|
||||
|
||||
pk12file = os.path.join(tmpdir, 'import.p12')
|
||||
with open(pk12file, 'wb') as f:
|
||||
f.write(p12data)
|
||||
|
||||
nssdb = NSSDatabase(args.nssdb_path)
|
||||
nssdb.run_pk12util([
|
||||
"-i", pk12file,
|
||||
"-n", args.nickname,
|
||||
"-k", args.nssdb_pwdfile,
|
||||
"-w", pk12pwfile,
|
||||
])
|
||||
|
||||
|
||||
def default_parser():
|
||||
"""Generic interface
|
||||
"""
|
||||
parser = common.mkparser(
|
||||
description='ipa-custodia NSS cert handler'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nssdb',
|
||||
dest='nssdb_path',
|
||||
help='path to NSS DB',
|
||||
required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pwdfile',
|
||||
dest='nssdb_pwdfile',
|
||||
help='path to password file for NSS DB',
|
||||
required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nickname',
|
||||
help='nick name of certificate',
|
||||
required=True
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def pki_tomcat_parser():
|
||||
"""Hard-code Dogtag's NSSDB and its password file
|
||||
"""
|
||||
parser = common.mkparser(
|
||||
description='ipa-custodia pki-tomcat NSS cert handler'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nickname',
|
||||
help='nick name of certificate',
|
||||
required=True
|
||||
)
|
||||
parser.set_defaults(
|
||||
nssdb_path=paths.PKI_TOMCAT_ALIAS_DIR,
|
||||
nssdb_pwdfile=paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT,
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(parser=None):
|
||||
if parser is None:
|
||||
parser = default_parser()
|
||||
|
||||
common.main(parser, export_key, import_key)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
126
ipaserver/secrets/handlers/nsswrappedcert.py
Normal file
126
ipaserver/secrets/handlers/nsswrappedcert.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#
|
||||
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
|
||||
#
|
||||
"""Export and wrap key from NSS DB
|
||||
"""
|
||||
import os
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython import ipautil
|
||||
from ipapython.certdb import NSSDatabase
|
||||
from . import common
|
||||
|
||||
|
||||
def export_key(args, tmpdir):
|
||||
"""Export key and certificate from the NSS DB
|
||||
|
||||
The private key is encrypted using key wrapping.
|
||||
"""
|
||||
wrapped_key_file = os.path.join(tmpdir, 'wrapped_key')
|
||||
certificate_file = os.path.join(tmpdir, 'certificate')
|
||||
|
||||
ipautil.run([
|
||||
paths.PKI,
|
||||
'-d', args.nssdb_path,
|
||||
'-C', args.nssdb_pwdfile,
|
||||
'ca-authority-key-export',
|
||||
'--wrap-nickname', args.wrap_nickname,
|
||||
'--target-nickname', args.nickname,
|
||||
'--algorithm', args.algorithm,
|
||||
'-o', wrapped_key_file
|
||||
])
|
||||
|
||||
nssdb = NSSDatabase(args.nssdb_path)
|
||||
nssdb.run_certutil([
|
||||
'-L',
|
||||
'-n', args.nickname,
|
||||
'-a',
|
||||
'-o', certificate_file,
|
||||
])
|
||||
with open(wrapped_key_file, 'rb') as f:
|
||||
wrapped_key = f.read()
|
||||
with open(certificate_file, 'r') as f:
|
||||
certificate = f.read()
|
||||
|
||||
data = {
|
||||
'wrapped_key': wrapped_key,
|
||||
'certificate': certificate
|
||||
}
|
||||
common.json_dump(data, args.exportfile)
|
||||
|
||||
|
||||
def default_parser():
|
||||
"""Generic interface
|
||||
"""
|
||||
parser = common.mkparser(
|
||||
supports_import=False,
|
||||
description='ipa-custodia NSS wrapped cert handler',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nssdb',
|
||||
dest='nssdb_path',
|
||||
help='path to NSS DB',
|
||||
required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pwdfile',
|
||||
dest='nssdb_pwdfile',
|
||||
help='path to password file for NSS DB',
|
||||
required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'--wrap-nickname',
|
||||
dest='wrap_nickname',
|
||||
help='nick name of wrapping key',
|
||||
required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nickname',
|
||||
dest='nickname',
|
||||
help='nick name of target key',
|
||||
required=True
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def pki_tomcat_parser():
|
||||
"""Hard-code Dogtag's NSS DB, its password file, and CA key for wrapping
|
||||
"""
|
||||
parser = common.mkparser(
|
||||
supports_import=False,
|
||||
description='ipa-custodia pki-tomcat NSS wrapped cert handler',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nickname',
|
||||
dest='nickname',
|
||||
help='nick name of target key',
|
||||
required=True
|
||||
)
|
||||
|
||||
# Caller must specify a cipher. This gets passed on to
|
||||
# the 'pki ca-authority-key-export' command (part of
|
||||
# Dogtag) via its own --algorithm option.
|
||||
parser.add_argument(
|
||||
'--algorithm',
|
||||
dest='algorithm',
|
||||
help='OID of symmetric wrap algorithm',
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.set_defaults(
|
||||
nssdb_path=paths.PKI_TOMCAT_ALIAS_DIR,
|
||||
nssdb_pwdfile=paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT,
|
||||
wrap_nickname='caSigningCert cert-pki-ca',
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(parser=None):
|
||||
if parser is None:
|
||||
parser = default_parser()
|
||||
|
||||
common.main(parser, export_key, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
118
ipaserver/secrets/handlers/pemfile.py
Normal file
118
ipaserver/secrets/handlers/pemfile.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#
|
||||
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
|
||||
#
|
||||
"""Export / import PEM cert and key file as PKCS#12 data
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython import ipautil
|
||||
from . import common
|
||||
|
||||
|
||||
def export_key(args, tmpdir):
|
||||
"""Export cert and private from PEM files as PKCS#12 file.
|
||||
|
||||
The PKCS#12 file is encrypted with a password.
|
||||
"""
|
||||
pk12file = os.path.join(tmpdir, 'export.p12')
|
||||
|
||||
password = ipautil.ipa_generate_password()
|
||||
pk12pwfile = os.path.join(tmpdir, 'passwd')
|
||||
with open(pk12pwfile, 'w') as f:
|
||||
f.write(password)
|
||||
|
||||
# OpenSSL does not support pkcs12 export of a cert without key
|
||||
ipautil.run([
|
||||
paths.OPENSSL, 'pkcs12', '-export',
|
||||
'-in', args.certfile,
|
||||
'-out', pk12file,
|
||||
'-inkey', args.keyfile,
|
||||
'-password', 'file:{pk12pwfile}'.format(pk12pwfile=pk12pwfile),
|
||||
])
|
||||
|
||||
with open(pk12file, 'rb') as f:
|
||||
p12data = f.read()
|
||||
|
||||
data = {
|
||||
'export password': password,
|
||||
'pkcs12 data': p12data,
|
||||
}
|
||||
common.json_dump(data, args.exportfile)
|
||||
|
||||
|
||||
def import_key(args, tmpdir):
|
||||
"""Export key and certificate from a PKCS#12 file to key and cert files.
|
||||
"""
|
||||
data = json.load(args.importfile)
|
||||
password = data['export password']
|
||||
p12data = base64.b64decode(data['pkcs12 data'])
|
||||
|
||||
pk12pwfile = os.path.join(tmpdir, 'passwd')
|
||||
with open(pk12pwfile, 'w') as f:
|
||||
f.write(password)
|
||||
|
||||
pk12file = os.path.join(tmpdir, 'import.p12')
|
||||
with open(pk12file, 'wb') as f:
|
||||
f.write(p12data)
|
||||
|
||||
# get the certificate from the file
|
||||
cmd = [
|
||||
paths.OPENSSL, 'pkcs12',
|
||||
'-in', pk12file,
|
||||
'-clcerts', '-nokeys',
|
||||
'-out', args.certfile,
|
||||
'-password', 'file:{pk12pwfile}'.format(pk12pwfile=pk12pwfile),
|
||||
]
|
||||
ipautil.run(cmd, umask=0o027)
|
||||
|
||||
# get the private key from the file
|
||||
cmd = [
|
||||
paths.OPENSSL, 'pkcs12',
|
||||
'-in', pk12file,
|
||||
'-nocerts', '-nodes',
|
||||
'-out', args.keyfile,
|
||||
'-password', 'file:{pk12pwfile}'.format(pk12pwfile=pk12pwfile),
|
||||
]
|
||||
ipautil.run(cmd, umask=0o027)
|
||||
|
||||
|
||||
def default_parser():
|
||||
parser = common.mkparser(
|
||||
description='ipa-custodia PEM file handler'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--certfile',
|
||||
help='path to PEM encoded cert file',
|
||||
required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'keyfile',
|
||||
help='path to PEM encoded key file',
|
||||
required=True
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def ra_agent_parser():
|
||||
parser = common.mkparser(
|
||||
description='ipa-custodia RA agent cert handler'
|
||||
)
|
||||
parser.set_defaults(
|
||||
certfile=paths.RA_AGENT_PEM,
|
||||
keyfile=paths.RA_AGENT_KEY
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(parser=None):
|
||||
if parser is None:
|
||||
parser = default_parser()
|
||||
|
||||
common.main(parser, export_key, import_key)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
289
ipaserver/secrets/kem.py
Normal file
289
ipaserver/secrets/kem.py
Normal file
@@ -0,0 +1,289 @@
|
||||
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
|
||||
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import errno
|
||||
import os
|
||||
|
||||
from configparser import ConfigParser
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dn import DN
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||
# pylint: disable=relative-import
|
||||
from custodia.message.kem import KEMKeysStore
|
||||
from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP
|
||||
# pylint: enable=relative-import
|
||||
from jwcrypto.common import json_decode, json_encode
|
||||
from jwcrypto.common import base64url_encode
|
||||
from jwcrypto.jwk import JWK
|
||||
from ipaserver.secrets.common import iSecLdap
|
||||
from binascii import unhexlify
|
||||
import ldap
|
||||
|
||||
|
||||
IPA_REL_BASE_DN = 'cn=custodia,cn=ipa,cn=etc'
|
||||
IPA_KEYS_QUERY = '(&(ipaKeyUsage={usage:s})(memberPrincipal={princ:s}))'
|
||||
IPA_CHECK_QUERY = '(cn=enc/{host:s})'
|
||||
RFC5280_USAGE_MAP = {KEY_USAGE_SIG: 'digitalSignature',
|
||||
KEY_USAGE_ENC: 'dataEncipherment'}
|
||||
|
||||
|
||||
class KEMLdap(iSecLdap):
|
||||
|
||||
@property
|
||||
def keysbase(self):
|
||||
return '%s,%s' % (IPA_REL_BASE_DN, self.basedn)
|
||||
|
||||
def _encode_int(self, i):
|
||||
I = hex(i).rstrip("L").lstrip("0x")
|
||||
return base64url_encode(unhexlify((len(I) % 2) * '0' + I))
|
||||
|
||||
def _parse_public_key(self, ipa_public_key):
|
||||
public_key = serialization.load_der_public_key(ipa_public_key,
|
||||
default_backend())
|
||||
num = public_key.public_numbers()
|
||||
if isinstance(num, rsa.RSAPublicNumbers):
|
||||
return {'kty': 'RSA',
|
||||
'e': self._encode_int(num.e),
|
||||
'n': self._encode_int(num.n)}
|
||||
elif isinstance(num, ec.EllipticCurvePublicNumbers):
|
||||
if num.curve.name == 'secp256r1':
|
||||
curve = 'P-256'
|
||||
elif num.curve.name == 'secp384r1':
|
||||
curve = 'P-384'
|
||||
elif num.curve.name == 'secp521r1':
|
||||
curve = 'P-521'
|
||||
else:
|
||||
raise TypeError('Unsupported Elliptic Curve')
|
||||
return {'kty': 'EC',
|
||||
'crv': curve,
|
||||
'x': self._encode_int(num.x),
|
||||
'y': self._encode_int(num.y)}
|
||||
else:
|
||||
raise TypeError('Unknown Public Key type')
|
||||
|
||||
def get_key(self, usage, principal):
|
||||
conn = self.connect()
|
||||
scope = ldap.SCOPE_SUBTREE
|
||||
|
||||
ldap_filter = self.build_filter(IPA_KEYS_QUERY,
|
||||
{'usage': RFC5280_USAGE_MAP[usage],
|
||||
'princ': principal})
|
||||
r = conn.search_s(self.keysbase, scope, ldap_filter)
|
||||
if len(r) != 1:
|
||||
raise ValueError("Incorrect number of results (%d) searching for "
|
||||
"public key for %s" % (len(r), principal))
|
||||
ipa_public_key = r[0][1]['ipaPublicKey'][0]
|
||||
jwk = self._parse_public_key(ipa_public_key)
|
||||
jwk['use'] = KEY_USAGE_MAP[usage]
|
||||
return json_encode(jwk)
|
||||
|
||||
def check_host_keys(self, host):
|
||||
conn = self.connect()
|
||||
scope = ldap.SCOPE_SUBTREE
|
||||
|
||||
ldap_filter = self.build_filter(IPA_CHECK_QUERY, {'host': host})
|
||||
r = conn.search_s(self.keysbase, scope, ldap_filter)
|
||||
if not r:
|
||||
raise ValueError("No public keys were found for %s" % host)
|
||||
return True
|
||||
|
||||
def _format_public_key(self, key):
|
||||
if isinstance(key, str):
|
||||
jwkey = json_decode(key)
|
||||
if 'kty' not in jwkey:
|
||||
raise ValueError('Invalid key, missing "kty" attribute')
|
||||
if jwkey['kty'] == 'RSA':
|
||||
pubnum = rsa.RSAPublicNumbers(jwkey['e'], jwkey['n'])
|
||||
pubkey = pubnum.public_key(default_backend())
|
||||
elif jwkey['kty'] == 'EC':
|
||||
if jwkey['crv'] == 'P-256':
|
||||
curve = ec.SECP256R1
|
||||
elif jwkey['crv'] == 'P-384':
|
||||
curve = ec.SECP384R1
|
||||
elif jwkey['crv'] == 'P-521':
|
||||
curve = ec.SECP521R1
|
||||
else:
|
||||
raise TypeError('Unsupported Elliptic Curve')
|
||||
pubnum = ec.EllipticCurvePublicNumbers(
|
||||
jwkey['x'], jwkey['y'], curve)
|
||||
pubkey = pubnum.public_key(default_backend())
|
||||
else:
|
||||
raise ValueError('Unknown key type: %s' % jwkey['kty'])
|
||||
elif isinstance(key, rsa.RSAPublicKey):
|
||||
pubkey = key
|
||||
elif isinstance(key, ec.EllipticCurvePublicKey):
|
||||
pubkey = key
|
||||
else:
|
||||
raise TypeError('Unknown key type: %s' % type(key))
|
||||
|
||||
return pubkey.public_bytes(
|
||||
encoding=serialization.Encoding.DER,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||
|
||||
def _get_dn(self, usage, principal):
|
||||
servicename, host = principal.split('@')[0].split('/')
|
||||
name = '%s/%s' % (KEY_USAGE_MAP[usage], host)
|
||||
service_rdn = ('cn', servicename) if servicename != 'host' else DN()
|
||||
return DN(('cn', name), service_rdn, self.keysbase)
|
||||
|
||||
def set_key(self, usage, principal, key):
|
||||
"""
|
||||
Write key for the host or service.
|
||||
|
||||
Service keys are nested one level beneath the 'cn=custodia'
|
||||
container, in the 'cn=<servicename>' container; this allows
|
||||
fine-grained control over key management permissions for
|
||||
specific services.
|
||||
|
||||
The container is assumed to exist.
|
||||
|
||||
"""
|
||||
public_key = self._format_public_key(key)
|
||||
dn = self._get_dn(usage, principal)
|
||||
conn = self.connect()
|
||||
try:
|
||||
mods = [('objectClass', [b'nsContainer',
|
||||
b'ipaKeyPolicy',
|
||||
b'ipaPublicKeyObject',
|
||||
b'groupOfPrincipals']),
|
||||
('cn', dn[0].value.encode('utf-8')),
|
||||
('ipaKeyUsage', RFC5280_USAGE_MAP[usage].encode('utf-8')),
|
||||
('memberPrincipal', principal.encode('utf-8')),
|
||||
('ipaPublicKey', public_key)]
|
||||
conn.add_s(str(dn), mods)
|
||||
except ldap.ALREADY_EXISTS:
|
||||
mods = [(ldap.MOD_REPLACE, 'ipaPublicKey', public_key)]
|
||||
conn.modify_s(str(dn), mods)
|
||||
|
||||
def del_key(self, usage, principal):
|
||||
"""Delete key for host or service
|
||||
|
||||
:returns: DN of removed key or None when key was not found
|
||||
"""
|
||||
dn = self._get_dn(usage, principal)
|
||||
conn = self.connect()
|
||||
try:
|
||||
conn.delete_s(str(dn))
|
||||
except ldap.NO_SUCH_OBJECT:
|
||||
return None
|
||||
else:
|
||||
return dn
|
||||
|
||||
|
||||
def newServerKeys(path, keyid):
|
||||
skey = JWK(generate='RSA', use='sig', kid=keyid)
|
||||
ekey = JWK(generate='RSA', use='enc', kid=keyid)
|
||||
with open(path, 'w') as f:
|
||||
os.fchmod(f.fileno(), 0o600)
|
||||
os.fchown(f.fileno(), 0, 0)
|
||||
f.write('[%s,%s]' % (skey.export(), ekey.export()))
|
||||
return [skey.get_op_key('verify'), ekey.get_op_key('encrypt')]
|
||||
|
||||
|
||||
class IPAKEMKeys(KEMKeysStore):
|
||||
"""A KEM Keys Store.
|
||||
|
||||
This is a store that holds public keys of registered
|
||||
clients allowed to use KEM messages. It takes the form
|
||||
of an authorizer merely for the purpose of attaching
|
||||
itself to a 'request' so that later on the KEM Parser
|
||||
can fetch the appropariate key to verify/decrypt an
|
||||
incoming request and make the payload available.
|
||||
|
||||
The KEM Parser will actually perform additional
|
||||
authorization checks in this case.
|
||||
|
||||
SimplePathAuthz is extended here as we want to attach the
|
||||
store only to requests on paths we are configured to
|
||||
manage.
|
||||
"""
|
||||
|
||||
def __init__(self, config=None, ipaconf=paths.IPA_DEFAULT_CONF):
|
||||
super(IPAKEMKeys, self).__init__(config)
|
||||
conf = ConfigParser()
|
||||
self.host = None
|
||||
self.realm = None
|
||||
self.ldap_uri = config.get('ldap_uri', None)
|
||||
if conf.read(ipaconf):
|
||||
self.host = conf.get('global', 'host')
|
||||
self.realm = conf.get('global', 'realm')
|
||||
if self.ldap_uri is None:
|
||||
self.ldap_uri = conf.get('global', 'ldap_uri', raw=True)
|
||||
|
||||
self._server_keys = None
|
||||
|
||||
def find_key(self, kid, usage):
|
||||
if kid is None:
|
||||
raise TypeError('Key ID is None, should be a SPN')
|
||||
conn = KEMLdap(self.ldap_uri)
|
||||
return conn.get_key(usage, kid)
|
||||
|
||||
def generate_server_keys(self):
|
||||
self.generate_keys('host')
|
||||
|
||||
def generate_keys(self, servicename):
|
||||
principal = '%s/%s@%s' % (servicename, self.host, self.realm)
|
||||
# Neutralize the key with read if any
|
||||
self._server_keys = None
|
||||
# Generate private key and store it
|
||||
pubkeys = newServerKeys(self.config['server_keys'], principal)
|
||||
# Store public key in LDAP
|
||||
ldapconn = KEMLdap(self.ldap_uri)
|
||||
ldapconn.set_key(KEY_USAGE_SIG, principal, pubkeys[0])
|
||||
ldapconn.set_key(KEY_USAGE_ENC, principal, pubkeys[1])
|
||||
|
||||
def remove_server_keys_file(self):
|
||||
"""Remove keys from disk
|
||||
|
||||
The method does not fail when the file is missing.
|
||||
"""
|
||||
try:
|
||||
os.unlink(self.config['server_keys'])
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def remove_server_keys(self):
|
||||
"""Remove keys from LDAP and disk
|
||||
"""
|
||||
self.remove_keys('host')
|
||||
|
||||
def remove_keys(self, servicename):
|
||||
"""Remove keys from LDAP and disk
|
||||
"""
|
||||
self.remove_server_keys_file()
|
||||
principal = '%s/%s@%s' % (servicename, self.host, self.realm)
|
||||
if self.ldap_uri is not None:
|
||||
ldapconn = KEMLdap(self.ldap_uri)
|
||||
ldapconn.del_key(KEY_USAGE_SIG, principal)
|
||||
ldapconn.del_key(KEY_USAGE_ENC, principal)
|
||||
|
||||
@property
|
||||
def server_keys(self):
|
||||
if self._server_keys is None:
|
||||
with open(self.config['server_keys']) as f:
|
||||
jsonkeys = f.read()
|
||||
dictkeys = json_decode(jsonkeys)
|
||||
self._server_keys = (JWK(**dictkeys[KEY_USAGE_SIG]),
|
||||
JWK(**dictkeys[KEY_USAGE_ENC]))
|
||||
return self._server_keys
|
||||
|
||||
|
||||
# Manual testing
|
||||
if __name__ == '__main__':
|
||||
IKK = IPAKEMKeys({'paths': '/',
|
||||
'server_keys': '/etc/ipa/custodia/server.keys'})
|
||||
IKK.generate_server_keys()
|
||||
print(('SIG', IKK.server_keys[0].export_public()))
|
||||
print(('ENC', IKK.server_keys[1].export_public()))
|
||||
print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm),
|
||||
usage=KEY_USAGE_SIG))
|
||||
print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm),
|
||||
usage=KEY_USAGE_ENC))
|
||||
30
ipaserver/secrets/service.py
Normal file
30
ipaserver/secrets/service.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2017 IPA Project Contributors, see COPYING for license
|
||||
import argparse
|
||||
|
||||
import custodia.server # pylint: disable=relative-import
|
||||
|
||||
|
||||
argparser = argparse.ArgumentParser(
|
||||
prog='ipa-custodia',
|
||||
description='IPA Custodia service'
|
||||
)
|
||||
argparser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Debug mode'
|
||||
)
|
||||
argparser.add_argument(
|
||||
'configfile',
|
||||
nargs='?',
|
||||
type=argparse.FileType('r'),
|
||||
help="Path to IPA's custodia server config",
|
||||
default='/etc/ipa/custodia/custodia.conf'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
return custodia.server.main(argparser)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
233
ipaserver/secrets/store.py
Normal file
233
ipaserver/secrets/store.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
|
||||
|
||||
from __future__ import print_function, absolute_import
|
||||
import os
|
||||
import sys
|
||||
|
||||
from custodia.plugin import CSStore
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipaplatform.constants import constants
|
||||
from ipapython import ipautil
|
||||
|
||||
|
||||
class UnknownKeyName(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidKeyArguments(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DBMAPHandler:
|
||||
dbtype = None
|
||||
supports_extra_args = False
|
||||
|
||||
def __init__(self, config, dbmap, nickname):
|
||||
dbtype = dbmap.get('type')
|
||||
if dbtype is None or dbtype != self.dbtype:
|
||||
raise ValueError(
|
||||
"Invalid type '{}', expected '{}'".format(
|
||||
dbtype, self.dbtype
|
||||
)
|
||||
)
|
||||
self.config = config
|
||||
self.dbmap = dbmap
|
||||
self.nickname = nickname
|
||||
|
||||
def export_key(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def import_key(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DBMAPCommandHandler(DBMAPHandler):
|
||||
def __init__(self, config, dbmap, nickname):
|
||||
super().__init__(config, dbmap, nickname)
|
||||
self.runas = dbmap.get('runas')
|
||||
self.command = os.path.join(
|
||||
paths.IPA_CUSTODIA_HANDLER,
|
||||
dbmap['command']
|
||||
)
|
||||
|
||||
def run_handler(self, extra_args=(), stdin=None):
|
||||
"""Run handler script to export / import key material
|
||||
"""
|
||||
args = [self.command]
|
||||
args.extend(extra_args)
|
||||
kwargs = dict(
|
||||
runas=self.runas,
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
if stdin:
|
||||
args.extend(['--import', '-'])
|
||||
kwargs.update(stdin=stdin)
|
||||
else:
|
||||
args.extend(['--export', '-'])
|
||||
kwargs.update(capture_output=True)
|
||||
|
||||
result = ipautil.run(args, **kwargs)
|
||||
|
||||
if stdin is None:
|
||||
return result.output
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def log_error(error):
|
||||
print(error, file=sys.stderr)
|
||||
|
||||
|
||||
class NSSWrappedCertDB(DBMAPCommandHandler):
|
||||
"""
|
||||
Store that extracts private keys from an NSSDB, wrapped with the
|
||||
private key of the primary CA.
|
||||
"""
|
||||
dbtype = 'NSSDB'
|
||||
supports_extra_args = True
|
||||
|
||||
OID_DES_EDE3_CBC = '1.2.840.113549.3.7'
|
||||
|
||||
def __init__(self, config, dbmap, nickname, *extra_args):
|
||||
super().__init__(config, dbmap, nickname)
|
||||
|
||||
# Extra args is either a single OID specifying desired wrap
|
||||
# algorithm, or empty. If empty, we must assume that the
|
||||
# client is an old version that only supports DES-EDE3-CBC.
|
||||
#
|
||||
# Using either the client's requested algorithm or the
|
||||
# default of DES-EDE3-CBC, we pass it along to the handler
|
||||
# via the --algorithm option. The handler, in turn, passes
|
||||
# it along to the 'pki ca-authority-key-export' program
|
||||
# (which is part of Dogtag).
|
||||
#
|
||||
if len(extra_args) > 1:
|
||||
raise InvalidKeyArguments("Too many arguments")
|
||||
if len(extra_args) == 1:
|
||||
self.alg = extra_args[0]
|
||||
else:
|
||||
self.alg = self.OID_DES_EDE3_CBC
|
||||
|
||||
def export_key(self):
|
||||
return self.run_handler([
|
||||
'--nickname', self.nickname,
|
||||
'--algorithm', self.alg,
|
||||
])
|
||||
|
||||
|
||||
class NSSCertDB(DBMAPCommandHandler):
|
||||
dbtype = 'NSSDB'
|
||||
|
||||
def export_key(self):
|
||||
return self.run_handler(['--nickname', self.nickname])
|
||||
|
||||
def import_key(self, value):
|
||||
return self.run_handler(
|
||||
['--nickname', self.nickname],
|
||||
stdin=value
|
||||
)
|
||||
|
||||
|
||||
# Exfiltrate the DM password Hash so it can be set in replica's and this
|
||||
# way let a replica be install without knowing the DM password and yet
|
||||
# still keep the DM password synchronized across replicas
|
||||
class DMLDAP(DBMAPCommandHandler):
|
||||
dbtype = 'DMLDAP'
|
||||
|
||||
def __init__(self, config, dbmap, nickname):
|
||||
super().__init__(config, dbmap, nickname)
|
||||
if nickname != 'DMHash':
|
||||
raise UnknownKeyName("Unknown Key Named '%s'" % nickname)
|
||||
|
||||
def export_key(self):
|
||||
return self.run_handler()
|
||||
|
||||
def import_key(self, value):
|
||||
self.run_handler(stdin=value)
|
||||
|
||||
|
||||
class PEMFileHandler(DBMAPCommandHandler):
|
||||
dbtype = 'PEM'
|
||||
|
||||
def export_key(self):
|
||||
return self.run_handler()
|
||||
|
||||
def import_key(self, value):
|
||||
return self.run_handler(stdin=value)
|
||||
|
||||
|
||||
NAME_DB_MAP = {
|
||||
'ca': {
|
||||
'type': 'NSSDB',
|
||||
'handler': NSSCertDB,
|
||||
'command': 'ipa-custodia-pki-tomcat',
|
||||
'runas': constants.PKI_USER,
|
||||
},
|
||||
'ca_wrapped': {
|
||||
'type': 'NSSDB',
|
||||
'handler': NSSWrappedCertDB,
|
||||
'command': 'ipa-custodia-pki-tomcat-wrapped',
|
||||
'runas': constants.PKI_USER,
|
||||
},
|
||||
'ra': {
|
||||
'type': 'PEM',
|
||||
'handler': PEMFileHandler,
|
||||
'command': 'ipa-custodia-ra-agent',
|
||||
'runas': None, # import needs root permission to write to directory
|
||||
},
|
||||
'dm': {
|
||||
'type': 'DMLDAP',
|
||||
'handler': DMLDAP,
|
||||
'command': 'ipa-custodia-dmldap',
|
||||
'runas': None, # root
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IPASecStore(CSStore):
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config
|
||||
|
||||
def _get_handler(self, key):
|
||||
path = key.split('/', 3)
|
||||
if len(path) < 3 or path[0] != 'keys':
|
||||
raise ValueError('Invalid name')
|
||||
if path[1] not in NAME_DB_MAP:
|
||||
raise UnknownKeyName("Unknown DB named '%s'" % path[1])
|
||||
dbmap = NAME_DB_MAP[path[1]]
|
||||
handler = dbmap['handler']
|
||||
if len(path) > 3 and not handler.supports_extra_args:
|
||||
raise InvalidKeyArguments('Handler does not support extra args')
|
||||
return handler(self.config, dbmap, path[2], *path[3:])
|
||||
|
||||
def get(self, key):
|
||||
try:
|
||||
key_handler = self._get_handler(key)
|
||||
value = key_handler.export_key()
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
log_error('Error retrieving key "%s": %s' % (key, str(e)))
|
||||
value = None
|
||||
return value
|
||||
|
||||
def set(self, key, value, replace=False):
|
||||
try:
|
||||
key_handler = self._get_handler(key)
|
||||
key_handler.import_key(value)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
log_error('Error storing key "%s": %s' % (key, str(e)))
|
||||
|
||||
def list(self, keyfilter=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def cut(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def span(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# backwards compatibility with FreeIPA 4.3 and 4.4.
|
||||
iSecStore = IPASecStore
|
||||
Reference in New Issue
Block a user