From 9182c10b03a7841c9318ad64ae6c5deda77d93d1 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 13 Apr 2009 13:39:15 -0400 Subject: Issue DS and Apache server certs during CA installation. Notes: - will create a CA instance (pki-ca) if it doesn't exist - maintains support for a self-signed CA - A signing cert is still not created so Firefox autoconfig still won't work --- ipaserver/install/certs.py | 254 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 212 insertions(+), 42 deletions(-) (limited to 'ipaserver/install/certs.py') diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index e9061fe1..45958f2a 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -22,15 +22,46 @@ import sha import errno import tempfile import shutil +import logging +import urllib +import xml.dom.minidom +from ipapython import nsslib from ipapython import sysrestore from ipapython import ipautil +from nss.error import NSPRError +import nss.nss as nss + CA_SERIALNO="/var/lib/ipa/ca_serialno" +def client_auth_data_callback(ca_names, chosen_nickname, password, certdb): + cert = None + if chosen_nickname: + try: + cert = nss.find_cert_from_nickname(chosen_nickname, password) + priv_key = nss.find_key_by_any_cert(cert, password) + return cert, priv_key + except NSPRError, e: + logging.debug("client auth callback failed %s" % str(e)) + return False + else: + nicknames = nss.get_cert_nicknames(certdb, nss.SEC_CERT_NICKNAMES_USER) + for nickname in nicknames: + try: + cert = nss.find_cert_from_nickname(nickname, password) + if cert.check_valid_times(): + if cert.has_signer_in_ca_names(ca_names): + priv_key = nss.find_key_by_any_cert(cert, password) + return cert, priv_key + except NSPRError, e: + logging.debug("client auth callback failed %s" % str(e)) + return False + return False + class CertDB(object): - def __init__(self, dir, fstore=None): - self.secdir = dir + def __init__(self, nssdir, fstore=None, host_name=None): + self.secdir = nssdir self.noise_fname = self.secdir + "/noise.txt" self.passwd_fname = self.secdir + "/pwdfile.txt" @@ -40,9 +71,16 @@ class CertDB(object): self.cacert_fname = self.secdir + "/cacert.asc" 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.host_name = host_name + + if ipautil.file_exists(CA_SERIALNO): + self.self_signed_ca = True + else: + self.self_signed_ca = False # Making this a starting value that will generate # unique values for the current DB is the @@ -72,6 +110,22 @@ class CertDB(object): def __del__(self): 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. + """ + s = cert.find('-----BEGIN CERTIFICATE-----') + e = cert.find('-----END CERTIFICATE-----') + if e > 0: e = e + 25 + + if s < 0 or e < 0: + raise RuntimeError("Unable to find certificate") + + cert = cert[s:e] + return cert + def set_serial_from_pkcs12(self): """A CA cert was loaded from a PKCS#12 file. Set up our serial file""" @@ -94,6 +148,7 @@ class CertDB(object): 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)) @@ -131,7 +186,8 @@ class CertDB(object): ipautil.run(new_args, stdin) def create_noise_file(self): - ipautil.backup_file(self.noise_fname) + if ipautil.file_exists(self.noise_fname): + os.remove(self.noise_fname) f = open(self.noise_fname, "w") f.write(self.gen_password()) self.set_perms(self.noise_fname) @@ -193,6 +249,14 @@ class CertDB(object): "-a", "-i", cacert_fname]) + def get_cert_from_db(self, nickname): + try: + args = ["-L", "-n", nickname, "-a"] + (cert, err) = self.run_certutil(args) + return cert + except ipautil.CalledProcessError: + return '' + def find_cacert_serial(self): (out,err) = self.run_certutil(["-L", "-n", self.cacert_name]) data = out.split('\n') @@ -203,11 +267,20 @@ class CertDB(object): raise RuntimeError("Unable to find serial number") - def create_server_cert(self, nickname, name, other_certdb=None): + def create_server_cert(self, nickname, subject, other_certdb=None): + """ + other_certdb can mean one of two things, depending on the context. + + If we are using a self-signed CA then other_certdb contains the + CA that will be signing our CSR. + + If we are using a dogtag CA then it contains the RA agent key + that will issue our cert. + """ cdb = other_certdb if not cdb: cdb = self - self.request_cert(name) + (out, err) = self.request_cert(subject) cdb.issue_server_cert(self.certreq_fname, self.certder_fname) self.add_cert(self.certder_fname, nickname) os.unlink(self.certreq_fname) @@ -223,41 +296,103 @@ class CertDB(object): os.unlink(self.certreq_fname) os.unlink(self.certder_fname) - def request_cert(self, name): - self.run_certutil(["-R", "-s", name, - "-o", self.certreq_fname, - "-g", self.keysize, - "-z", self.noise_fname, - "-f", self.passwd_fname]) + def request_cert(self, subject, certtype="rsa", keysize="2048"): + self.create_noise_file() + args = ["-R", "-s", subject, + "-o", self.certreq_fname, + "-k", certtype, + "-g", keysize, + "-z", self.noise_fname, + "-f", self.passwd_fname] + if not self.self_signed_ca: + args.append("-a") + (stdout, stderr) = self.run_certutil(args) + os.remove(self.noise_fname) + + return (stdout, stderr) def issue_server_cert(self, certreq_fname, cert_fname): - p = subprocess.Popen(["/usr/bin/certutil", - "-d", self.secdir, - "-C", "-c", self.cacert_name, - "-i", certreq_fname, - "-o", cert_fname, - "-m", self.next_serial(), - "-v", self.valid_months, - "-f", self.passwd_fname, - "-1", "-5"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) + 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(), + "-v", self.valid_months, + "-f", self.passwd_fname, + "-1", "-5"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + # Bah - this sucks, but I guess it isn't possible to fully + # control this with command line arguments. + # + # What this is requesting is: + # -1 (Create key usage extension) + # 2 - Key encipherment + # 9 - done + # n - not critical + # + # -5 (Create netscape cert type extension) + # 1 - SSL Server + # 9 - done + # n - not critical + p.stdin.write("2\n9\nn\n1\n9\nn\n") + p.wait() + else: + if self.host_name is None: + raise RuntimeError("CA Host is not set.") - # Bah - this sucks, but I guess it isn't possible to fully - # control this with command line arguments. - # - # What this is requesting is: - # -1 (Create key usage extension) - # 2 - Key encipherment - # 9 - done - # n - not critical - # - # -5 (Create netscape cert type extension) - # 1 - SSL Server - # 9 - done - # n - not critical - p.stdin.write("2\n9\nn\n1\n9\nn\n") - p.wait() + f = open(certreq_fname, "r") + csr = f.readlines() + f.close() + csr = "".join(csr) + + # We just want the CSR bits, make sure there is nothing else + s = csr.find("-----BEGIN NEW CERTIFICATE REQUEST-----") + e = csr.find("-----END NEW CERTIFICATE REQUEST-----") + if e > 0: + e = e + 37 + if s >= 0: + csr = csr[s:] + + params = urllib.urlencode({'profileId': 'caRAserverCert', + 'cert_request_type': 'pkcs10', + 'requestor_name': 'IPA Installer', + 'cert_request': csr, + 'xmlOutput': 'true'}) + headers = {"Content-type": "application/x-www-form-urlencoded", + "Accept": "text/plain"} + + # Send the CSR request to the CA + f = open(self.passwd_fname) + password = f.readline() + f.close() + conn = nsslib.NSSConnection(self.host_name, 9444, dbdir=self.secdir) + conn.sslsock.set_client_auth_data_callback(client_auth_data_callback, "ipaCert", password, nss.get_default_certdb()) + conn.set_debuglevel(0) + + conn.request("POST", "/ca/ee/ca/profileSubmit", params, headers) + res = conn.getresponse() + data = res.read() + conn.close() + if res.status != 200: + raise RuntimeError("Unable to submit cert request") + + # 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() + + # Write the certificate to a file. It will be imported in a later + # step. + f = open(cert_fname, "w") + f.write(cert) + f.close() + + return def issue_signing_cert(self, certreq_fname, cert_fname): p = subprocess.Popen(["/usr/bin/certutil", @@ -290,12 +425,18 @@ class CertDB(object): p.wait() def add_cert(self, cert_fname, nickname): - self.run_certutil(["-A", "-n", nickname, - "-t", "u,u,u", - "-i", cert_fname, - "-f", cert_fname]) + args = ["-A", "-n", nickname, + "-t", "u,u,u", + "-i", cert_fname, + "-f", cert_fname] + if not self.self_signed_ca: + args.append("-a") + self.run_certutil(args) def create_pin_file(self): + """ + This is the format of Directory Server pin files. + """ ipautil.backup_file(self.pin_fname) f = open(self.pin_fname, "w") f.write("Internal (Software) Token:") @@ -304,6 +445,17 @@ class CertDB(object): f.close() self.set_perms(self.pin_fname) + def create_password_conf(self): + """ + This is the format of mod_nss pin files. + """ + ipautil.backup_file(self.pwd_conf) + f = open(self.pwd_conf, "w") + f.write("internal:") + pwd = open(self.passwd_fname) + f.write(pwd.read()) + f.close() + def find_root_cert(self, nickname): p = subprocess.Popen(["/usr/bin/certutil", "-d", self.secdir, "-O", "-n", nickname], stdout=subprocess.PIPE) @@ -375,7 +527,24 @@ class CertDB(object): self.create_pin_file() def create_from_cacert(self, cacert_fname, passwd=""): - self.create_noise_file() + if ipautil.file_exists(self.certdb_fname): + # We already have a cert db, see if it is for the same CA. + # If it is we leave things as they are. + f = open(cacert_fname, "r") + newca = f.readlines() + f.close() + newca = "".join(newca) + newca = self.find_cert_from_txt(newca) + + cacert = self.get_cert_from_db(self.cacert_name) + if cacert != '': + cacert = self.find_cert_from_txt(cacert) + + if newca == cacert: + return + + # The CA certificates are different or something went wrong. Start with + # a new certificate database. self.create_passwd_file(passwd) self.create_certdbs() self.load_cacert(cacert_fname) @@ -403,6 +572,7 @@ class CertDB(object): self.trust_root_cert(nickname) self.create_pin_file() self.export_ca_cert(self.cacert_name, False) + self.self_signed_ca=False # This file implies that we have our own self-signed CA. Ensure # that it no longer exists (from previous installs, for example). -- cgit