diff options
Diffstat (limited to 'ipaserver/install/certs.py')
-rw-r--r-- | ipaserver/install/certs.py | 294 |
1 files changed, 207 insertions, 87 deletions
diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index df59acc3f..ca5351201 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -25,14 +25,18 @@ import logging import urllib import xml.dom.minidom import pwd +import fcntl from ipapython import nsslib from ipapython import sysrestore from ipapython import ipautil +from ConfigParser import RawConfigParser from nss.error import NSPRError import nss.nss as nss +from ipalib import api + # The sha module is deprecated in Python 2.6, replaced by hashlib. Try # that first and fall back to sha.sha if it isn't available. try: @@ -46,12 +50,9 @@ def ipa_self_signed(): """ Determine if the current IPA CA is self-signed or using another CA - Note that this doesn't distinguish between dogtag and being provided - PKCS#12 files from another CA. - - A server is self-signed if /var/lib/ipa/ca_serialno exists + We do this based on the CA plugin that is currently in use. """ - if ipautil.file_exists(CA_SERIALNO): + if api.env.ra_plugin == 'selfsign': return True else: return False @@ -80,6 +81,96 @@ def client_auth_data_callback(ca_names, chosen_nickname, password, certdb): return False return False +def find_cert_from_txt(cert, start=0): + """ + Given a cert blob (str) which may or may not contian leading and + trailing text, pull out just the certificate part. This will return + the FIRST cert in a stream of data. + + Returns a tuple (certificate, last position in cert) + """ + s = cert.find('-----BEGIN CERTIFICATE-----', start) + e = cert.find('-----END CERTIFICATE-----', s) + if e > 0: e = e + 25 + + if s < 0 or e < 0: + raise RuntimeError("Unable to find certificate") + + cert = cert[s:e] + return (cert, e) + +def next_serial(serial_file=CA_SERIALNO): + """ + Get the next serial number if we're using an NSS-based self-signed CA. + + The file is an ini-like file with following properties: + lastvalue = the last serial number handed out + nextreplica = the serial number the next replica should start with + replicainterval = the number to add to nextreplica the next time a + replica is created + + File locking is attempted so we have unique serial numbers. + """ + fp = None + parser = RawConfigParser() + if ipautil.file_exists(serial_file): + try: + fp = open(serial_file, "r+") + fcntl.flock(fp.fileno(), fcntl.LOCK_EX) + parser.readfp(fp) + serial = parser.getint('selfsign', 'lastvalue') + cur_serial = serial + 1 + except IOError, e: + raise RuntimeError("Unable to determine serial number: %s" % str(e)) + else: + fp = open(serial_file, "w") + fcntl.flock(fp.fileno(), fcntl.LOCK_EX) + parser.add_section('selfsign') + parser.set('selfsign', 'nextreplica', 500000) + parser.set('selfsign', 'replicainterval', 500000) + cur_serial = 1000 + + try: + fp.seek(0) + parser.set('selfsign', 'lastvalue', cur_serial) + parser.write(fp) + fp.flush() + fcntl.flock(fp.fileno(), fcntl.LOCK_UN) + fp.close() + except IOError, e: + raise RuntimeError("Unable to increment serial number: %s" % str(e)) + + return str(cur_serial) + +def next_replica(serial_file=CA_SERIALNO): + """ + Return the starting serial number for a new self-signed replica + """ + fp = None + parser = RawConfigParser() + if ipautil.file_exists(serial_file): + try: + fp = open(serial_file, "r+") + fcntl.flock(fp.fileno(), fcntl.LOCK_EX) + parser.readfp(fp) + serial = parser.getint('selfsign', 'nextreplica') + nextreplica = serial + parser.getint('selfsign', 'replicainterval') + except IOError, e: + raise RuntimeError("Unable to determine serial number: %s" % str(e)) + else: + raise RuntimeError("%s does not exist, cannot create replica" % serial_file) + try: + fp.seek(0) + parser.set('selfsign', 'nextreplica', nextreplica) + parser.write(fp) + fp.flush() + fcntl.flock(fp.fileno(), fcntl.LOCK_UN) + fp.close() + except IOError, e: + raise RuntimeError("Unable to increment serial number: %s" % str(e)) + + return str(serial) + class CertDB(object): def __init__(self, nssdir, fstore=None, host_name=None): self.secdir = nssdir @@ -93,9 +184,9 @@ class CertDB(object): self.pk12_fname = self.secdir + "/cacert.p12" self.pin_fname = self.secdir + "/pin.txt" self.pwd_conf = "/etc/httpd/conf/password.conf" - self.reqdir = tempfile.mkdtemp('', 'ipa-', '/var/lib/ipa') - self.certreq_fname = self.reqdir + "/tmpcertreq" - self.certder_fname = self.reqdir + "/tmpcert.der" + self.reqdir = None + self.certreq_fname = None + self.certder_fname = None self.host_name = host_name self.self_signed_ca = ipa_self_signed() @@ -105,13 +196,6 @@ class CertDB(object): else: self.subject_format = "CN=%s,OU=pki-ipa,O=IPA" - # Making this a starting value that will generate - # unique values for the current DB is the - # responsibility of the caller for now. In the - # future we might automatically determine this - # for a given db. - self.cur_serial = -1 - self.cacert_name = "CA certificate" self.valid_months = "120" self.keysize = "1024" @@ -131,63 +215,40 @@ class CertDB(object): self.fstore = sysrestore.FileStore('/var/lib/ipa/sysrestore') def __del__(self): - shutil.rmtree(self.reqdir, ignore_errors=True) + if self.reqdir is not None: + shutil.rmtree(self.reqdir, ignore_errors=True) - def find_cert_from_txt(self, cert): - """ - Given a cert blob (str) which may or may not contian leading and - trailing text, pull out just the certificate part. This will return - the FIRST cert in a stream of data. + def setup_cert_request(self): """ - s = cert.find('-----BEGIN CERTIFICATE-----') - e = cert.find('-----END CERTIFICATE-----') - if e > 0: e = e + 25 + Create a temporary directory to store certificate requests and + certificates. This should be called before requesting certificates. - if s < 0 or e < 0: - raise RuntimeError("Unable to find certificate") + This is set outside of __init__ to avoid creating a temporary + directory every time we open a cert DB. + """ + if self.reqdir is not None: + return - cert = cert[s:e] - return cert + self.reqdir = tempfile.mkdtemp('', 'ipa-', '/var/lib/ipa') + self.certreq_fname = self.reqdir + "/tmpcertreq" + self.certder_fname = self.reqdir + "/tmpcert.der" def set_serial_from_pkcs12(self): """A CA cert was loaded from a PKCS#12 file. Set up our serial file""" - self.cur_serial = self.find_cacert_serial() + cur_serial = self.find_cacert_serial() try: - f=open(CA_SERIALNO,"w") - f.write(str(self.cur_serial)) - f.close() - except IOError, e: - raise RuntimeError("Unable to increment serial number: %s" % str(e)) - - def next_serial(self): - try: - f=open(CA_SERIALNO,"r") - r = f.readline() - try: - self.cur_serial = int(r) + 1 - except ValueError: - raise RuntimeError("The value in %s is not an integer" % CA_SERIALNO) - f.close() - except IOError, e: - if e.errno == errno.ENOENT: - self.self_signed_ca = True - self.cur_serial = 1000 - f=open(CA_SERIALNO,"w") - f.write(str(self.cur_serial)) - f.close() - else: - raise RuntimeError("Unable to determine serial number: %s" % str(e)) - - try: - f=open(CA_SERIALNO,"w") - f.write(str(self.cur_serial)) - f.close() + fp = open(CA_SERIALNO, "w") + parser = RawConfigParser() + parser.add_section('selfsign') + parser.set('selfsign', 'lastvalue', cur_serial) + parser.set('selfsign', 'nextreplica', 500000) + parser.set('selfsign', 'replicainterval', 500000) + parser.write(fp) + fp.close() except IOError, e: raise RuntimeError("Unable to increment serial number: %s" % str(e)) - return str(self.cur_serial) - def set_perms(self, fname, write=False, uid=None): if uid: pent = pwd.getpwnam(uid) @@ -280,27 +341,35 @@ class CertDB(object): return False def create_ca_cert(self): - # Generate the encryption key - self.run_certutil(["-G", "-z", self.noise_fname, "-f", self.passwd_fname]) - # Generate the self-signed cert p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir, "-S", "-n", self.cacert_name, "-s", "cn=IPA Test Certificate Authority", "-x", "-t", "CT,,C", + "-1", "-2", - "-m", self.next_serial(), + "-5", + "-m", next_serial(), "-v", self.valid_months, "-z", self.noise_fname, "-f", self.passwd_fname], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # Create key usage extension + # 0 - Digital Signature + # 1 - Non-repudiation + # 5 - Cert signing key + # Is this a critical extension [y/N]? y + p.stdin.write("0\n1\n5\n9\ny\n") + # Create basic constraint extension # Is this a CA certificate [y/N]? y # Enter the path length constraint, enter to skip [<0 for unlimited pat # Is this a critical extension [y/N]? y - p.stdin.write("y\n\n7\n") + # 5 6 7 9 n -> SSL, S/MIME, Object signing CA + p.stdin.write("y\n\ny\n") + p.stdin.write("5\n6\n7\n9\nn\n") p.wait() def export_ca_cert(self, nickname, create_pkcs12=False): @@ -310,9 +379,12 @@ class CertDB(object): do that step.""" # export the CA cert for use with other apps ipautil.backup_file(self.cacert_fname) - self.run_certutil(["-L", "-n", nickname, - "-a", - "-o", self.cacert_fname]) + root_nicknames = self.find_root_cert(nickname) + fd = open(self.cacert_fname, "w") + for root in root_nicknames: + (cert, stderr) = self.run_certutil(["-L", "-n", root, "-a"]) + fd.write(cert) + fd.close() os.chmod(self.cacert_fname, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) if create_pkcs12: ipautil.backup_file(self.pk12_fname) @@ -324,10 +396,30 @@ class CertDB(object): self.set_perms(self.pk12_fname) def load_cacert(self, cacert_fname): - self.run_certutil(["-A", "-n", self.cacert_name, - "-t", "CT,,C", - "-a", - "-i", cacert_fname]) + """ + Load all the certificates from a given file. It is assumed that + this file creates CA certificates. + """ + fd = open(cacert_fname) + certs = fd.read() + fd.close() + + st = 0 + subid=0 + while True: + try: + (cert, st) = find_cert_from_txt(certs, st) + if subid == 0: + nick = self.cacert_name + else: + nick = "%s sub %d" % (self.cacert_name, subid) + subid = subid + 1 + self.run_certutil(["-A", "-n", nick, + "-t", "CT,,C", + "-a"], + stdin=cert) + except RuntimeError: + break def get_cert_from_db(self, nickname): try: @@ -384,6 +476,7 @@ class CertDB(object): def request_cert(self, subject, certtype="rsa", keysize="2048"): self.create_noise_file() + self.setup_cert_request() args = ["-R", "-s", subject, "-o", self.certreq_fname, "-k", certtype, @@ -398,13 +491,14 @@ class CertDB(object): return (stdout, stderr) def issue_server_cert(self, certreq_fname, cert_fname): + self.setup_cert_request() if self.self_signed_ca: p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir, "-C", "-c", self.cacert_name, "-i", certreq_fname, "-o", cert_fname, - "-m", self.next_serial(), + "-m", next_serial(), "-v", self.valid_months, "-f", self.passwd_fname, "-1", "-5"], @@ -469,9 +563,14 @@ class CertDB(object): # The result is an XML blob. Pull the certificate out of that doc = xml.dom.minidom.parseString(data) item_node = doc.getElementsByTagName("b64") - cert = item_node[0].childNodes[0].data - doc.unlink() - conn.close() + try: + try: + cert = item_node[0].childNodes[0].data + except IndexError: + raise RuntimeError("Certificate issuance failed") + finally: + doc.unlink() + conn.close() # Write the certificate to a file. It will be imported in a later # step. @@ -482,13 +581,14 @@ class CertDB(object): return def issue_signing_cert(self, certreq_fname, cert_fname): + self.setup_cert_request() if self.self_signed_ca: p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir, "-C", "-c", self.cacert_name, "-i", certreq_fname, "-o", cert_fname, - "-m", self.next_serial(), + "-m", next_serial(), "-v", self.valid_months, "-f", self.passwd_fname, "-1", "-5"], @@ -565,10 +665,13 @@ class CertDB(object): return def add_cert(self, cert_fname, nickname): + """ + Load a certificate from a PEM file and add minimal trust. + """ args = ["-A", "-n", nickname, "-t", "u,u,u", "-i", cert_fname, - "-f", cert_fname] + "-f", self.passwd_fname] if not self.self_signed_ca: args.append("-a") self.run_certutil(args) @@ -600,21 +703,38 @@ class CertDB(object): self.set_perms(self.pwd_conf, uid="apache") def find_root_cert(self, nickname): + """ + Given a nickname, return a list of the certificates that make up + the trust chain. + """ + root_nicknames = [] p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir, "-O", "-n", nickname], stdout=subprocess.PIPE) chain = p.stdout.read() chain = chain.split("\n") - root_nickname = re.match('\ *"(.*)" \[.*', chain[0]).groups()[0] + for c in chain: + m = re.match('\s*"(.*)" \[.*', c) + if m: + root_nicknames.append(m.groups()[0]) + + if len(root_nicknames) > 1: + # If you pass in the name of a CA to get the chain it may only + # return 1 (self-signed). Return that. + try: + root_nicknames.remove(nickname) + except ValueError: + # The nickname wasn't in the list + pass # Try to work around a change in the F-11 certutil where untrusted # CA's are not shown in the chain. This will make a default IPA # server installable. - if root_nickname is None and self.self_signed_ca: - return self.cacert_name + if len(root_nicknames) == 0 and self.self_signed_ca: + return [self.cacert_name] - return root_nickname + return root_nicknames def find_root_cert_from_pkcs12(self, pkcs12_fname, passwd_fname=None): """Given a PKCS#12 file, try to find any certificates that do @@ -723,11 +843,11 @@ class CertDB(object): newca = f.readlines() f.close() newca = "".join(newca) - newca = self.find_cert_from_txt(newca) + (newca, st) = find_cert_from_txt(newca) cacert = self.get_cert_from_db(self.cacert_name) if cacert != '': - cacert = self.find_cert_from_txt(cacert) + (cacert, st) = find_cert_from_txt(cacert) if newca == cacert: return @@ -765,11 +885,11 @@ class CertDB(object): raise RuntimeError("Could not find a CA cert in %s" % pkcs12_fname) self.cacert_name = ca_names[0] - for nickname in ca_names: - self.trust_root_cert(nickname) + for ca in ca_names: + self.trust_root_cert(ca) self.create_pin_file() - self.export_ca_cert(self.cacert_name, False) + self.export_ca_cert(nickname, False) self.self_signed_ca=False # This file implies that we have our own self-signed CA. Ensure |