Tested with OpenSSL 1.0.2j on OS X El Capitan (10.11.6).
Suppose you've got a variety of private servers and services that you want to make available via HTTPS endpoints, endpoints that are not accessible on the public Internet. HTTPS endpoints require a unique certificate for each endpoint host name, certificates that need to be trusted by a large number of clients. It is impractical to keep each client up to date with a trusted store of the complete and ever changing set of server certificates.
The solution is to use a small set of CA certificates that is distributed to each client's trusted certificate store, and then issue server certificates that have been signed by one of those CA certificates. Then server certificates can be created at will without the need to update every potential client that might encounter those server certificates.
The rest of this post assumes a local CA certificate and certificate database generates as described in Create a CA cert on local machine. This solution depends that some of the files and directories created in the process of generating the CA cert as per that TIL.
We want to make sure certain X509 extended attributes are set. To do that, update the OpenSSL config file (e.g. ~/.CA/ca_2016_openssl.cnf
) with this section:
[ v3_usr ]
# Extensions for a typical server certificate
basicConstraints = critical, CA:false
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = critical, serverAuth, clientAuth
I typically put that near the [ v3_ca ]
section.
Generating the server certificate is similar to generating a CA cert. We create a private key and a CSR, and then we generate a certificate that contains the public key corresponding to the private key. But we don't add a challenge passphrase to encrypt the private key (because we don't want web servers and other services to have to supply the passphrase when they start or when they use the private key in the TLS handshaking). And we use the same OpenSSL configuration file as we used to create the CA. And we sign the certificate using the private key associated with the CA certificate.
This script does all that:
#! /bin/bash
#
# Acquire arguments
#
if [ -z "$1" ]; then
echo "Please supply the FQDN"
echo "No action taken."
exit 2
fi
FQDN_=$1
# Initialization
#
SSLEAY_CONFIG="-config ./ca_2016_openssl.cnf"
ROOTCA_YEAR="2016"
CERT_SUBJ="/C=US/ST=TX/L=Houston/O=Testing\ Purposes/OU=HQ"
CERT_SUBJ="${CERT_SUBJ}/CN=${FQDN_}/emailAddress=none@none.org"
# Identify Root CA directories
#
ROOTCA_DIR="root_ca_${ROOTCA_YEAR}"
ROOTCA_CERTS_DIR="${ROOTCA_DIR}/certs"
ROOTCA_CRL_DIR="${ROOTCA_DIR}/crl"
ROOTCA_NEWCERTS_DIR="${ROOTCA_DIR}/newcerts"
ROOTCA_PRIVATE_DIR="${ROOTCA_DIR}/private"
ROOTCA_PASS="file:.ca_${ROOTCA_YEAR}_passphrase"
# Identify Cert directories
#
CERT_DIR="certs/${FQDN_}"
CERT_PRIVATE_DIR="${CERT_DIR}/private"
# Create the cert
#
mkdir "${CERT_DIR}"
mkdir "${CERT_PRIVATE_DIR}"
chmod 700 "${CERT_PRIVATE_DIR}"
openssl req $SSLEAY_CONFIG -new \
-nodes -keyout "${CERT_PRIVATE_DIR}/certkey.pem" \
-out "${CERT_DIR}/certreq.pem" \
-subj "${CERT_SUBJ}"
chmod 600 "${CERT_PRIVATE_DIR}/certkey.pem"
openssl ca $SSLEAY_CONFIG \
-cert "${ROOTCA_DIR}/rootCAcert.pem" \
-keyfile "${ROOTCA_PRIVATE_DIR}/rootCAkey.pem" \
-passin "$ROOTCA_PASS" \
-policy policy_anything \
-days 90 \
-out "${CERT_DIR}/cert.pem" \
-extensions v3_usr \
-infiles "${CERT_DIR}/certreq.pem"
# Validate the cert
#
openssl verify -CAfile "${ROOTCA_DIR}/rootCAcert.pem" "${CERT_DIR}/cert.pem"
You'll want to change these variables:
- SSLEAY_CONFIG
- ROOTCA_YEAR
- ROOTCA_SUBJ
cd ~/.CA
and run the cert_make.sh
script. Give it one argument that is the fully qualified domain name of the service host, e.g., "test.example.dev
". You'll see output something like this (from the command ./cert_make.sh test.example.dev
):
Generating a 2048 bit RSA private key
.+++
.....................+++
writing new private key to 'certs/test.example.dev/private/certkey.pem'
-----
Using configuration from ./ca_2016_openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number:
fc:59:6f:1e:0f:65:e6:43
Validity
Not Before: Dec 28 02:53:57 2016 GMT
Not After : Mar 28 02:53:57 2017 GMT
Subject:
countryName = US
stateOrProvinceName = TX
localityName = Houston
organizationName = Testing Purposes
organizationalUnitName = HQ
commonName = test.example.dev
emailAddress = none@none.org
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage: critical
TLS Web Server Authentication, TLS Web Client Authentication
Netscape Comment:
OpenSSL Generated Certificate
X509v3 Subject Key Identifier:
FB:9D:7C:DC:1B:7A:18:63:67:00:84:43:A7:0B:3E:DF:8D:5C:67:3B
X509v3 Authority Key Identifier:
keyid:59:05:10:61:58:22:D2:86:A5:E2:F1:99:57:5A:36:60:3C:58:9D:EA
Certificate is to be certified until Mar 28 02:53:57 2017 GMT (90 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
certs/test.example.dev/cert.pem: OK
(Note that you will have to respond to a couple interactive prompts.)
The important files resulting from this are:
- Private key:
~/.CA/certs/test-example-dev/private/certkey.pem
- Server certificate:
~/.CA/certs/test-example-dev/cert.pem
The openssl ca
command maintains a record of the server certificates. When you generate a new server certificate, the CA "database" updates the sequence number, stores a copy of the cert, and adds a line to an index.txt
file (which is just a list of each cert and it's subject information).
If you create a certificate and don't like how it came out, then you'll need to manually remove that cert from the "database" before generating another with the same subject information. It's pretty easy to do ...
- Look in the
./index.txt
file and note the sequence number of the cert (this is the 3rd field) - Remove the certificate file from the
./newcerts
folder that corresponds to that sequence number. - Remove that line from the
./index.txt
file.
That's it.
Note that if you've already issued the certificate and want to issue another with the same subject then you'll need to invalidate the certificate (which involves adding it to he CRL). Doing that is a topic for another day and another TIL.
When you use the certificate with a web server or other HTTPS endpoint then you'll need to "chain" together the CA cert and the server cert, and you'll need to provide the endpoint with the private key.
You chain the cert by appending PEM files. The order is important. Chain from outermost to innermost. Each item in the "chain" is to be verified by the subsequent item in the "chain". So you'd do something like this:
cat ~/.CA/certs/test.example.dev/cert.pem \
~/.CA/root_ca_2016/rootCAcert.pem > myserver.pem
For the general example of creating a CA cert and using it to create server certs, thanks goes to this superuser.com post.
For the tip about the sequencing of PEM files when building a chain PEM file, thanks goes to this Stack Overflow post.
© 2016 Dave Hein
This work by Dave Hein is licensed under a Creative Commons Attribution 4.0 International License.