Using openssl to Operate Your Own Certificate Authority

Usually you don’t need to manage your own PKI infrastructure with OpenSSL. The normal modus operandi is to generate your CSR, get a recognized certificate authority to sign it, use it until the CA reminds you it’s about to expire then repeat the process.

Occasionally, though, you need to establish your own chain of trust. Either doing it through a recognized CA would be expensive or you need the CA to be more responsive than a third party could ever be. For example, when you maintain an entire enterprise system filled with mail servers, LDAP servers, file shares, etc you need to be able to generate the certificates that you need without any regard for a CA’s verification procedure or having to wait on them before a new system can go into production.

Looking around I could see plenty of copy/paste tips online and incomplete guides but I couldn’t find anything that really served as an acceptable “Certificate Authority 101” as opposed to just one or two simple tricks done inefficiently.

Contents

Generate The Root CA’s Certificate and Private Key

First let’s create the private key we’re going to use for signing. This is only used for signing certs so it can be absurdly strong:

[root@localhost ~]# mkdir ssl
[root@localhost ~]# cd ssl
[root@localhost ssl]# openssl genrsa -out ca.key 10240
Generating RSA private key, 10240 bit long modulus
............................
[[....snip....]]

Now that we have the key we’re going to sign with, we need a certificate that describes the who and what of our. To do this you can either generate a self-signed certificate or have your CA certificate signed by a recognized CA with certificate signing as being enabled. Since that costs money, though, we’re just going to create a self-signed certificate with that key we just made.

To that end, let’s generate a CSR and then sign it ourselves:

[root@localhost ~]# openssl req -new -sha256 -key ca.key -out ca.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:US
State or Province Name (full name) []:North Carolina
Locality Name (eg, city) [Default City]:Durham
Organization Name (eg, company) [Default Company Ltd]:Mr Wammy Incorporated
Organizational Unit Name (eg, section) []:DoIT
Common Name (eg, your name or your server's hostname) []:ca.wammy.io
Email Address []:admin@wammy.ioPlease enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

[root@localhost ~]# openssl x509 -req -sha256 -days 1825 -in ca.csr -signkey ca.key -out ca.pem
Signature ok
subject=/C=US/ST=North Carolina/L=Durham/O=Mr Wammy Incorporated/OU=DoIT/CN=ca.wammy.io/emailAddress=admin@wammy.io
Getting Private key

You’ll notice that on my last command, I told it to generate a self-signed cert with a lifespan of 5 years. Compared to normal certificates that usually last a year at most, CA certificates need to stick around for a while. By CA standards this is a minuscule amount of time but for enterprise-level CA’s this middle ground is probably an ideal. It keeps you from continually renewing the root CA certificate while also changing it out every so often in case the server it’s on was compromised.

Now that we have our private signing key and root CA certificate, we need to get the system ready to use it as such.

Configuring your local OpenSSL install for CA Operation

Now for the CA-specific stuff. On Debian-based systems the root openssl configuration directory is located at /etc/ssl while on RHEL-based systems it’s located at /etc/pki. The layout underneath is substantially similar though so I’ll abbreviate using $dir to mean this directory.

Understanding How to Read openssl.cnf

Outside of updating $dir on Ubuntu below, you probably won’t have to change much anything in the stock openssl.cnf file of either Ubuntu-based or RHEL-based but you may need to check your distribution’s configuration to understand how it works.

First thing to note is its location. On Ubuntu-based systems it is located at /etc/ssl/openssl.cnf whereas on Red Hat-based systems it’s located at /etc/pki/tls/openssl.cnf. As for the file itself, though it may look like a regular .ini file, there are some important differences. Probably the biggest differences are:

* Pound signs (#) are used to begin comments, not semi-colons (;).

* Directive values can later on be referenced by treating them as a variable. For instance in the stock Ubuntu 16.04 config, the root of the CA tree is set in dir underneath the [CA_default] section and then subsequently referenced in the directives that point to various components of your CA (for example new_certs_dir and private_key).

* Many directives will point to other sections by way of a directive value. For instance, the hard coded section ca establishes the CA configuration for however many CA’s you end up running on this box. The default CA used is specified by the default_ca directive by setting it equal to the section that defines the CA. That CA configuration section will in turn include a CA signing policy referenced by the policy directive in that section.

Ensuring the CA directory tree is populated

On stock Ubuntu, issue the following commands to create the directory structure that your openssl ca commands will assume already exist:

[root@localhost ~]# mkdir -p /etc/ssl/demoCA/{certs,crl,private,newcerts}
[root@localhost ~]# chmod 755 /etc/ssl/demoCA/{certs,crl,private,newcerts}
[root@localhost ~]#

Open /etc/ssl/openssl.cnf in your preferred editor, locate the [ CA_default ] section and modify the dir directive so that the path to demoCA is absolute (e.g “/etc/ssl/demoCA“).

On RHEL-based systems, this directory structure and configuration has already been created (albeit at /etc/pki/CA instead of /etc/ssl/demoCA as mentioned previously).

In either case, the CA index (a summary of all issued certificates) and a starting serial number will need to be initialized:

[root@localhost ~]# touch $dir/index.txt
[root@localhost ~]# echo 1000 > $dir/serial
[root@localhost ~]#

Where $dir is the root directory of the default CA for your distro.

Once the directories are in place, you need to copy the private key and the root CA cert to the appropriate directories so that openssl can find them later on:

[root@localhost ~]# cp ca.pem $dir/cacert.pem
[root@localhost ~]# cp ca.key $dir/private/cakey.pem
[root@localhost ~]#

Procedure for Signing CSR’s

Now that our software infrastructure is in place, we can start the actual business of issuing certificates. For demonstration purposes, let’s just generate a bogus public/private key with a CSR:

[root@localhost ~]# openssl genrsa -out server.key 4096
Generating RSA private key, 4096 bit long modulus
........................................++
................................++
e is 65537 (0x10001)

[root@localhost ~]# openssl req -new -sha256 -key server.key -out server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:US
State or Province Name (full name) []:North Carolina
Locality Name (eg, city) [Default City]:Durham
Organization Name (eg, company) [Default Company Ltd]:Mr Wammy Incorporated
Organizational Unit Name (eg, section) []:DoIT
Common Name (eg, your name or your server's hostname) []:ca.wammy.io
Email Address []:admin@wammy.ioPlease enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Now that the “client” has a request for us to sign, we can use our root certificate to sign it for them:

[root@localhost ~]# openssl ca -in server.csr -out server.pem
Using configuration from /etc/pki/tls/openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number: 4096 (0x1000)
Validity
Not Before: Nov 25 04:41:44 2017 GMT
Not After : Nov 25 04:41:44 2018 GMT
Subject:
countryName = US
stateOrProvinceName = North Carolina
organizationName = Mr Wammy Incorporated
organizationalUnitName = DoIT
commonName = ca.wammy.io
emailAddress = admin@wammy.io
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
Netscape Comment:
OpenSSL Generated Certificate
X509v3 Subject Key Identifier:
2F:CC:90:79:54:F8:45:D2:2E:4B:F7:64:34:1A:C6:F1:CF:CD:7C:06
X509v3 Authority Key Identifier:
DirName:/C=US/ST=North Carolina/L=Durham/O=Mr Wammy Incorporated/OU=DoIT/CN=ca.wammy.io/emailAddress=admin@wammy.io
serial:A3:CA:7F:58:62:F7:0C:DACertificate is to be certified until Nov 25 04:41:44 2018 GMT (365 days)
Sign the certificate? [y/n]:y1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

A copy of the signed certificate will now live both in the current directory (since we specified an -out option) and in the $dir/newcerts directory where it will be named ${serialNumber}.pem this certificate can now be delivered to the client for use in their web server, SMTP server, whatever it is they’re protecting with SSL/TLS.

That’s basically all there is to it. Once your infrastructure is setup, all subsequent CSR’s users send you can be signed with that same openssl ca -in server.csr -out server.pem command.

Revoking a Signed Certificate

When is revocation useful?

Occasionally, a certificate will be given out but need to be revoked so that others don’t trust it anymore. For the majority of SSL uses, you’re just attempting to ensure private communication between nodes and simply replacing compromised certificates is sufficient. If all you’re interested in is that, then revocation is probably additional complexity you can do without.

The usual example for the usefulness of revocation would be SSL certificates used for client authentication. Compromised must be revoke and the user given new certificates. The latter process is usual and detail above, but the process for revoking them not so much.

Revoking a Signed Certificate

Revocation is pretty straight forward:

[root@localhost ~]# openssl ca -crl_reason keyCompromise -revoke server.pem
Using configuration from /etc/pki/tls/openssl.cnf
Revoking Certificate 1002.
Data Base Updated

In the above we just accessed the ca functions of openssl and instructed it to revoke a certificate called server.pem in the current directory. It then loaded the serial number from the certificate and then updated its local database to reflect its revoked status.

The -crl_reason is interesting because it provides us with a finite number of explanations on why we’re revoking the certificate:

  • keyCompromise :: The private key is known to an untrusted party.
  • CACompromise :: The entire CA was compromised.
  • privilegeWithdrawn :: The security of the previous certificate is fine, there is just a privilege listed in the previous certificate (such a certificate signing, code signing, etc) that they no longer need.
  • cessationOfOperation :: The previous certificate is fine, the CA that issued it is just no longer going to be around to verify/revoke it.
  • affiliationChanged :: Security is fine, user’s role in the organization has just changed.
  • certificateHold :: Certificate is on “hold.” It can still be used but this is an advertisement that it may be subject to revocation soon.
  • superseded :: The user has a newer certificate, likely substantially similar, just newer.
  • removeFromCRL :: An unimplemented feature where several CRL’s can be combined, this one instructs the application assembling the final CRL to not include any certificateHold instances. This has the effect of “unholding” the certificate.
  • unspecified :: Means “dunno, my reason wasn’t in the list.”

This is useful in the event of an audit since it establishes a paper trail if it’s later on found that a user’s account was used. If it was before the revocation but after the determined date of compromise, then their current credentials should be secure still.

Providing the CRL For Consumption

OK great, openssl doesn’t consider the certificate good anymore but who cares about that? I’m trying to prevent an attacker from using the certs to impersonate the user. To do this we have to publish a Certificate Revocation List (“CRL”).

Luckily generating a new CRL from the current database is pretty easy. First we need to make sure we have a valid serial for the CRL’s we create. If it doesn’t already exist create a file at $dir/crlnumber and populate it with a starting serial of 1000.

For example, on RHEL:

[root@localhost ~]# echo 1000 > /etc/pki/CA/crlnumber
[root@localhost ~]#

Then we generate a new CRL saving it to a file:

[root@localhost ~]# openssl ca -gencrl -out newer.crl
Using configuration from /etc/pki/tls/openssl.cnf
[root@localhost ~]#

You can then check this CRL out to make sure our latest revocation is in there:

[root@localhost ~]# openssl crl -in newer.crl -text | grep -A4 "Serial Number: 1002"
Serial Number: 1002
Revocation Date: Nov 25 21:26:47 2017 GMT
CRL entry extensions:
X509v3 CRL Reason Code:
Key Compromise

[root@localhost ~]#

Now that the CRL file has been generated you can then make it accessible to the clients that need it. As per RFC this can be anything you can make a URL out of but is usually an HTTP(S) address that returns a content type of application/pkix-crl along with the DER encoded CRL we generated above.

Alternatively, you can have some sort of automated job pull a static version of your enterprise’s CRL and give that to the application performing the SSL negotiation. For example Apache has the SSLRevocationFile directive for a PEM-encoded CRL file (note: we generated a DER encoded one above so it needs to be converted to PEM before giving it to apache).

Troubleshooting and Helpful Advice

There’s very little in the above that can actually go wrong in the text book use case. In the real world, though you’ll run across unique situations that may just happen to not line up with that cookie cutter situation I presented up above:

  • You may need to adjust the signing policy to allow the CA to sign certificate requests outside the CA’s organization or province.
  • You may need to modify the CA configuration to change defaults for default_days (default lifespan of signed certificates) or default_md (default message digest used when signing) to something more appropriate for your environment.
  • I would recommend against the tendency of many to roll all your openssl commands into a single command unless you absolutely need to. Being able to monitor a certificate’s progression from private key to signed cert will help you in the long run figure out where certain values are coming from or where a particular problem starts. If it’s all one command, you may not be able to guess what’s actually happening.
  • I would also recommend against another common tendency I see which is to use a special CA-specific configuration file. The stock config file that comes with your distribution is perfectly fine and keeping all relevant configuration in one standard place enables people who come behind you to more easily tell what you’re doing.
  • It may be worth investigating the distinguished name defaults in the req_distinguished_name section of openssl.cnf (specifically the directives ending in _default). By setting these to something specific to your organization you reduce the amount of typing required for a CSR and help ensure CSR’s have the correct information listed.

Further Reading

I plan on continually adding/updating this this post so please consider it a living document and submit and anachronisms or omissions. In the mean time, you can find more information on the internet: