From 32c2ba8e71dda732cdfaa20f5a5d64bbc3058391 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Fri, 13 Jul 2012 22:50:58 -0700 Subject: [PATCH] correctly emit subject alternative names and remove most user-supplied data from cert --- server-ca/CA.sh | 9 +++++++ server-ca/CSR.py | 53 ++++++++++++++++++++++++++++------------- server-ca/daemon.py | 3 ++- server-ca/demoCA/ca.cfg | 38 +++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 server-ca/demoCA/ca.cfg diff --git a/server-ca/CA.sh b/server-ca/CA.sh index 488c917af..1e12eea83 100755 --- a/server-ca/CA.sh +++ b/server-ca/CA.sh @@ -77,6 +77,7 @@ if [ -z "$CATOP" ] ; then CATOP=./demoCA ; fi CAKEY=./cakey.pem CAREQ=./careq.pem CACERT=./cacert.pem +CACFG=./ca.cfg RET=0 @@ -168,6 +169,14 @@ case $1 in RET=$? exit $RET ;; +-complete) + # TODO: for deployed system, add -notext to avoid getting human-readable + # text output. + /bin/echo -e "y\ny\n" | $CA -passin env:PASSWORD -config ${CATOP}/${CACFG} -extfile "$3" -subj "$2" -out "$5" -infiles "$4" + RET=$? + exit $RET + ;; + -signCA) $CA -policy policy_anything -out newcert.pem -extensions v3_ca -infiles newreq.pem RET=$? diff --git a/server-ca/CSR.py b/server-ca/CSR.py index f7932143e..02f2daefb 100644 --- a/server-ca/CSR.py +++ b/server-ca/CSR.py @@ -5,13 +5,13 @@ import site, os assert os.path.exists("../m3/lib/python"), "\nPlease install m3crypto into ../m3/lib/python by running\nmkdir -p ../m3/lib/python; PYTHONPATH=../m3/lib/python python setup.py install --home=../m3\nfrom inside the m3crypto directory." site.addsitedir("../m3/lib/python") -import subprocess, tempfile, re +import subprocess, re +from tempfile import NamedTemporaryFile as temp import M2Crypto from distutils.version import LooseVersion assert LooseVersion(M2Crypto.version) >= LooseVersion("0.22") import hashlib -# we can use tempfile.NamedTemporaryFile() to get tempfiles -# to pass to OpenSSL subprocesses. +# we can use temp() to get tempfiles to pass to OpenSSL subprocesses. def parse(csr): """ @@ -205,21 +205,42 @@ def encrypt(key, data): pubkey = M2Crypto.RSA.load_pub_key_bio(bio) return pubkey.public_encrypt(data, M2Crypto.RSA.pkcs1_oaep_padding) - -def issue(csr): - """Issue the certificate requested by this CSR and return it!""" - # TODO: a real CA should severely restrict the content of the cert, not - # just grant what's asked for. (For example, the CA shouldn't trust - # all the data in the subject field if it hasn't been validated.) - # Therefore, we should construct a new CSR from scratch using the - # parsed-out data from the input CSR, and then pass that to OpenSSL. +def issue(csr, subjects): + """Issue a certificate requested by CSR, specifying the subject names + indicated in subjects, and return the certificate.""" + if not subjects: + return None csr = str(csr) + subjects = [str(s) for s in subjects] + for s in subjects: + if ":" in s or "," in s or " " in s or "\n" in s or "\r" in s: + # We should already have validated the names to be issued a + # long time ago, but this is an extra sanity check to make + # sure that the cert issuing process can't be corrupted by + # attempting to issue certs for names with special characters. + return None cert = None - with tempfile.NamedTemporaryFile() as csr_tmp: + # We need three temporary files: for the CSR, for the extension config + # file, and for the resulting certificate. + with temp() as csr_tmp, temp() as ext_tmp, temp() as cert_tmp: csr_tmp.write(csr) csr_tmp.flush() - with tempfile.NamedTemporaryFile() as cert_tmp: - ret = subprocess.Popen(["./CA.sh", "-chocolate", csr_tmp.name, cert_tmp.name],shell=False,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE).wait() - if ret == 0: - cert = cert_tmp.read() + dn = "/CN=%s" % subjects[0] + ext_tmp.write(""" +basicConstraints=CA:FALSE +keyUsage=digitalSignature, keyEncipherment, keyAgreement +extendedKeyUsage=serverAuth +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer +nsComment = "Chocolatey" +""") + if subjects[1:]: + san_line = "subjectAltName=" + san_line += ",".join("DNS:%s" % n for n in subjects[1:]) + "\n" + ext_tmp.write(san_line) + ext_tmp.flush() + print ["./CA.sh", "-complete", dn, ext_tmp.name, csr_tmp.name, cert_tmp.name] + ret = subprocess.Popen(["./CA.sh", "-complete", dn, ext_tmp.name, csr_tmp.name, cert_tmp.name],shell=False,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE).wait() + if ret == 0: + cert = cert_tmp.read() return cert diff --git a/server-ca/daemon.py b/server-ca/daemon.py index 603db10dc..856ca37f7 100644 --- a/server-ca/daemon.py +++ b/server-ca/daemon.py @@ -223,7 +223,8 @@ def issue(session): r.lrem("pending-requests", session) return csr = r.hget(session, "csr") - cert = CSR.issue(csr) + names = r.lrange("%s:names" % session, 0, -1) + cert = CSR.issue(csr, names) r.hset(session, "cert", cert) if cert: # once issuing cert succeeded if debug: print "issued for", session diff --git a/server-ca/demoCA/ca.cfg b/server-ca/demoCA/ca.cfg new file mode 100644 index 000000000..3d39d6818 --- /dev/null +++ b/server-ca/demoCA/ca.cfg @@ -0,0 +1,38 @@ +[ ca ] +default_ca = CA_default # The default ca section + +[ CA_default ] +policy = policy_blank +email_in_dn = no +name_opt = ca_default +cert_opt = ca_default +copy_extensions = none + +dir = ./demoCA # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +unique_subject = no # Set to 'no' to allow creation of + # several ctificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/private/cakey.pem# The private key +RANDFILE = $dir/private/.rand # private random number file + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = default # use public key default MD +preserve = no # keep passed DN ordering + +[ policy_blank ] +commonName = supplied