There are several good tutorials on how to set up a certificate authority with openssl, but once you have one in place, what is a good way to manage it? Sure there are some tools out there that can help, but I’ve found them all to be a bit of a pain, especially when it comes time to renew a bunch of user certificates. For this purpose, a home-grown script is almost always better than a generic tool. Scripting allows you to customize each and every step of the process according to your specific organization’s needs. In this article I’ll give an example of how I use simple scripts to make key generation and regeneration easy.
It’s worth noting that lots of people probably don’t need their own CA. Generally, using a self-signed key or getting a key signed by a recognized authority will be simpler and easier, but in some cases this isn’t true. For example, at my office we have a server that is accessible via the Internet and contains proprietary information. It’s behind a solid firewall and is pretty well protected. The server is restricted to SSL only, but if username/password systems are used, they constantly get hammered by idiots looking to log in. By restricting the server to sessions authorized with an SSL key signed by our local CA only, we can limit the users that connect. Note that if we used a recognized authority (Versign et. al.) instead of our own, then we would still have the same problem. By using our own CA, no other keys will make it past the SSL authentication stage. We noticed a 86% drop in hack attempts two weeks after we went to this setup on this particular server. YMMV. Note that by doing this we also gain the advantage of users not having to enter their passwords every time they access the server, and the system admin (me) doesn’t have to worry about whether or not users are circumventing the strong password requirements (see my previous post: Subversion, SSL and Apache for Secure, Passwordless, User-based repository access controls.)
Our CA directory structure looks like this:
CA - certs - ca - user - server - private - ca - user - server - csr - ca - user - server - userp12
It’s a bit convoluted, but it works for our needs. As I said, that’s the beauty of scripting.
I use an openssl.cnf file to maintain all of the defaults and file locations. Here it is:
# # OpenSSL example configuration file. # This is mostly being used for generation of certificate requests. # # This definition stops the following lines choking if HOME isn't # defined. HOME = . RANDFILE = $ENV::HOME/.rnd # Extra OBJECT IDENTIFIER info: #oid_file = $ENV::HOME/.oid oid_section = new_oids # To use this configuration file with the "-extfile" option of the # "openssl x509" utility, name here the section containing the # X.509v3 extensions to use: # extensions = # (Alternatively, use a configuration file that has only # X.509v3 extensions in its main [= default] section.) [ new_oids ] # We can add new OIDs in here for use by 'ca' and 'req'. # Add a simple OID like this: # testoid1=1.2.3.4 # Or use config file substitution like this: # testoid2=${testoid1}.5.6 #################################################################### [ ca ] default_ca = CA_default # The default ca section #################################################################### [ CA_default ] dir = . # 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/certs/ca/myca.crt # 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/ca/myca.key # The private key RANDFILE = $dir/private/.rand # private random number file x509_extensions = usr_cert # The extentions to add to the cert # Comment out the following two lines for the "traditional" # (and highly broken) format. name_opt = ca_default # Subject Name options cert_opt = ca_default # Certificate field options # Extension copying option: use with caution. # copy_extensions = copy # 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 = sha1 # which md to use. preserve = no # keep passed DN ordering # A few difference way of specifying how similar the request should look # For type CA, the listed attributes must be the same, and the optional # and supplied fields are just that :-) policy = policy_match # For the CA policy [ policy_match ] countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = supplied emailAddress = optional # For the 'anything' policy # At this point in time, you must list all acceptable 'object' # types. [ policy_anything ] countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = supplied #################################################################### [ req ] default_bits = 1024 default_md = sha1 default_keyfile = privkey.pem distinguished_name = req_distinguished_name attributes = req_attributes x509_extensions = v3_ca # The extentions to add to the self signed cert # Passwords for private keys if not present they will be prompted for # input_password = secret # output_password = secret # This sets a mask for permitted string types. There are several options. # default: PrintableString, T61String, BMPString. # pkix : PrintableString, BMPString. # utf8only: only UTF8Strings. # nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). # MASK:XXXX a literal mask value. # WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings # so use this option with caution! # we use PrintableString+UTF8String mask so if pure ASCII texts are used # the resulting certificates are compatible with Netscape string_mask = MASK:0x2002 # req_extensions = v3_req # The extensions to add to a certificate request [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = US countryName_min = 2 countryName_max = 2 stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = YourState localityName = Locality Name (eg, city) localityName_default = YourCity 0.organizationName = Organization Name (eg, company/unit) 0.organizationName_default = YourOrganization # we can do this but it is not needed normally :-) 1.organizationName = Division 1.organizationName_default = YouCanSkipThisOneIfYouWantTo organizationalUnitName = Organizational Unit Name (eg, section) organizationalUnitName_default = ThisOneCanBeSkippedToo commonName = Common Name (eg, your name or your server\'s hostname) commonName_max = 64 emailAddress = Email Address emailAddress_max = 64 # SET-ex3 = SET extension number 3 [ req_attributes ] #challengePassword = A challenge password #challengePassword_min = 4 #challengePassword_max = 20 #unstructuredName = An optional company name [ usr_cert ] # These extensions are added when 'ca' signs a request. # This goes against PKIX guidelines but some CAs do it and some software # requires this to avoid interpreting an end user certificate as a CA. basicConstraints=CA:FALSE # Here are some examples of the usage of nsCertType. If it is omitted # the certificate can be used for anything *except* object signing. # This is OK for an SSL server. # nsCertType = server # For an object signing certificate this would be used. # nsCertType = objsign # For normal client use this is typical # nsCertType = client, email # and for everything including object signing: # nsCertType = client, email, objsign # This is typical in keyUsage for a client certificate. # keyUsage = nonRepudiation, digitalSignature, keyEncipherment # This will be displayed in Netscape's comment listbox. nsComment = "Signed by my private Certificate Authority" # PKIX recommendations harmless if included in all certificates. subjectKeyIdentifier=hash authorityKeyIdentifier=keyid,issuer # This stuff is for subjectAltName and issuerAltname. # Import the email address. # subjectAltName=email:copy # An alternative to produce certificates that aren't # deprecated according to PKIX. # subjectAltName=email:move # Copy subject details # issuerAltName=issuer:copy #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem #nsBaseUrl #nsRevocationUrl #nsRenewalUrl #nsCaPolicyUrl #nsSslServerName [ v3_req ] # Extensions to add to a certificate request basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment [ v3_ca ] # Extensions for a typical CA # PKIX recommendation. subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer:always # This is what PKIX recommends but some broken software chokes on critical # extensions. #basicConstraints = critical,CA:true # So we do this instead. basicConstraints = CA:true # Key usage: this is typical for a CA certificate. However since it will # prevent it being used as an test self-signed certificate it is best # left out by default. # keyUsage = cRLSign, keyCertSign # Some might want this also # nsCertType = sslCA, emailCA # Include email address in subject alt name: another PKIX recommendation # subjectAltName=email:copy # Copy issuer details # issuerAltName=issuer:copy # DER hex encoding of an extension: beware experts only! # obj=DER:02:03 # Where 'obj' is a standard or added object # You can even override a supported extension: # basicConstraints= critical, DER:30:03:01:01:FF [ crl_ext ] # CRL extensions. # Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. # issuerAltName=issuer:copy authorityKeyIdentifier=keyid:always,issuer:always [ proxy_cert_ext ] # These extensions should be added when creating a proxy certificate # This goes against PKIX guidelines but some CAs do it and some software # requires this to avoid interpreting an end user certificate as a CA. basicConstraints=CA:FALSE # Here are some examples of the usage of nsCertType. If it is omitted # the certificate can be used for anything *except* object signing. # This is OK for an SSL server. # nsCertType = server # For an object signing certificate this would be used. # nsCertType = objsign # For normal client use this is typical # nsCertType = client, email # and for everything including object signing: # nsCertType = client, email, objsign # This is typical in keyUsage for a client certificate. # keyUsage = nonRepudiation, digitalSignature, keyEncipherment # This will be displayed in Netscape's comment listbox. nsComment = "My CA Signed Certificate" # PKIX recommendations harmless if included in all certificates. subjectKeyIdentifier=hash authorityKeyIdentifier=keyid,issuer:always # This stuff is for subjectAltName and issuerAltname. # Import the email address. # subjectAltName=email:copy # An alternative to produce certificates that aren't # deprecated according to PKIX. # subjectAltName=email:move # Copy subject details # issuerAltName=issuer:copy #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem #nsBaseUrl #nsRevocationUrl #nsRenewalUrl #nsCaPolicyUrl #nsSslServerName # This really needs to be in place for it to be a proxy certificate. proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
For generating a single user certificate, which we only do when someone new gets hired, we have a simple shell script. All information is entered by hand.
Here is generate-user-key:
#!/bin/bash [ "$1" == "" ] && echo "Usage: generate-user-key <username>" && exit -1; openssl req -config openssl.cnf -new -sha1 -newkey rsa:1024 -nodes -keyout private/user/$1.key -out csr/user/$1.pem openssl ca -config openssl.cnf -policy policy_anything -extensions usr_cert -out certs/user/$1.pem -infiles csr/user/$1.pem openssl pkcs12 -export -clcerts -in certs/user/$1.pem -inkey private/user/$1.key -out userp12/$1.p12
It checks to make sure a username is present, and then runs through the three openssl commands necessary for generating the certificates.
Now, as you can see in the openssl.cnf file, the user keys only last for 365 days. So every year we have to regenerate all the keys, most of them on the same day. To do that, we use a perl script: regenerate-all-user-keys
#!/usr/bin/perl
We check to make sure the CA password is provided:
$ARGV[0] =~ /.+/ or die "usage: regenerate-user-keys <ca-password>"; $password = $ARGV[0]; chomp($password);
And grab all the user keys by checking the private/user directory and stripping off the extra characters.
@keys=`ls private/user/*.key`; chomp for(@keys); s/\.key//g for(@keys); s/.*\/(.*$)/\1/g for(@keys);
Now for each key we’ll go through the regeneration process.
for $key(@keys) {
Grab the existing subject line from the existing certificate and re-format it for the command line.
$subjects=`openssl x509 -in certs/user/$key.pem -noout -text | grep Subject:`; chomp ($subjects); $subjects =~ s/, /\//g; $subjects =~ s/\s+Subject: (.*)/\/\1/;
Make a copy of all of the keys and certificates in case we have a failure and need to roll back.
system "cp private/user/$key.key private/user/$key.key.last"; system "cp csr/user/$key.pem csr/user/$key.pem.last"; system "cp certs/user/$key.pem certs/user/$key.pem.last"; system "cp userp12/$key.p12 userp12/$key.p12.last";
Regenerate the key and signing request
print "\n\nopenssl req -config openssl.cnf -new -sha1 -newkey rsa:1024 -nodes -keyout private/user/$key.key -out csr/user/$key.pem -multivalue-rdn -subj '$subjects'\n"; system "openssl req -config openssl.cnf -new -sha1 -newkey rsa:1024 -nodes -keyout private/user/$key.key -out csr/user/$key.pem -multivalue-rdn -subj '$subjects'";
Check to be certain that the process ended correctly. If it didn’t then roll back the keys.
if ($? == -1) { print "failed to execute: $!\n"; } if ($? & 127) { printf "child died with signal %d, %s coredump\n", ($? & 127), ($? & 128) ? 'with' : 'without'; } else { $exitval = $? >> 8; if ($exitval != 0) { printf "child exited with value %d\n", $exitval; print "$key failed to regenerate. Restoring old keys.\n"; system "cp private/user/$key.key.last private/user/$key.key"; system "cp csr/user/$key.pem.last csr/user/$key.pem"; system "cp certs/user/$key.pem.last certs/user/$key.pem"; system "cp userp12/$key.p12.last userp12/$key.p12"; push(@errored_out, $key); next; } }
Sign the key using the password supplied on the command line.
print "\n\nopenssl ca -config openssl.cnf -policy policy_anything -extensions usr_cert -out certs/user/$key.pem -in csr/user/$key.pem -multivalue-rdn -subj '$subjects' -batch -key '$password'\n"; system "openssl ca -config openssl.cnf -policy policy_anything -extensions usr_cert -out certs/user/$key.pem -in csr/user/$key.pem -multivalue-rdn -subj '$subjects' -batch -key '$password'"; if ($? == -1) { print "failed to execute: $!\n"; } elsif ($? & 127) { printf "child died with signal %d, %s coredump\n", ($? & 127), ($? & 128) ? 'with' : 'without'; } else { $exitval = $? >> 8; if ($exitval != 0) { printf "child exited with value %d\n", $exitval; print "$key failed to regenerate. Restoring old keys.\n"; system "cp private/user/$key.key.last private/user/$key.key"; system "cp csr/user/$key.pem.last csr/user/$key.pem"; system "cp certs/user/$key.pem.last certs/user/$key.pem"; system "cp userp12/$key.p12.last userp12/$key.p12"; push(@errored_out, $key); next; } }
And finally, output the pcks12 formatted certificate to send to the users. Note that the output is encrypted with a passcode postpended with the username. This is sufficient for our needs, but probably not everyones. To make it so that each user gets a unique password, remove the -passout parameter and the sytem will prompt each time it goes to export a pkcs12 certificate.
print "\n\nopenssl pkcs12 -export -clcerts -in certs/user/$key.pem -inkey private/user/$key.key -out userp12/$key.p12 -des3 -passout 'pass:ourcode$key'\n"; system "openssl pkcs12 -export -clcerts -in certs/user/$key.pem -inkey private/user/$key.key -out userp12/$key.p12 -des3 -passout 'pass:ourcode$key'"; if ($? == -1) { print "failed to execute: $!\n"; } elsif ($? & 127) { printf "child died with signal %d, %s coredump\n", ($? & 127), ($? & 128) ? 'with' : 'without'; } else { $exitval = $? >> 8; if ($exitval != 0) { printf "child exited with value %d\n", $exitval; print "$key failed to regenerate. Restoring old keys.\n"; system "cp private/user/$key.key.last private/user/$key.key"; system "cp csr/user/$key.pem.last csr/user/$key.pem"; system "cp certs/user/$key.pem.last certs/user/$key.pem"; system "cp userp12/$key.p12.last userp12/$key.p12"; push(@errored_out, $key); next; } } }
Output each of the certificates that failed for one reason or another so that they can be addressed manually.
for $fail(@errored_out) { print "WARNING: $fail did not regenerate.\n"; }
And remove any remaining backup files.
system "rm -f private/user/*.last"; system "rm -f csr/user/*.last"; system "rm -f certs/user/*.last"; system "rm -f userp12/*.last";
Pretty straight forward, and makes regenerating hundreds of keys on a single day much less of a problem. A task left to the reader is to have the script email the user their new key based on the e-mail address captured in the subject line. Our doesn’t do that because we have to get VP level approval to send automated e-mails.
Cheers.
–JATF