From beffa7bcdaea8404472dfd7b0c5f7224fbeb49fa Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 30 Jan 2019 14:08:38 +0100 Subject: [PATCH] Move Custodia secrets handler to scripts Implement the import and export handlers for Custodia keys as external scripts. It's a prerequisite to drop DAC override permission and proper SELinux rules for ipa-custodia. Except for DMLDAP, handlers no longer run as root but as handler specific users with reduced privileges. The Dogtag-related handlers run as pkiuser, which also help with HSM support. The export and import handles are designed to be executed by sudo, too. In the future, ipa-custodia could be executed as an unprivileged process that runs the minimal helper scripts with higher privileges. Fixes: https://pagure.io/freeipa/issue/6888 Signed-off-by: Christian Heimes Reviewed-By: Alexander Bokovoy --- .gitignore | 4 + configure.ac | 1 + freeipa.spec.in | 4 + install/Makefile.am | 1 + install/custodia/Makefile.am | 22 ++ install/custodia/ipa-custodia-dmldap.in | 8 + .../ipa-custodia-pki-tomcat-wrapped.in | 8 + install/custodia/ipa-custodia-pki-tomcat.in | 8 + install/custodia/ipa-custodia-ra-agent.in | 8 + ipaplatform/base/paths.py | 1 + ipaserver/secrets/handlers/__init__.py | 2 + ipaserver/secrets/handlers/common.py | 75 +++++ ipaserver/secrets/handlers/dmldap.py | 63 +++++ ipaserver/secrets/handlers/nsscert.py | 122 ++++++++ ipaserver/secrets/handlers/nsswrappedcert.py | 114 ++++++++ ipaserver/secrets/handlers/pemfile.py | 118 ++++++++ ipaserver/secrets/store.py | 265 ++++++------------ ipaserver/setup.py | 1 + 18 files changed, 641 insertions(+), 184 deletions(-) create mode 100644 install/custodia/Makefile.am create mode 100644 install/custodia/ipa-custodia-dmldap.in create mode 100644 install/custodia/ipa-custodia-pki-tomcat-wrapped.in create mode 100644 install/custodia/ipa-custodia-pki-tomcat.in create mode 100644 install/custodia/ipa-custodia-ra-agent.in create mode 100644 ipaserver/secrets/handlers/__init__.py create mode 100644 ipaserver/secrets/handlers/common.py create mode 100644 ipaserver/secrets/handlers/dmldap.py create mode 100644 ipaserver/secrets/handlers/nsscert.py create mode 100644 ipaserver/secrets/handlers/nsswrappedcert.py create mode 100644 ipaserver/secrets/handlers/pemfile.py diff --git a/.gitignore b/.gitignore index 775d02171..890af1749 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,10 @@ daemons/dnssec/ipa-dnskeysync-replica daemons/dnssec/ipa-ods-exporter install/certmonger/dogtag-ipa-ca-renew-agent-submit install/certmonger/ipa-server-guard +install/custodia/ipa-custodia-dmldap +install/custodia/ipa-custodia-pki-tomcat +install/custodia/ipa-custodia-pki-tomcat-wrapped +install/custodia/ipa-custodia-ra-agent install/oddjob/com.redhat.idm.trust-fetch-domains install/oddjob/etc/oddjobd.conf.d/ipa-server.conf install/oddjob/etc/oddjobd.conf.d/oddjobd-ipa-trust.conf diff --git a/configure.ac b/configure.ac index 7ef0f560c..f060f3a52 100644 --- a/configure.ac +++ b/configure.ac @@ -538,6 +538,7 @@ AC_CONFIG_FILES([ init/Makefile install/Makefile install/certmonger/Makefile + install/custodia/Makefile install/html/Makefile install/migration/Makefile install/share/Makefile diff --git a/freeipa.spec.in b/freeipa.spec.in index b8594b47a..40f7353d5 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -1003,6 +1003,10 @@ fi %{_sbindir}/ipa-crlgen-manage %{_libexecdir}/certmonger/dogtag-ipa-ca-renew-agent-submit %{_libexecdir}/certmonger/ipa-server-guard +%{_libexecdir}/ipa/custodia/ipa-custodia-dmldap +%{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat +%{_libexecdir}/ipa/custodia/ipa-custodia-pki-tomcat-wrapped +%{_libexecdir}/ipa/custodia/ipa-custodia-ra-agent %dir %{_libexecdir}/ipa %{_libexecdir}/ipa/ipa-custodia %{_libexecdir}/ipa/ipa-custodia-check diff --git a/install/Makefile.am b/install/Makefile.am index caff1be57..4e5f783e8 100644 --- a/install/Makefile.am +++ b/install/Makefile.am @@ -6,6 +6,7 @@ NULL = SUBDIRS = \ certmonger \ + custodia \ html \ migration \ share \ diff --git a/install/custodia/Makefile.am b/install/custodia/Makefile.am new file mode 100644 index 000000000..ad403cc77 --- /dev/null +++ b/install/custodia/Makefile.am @@ -0,0 +1,22 @@ +NULL = + +appdir = $(libexecdir)/ipa/custodia/ +nodist_app_SCRIPTS = \ + ipa-custodia-dmldap \ + ipa-custodia-pki-tomcat \ + ipa-custodia-pki-tomcat-wrapped \ + ipa-custodia-ra-agent \ + $(NULL) + +dist_noinst_DATA = \ + ipa-custodia-dmldap.in \ + ipa-custodia-pki-tomcat.in \ + ipa-custodia-pki-tomcat-wrapped.in \ + ipa-custodia-ra-agent.in \ + $(NULL) + +PYTHON_SHEBANG = $(nodist_app_SCRIPTS) + +CLEANFILES = $(PYTHON_SHEBANG) + +include $(top_srcdir)/Makefile.pythonscripts.am \ No newline at end of file diff --git a/install/custodia/ipa-custodia-dmldap.in b/install/custodia/ipa-custodia-dmldap.in new file mode 100644 index 000000000..8cf2411a8 --- /dev/null +++ b/install/custodia/ipa-custodia-dmldap.in @@ -0,0 +1,8 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2019 IPA Project Contributors, see COPYING for license +# + +from ipaserver.secrets.handlers.dmldap import main + +main() diff --git a/install/custodia/ipa-custodia-pki-tomcat-wrapped.in b/install/custodia/ipa-custodia-pki-tomcat-wrapped.in new file mode 100644 index 000000000..26abac471 --- /dev/null +++ b/install/custodia/ipa-custodia-pki-tomcat-wrapped.in @@ -0,0 +1,8 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2019 IPA Project Contributors, see COPYING for license +# + +from ipaserver.secrets.handlers.nsswrappedcert import main, pki_tomcat_parser + +main(pki_tomcat_parser()) diff --git a/install/custodia/ipa-custodia-pki-tomcat.in b/install/custodia/ipa-custodia-pki-tomcat.in new file mode 100644 index 000000000..5bf51c81a --- /dev/null +++ b/install/custodia/ipa-custodia-pki-tomcat.in @@ -0,0 +1,8 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2019 IPA Project Contributors, see COPYING for license +# + +from ipaserver.secrets.handlers.nsscert import main, pki_tomcat_parser + +main(pki_tomcat_parser()) diff --git a/install/custodia/ipa-custodia-ra-agent.in b/install/custodia/ipa-custodia-ra-agent.in new file mode 100644 index 000000000..8cade0260 --- /dev/null +++ b/install/custodia/ipa-custodia-ra-agent.in @@ -0,0 +1,8 @@ +@PYTHONSHEBANG@ +# +# Copyright (C) 2019 IPA Project Contributors, see COPYING for license +# + +from ipaserver.secrets.handlers.pemfile import main, ra_agent_parser + +main(ra_agent_parser()) diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 619b5fc89..3506557b1 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -362,6 +362,7 @@ class BasePathNamespace: IPA_CUSTODIA_KEYS = '/etc/ipa/custodia/server.keys' IPA_CUSTODIA_SOCKET = '/run/httpd/ipa-custodia.sock' IPA_CUSTODIA_AUDIT_LOG = '/var/log/ipa-custodia.audit.log' + IPA_CUSTODIA_HANDLER = "/usr/libexec/ipa/custodia" IPA_GETKEYTAB = '/usr/sbin/ipa-getkeytab' EXTERNAL_SCHEMA_DIR = '/usr/share/ipa/schema.d' GSSPROXY_CONF = '/etc/gssproxy/10-ipa.conf' diff --git a/ipaserver/secrets/handlers/__init__.py b/ipaserver/secrets/handlers/__init__.py new file mode 100644 index 000000000..7300be078 --- /dev/null +++ b/ipaserver/secrets/handlers/__init__.py @@ -0,0 +1,2 @@ +"""Export / import handlers +""" diff --git a/ipaserver/secrets/handlers/common.py b/ipaserver/secrets/handlers/common.py new file mode 100644 index 000000000..a700e56cc --- /dev/null +++ b/ipaserver/secrets/handlers/common.py @@ -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) diff --git a/ipaserver/secrets/handlers/dmldap.py b/ipaserver/secrets/handlers/dmldap.py new file mode 100644 index 000000000..876440f16 --- /dev/null +++ b/ipaserver/secrets/handlers/dmldap.py @@ -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 ipapython.dn import DN +from ipapython.ipaldap import LDAPClient +from ipaserver.install.installutils import 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() + 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() diff --git a/ipaserver/secrets/handlers/nsscert.py b/ipaserver/secrets/handlers/nsscert.py new file mode 100644 index 000000000..e72927ace --- /dev/null +++ b/ipaserver/secrets/handlers/nsscert.py @@ -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() diff --git a/ipaserver/secrets/handlers/nsswrappedcert.py b/ipaserver/secrets/handlers/nsswrappedcert.py new file mode 100644 index 000000000..6b7b142f9 --- /dev/null +++ b/ipaserver/secrets/handlers/nsswrappedcert.py @@ -0,0 +1,114 @@ +# +# 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, + '-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 + ) + 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() diff --git a/ipaserver/secrets/handlers/pemfile.py b/ipaserver/secrets/handlers/pemfile.py new file mode 100644 index 000000000..4e8eff0e3 --- /dev/null +++ b/ipaserver/secrets/handlers/pemfile.py @@ -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() diff --git a/ipaserver/secrets/store.py b/ipaserver/secrets/store.py index 8736a8b47..b40008c18 100644 --- a/ipaserver/secrets/store.py +++ b/ipaserver/secrets/store.py @@ -1,18 +1,14 @@ # Copyright (C) 2015 IPA Project Contributors, see COPYING for license from __future__ import print_function, absolute_import -from base64 import b64encode, b64decode -from custodia.store.interface import CSStore # pylint: disable=relative-import -from jwcrypto.common import json_decode, json_encode -from ipaplatform.paths import paths -from ipapython import ipautil -from ipapython.certdb import NSSDatabase -from ipaserver.secrets.common import iSecLdap -import ldap import os -import shutil import sys -import tempfile + +from custodia.plugin import CSStore + +from ipaplatform.paths import paths +from ipaplatform.constants import constants +from ipapython import ipautil class UnknownKeyName(Exception): @@ -20,9 +16,19 @@ class UnknownKeyName(Exception): class DBMAPHandler: + dbtype = None def __init__(self, config, dbmap, nickname): - raise NotImplementedError + 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 @@ -31,229 +37,120 @@ class DBMAPHandler: 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(DBMAPHandler): - ''' +class NSSWrappedCertDB(DBMAPCommandHandler): + """ Store that extracts private keys from an NSSDB, wrapped with the private key of the primary CA. - ''' - - def __init__(self, config, dbmap, nickname): - if 'path' not in dbmap: - raise ValueError( - 'Configuration does not provide NSSDB path') - if 'pwdfile' not in dbmap: - raise ValueError('Configuration does not provide password file') - if 'wrap_nick' not in dbmap: - raise ValueError( - 'Configuration does not provide nickname of wrapping key') - self.nssdb_path = dbmap['path'] - self.nssdb_pwdfile = dbmap['pwdfile'] - self.wrap_nick = dbmap['wrap_nick'] - self.target_nick = nickname + """ + dbtype = 'NSSDB' def export_key(self): - tdir = tempfile.mkdtemp(dir=paths.TMP) - try: - wrapped_key_file = os.path.join(tdir, 'wrapped_key') - certificate_file = os.path.join(tdir, 'certificate') - ipautil.run([ - paths.PKI, '-d', self.nssdb_path, '-C', self.nssdb_pwdfile, - 'ca-authority-key-export', - '--wrap-nickname', self.wrap_nick, - '--target-nickname', self.target_nick, - '-o', wrapped_key_file]) - nssdb = NSSDatabase(self.nssdb_path) - nssdb.run_certutil([ - '-L', '-n', self.target_nick, - '-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() - finally: - shutil.rmtree(tdir) - return json_encode({ - 'wrapped_key': b64encode(wrapped_key).decode('ascii'), - 'certificate': certificate}) + return self.run_handler(['--nickname', self.nickname]) -class NSSCertDB(DBMAPHandler): - - def __init__(self, config, dbmap, nickname): - if 'type' not in dbmap or dbmap['type'] != 'NSSDB': - raise ValueError('Invalid type "%s",' - ' expected "NSSDB"' % (dbmap['type'],)) - if 'path' not in dbmap: - raise ValueError('Configuration does not provide NSSDB path') - if 'pwdfile' not in dbmap: - raise ValueError('Configuration does not provide password file') - self.nssdb_path = dbmap['path'] - self.nssdb_pwdfile = dbmap['pwdfile'] - self.nickname = nickname +class NSSCertDB(DBMAPCommandHandler): + dbtype = 'NSSDB' def export_key(self): - tdir = tempfile.mkdtemp(dir=paths.TMP) - try: - pk12pwfile = os.path.join(tdir, 'pk12pwfile') - password = ipautil.ipa_generate_password() - with open(pk12pwfile, 'w') as f: - f.write(password) - pk12file = os.path.join(tdir, 'pk12file') - nssdb = NSSDatabase(self.nssdb_path) - nssdb.run_pk12util([ - "-o", pk12file, - "-n", self.nickname, - "-k", self.nssdb_pwdfile, - "-w", pk12pwfile, - ]) - with open(pk12file, 'rb') as f: - data = f.read() - finally: - shutil.rmtree(tdir) - return json_encode({'export password': password, - 'pkcs12 data': b64encode(data).decode('ascii')}) + return self.run_handler(['--nickname', self.nickname]) def import_key(self, value): - v = json_decode(value) - tdir = tempfile.mkdtemp(dir=paths.TMP) - try: - pk12pwfile = os.path.join(tdir, 'pk12pwfile') - with open(pk12pwfile, 'w') as f: - f.write(v['export password']) - pk12file = os.path.join(tdir, 'pk12file') - with open(pk12file, 'wb') as f: - f.write(b64decode(v['pkcs12 data'])) - nssdb = NSSDatabase(self.nssdb_path) - nssdb.run_pk12util([ - "-i", pk12file, - "-n", self.nickname, - "-k", self.nssdb_pwdfile, - "-w", pk12pwfile, - ]) - finally: - shutil.rmtree(tdir) + 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(DBMAPHandler): +class DMLDAP(DBMAPCommandHandler): + dbtype = 'DMLDAP' def __init__(self, config, dbmap, nickname): - if 'type' not in dbmap or dbmap['type'] != 'DMLDAP': - raise ValueError('Invalid type "%s",' - ' expected "DMLDAP"' % (dbmap['type'],)) + super().__init__(config, dbmap, nickname) if nickname != 'DMHash': raise UnknownKeyName("Unknown Key Named '%s'" % nickname) - self.ldap = iSecLdap(config['ldap_uri'], - config.get('auth_type', None)) def export_key(self): - conn = self.ldap.connect() - r = conn.search_s('cn=config', ldap.SCOPE_BASE, - attrlist=['nsslapd-rootpw']) - if len(r) != 1: - raise RuntimeError('DM Hash not found!') - rootpw = r[0][1]['nsslapd-rootpw'][0] - return json_encode({'dmhash': rootpw.decode('ascii')}) + return self.run_handler() def import_key(self, value): - v = json_decode(value) - rootpw = v['dmhash'].encode('ascii') - conn = self.ldap.connect() - mods = [(ldap.MOD_REPLACE, 'nsslapd-rootpw', rootpw)] - conn.modify_s('cn=config', mods) + self.run_handler(stdin=value) -class PEMFileHandler(DBMAPHandler): - def __init__(self, config, dbmap, nickname=None): - if 'type' not in dbmap or dbmap['type'] != 'PEM': - raise ValueError('Invalid type "{t}", expected PEM' - .format(t=dbmap['type'])) - self.certfile = dbmap['certfile'] - self.keyfile = dbmap.get('keyfile') +class PEMFileHandler(DBMAPCommandHandler): + dbtype = 'PEM' def export_key(self): - _fd, tmpfile = tempfile.mkstemp(dir=paths.TMP) - password = ipautil.ipa_generate_password() - args = [ - paths.OPENSSL, - "pkcs12", "-export", - "-in", self.certfile, - "-out", tmpfile, - "-password", "pass:{pwd}".format(pwd=password) - ] - if self.keyfile is not None: - args.extend(["-inkey", self.keyfile]) - - try: - ipautil.run(args, nolog=(password, )) - with open(tmpfile, 'rb') as f: - data = f.read() - finally: - os.remove(tmpfile) - return json_encode({'export password': password, - 'pkcs12 data': b64encode(data).decode('ascii')}) + return self.run_handler() def import_key(self, value): - v = json_decode(value) - data = b64decode(v['pkcs12 data']) - password = v['export password'] - fd, tmpdata = tempfile.mkstemp(dir=paths.TMP) - os.close(fd) - try: - with open(tmpdata, 'wb') as f: - f.write(data) - - # get the certificate from the file - ipautil.run([paths.OPENSSL, - "pkcs12", - "-in", tmpdata, - "-clcerts", "-nokeys", - "-out", self.certfile, - "-passin", "pass:{pwd}".format(pwd=password)], - nolog=(password, )) - - if self.keyfile is not None: - # get the private key from the file - ipautil.run([paths.OPENSSL, - "pkcs12", - "-in", tmpdata, - "-nocerts", "-nodes", - "-out", self.keyfile, - "-passin", "pass:{pwd}".format(pwd=password)], - nolog=(password, )) - finally: - os.remove(tmpdata) + return self.run_handler(stdin=value) NAME_DB_MAP = { 'ca': { 'type': 'NSSDB', - 'path': paths.PKI_TOMCAT_ALIAS_DIR, 'handler': NSSCertDB, - 'pwdfile': paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT, + 'command': 'ipa-custodia-pki-tomcat', + 'runas': constants.PKI_USER, }, 'ca_wrapped': { + 'type': 'NSSDB', 'handler': NSSWrappedCertDB, - 'path': paths.PKI_TOMCAT_ALIAS_DIR, - 'pwdfile': paths.PKI_TOMCAT_ALIAS_PWDFILE_TXT, - 'wrap_nick': 'caSigningCert cert-pki-ca', + 'command': 'ipa-custodia-pki-tomcat-wrapped', + 'runas': constants.PKI_USER, }, 'ra': { 'type': 'PEM', 'handler': PEMFileHandler, - 'certfile': paths.RA_AGENT_PEM, - 'keyfile': paths.RA_AGENT_KEY, + '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 } } diff --git a/ipaserver/setup.py b/ipaserver/setup.py index a6976e75a..5d4bf0895 100644 --- a/ipaserver/setup.py +++ b/ipaserver/setup.py @@ -39,6 +39,7 @@ if __name__ == '__main__': 'ipaserver.dnssec', 'ipaserver.plugins', 'ipaserver.secrets', + 'ipaserver.secrets.handlers', 'ipaserver.install', 'ipaserver.install.plugins', 'ipaserver.install.server',