mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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 <cheimes@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -6,6 +6,7 @@ NULL =
|
||||
|
||||
SUBDIRS = \
|
||||
certmonger \
|
||||
custodia \
|
||||
html \
|
||||
migration \
|
||||
share \
|
||||
|
22
install/custodia/Makefile.am
Normal file
22
install/custodia/Makefile.am
Normal file
@@ -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
|
8
install/custodia/ipa-custodia-dmldap.in
Normal file
8
install/custodia/ipa-custodia-dmldap.in
Normal file
@@ -0,0 +1,8 @@
|
||||
@PYTHONSHEBANG@
|
||||
#
|
||||
# Copyright (C) 2019 IPA Project Contributors, see COPYING for license
|
||||
#
|
||||
|
||||
from ipaserver.secrets.handlers.dmldap import main
|
||||
|
||||
main()
|
8
install/custodia/ipa-custodia-pki-tomcat-wrapped.in
Normal file
8
install/custodia/ipa-custodia-pki-tomcat-wrapped.in
Normal file
@@ -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())
|
8
install/custodia/ipa-custodia-pki-tomcat.in
Normal file
8
install/custodia/ipa-custodia-pki-tomcat.in
Normal file
@@ -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())
|
8
install/custodia/ipa-custodia-ra-agent.in
Normal file
8
install/custodia/ipa-custodia-ra-agent.in
Normal file
@@ -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())
|
@@ -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'
|
||||
|
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 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()
|
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()
|
114
ipaserver/secrets/handlers/nsswrappedcert.py
Normal file
114
ipaserver/secrets/handlers/nsswrappedcert.py
Normal file
@@ -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()
|
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()
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -39,6 +39,7 @@ if __name__ == '__main__':
|
||||
'ipaserver.dnssec',
|
||||
'ipaserver.plugins',
|
||||
'ipaserver.secrets',
|
||||
'ipaserver.secrets.handlers',
|
||||
'ipaserver.install',
|
||||
'ipaserver.install.plugins',
|
||||
'ipaserver.install.server',
|
||||
|
Reference in New Issue
Block a user