From 37f81cc566cc37a47b7d1b0d900a53273eae01ac Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Jan 28 2020 19:45:54 +0000 Subject: Add delete option to ipa-cacert-manage to remove CA certificates Before removing a CA re-verify all the other CAs to ensure that the chain is not broken. Provide a force option to handle cases where the CA is expired or verification fails for some other reason, or you really just want them gone. https://pagure.io/freeipa/issue/8124 Reviewed-By: Florence Blanc-Renaud --- diff --git a/install/tools/man/ipa-cacert-manage.1 b/install/tools/man/ipa-cacert-manage.1 index 31df3d1..b76ee18 100644 --- a/install/tools/man/ipa-cacert-manage.1 +++ b/install/tools/man/ipa-cacert-manage.1 @@ -24,6 +24,8 @@ ipa\-cacert\-manage \- Manage CA certificates in IPA .br \fBipa\-cacert\-manage\fR [\fIOPTIONS\fR...] install \fICERTFILE\fR... .br +\fBipa\-cacert\-manage\fR [\fIOPTIONS\fR...] delete \fINICKNAME\fR +.br \fBipa\-cacert\-manage\fR [\fIOPTIONS\fR...] list .SH "DESCRIPTION" \fBipa\-cacert\-manage\fR can be used to manage CA certificates in IPA. @@ -54,6 +56,16 @@ Please do not forget to run ipa-certupdate on the master, all the replicas and a .sp The supported formats for the certificate files are DER, PEM and PKCS#7 format. .RE +.TP +\fBdelete\fR +\- Remove a CA certificate +.sp +.RS +Remove a CA from IPA. The nickname of a CA to be removed can be found using the list command. The CA chain is validated before allowing a CA to be removed so leaf certificates in a chain need to be removed first. +.sp +Please do not forget to run ipa-certupdate on the master, all the replicas and all the clients after this command in order to update IPA certificates databases. +.RE +.TP \fBlist\fR \- List the stored CA certificates .sp @@ -130,6 +142,11 @@ T \- CA trusted to issue client certificates .IP p \- not trusted .RE +.SH "DELETE OPTIONS" +.TP +\fB\-f\fR, \fB\-\-force\fR +Force a CA certificate to be removed even if chain validation fails. +.RE .SH "EXIT STATUS" 0 if the command was successful diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py index 37dcc2b..6d067a1 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -30,7 +30,8 @@ from ipapython import admintool, ipautil from ipapython.certdb import (EMPTY_TRUST_FLAGS, EXTERNAL_CA_TRUST_FLAGS, TrustFlags, - parse_trust_flags) + parse_trust_flags, + get_ca_nickname) from ipapython.dn import DN from ipaplatform.paths import paths from ipalib import api, errors, x509 @@ -42,7 +43,8 @@ logger = logging.getLogger(__name__) class CACertManage(admintool.AdminTool): command_name = 'ipa-cacert-manage' - usage = "%prog renew [options]\n%prog install [options] CERTFILE" + usage = "%prog renew [options]\n%prog install [options] CERTFILE\n" \ + "%prog delete [options] NICKNAME\n%prog list" description = "Manage CA certificates." @@ -93,6 +95,12 @@ class CACertManage(admintool.AdminTool): help="Trust flags for the certificate in certutil format") parser.add_option_group(install_group) + delete_group = OptionGroup(parser, "Delete options") + delete_group.add_option( + "-f", "--force", action='store_true', + help="Force removing the CA even if chain validation fails") + parser.add_option_group(delete_group) + def validate_options(self): super(CACertManage, self).validate_options(needs_root=True) @@ -112,6 +120,9 @@ class CACertManage(admintool.AdminTool): parser.error("certificate file name not provided") elif command == 'list': pass + elif command == 'delete': + if len(self.args) < 2: + parser.error("nickname not provided") else: parser.error("unknown command \"%s\"" % command) @@ -130,6 +141,8 @@ class CACertManage(admintool.AdminTool): return self.install() elif command == 'list': return self.list() + elif command == 'delete': + return self.delete() else: raise NotImplementedError finally: @@ -466,6 +479,71 @@ class CACertManage(admintool.AdminTool): for _ca_cert, ca_nickname, _ca_trust_flags in ca_certs: print(ca_nickname) + def delete(self): + options = self.options + nickname = self.args[1] + conn = api.Backend.ldap2 + + ca_certs = certstore.get_ca_certs_nss(api.Backend.ldap2, + api.env.basedn, + api.env.realm, + False) + + ipa_ca_nickname = get_ca_nickname(api.env.realm) + + found = False + for _ca_cert, ca_nickname, _ca_trust_flags in ca_certs: + if ca_nickname == nickname: + if ca_nickname == ipa_ca_nickname: + raise admintool.ScriptError( + 'The IPA CA cannot be removed with this tool' + ) + else: + found = True + break + + if not found: + raise admintool.ScriptError( + 'Unknown CA \'{}\''.format(nickname) + ) + + with certs.NSSDatabase() as tmpdb: + tmpdb.create_db() + for ca_cert, ca_nickname, ca_trust_flags in ca_certs: + tmpdb.add_cert(ca_cert, ca_nickname, ca_trust_flags) + loaded = tmpdb.list_certs() + logger.debug("loaded raw certs '%s'", loaded) + + tmpdb.delete_cert(nickname) + + for ca_nickname, _trust_flags in loaded: + if ca_nickname == nickname: + continue + elif ipa_ca_nickname == nickname: + raise admintool.ScriptError( + "The IPA CA cannot be removed") + logger.debug("Verifying %s", ca_nickname) + try: + tmpdb.verify_ca_cert_validity(ca_nickname) + except ValueError as e: + msg = "Verifying \'%s\' failed. Removing part of the " \ + "chain? %s" % (nickname, e) + if options.force: + print(msg) + continue + raise admintool.ScriptError(msg) + else: + logger.debug("Verified %s", ca_nickname) + + for _ca_cert, ca_nickname, _ca_trust_flags in ca_certs: + if ca_nickname == nickname: + container_dn = DN(('cn', 'certificates'), ('cn', 'ipa'), + ('cn', 'etc'), api.env.basedn) + dn = DN(('cn', nickname), container_dn) + logger.debug("Deleting %s", ca_nickname) + conn.delete_entry(dn) + return + def update_ipa_ca_entry(api, cert): """