CRL generation master: new utility to enable|disable

Implement a new command ipa-clrgen-manage to enable, disable, or check
the status of CRL generation on the localhost.
The command automates the manual steps described in the wiki
https://www.freeipa.org/page/Howto/Promote_CA_to_Renewal_and_CRL_Master

Fixes: https://pagure.io/freeipa/issue/5803
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Francois Cami <fcami@redhat.com>
This commit is contained in:
Florence Blanc-Renaud 2019-02-22 17:19:22 +01:00
parent f90a4b9554
commit 0d23fa9278
9 changed files with 450 additions and 1 deletions

View File

@ -997,6 +997,7 @@ fi
%{_sbindir}/ipa-cacert-manage
%{_sbindir}/ipa-winsync-migrate
%{_sbindir}/ipa-pkinit-manage
%{_sbindir}/ipa-crlgen-manage
%{_libexecdir}/certmonger/dogtag-ipa-ca-renew-agent-submit
%{_libexecdir}/certmonger/ipa-server-guard
%dir %{_libexecdir}/ipa
@ -1055,6 +1056,7 @@ fi
%{_mandir}/man1/ipa-cacert-manage.1*
%{_mandir}/man1/ipa-winsync-migrate.1*
%{_mandir}/man1/ipa-pkinit-manage.1*
%{_mandir}/man1/ipa-crlgen-manage.1*
%files -n python3-ipaserver

View File

@ -28,6 +28,7 @@ dist_noinst_DATA = \
ipa-cacert-manage.in \
ipa-winsync-migrate.in \
ipa-pkinit-manage.in \
ipa-crlgen-manage.in \
ipa-custodia.in \
ipa-custodia-check.in \
ipa-httpd-kdcproxy.in \
@ -58,6 +59,7 @@ nodist_sbin_SCRIPTS = \
ipa-cacert-manage \
ipa-winsync-migrate \
ipa-pkinit-manage \
ipa-crlgen-manage \
$(NULL)
appdir = $(libexecdir)/ipa/

View File

@ -0,0 +1,8 @@
@PYTHONSHEBANG@
#
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from ipaserver.install.ipa_crlgen_manage import CRLGenManage
CRLGenManage.run_cli()

View File

@ -27,6 +27,7 @@ dist_man1_MANS = \
ipa-cacert-manage.1 \
ipa-winsync-migrate.1 \
ipa-pkinit-manage.1 \
ipa-crlgen-manage.1 \
$(NULL)
dist_man8_MANS = \

View File

@ -0,0 +1,47 @@
.\"
.\" Copyright (C) 2019 FreeIPA Contributors see COPYING for license
.\"
.TH "ipa-crlgen-manage" "1" "Feb 12 2019" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
ipa\-crlgen\-manage \- Enables or disables CRL generation
.SH "SYNOPSIS"
ipa\-crlgen\-manage [options] <enable|disable|status>
.SH "DESCRIPTION"
Run the command with the \fBenable\fR option to enable CRL generation on the
local host. This requires that the IPA server is already installed and
configured, including a CA. The command will restart Dogtag and Apache.
Run the command with the \fBdisable\fR option to disable CRL generation on the
local host. The command will restart Dogtag and Apache.
Run the command with the \fBstatus\fR option to determine the current status
of CRL generation. If the local host is configured for CRL generation, the
command also prints the last CRL generation date and number.
Important: the administrator must ensure that there is only one IPA server
generating CRLs. In order to transfer the CRL generation from one server to
another, please run \fBipa-crlgen-manage disable\fR on the current CRL
generation master, followed by \fBipa-crlgen-manage enable\fR on the new
CRL generation master.
.SH "OPTIONS"
.TP
\fB\-\-version\fR
Show the program's version and exit.
.TP
\fB\-h\fR, \fB\-\-help\fR
Show the help for this program.
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Print debugging information.
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Output only errors.
.TP
\fB\-\-log\-file\fR=\fIFILE\fR
Log to the given file.
.SH "EXIT STATUS"
0 if the command was successful
1 if an error occurred
2 if the local host is not an IPA server

View File

@ -256,6 +256,10 @@ def is_ca_installed_locally():
return os.path.exists(paths.CA_CS_CFG_PATH)
class InconsistentCRLGenConfigException(Exception):
pass
class CAInstance(DogtagInstance):
"""
When using a dogtag CA the DS database contains just the
@ -278,6 +282,14 @@ class CAInstance(DogtagInstance):
'subsystemCert cert-pki-ca',
'caSigningCert cert-pki-ca')
server_cert_name = 'Server-Cert cert-pki-ca'
# The following must be aligned with the RewriteRule defined in
# install/share/ipa-pki-proxy.conf.template
crl_rewrite_pattern = r"^\s*(RewriteRule\s+\^/ipa/crl/MasterCRL.bin\s.*)$"
crl_rewrite_comment = r"^#\s*RewriteRule\s+\^/ipa/crl/MasterCRL.bin\s.*$"
crl_rewriterule = "\nRewriteRule ^/ipa/crl/MasterCRL.bin " \
"http://{}/ca/ee/ca/getCRL?" \
"op=getCRL&crlIssuingPoint=MasterCRL " \
"[L,R=301,NC]"
def __init__(self, realm=None, host_name=None, custodia=None):
super(CAInstance, self).__init__(
@ -1386,6 +1398,155 @@ class CAInstance(DogtagInstance):
'50-dogtag10-migration.update')]
)
def is_crlgen_enabled(self):
"""Check if the local CA instance is generating CRL
Three conditions must be met to consider that the local CA is CRL
generation master:
- in CS.cfg ca.crl.MasterCRL.enableCRLCache=true
- in CS.cfg ca.crl.MasterCRL.enableCRLUpdates=true
- in /etc/httpd/conf.d/ipa-pki-proxy.conf the RewriteRule
^/ipa/crl/MasterCRL.bin is disabled (commented or removed)
If the values are inconsistent, an exception is raised
:returns: True/False
:raises: InconsistentCRLGenConfigException if the config is
inconsistent
"""
try:
cache = directivesetter.get_directive(
self.config, 'ca.crl.MasterCRL.enableCRLCache', '=')
enableCRLCache = cache.lower() == 'true'
updates = directivesetter.get_directive(
self.config, 'ca.crl.MasterCRL.enableCRLUpdates', '=')
enableCRLUpdates = updates.lower() == 'true'
# If the values are different, the config is inconsistent
if enableCRLCache != enableCRLUpdates:
raise InconsistentCRLGenConfigException(
"Configuration is inconsistent, please check "
"ca.crl.MasterCRL.enableCRLCache and "
"ca.crl.MasterCRL.enableCRLUpdates in {} and "
"run ipa-crlgen-manage [enable|disable] to repair".format(
self.config))
except IOError:
raise RuntimeError(
"Unable to read {}".format(self.config))
# At this point enableCRLCache and enableCRLUpdates have the same value
try:
rewriteRuleDisabled = True
p = re.compile(self.crl_rewrite_pattern)
with open(paths.HTTPD_IPA_PKI_PROXY_CONF) as f:
for line in f.readlines():
if p.search(line):
rewriteRuleDisabled = False
break
except IOError:
raise RuntimeError(
"Unable to read {}".format(paths.HTTPD_IPA_PKI_PROXY_CONF))
# if enableCRLUpdates and rewriteRuleDisabled are different, the config
# is inconsistent
if enableCRLUpdates != rewriteRuleDisabled:
raise InconsistentCRLGenConfigException(
"Configuration is inconsistent, please check "
"ca.crl.MasterCRL.enableCRLCache in {} and the "
"RewriteRule ^/ipa/crl/MasterCRL.bin in {} and "
"run ipa-crlgen-manage [enable|disable] to repair".format(
self.config, paths.HTTPD_IPA_PKI_PROXY_CONF))
return enableCRLUpdates
def setup_crlgen(self, setup_crlgen):
"""Configure the local host for CRL generation
:param setup_crlgen: if True enable CRL generation, if False, disable
"""
try:
crlgen_enabled = self.is_crlgen_enabled()
if crlgen_enabled == setup_crlgen:
logger.info(
"Nothing to do, CRL generation already %s",
"enabled" if crlgen_enabled else "disabled")
return
except InconsistentCRLGenConfigException:
logger.warning("CRL generation is partially enabled, repairing...")
# Stop PKI
logger.info("Stopping %s", self.service_name)
self.stop_instance()
logger.debug("%s successfully stopped", self.service_name)
# Edit the CS.cfg directives
logger.info("Editing %s", self.config)
with directivesetter.DirectiveSetter(
self.config, quotes=False, separator='=') as ds:
# Convert the bool setup_crlgen to a lowercase string
str_value = str(setup_crlgen).lower()
ds.set('ca.crl.MasterCRL.enableCRLCache', str_value)
ds.set('ca.crl.MasterCRL.enableCRLUpdates', str_value)
# Start pki-tomcat
logger.info("Starting %s", self.service_name)
self.start_instance()
logger.debug("%s successfully started", self.service_name)
# Edit the RewriteRule
def comment_rewriterule():
logger.info("Editing %s", paths.HTTPD_IPA_PKI_PROXY_CONF)
# look for the pattern RewriteRule ^/ipa/crl/MasterCRL.bin ..
# and comment out
p = re.compile(self.crl_rewrite_pattern, re.MULTILINE)
with open(paths.HTTPD_IPA_PKI_PROXY_CONF) as f:
content = f.read()
new_content = p.sub(r"#\1", content)
with open(paths.HTTPD_IPA_PKI_PROXY_CONF, 'w') as f:
f.write(new_content)
def uncomment_rewriterule():
logger.info("Editing %s", paths.HTTPD_IPA_PKI_PROXY_CONF)
# check if the pattern RewriteRule ^/ipa/crl/MasterCRL.bin ..
# is already present
present = False
p = re.compile(self.crl_rewrite_pattern, re.MULTILINE)
with open(paths.HTTPD_IPA_PKI_PROXY_CONF) as f:
content = f.read()
present = p.search(content)
# Remove the comment
p_comment = re.compile(self.crl_rewrite_comment, re.MULTILINE)
new_content = p_comment.sub("", content)
# If not already present, add RewriteRule
if not present:
new_content += self.crl_rewriterule.format(api.env.host)
# Finally write the file
with open(paths.HTTPD_IPA_PKI_PROXY_CONF, 'w') as f:
f.write(new_content)
try:
if setup_crlgen:
comment_rewriterule()
else:
uncomment_rewriterule()
except IOError:
raise RuntimeError(
"Unable to access {}".format(paths.HTTPD_IPA_PKI_PROXY_CONF))
# Restart httpd
http_service = services.knownservices.httpd
logger.info("Restarting %s", http_service.service_name)
http_service.restart()
logger.debug("%s successfully restarted", http_service.service_name)
# make sure a CRL is generated if setup_crl is True
if setup_crlgen:
logger.info("Forcing CRL update")
api.Backend.ra.override_port = 8443
result = api.Backend.ra.updateCRL(wait='true')
if result.get('crlUpdate', 'Failure') == 'Success':
logger.debug("Successfully updated CRL")
api.Backend.ra.override_port = None
def __update_entry_from_cert(make_filter, make_entry, cert):
"""
@ -1466,7 +1627,6 @@ def __update_entry_from_cert(make_filter, make_entry, cert):
return True
def update_people_entry(cert):
"""
Update the userCerticate for an entry in the dogtag ou=People. This

View File

@ -0,0 +1,118 @@
#
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
from __future__ import print_function, absolute_import
import os
import logging
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from ipalib import api
from ipalib.errors import NetworkError
from ipaplatform.paths import paths
from ipapython.admintool import AdminTool
from ipaserver.install import cainstance
from ipaserver.install import installutils
logger = logging.getLogger(__name__)
class CRLGenManage(AdminTool):
command_name = "ipa-crlgen-manage"
usage = "%prog <enable|disable|status>"
description = "Manage CRL Generation Master."
def validate_options(self):
super(CRLGenManage, self).validate_options(needs_root=True)
installutils.check_server_configuration()
option_parser = self.option_parser
if not self.args:
option_parser.error("action not specified")
elif len(self.args) > 1:
option_parser.error("too many arguments")
action = self.args[0]
if action not in {'enable', 'disable', 'status'}:
option_parser.error("unrecognized action '{}'".format(action))
def run(self):
api.bootstrap(in_server=True, confdir=paths.ETC_IPA)
api.finalize()
try:
api.Backend.ldap2.connect()
except NetworkError as e:
logger.debug("Unable to connect to the local instance: %s", e)
raise RuntimeError("IPA must be running, please run ipactl start")
ca = cainstance.CAInstance(api.env.realm)
try:
action = self.args[0]
if action == 'enable':
self.enable(ca)
elif action == 'disable':
self.disable(ca)
elif action == 'status':
self.status(ca)
finally:
api.Backend.ldap2.disconnect()
return 0
def check_local_ca_instance(self, raiseOnErr=False):
if not api.Command.ca_is_enabled()['result'] or \
not cainstance.is_ca_installed_locally():
if raiseOnErr:
raise RuntimeError("Dogtag CA is not installed. "
"Please install a CA first with the "
"`ipa-ca-install` command.")
else:
logger.warning(
"Warning: Dogtag CA is not installed on this server.")
return False
return True
def enable(self, ca):
# When the local node is not a CA, raise an Exception
self.check_local_ca_instance(raiseOnErr=True)
ca.setup_crlgen(True)
logger.info("CRL generation enabled on the local host. "
"Please make sure to have only a single CRL generation "
"master.")
def disable(self, ca):
# When the local node is not a CA, nothing to do
if not self.check_local_ca_instance():
return
ca.setup_crlgen(False)
logger.info("CRL generation disabled on the local host. "
"Please make sure to configure CRL generation on another "
"master with %s enable", self.command_name)
def status(self, ca):
# When the local node is not a CA, return "disabled"
if not self.check_local_ca_instance():
print("CRL generation: disabled")
return
# Local node is a CA, check its configuration
if ca.is_crlgen_enabled():
print("CRL generation: enabled")
try:
crl_filename = os.path.join(paths.PKI_CA_PUBLISH_DIR,
'MasterCRL.bin')
with open(crl_filename, 'rb') as f:
crl = x509.load_der_x509_crl(f.read(), default_backend())
print("Last CRL update: {}".format(crl.last_update))
for ext in crl.extensions:
if ext.oid == x509.oid.ExtensionOID.CRL_NUMBER:
print("Last CRL Number: {}".format(
ext.value.crl_number))
except IOError:
logger.error("Unable to find last CRL")
else:
print("CRL generation: disabled")

View File

@ -1148,6 +1148,67 @@ def parse_unrevoke_cert_xml(doc):
return response
def parse_updateCRL_xml(doc):
'''
:param doc: The root node of the xml document to parse
:returns: result dict
:except ValueError:
After parsing the results are returned in a result dict. The following
table illustrates the mapping from the CMS data item to what may be found
in the result dict. If a CMS data item is absent it will also be absent in
the result dict.
If the requestStatus is not SUCCESS then the response dict will have the
contents described in `parse_error_template_xml`.
+-----------------+-------------+-----------------------+---------------+
|cms name |cms type |result name |result type |
+=================+=============+=======================+===============+
|crlIssuingPoint |string |crl_issuing_point |unicode |
+-----------------+-------------+-----------------------+---------------+
|crlUpdate |string |crl_update [1] |unicode |
+-----------------+-------------+-----------------------+---------------+
.. [1] crlUpdate may be one of:
- "Success"
- "Failure"
- "missingParameters"
- "testingNotEnabled"
- "testingInProgress"
- "Scheduled"
- "inProgress"
- "disabled"
- "notInitialized"
'''
request_status = get_request_status_xml(doc)
if request_status != CMS_STATUS_SUCCESS:
response = parse_error_template_xml(doc)
return response
response = {}
response['request_status'] = request_status
crl_issuing_point = doc.xpath('//xml/header/crlIssuingPoint[1]')
if len(crl_issuing_point) == 1:
crl_issuing_point = etree.tostring(
crl_issuing_point[0], method='text',
encoding=unicode).strip()
response['crl_issuing_point'] = crl_issuing_point
crl_update = doc.xpath('//xml/header/crlUpdate[1]')
if len(crl_update) == 1:
crl_update = etree.tostring(crl_update[0], method='text',
encoding=unicode).strip()
response['crl_update'] = crl_update
return response
#-------------------------------------------------------------------------------
from ipalib import Registry, errors, SkipPluginModule
@ -1923,6 +1984,47 @@ class ra(rabase.rabase, RestClient):
return results
def updateCRL(self, wait='false'):
"""
Force update of the CRL
:param wait: if true, the call will be synchronous and return only
when the CRL has been generated
"""
logger.debug('%s.updateCRL()', type(self).__name__)
# Call CMS
http_status, _http_headers, http_body = (
self._sslget('/ca/agent/ca/updateCRL',
self.override_port or self.env.ca_agent_port,
crlIssuingPoint='MasterCRL',
waitForUpdate=wait,
xml='true')
)
# Parse and handle errors
if http_status != 200:
self.raise_certificate_operation_error('updateCRL',
detail=http_status)
parse_result = self.get_parse_result_xml(http_body,
parse_updateCRL_xml)
request_status = parse_result['request_status']
if request_status != CMS_STATUS_SUCCESS:
self.raise_certificate_operation_error(
'updateCRL',
cms_request_status_to_string(request_status),
parse_result.get('error_string'))
# Return command result
cmd_result = {}
if 'crl_issuing_point' in parse_result:
cmd_result['crlIssuingPoint'] = parse_result['crl_issuing_point']
if 'crl_update' in parse_result:
cmd_result['crlUpdate'] = parse_result['crl_update']
return cmd_result
# ----------------------------------------------------------------------------
@register()

View File

@ -123,3 +123,12 @@ class rabase(Backend):
:param options: dictionary of search options
"""
raise errors.NotImplementedError(name='%s.find' % self.name)
def updateCRL(self, wait='false'):
"""
Force update of the CRL
:param wait: if true, the call will be synchronous and return only
when the CRL has been generated
"""
raise errors.NotImplementedError(name='%s.updateCRL' % self.name)