Hi,
the attached patches implement automatic CA certificate renewal as well
as the initial version of the CA certificate management tool.
Requires my patches 172-196.
In order to test, you must install current git version of certmonger
(see <https://fedorahosted.org/certmonger/ticket/26>) and set SELinux to
permissive (see <https://bugzilla.redhat.com/show_bug.cgi?id=1078783>).
Make sure you install certmonger before running
ipa-server-install/ipa-replica-install. On F20 you can use RPMs located
at <http://jcholast.fedorapeople.org/certmonger-git/>.
To test automatic renewal, move system time forward (see
<https://fedorahosted.org/freeipa/ticket/2803#comment:17> for more info
about certificate renewal testing, nickname of the CA certificate is
"caSigningCert cert-pki-ca"). In CA-full installs the renewal should be
fully automatic, in CA-less installs you should be alerted via syslog to
renew the certificate using ipa-cacert-manage.
To test manual renewal, run "ipa-cacert-manage renew". You can run it on
any CA master. To make the renewed certificate available on other CA
masters, you must run "getcert resubmit -d /etc/pki/pki-tomcat/alias -n
'caSigningCert cert-pki-ca'" on each of them. Note that currently you
can't change the chaining of the CA certificate.
Honza
--
Jan Cholasta
>From 3b3c5b99c1005a049436dc262cf8258daf7486c3 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 12 Mar 2014 11:41:02 +0100
Subject: [PATCH 01/13] Add function for checking if certificate is self-signed
to ipalib.x509.
---
ipalib/x509.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/ipalib/x509.py b/ipalib/x509.py
index ca6eac5..8d049bd 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -164,6 +164,12 @@ def get_serial_number(certificate, datatype=PEM, dbdir=None):
del(nsscert)
return serial_number
+def is_self_signed(certificate, datatype=PEM, dbdir=None):
+ nsscert = load_certificate(certificate, datatype, dbdir)
+ self_signed = (nsscert.issuer == nsscert.subject)
+ del nsscert
+ return self_signed
+
def make_pem(data):
"""
Convert a raw base64-encoded blob into something that looks like a PE
--
1.8.5.3
>From 47635bbc42b36928d57ea48cfb6e40b621e6082a Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Tue, 18 Feb 2014 18:14:47 +0100
Subject: [PATCH 02/13] Support CA certificate renewal in
dogtag-ipa-ca-renew-agent.
---
.../certmonger/dogtag-ipa-ca-renew-agent-submit | 49 ++++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 57eb4e5..8a1bd83 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -270,11 +270,60 @@ def export_csr():
return (ISSUED, cert)
+def renew_ca_cert():
+ """
+ This is used for automatic CA certificate renewal.
+ """
+ cert = os.environ.get('CERTMONGER_CERTIFICATE')
+ if not cert:
+ return (REJECTED, "New certificate requests not supported")
+
+ operation = os.environ.get('CERTMONGER_OPERATION')
+ if operation == 'SUBMIT':
+ result = retrieve_cert()
+ if result[0] == ISSUED:
+ new_cert = x509.normalize_certificate(result[1])
+ old_cert = x509.normalize_certificate(cert)
+ if new_cert != old_cert:
+ return result
+
+ state = 'retrieve'
+ if x509.is_self_signed(cert):
+ ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
+ if ca.is_renewal_master():
+ state = 'request'
+ elif operation == 'POLL':
+ cookie = os.environ.get('CERTMONGER_CA_COOKIE')
+ if not cookie:
+ return (UNCONFIGURED, "Cookie not provided")
+
+ state, sep, cookie = cookie.partition(':')
+ if state not in ('retrieve', 'request'):
+ return (UNCONFIGURED, "Invalid cookie")
+
+ os.environ['CERTMONGER_CA_COOKIE'] = cookie
+ else:
+ return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
+
+ if state == 'retrieve':
+ result = retrieve_cert()
+ elif state == 'request':
+ os.environ['CERTMONGER_CA_PROFILE'] = 'caCACert'
+ result = request_and_store_cert()
+
+ if result[0] == WAIT:
+ return (result[0], '%s:%s' % (state, result[1]))
+ elif result[0] == WAIT_WITH_DELAY:
+ return (result[0], result[1], '%s:%s' % (state, result[2]))
+ else:
+ return result
+
def main():
handlers = {
'ipaStorage': store_cert,
'ipaRetrieval': retrieve_cert,
'ipaCSRExport': export_csr,
+ 'ipaCACertRenewal': renew_ca_cert,
}
api.bootstrap(context='renew')
--
1.8.5.3
>From 17c951b69870224adde95b8c919c7556a5890dcb Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Tue, 18 Feb 2014 18:15:49 +0100
Subject: [PATCH 03/13] Allow IPA master hosts to update CA certificate in
LDAP.
---
install/updates/40-delegation.update | 3 +++
1 file changed, 3 insertions(+)
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
index 3fabdf9..3f1ff16 100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -374,6 +374,9 @@ add: member: 'cn=Host Administrators,cn=privileges,cn=pbac,$SUFFIX'
dn: cn=Revoke Certificate,cn=permissions,cn=pbac,$SUFFIX
add: member: 'cn=Host Administrators,cn=privileges,cn=pbac,$SUFFIX'
+dn: cn=ipa,cn=etc,$SUFFIX
+add:aci:'(target = "ldap:///cn=CAcert,cn=ipa,cn=etc,$SUFFIX")(targetattr = cACertificate)(version 3.0; acl "Modify compatibility CA certificate"; allow (write) userdn = "ldap:///fqdn=$FQDN,cn=computers,cn=accounts,$SUFFIX";)'
+
# Automember tasks
dn: cn=Automember Task Administrator,cn=privileges,cn=pbac,$SUFFIX
default:objectClass: nestedgroup
--
1.8.5.3
>From 5238d0260bb853bd630cd3bcc5ff40522307dd52 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 12 Mar 2014 11:32:59 +0100
Subject: [PATCH 04/13] Automatically update CA certificate in LDAP on renewal.
---
install/restart_scripts/renew_ca_cert | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/install/restart_scripts/renew_ca_cert b/install/restart_scripts/renew_ca_cert
index 2663887..e08c5d3 100644
--- a/install/restart_scripts/renew_ca_cert
+++ b/install/restart_scripts/renew_ca_cert
@@ -29,6 +29,7 @@ import traceback
from ipapython import dogtag, certmonger, ipautil
from ipapython import services as ipaservices
+from ipapython.dn import DN
from ipalib import api, errors, x509, util
from ipaserver.install import certs, cainstance, installutils
from ipaserver.plugins.ldap2 import ldap2
@@ -88,6 +89,32 @@ def main():
syslog.LOG_ERR,
"Updating trust on certificate %s failed in %s" %
(nickname, db.secdir))
+ elif nickname == 'caSigningCert cert-pki-ca' and ca.is_renewal_master():
+ # Update CA certificate in LDAP
+ tmpdir = tempfile.mkdtemp(prefix="tmp-")
+ try:
+ principal = str('host/%s@%s' % (api.env.host, api.env.realm))
+ ccache = ipautil.kinit_hostprincipal('/etc/krb5.keytab', tmpdir,
+ principal)
+
+ conn = ldap2(shared_instance=False, ldap_uri=api.env.ldap_uri)
+ conn.connect(ccache=ccache)
+
+ dn = DN(('cn', 'CAcert'), ('cn', 'ipa'), ('cn', 'etc'),
+ api.env.basedn)
+ try:
+ entry = conn.get_entry(dn, attrs_list=['cACertificate;binary'])
+ entry['cACertificate;binary'] = [cert]
+ conn.update_entry(entry)
+ except errors.EmptyModlist:
+ pass
+
+ conn.disconnect()
+ except Exception, e:
+ syslog.syslog(
+ syslog.LOG_ERR, "Updating CA certificate failed: %s" % e)
+ finally:
+ shutil.rmtree(tmpdir)
# Now we can start the CA. Using the ipaservices start should fire
# off the servlet to verify that the CA is actually up and responding so
--
1.8.5.3
>From 626517f155eacf93de6665f3221accc17f4c5a07 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 12 Mar 2014 11:33:18 +0100
Subject: [PATCH 05/13] Track CA certificate using dogtag-ipa-ca-renew-agent.
---
install/tools/ipa-upgradeconfig | 19 +++++++++++++++++--
ipaserver/install/cainstance.py | 20 +++++++++++++-------
2 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/install/tools/ipa-upgradeconfig b/install/tools/ipa-upgradeconfig
index d77a338..433311b 100644
--- a/install/tools/ipa-upgradeconfig
+++ b/install/tools/ipa-upgradeconfig
@@ -594,7 +594,7 @@ def certificate_renewal_update(ca):
dogtag_constants = dogtag.configured_constants()
# bump version when requests is changed
- version = 1
+ version = 2
requests = (
(
dogtag_constants.ALIAS_DIR,
@@ -602,6 +602,7 @@ def certificate_renewal_update(ca):
'dogtag-ipa-ca-renew-agent',
'stop_pkicad',
'renew_ca_cert',
+ None,
),
(
dogtag_constants.ALIAS_DIR,
@@ -609,6 +610,7 @@ def certificate_renewal_update(ca):
'dogtag-ipa-ca-renew-agent',
'stop_pkicad',
'renew_ca_cert',
+ None,
),
(
dogtag_constants.ALIAS_DIR,
@@ -616,6 +618,15 @@ def certificate_renewal_update(ca):
'dogtag-ipa-ca-renew-agent',
'stop_pkicad',
'renew_ca_cert',
+ None,
+ ),
+ (
+ dogtag_constants.ALIAS_DIR,
+ 'caSigningCert cert-pki-ca',
+ 'dogtag-ipa-ca-renew-agent',
+ 'stop_pkicad',
+ 'renew_ca_cert',
+ 'ipaCACertRenewal',
),
(
'/etc/httpd/alias',
@@ -623,6 +634,7 @@ def certificate_renewal_update(ca):
'dogtag-ipa-ca-renew-agent',
None,
'renew_ra_cert',
+ None,
),
(
dogtag_constants.ALIAS_DIR,
@@ -630,6 +642,7 @@ def certificate_renewal_update(ca):
'dogtag-ipa-renew-agent',
None,
None,
+ None,
),
)
@@ -644,11 +657,13 @@ def certificate_renewal_update(ca):
return False
# State not set, lets see if we are already configured
- for nss_dir, nickname, ca_name, pre_command, post_command in requests:
+ for request in requests:
+ nss_dir, nickname, ca_name, pre_command, post_command, profile = request
criteria = (
('cert_storage_location', nss_dir, certmonger.NPATH),
('cert_nickname', nickname, None),
('ca_name', ca_name, None),
+ ('template_profile', profile, None),
)
request_id = certmonger.get_request_id(criteria)
if request_id is None:
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 0b93b72..e78bdb8 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -310,9 +310,10 @@ def stop_tracking_certificates(dogtag_constants):
cmonger.start()
for nickname in ['Server-Cert cert-pki-ca',
- 'auditSigningCert cert-pki-ca',
- 'ocspSigningCert cert-pki-ca',
- 'subsystemCert cert-pki-ca']:
+ 'auditSigningCert cert-pki-ca',
+ 'ocspSigningCert cert-pki-ca',
+ 'subsystemCert cert-pki-ca',
+ 'caSigningCert cert-pki-ca']:
try:
certmonger.stop_tracking(
dogtag_constants.ALIAS_DIR, nickname=nickname)
@@ -1425,12 +1426,16 @@ class CAInstance(service.Service):
'Unable to determine PIN for CA instance: %s' % e)
def configure_renewal(self):
+ reqs = (
+ ('auditSigningCert cert-pki-ca', None),
+ ('ocspSigningCert cert-pki-ca', None),
+ ('subsystemCert cert-pki-ca', None),
+ ('caSigningCert cert-pki-ca', 'ipaCACertRenewal'),
+ )
pin = self.__get_ca_pin()
# Server-Cert cert-pki-ca is renewed per-server
- for nickname in ['auditSigningCert cert-pki-ca',
- 'ocspSigningCert cert-pki-ca',
- 'subsystemCert cert-pki-ca']:
+ for nickname, profile in reqs:
try:
certmonger.dogtag_start_tracking(
ca='dogtag-ipa-ca-renew-agent',
@@ -1439,7 +1444,8 @@ class CAInstance(service.Service):
pinfile=None,
secdir=self.dogtag_constants.ALIAS_DIR,
pre_command='stop_pkicad',
- post_command='renew_ca_cert "%s"' % nickname)
+ post_command='renew_ca_cert "%s"' % nickname,
+ profile=profile)
except (ipautil.CalledProcessError, RuntimeError), e:
root_logger.error(
"certmonger failed to start tracking certificate: %s" % e)
--
1.8.5.3
>From e9cf41b6742103cfef7b6edaf3548e73c362ebdb Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Mon, 24 Mar 2014 15:30:53 +0100
Subject: [PATCH 06/13] Add method for setting CA renewal master in LDAP to
CAInstance.
---
ipaserver/install/cainstance.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index e78bdb8..0f134cf 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1579,6 +1579,25 @@ class CAInstance(service.Service):
return True
+ def set_renewal_master(self):
+ if not self.admin_conn:
+ self.ldap_connect()
+
+ base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
+ api.env.basedn)
+ filter = '(&(cn=CA)(ipaConfigString=caRenewalMaster))'
+ entries = self.admin_conn.get_entries(base_dn=base_dn, filter=filter,
+ attrs_list=['ipaConfigString'])
+ for entry in entries:
+ entry['ipaConfigString'] = [x for x in entry['ipaConfigString']
+ if x.lower() != 'carenewalmaster']
+ self.admin_conn.update_entry(entry)
+
+ dn = DN(('cn', 'CA'), ('cn', api.env.host), base_dn)
+ entry = self.admin_conn.get_entry(dn, ['ipaConfigString'])
+ entry['ipaConfigString'].append('caRenewalMaster')
+ self.admin_conn.update_entry(entry)
+
def replica_ca_install_check(config):
if not config.setup_ca:
--
1.8.5.3
>From 17f0238a53b0bac8156da82ed09e9bf31bd503bc Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 12 Mar 2014 11:35:10 +0100
Subject: [PATCH 07/13] Provide additional functions to ipapython.certmonger.
---
ipapython/certmonger.py | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/ipapython/certmonger.py b/ipapython/certmonger.py
index e7edc64..782d38b 100644
--- a/ipapython/certmonger.py
+++ b/ipapython/certmonger.py
@@ -277,6 +277,20 @@ def stop_tracking(secdir, request_id=None, nickname=None):
return (stdout, stderr, returncode)
+def modify(request_id, profile=None):
+ args = ['/usr/bin/getcert', 'start-tracking',
+ '-i', request_id]
+ if profile:
+ args += ['-T', profile]
+ return ipautil.run(args)
+
+def resubmit_request(request_id, profile=None):
+ args = ['/usr/bin/ipa-getcert', 'resubmit',
+ '-i', request_id]
+ if profile:
+ args += ['-T', profile]
+ return ipautil.run(args)
+
def _find_IPA_ca():
"""
Look through all the certmonger CA files to find the one that
@@ -445,6 +459,18 @@ def check_state(dirs):
return reqids
+def wait_for_request(request_id):
+ for i in range(0, 120, 5):
+ state = get_request_value(request_id, 'state').strip()
+ if state in ('CA_REJECTED', 'CA_UNREACHABLE', 'CA_UNCONFIGURED',
+ 'NEED_GUIDANCE', 'NEED_CA', 'MONITORING'):
+ break
+ time.sleep(5)
+ else:
+ raise RuntimeError("request timed out")
+
+ return state
+
if __name__ == '__main__':
request_id = request_cert("/etc/httpd/alias", "Test", "cn=tiger.example.com,O=IPA", "HTTP/[email protected]")
csr = get_request_value(request_id, 'csr')
--
1.8.5.3
>From 3454961fefd9ba492d9517bc1f101cadd52f1247 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Thu, 27 Feb 2014 15:09:10 +0100
Subject: [PATCH 08/13] Move external cert validation from ipa-server-install
to installutils.
---
install/tools/ipa-server-install | 45 +++-------------------------------
ipaserver/install/installutils.py | 51 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 54 insertions(+), 42 deletions(-)
diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index 34393b7..90f8b70 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -70,7 +70,6 @@ from ipapython import ipautil
from ipapython import dogtag
from ipalib import api, errors, util
from ipapython.config import IPAOptionParser
-from ipalib.x509 import load_certificate_from_file, load_certificate_chain_from_file
from ipalib.util import validate_domain_name
from ipalib.constants import CACERT
from ipapython import services as ipaservices
@@ -707,48 +706,12 @@ def main():
if options.external_cert_file:
try:
- extcert = load_certificate_from_file(options.external_cert_file)
- except IOError, e:
- print "Can't load the PEM certificate: %s." % str(e)
- sys.exit(1)
- except nss.error.NSPRError:
- print "'%s' is not a valid PEM-encoded certificate." % options.external_cert_file
- sys.exit(1)
-
- certsubject = DN(str(extcert.subject))
- wantsubject = DN(('CN','Certificate Authority'), options.subject)
- if certsubject != wantsubject:
- print "Subject of the external certificate is not correct (got %s, expected %s)." % (certsubject, wantsubject)
- sys.exit(1)
-
- try:
- extchain = load_certificate_chain_from_file(options.external_ca_file)
- except IOError, e:
- print "Can't load the external CA chain: %s." % str(e)
- sys.exit(1)
- except nss.error.NSPRError:
- print "'%s' is not a valid PEM-encoded certificate chain." % options.external_ca_file
- sys.exit(1)
-
- certdict = dict((DN(str(cert.subject)), cert) for cert in extchain)
- del extchain
- certissuer = DN(str(extcert.issuer))
- if certissuer not in certdict:
- print "The external certificate is not signed by the external CA (unknown issuer %s)." % certissuer
+ validate_external_cert(options.external_cert_file,
+ options.external_ca_file, options.subject)
+ except ValueError, e:
+ print e
sys.exit(1)
- cert = extcert
- del extcert
- while cert.issuer != cert.subject:
- certissuer = DN(str(cert.issuer))
- if certissuer not in certdict:
- print "The external CA chain is incomplete (%s is missing from the chain)." % certissuer
- sys.exit(1)
- del cert
- cert = certdict[certissuer]
- del certdict
- del cert
-
# We only set up the CA if the PKCS#12 options are not given.
if options.dirsrv_pkcs12:
setup_ca = False
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index daf81e8..1c7afe2 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -33,13 +33,14 @@ from contextlib import contextmanager
from dns import resolver, rdatatype
from dns.exception import DNSException
import ldap
+from nss.error import NSPRError
from ipapython import ipautil, sysrestore, admintool, dogtag
from ipapython.admintool import ScriptError
from ipapython.ipa_log_manager import *
from ipalib.util import validate_hostname
from ipapython import config
-from ipalib import errors
+from ipalib import errors, x509
from ipapython.dn import DN
from ipaserver.install import certs
from ipapython import services as ipaservices
@@ -846,3 +847,51 @@ def stopped_service(service, instance_name=""):
finally:
root_logger.debug('Starting %s%s.', service, log_instance_name)
ipaservices.knownservices[service].start(instance_name)
+
+
+def validate_external_cert(cert_file, ca_file, subject_base):
+ extcert = None
+ try:
+ extcert = x509.load_certificate_from_file(cert_file)
+ certsubject = DN(str(extcert.subject))
+ certissuer = DN(str(extcert.issuer))
+ except IOError, e:
+ raise ValueError("Can't load the PEM certificate: %s." % e)
+ except (TypeError, NSPRError):
+ raise ValueError(
+ "'%s' is not a valid PEM-encoded certificate." % cert_file)
+ finally:
+ del extcert
+
+ wantsubject = DN(('CN', 'Certificate Authority'), subject_base)
+ if certsubject != wantsubject:
+ raise ValueError(
+ "Subject of the external certificate is not correct (got %s, "
+ "expected %s)." % (certsubject, wantsubject))
+
+ extchain = None
+ try:
+ extchain = x509.load_certificate_chain_from_file(ca_file)
+ certdict = dict((DN(str(cert.subject)), DN(str(cert.issuer)))
+ for cert in extchain)
+ except IOError, e:
+ raise ValueError("Can't load the external CA chain: %s." % e)
+ except (TypeError, NSPRError):
+ raise ValueError(
+ "'%s' is not a valid PEM-encoded certificate chain." % ca_file)
+ finally:
+ del extchain
+
+ if certissuer not in certdict:
+ raise ValueError(
+ "The external certificate is not signed by the external CA "
+ "(unknown issuer %s)." % certissuer)
+
+ while certsubject != certissuer:
+ certsubject = certissuer
+ try:
+ certissuer = certdict[certsubject]
+ except KeyError:
+ raise ValueError(
+ "The external CA chain is incomplete (%s is missing from the "
+ "chain)." % certsubject)
--
1.8.5.3
>From b8af412e34cd1f5ddf4a27fbe3596f124f129d2d Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Thu, 13 Mar 2014 10:27:54 +0100
Subject: [PATCH 09/13] Add method for verifying CA certificates to
NSSDatabase.
---
ipaserver/install/certs.py | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py
index 9ee854e..b7c0465 100644
--- a/ipaserver/install/certs.py
+++ b/ipaserver/install/certs.py
@@ -276,6 +276,28 @@ class NSSDatabase(object):
return None
+ def verify_ca_cert_validity(self, nickname):
+ certdb = cert = None
+ nss.nss_init(self.secdir)
+ try:
+ certdb = nss.get_default_certdb()
+ cert = nss.find_cert_from_nickname(nickname)
+ intended_usage = (nss.certificateUsageUserCertImport |
+ nss.certificateUsageVerifyCA |
+ nss.certificateUsageProtectedObjectSigner |
+ nss.certificateUsageAnyCA)
+ try:
+ approved_usage = cert.verify_now(certdb, True, intended_usage)
+ except NSPRError, e:
+ if e.errno != -8102: # SEC_ERROR_INADEQUATE_KEY_USAGE
+ raise ValueError(e.strerror)
+ approved_usage = 0
+ if approved_usage & intended_usage != intended_usage:
+ raise ValueError('invalid for a CA')
+ finally:
+ del certdb, cert
+ nss.nss_shutdown()
+
class CertDB(object):
"""An IPA-server-specific wrapper around NSS
--
1.8.5.3
>From d61bb0bceae81c3407d76bd2674e02b4b3c9d424 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 19 Mar 2014 14:11:23 +0100
Subject: [PATCH 10/13] Add permissions for CA certificate renewal.
---
install/updates/40-delegation.update | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
index 3f1ff16..f77461a 100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -362,6 +362,26 @@ replace:aci:'(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr
# admins group
replace:aci:'(targetattr = "member")(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Group membership";allow (write) groupdn = "ldap:///cn=Modify Group membership,cn=permissions,cn=pbac,$SUFFIX";)::(targetfilter = "(!(cn=admins))")(targetattr = "member")(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Group membership";allow (write) groupdn = "ldap:///cn=Modify Group membership,cn=permissions,cn=pbac,$SUFFIX";)'
+dn: cn=Add CA Signing Certificate For Renewal,cn=permissions,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: ipapermission
+default:cn: Add CA Signing Certificate For Renewal
+default:member: cn=Certificate Administrators,cn=privileges,cn=pbac,$SUFFIX
+default:ipapermissiontype: SYSTEM
+
+dn: cn=Modify CA Signing Certificate For Renewal,cn=permissions,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: ipapermission
+default:cn: Modify CA Signing Certificate For Renewal
+default:member: cn=Certificate Administrators,cn=privileges,cn=pbac,$SUFFIX
+default:ipapermissiontype: SYSTEM
+
+dn: $SUFFIX
+add:aci:'(target = "ldap:///cn=caSigningCert cert-pki-ca,cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX")(version 3.0; acl "permission:Add CA Signing Certificate For Renewal"; allow (add) groupdn = "ldap:///cn=Add CA Signing Certificate For Renewal,cn=permissions,cn=pbac,$SUFFIX";)'
+add:aci:'(target = "ldap:///cn=caSigningCert cert-pki-ca,cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX")(targetattr = userCertificate)(version 3.0; acl "permission:Modify CA Signing Certificate For Renewal"; allow (write) groupdn = "ldap:///cn=Modify CA Signing Certificate For Renewal,cn=permissions,cn=pbac,$SUFFIX";)'
+
dn: cn=ipa,cn=etc,$SUFFIX
add:aci:'(target = "ldap:///cn=*,cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX")(version 3.0; acl "Add CA Certificates for renewals"; allow(add) userdn = "ldap:///fqdn=$FQDN,cn=computers,cn=accounts,$SUFFIX";)'
add:aci:'(target = "ldap:///cn=*,cn=ca_renewal,cn=ipa,cn=etc,$SUFFIX")(targetattr = "userCertificate")(version 3.0; acl "Modify CA Certificates for renewals"; allow(write) userdn = "ldap:///fqdn=$FQDN,cn=computers,cn=accounts,$SUFFIX";)'
--
1.8.5.3
>From ce0fc3046dd9bcefb2f82a7d71c9b27661451ac4 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Thu, 13 Mar 2014 10:28:27 +0100
Subject: [PATCH 11/13] Add CA certificate management tool ipa-cacert-manage.
Part of https://fedorahosted.org/freeipa/ticket/3737
---
freeipa.spec.in | 2 +
install/tools/Makefile.am | 1 +
install/tools/ipa-cacert-manage | 23 +++
install/tools/man/Makefile.am | 1 +
install/tools/man/ipa-cacert-manage.1 | 51 ++++++
ipaserver/install/ipa_cacert_manage.py | 280 +++++++++++++++++++++++++++++++++
6 files changed, 358 insertions(+)
create mode 100644 install/tools/ipa-cacert-manage
create mode 100644 install/tools/man/ipa-cacert-manage.1
create mode 100644 ipaserver/install/ipa_cacert_manage.py
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 763e40b..d4eb603 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -635,6 +635,7 @@ fi
%{_sbindir}/ipactl
%{_sbindir}/ipa-upgradeconfig
%{_sbindir}/ipa-advise
+%{_sbindir}/ipa-cacert-manage
%{_libexecdir}/certmonger/dogtag-ipa-ca-renew-agent-submit
%{_libexecdir}/ipa-otpd
%config(noreplace) %{_sysconfdir}/sysconfig/ipa_memcached
@@ -770,6 +771,7 @@ fi
%{_mandir}/man1/ipa-backup.1.gz
%{_mandir}/man1/ipa-restore.1.gz
%{_mandir}/man1/ipa-advise.1.gz
+%{_mandir}/man1/ipa-cacert-manage.1.gz
%files server-trust-ad
%{_sbindir}/ipa-adtrust-install
diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am
index 2cf66c6..76857fb 100644
--- a/install/tools/Makefile.am
+++ b/install/tools/Makefile.am
@@ -24,6 +24,7 @@ sbin_SCRIPTS = \
ipa-backup \
ipa-restore \
ipa-advise \
+ ipa-cacert-manage \
$(NULL)
EXTRA_DIST = \
diff --git a/install/tools/ipa-cacert-manage b/install/tools/ipa-cacert-manage
new file mode 100644
index 0000000..5e969b7
--- /dev/null
+++ b/install/tools/ipa-cacert-manage
@@ -0,0 +1,23 @@
+#! /usr/bin/python2 -E
+# Authors: Jan Cholasta <[email protected]>
+#
+# Copyright (C) 2014 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from ipaserver.install.ipa_cacert_manage import CACertManage
+
+CACertManage.run_cli()
diff --git a/install/tools/man/Makefile.am b/install/tools/man/Makefile.am
index 33e8a9e..413f0fd 100644
--- a/install/tools/man/Makefile.am
+++ b/install/tools/man/Makefile.am
@@ -22,6 +22,7 @@ man1_MANS = \
ipa-backup.1 \
ipa-restore.1 \
ipa-advise.1 \
+ ipa-cacert-manage.1 \
$(NULL)
man8_MANS = \
diff --git a/install/tools/man/ipa-cacert-manage.1 b/install/tools/man/ipa-cacert-manage.1
new file mode 100644
index 0000000..007cbf2
--- /dev/null
+++ b/install/tools/man/ipa-cacert-manage.1
@@ -0,0 +1,51 @@
+.\" A man page for ipa-cacert-manage
+.\" Copyright (C) 2014 Red Hat, Inc.
+.\"
+.\" This program is free software; you can redistribute it and/or modify
+.\" it under the terms of the GNU General Public License as published by
+.\" the Free Software Foundation, either version 3 of the License, or
+.\" (at your option) any later version.
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of
+.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+.\" General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU General Public License
+.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
+.\"
+.\" Author: Jan Cholasta <[email protected]>
+.\"
+.TH "ipa-cacert-manage" "1" "Aug 12 2013" "FreeIPA" "FreeIPA Manual Pages"
+.SH "NAME"
+ipa\-cacert\-manage \- Manage IPA CA certificates
+.SH "SYNOPSIS"
+ipa\-cacert\-manage [\fIOPTION\fR]... [COMMAND]
+.SH "DESCRIPTION"
+Manage CA certificates. The available commands are:
+.TP
+\fBrenew\fR
+\- Renew IPA CA certificate. Self-signed CA certificates are renewed in one step. CA certificates signed by an external CA are renewed in two steps. In the first step a certificate request is generated in /var/lib/ipa/ca.csr. In the second step, the certificate created by signing the request and the external CA certificate are set using the \-\-external\-cert\-file and \-\-external\-ca\-file options.
+.SH "OPTIONS"
+.TP
+\fB\-p\fR \fIDM_PASSWORD\fR, \fB\-\-password\fR=\fIDM_PASSWORD\fR
+The Directory Manager password to use for authentication.
+.TP
+\fB\-\-external\-cert\-file\fR=\fIFILE\fR
+PEM file containing a certificate signed by the external CA. Must be given with \-\-external\-ca\-file.
+.TP
+\fB\-\-external\-ca\-file\fR=\fIFILE\fR
+PEM file containing the external CA chain.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Print debugging information.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+Output only errors.
+.TP
+\fB\-\-external\-cert\-file\fR=\fIFILE\fR
+Log to the given file.
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py
new file mode 100644
index 0000000..9ab0e4d
--- /dev/null
+++ b/ipaserver/install/ipa_cacert_manage.py
@@ -0,0 +1,280 @@
+# Authors: Jan Cholasta <[email protected]>
+#
+# Copyright (C) 2014 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import time
+from optparse import OptionGroup
+import base64
+from nss import nss
+from nss.error import NSPRError
+import krbV
+
+from ipapython import admintool, certmonger, ipautil
+from ipapython.dn import DN
+from ipalib import api, errors, x509, util
+from ipaserver.install import certs, cainstance, installutils
+from ipaserver.plugins.ldap2 import ldap2
+
+
+class CACertManage(admintool.AdminTool):
+ command_name = 'ipa-cacert-manage'
+
+ usage = "%prog renew [options]"
+
+ description = "Manage CA certificates."
+
+ cert_nickname = 'caSigningCert cert-pki-ca'
+
+ @classmethod
+ def add_options(cls, parser):
+ super(CACertManage, cls).add_options(parser)
+
+ parser.add_option(
+ "-p", "--password", dest='password',
+ help="Directory Manager password")
+
+ renew_group = OptionGroup(parser, "Renew options")
+ renew_group.add_option(
+ "--external-cert-file", dest='external_cert_file',
+ help="PEM file containing a certificate signed by the external CA")
+ renew_group.add_option(
+ "--external-ca-file", dest='external_ca_file',
+ help="PEM file containing the external CA chain")
+ parser.add_option_group(renew_group)
+
+ def validate_options(self):
+ super(CACertManage, self).validate_options(needs_root=True)
+
+ installutils.check_server_configuration()
+
+ parser = self.option_parser
+
+ if not self.args:
+ parser.error("command not provided")
+
+ command = self.command = self.args[0]
+ options = self.options
+
+ if command == 'renew':
+ if options.external_cert_file and not options.external_ca_file:
+ parser.error("--external-ca-file not specified")
+ elif not options.external_cert_file and options.external_ca_file:
+ parser.error("--external-cert-file not specified")
+ else:
+ parser.error("unknown command \"%s\"" % command)
+
+ def run(self):
+ command = self.command
+ options = self.options
+
+ api.bootstrap(in_server=True)
+ api.finalize()
+
+ if command == 'renew' and options.external_cert_file:
+ self.conn = self.ldap_connect()
+ else:
+ self.conn = None
+
+ try:
+ if command == 'renew':
+ rc = self.renew()
+ finally:
+ if self.conn is not None:
+ self.conn.disconnect()
+
+ return rc
+
+ def ldap_connect(self):
+ conn = ldap2()
+
+ password = self.options.password
+ if not password:
+ try:
+ ccache = krbV.default_context().default_ccache()
+ conn.connect(ccache=ccache)
+ except (krbV.Krb5Error, errors.ACIError):
+ pass
+ else:
+ return conn
+
+ password = installutils.read_password(
+ "Directory Manager", confirm=False, validate=False)
+ if password is None:
+ raise admintool.ScriptError(
+ "Directory Manager password required")
+
+ conn.connect(bind_dn=DN(('cn', 'Directory Manager')), bind_pw=password)
+
+ return conn
+
+ def renew(self):
+ ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
+ if not ca.is_configured():
+ raise admintool.ScriptError("CA is not configured on this system")
+
+ nss_dir = ca.dogtag_constants.ALIAS_DIR
+ criteria = (('cert_storage_location', nss_dir, certmonger.NPATH),
+ ('cert_nickname', self.cert_nickname, None))
+ self.request_id = certmonger.get_request_id(criteria)
+ if self.request_id is None:
+ raise admintool.ScriptError(
+ "CA certificate is not tracked by certmonger")
+ self.log.debug(
+ "Found certmonger request id %r", self.request_id)
+
+ db = certs.CertDB(api.env.realm, nssdir=nss_dir)
+ cert = db.get_cert_from_db(self.cert_nickname, pem=False)
+
+ options = self.options
+ if options.external_cert_file:
+ return self.renew_external_step_2(ca, cert)
+
+ if x509.is_self_signed(cert, x509.DER):
+ return self.renew_self_signed(ca)
+ else:
+ return self.renew_external_step_1(ca)
+
+ def renew_self_signed(self, ca):
+ try:
+ ca.set_renewal_master()
+ except errors.NotFound:
+ raise admintool.ScriptError("CA renewal master not found")
+
+ self.resubmit_request('ipaCACertRenewal')
+
+ print "CA certificate successfully renewed"
+
+ def renew_external_step_1(self, ca):
+ self.resubmit_request('ipaCSRExport')
+
+ print("The next step is to get /var/lib/ipa/ca.csr signed by your CA "
+ "and re-run ipa-cacert-manage as:")
+ print("ipa-cacert-manage renew "
+ "--external-cert-file=/path/to/signed_certificate "
+ "--external-ca-file=/path/to/external_ca_certificate")
+
+ def renew_external_step_2(self, ca, old_cert):
+ options = self.options
+ cert_filename = options.external_cert_file
+
+ nss_cert = None
+ nss.nss_init(ca.dogtag_constants.ALIAS_DIR)
+ try:
+ try:
+ installutils.validate_external_cert(
+ cert_filename, options.external_ca_file,
+ x509.subject_base())
+ except ValueError, e:
+ raise admintool.ScriptError(e)
+
+ nss_cert = x509.load_certificate(old_cert, x509.DER)
+ subject = nss_cert.subject
+ issuer = nss_cert.issuer
+ #pylint: disable=E1101
+ pkinfo = nss_cert.subject_public_key_info.format()
+ #pylint: enable=E1101
+
+ nss_cert = x509.load_certificate_from_file(cert_filename)
+ if not nss_cert.is_ca_cert():
+ raise admintool.ScriptError("Not a CA certificate")
+ if nss_cert.subject != subject:
+ raise admintool.ScriptError("Subject name mismatch")
+ if nss_cert.issuer != issuer:
+ raise admintool.ScriptError("Issuer mismatch")
+ #pylint: disable=E1101
+ if nss_cert.subject_public_key_info.format() != pkinfo:
+ raise admintool.ScriptError("Subject public key info mismatch")
+ #pylint: enable=E1101
+ cert = nss_cert.der_data
+ finally:
+ del nss_cert
+ nss.nss_shutdown()
+
+ with certs.NSSDatabase() as tmpdb:
+ pw = ipautil.write_tmp_file(ipautil.ipa_generate_password())
+ tmpdb.create_db(pw.name)
+ tmpdb.add_single_pem_cert('IPA CA', ',,',
+ x509.make_pem(base64.b64encode(old_cert)))
+
+ try:
+ tmpdb.add_single_pem_cert('IPA CA', ',,',
+ x509.make_pem(base64.b64encode(cert)))
+ except ipautil.CalledProcessError:
+ raise admintool.ScriptError("Failed to import the certificate")
+
+ try:
+ tmpdb.verify_ca_cert_validity('IPA CA')
+ except ValueError, e:
+ raise admintool.ScriptError(
+ "Not a valid CA certificate: %s" % e)
+
+ dn = DN(('cn', self.cert_nickname), ('cn', 'ca_renewal'),
+ ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
+ try:
+ entry = self.conn.get_entry(dn, ['usercertificate'])
+ entry['usercertificate'] = [cert]
+ self.conn.update_entry(entry)
+ except errors.NotFound:
+ entry = self.conn.make_entry(
+ dn,
+ objectclass=['top', 'pkiuser', 'nscontainer'],
+ cn=[self.cert_nickname],
+ usercertificate=[cert])
+ self.conn.add_entry(entry)
+ except errors.EmptyModlist:
+ pass
+
+ try:
+ ca.set_renewal_master()
+ except errors.NotFound:
+ raise admintool.ScriptError("CA renewal master not found")
+
+ self.resubmit_request('ipaCACertRenewal')
+
+ print "CA certificate successfully renewed"
+
+ def resubmit_request(self, profile):
+ self.log.debug("resubmitting certmonger request '%s'", self.request_id)
+ certmonger.resubmit_request(self.request_id, profile=profile)
+ try:
+ state = certmonger.wait_for_request(self.request_id)
+ except RuntimeError:
+ raise admintool.ScriptError(
+ "Resubmitting certmonger request '%s' timed out, "
+ "please check the request manually" % self.request_id)
+ self.log.debug("certmonger request is in state %r", state)
+ if state != 'MONITORING':
+ raise admintool.ScriptError(
+ "Error resubmitting certmonger request '%s', "
+ "please check the request manually" % self.request_id)
+
+ if profile != 'ipaCACertRenewal':
+ self.log.debug("modifying certmonger request '%s'", self.request_id)
+ certmonger.modify(self.request_id, profile='ipaCACertRenewal')
+ try:
+ state = certmonger.wait_for_request(self.request_id)
+ except RuntimeError:
+ raise admintool.ScriptError(
+ "Modifying certmonger request '%s' timed out, "
+ "please check the request manually" % self.request_id)
+ self.log.debug("certmonger request is in state %r", state)
+ if state != 'MONITORING':
+ raise admintool.ScriptError(
+ "Error modifying certmonger request '%s', "
+ "please check the request manually" % self.request_id)
--
1.8.5.3
>From facffe21445c1e6615e844247ef6b1dbbde11d7f Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 12 Mar 2014 11:36:30 +0100
Subject: [PATCH 12/13] Alert user when externally signed CA is about to
expire.
---
install/certmonger/dogtag-ipa-ca-renew-agent-submit | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 8a1bd83..ea6b433 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -278,6 +278,8 @@ def renew_ca_cert():
if not cert:
return (REJECTED, "New certificate requests not supported")
+ is_self_signed = x509.is_self_signed(cert)
+
operation = os.environ.get('CERTMONGER_OPERATION')
if operation == 'SUBMIT':
result = retrieve_cert()
@@ -288,7 +290,7 @@ def renew_ca_cert():
return result
state = 'retrieve'
- if x509.is_self_signed(cert):
+ if is_self_signed:
ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
if ca.is_renewal_master():
state = 'request'
@@ -307,6 +309,10 @@ def renew_ca_cert():
if state == 'retrieve':
result = retrieve_cert()
+ if not is_self_signed and result[0] != ISSUED:
+ syslog.syslog(syslog.LOG_ALERT,
+ "IPA CA certificate is about to expire, "
+ "use ipa-cacert-manage to renew it")
elif state == 'request':
os.environ['CERTMONGER_CA_PROFILE'] = 'caCACert'
result = request_and_store_cert()
--
1.8.5.3
>From 4ce1dc442f1e2753ce140faaf946244211bcbbec Mon Sep 17 00:00:00 2001
From: Jan Cholasta <[email protected]>
Date: Wed, 19 Mar 2014 12:56:03 +0100
Subject: [PATCH 13/13] Load sysupgrade.state on demand.
This prevents SELinux denials when the sysupgrade module is imported in a
confined process.
---
ipaserver/install/sysupgrade.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/ipaserver/install/sysupgrade.py b/ipaserver/install/sysupgrade.py
index c508d2b..b02ab8e 100644
--- a/ipaserver/install/sysupgrade.py
+++ b/ipaserver/install/sysupgrade.py
@@ -26,17 +26,25 @@ from ipapython.ipa_log_manager import *
STATEFILE_DIR = '/var/lib/ipa/sysupgrade'
STATEFILE_FILE = 'sysupgrade.state'
-_sstore = sysrestore.StateFile(STATEFILE_DIR, STATEFILE_FILE)
+_sstore = None
+
+def _load_sstore():
+ global _sstore
+ if _sstore is None:
+ _sstore = sysrestore.StateFile(STATEFILE_DIR, STATEFILE_FILE)
def get_upgrade_state(module, state):
+ _load_sstore()
global _sstore
return _sstore.get_state(module, state)
def set_upgrade_state(module, state, value):
+ _load_sstore()
global _sstore
_sstore.backup_state(module, state, value)
def remove_upgrade_state(module, state):
+ _load_sstore()
global _sstore
_sstore.delete_state(module, state)
--
1.8.5.3
_______________________________________________
Freeipa-devel mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/freeipa-devel