x509 certificate authentication using OpenSSL and Apache

1/3/2023

There are times when we need to limit access to a web resource to an authorized user using an authorized device. This is necessary, for instance, when cybersecurity postures, insurance requirements, or compliance rules dictate that access to something must be limited to specific employees and specific devices. In such cases, simply requiring a password is not enough, since passwords are only a proxy for a user — enter x509 client certificates, which we can treat as a proxy for a device.

To initiate x509 certificate authentication the web server must be configured to respond to the initial ClientHello message of the TLS handshake with a CertificateRequest message (see this blog post and this paper for more on TLS handshakes and client certificate authentication). This will cause the user's browser to prompt for a certificate, which once selected, is sent to the server as the payload of a client Certificate message. The server then assesses the certificate's validity, and if everything looks right, completes the handshake.

I'll run through a simple example with Apache and PHP but this is possible with pretty much any web server and programming language.

Broadly, the steps are to:

  1. generate a certificate authority (CA) certificate that will be used to sign client certificates,
  2. then, for each client to:
    • generate a certificate signing request (CSR),
    • sign the CSR with the CA certificate, producing a client certificate,
    • install the CA and client certificates on the operating system and browser, respectively.
  3. configure your web server to require certificate verification.

Step one is straightforward and many organizations will already have an internal CA certificate. If yours does not, you can easily generate one with OpenSSL:

openssl req -x509 -new -sha256 \
    -days 9125 \
    -subj "/C=CA/ST=BC/L=Vancouver/O=Ismailzai Local/OU=IT/CN=Ismailzai Local" \
    -newkey rsa:4096 \
    -keyout ismailzai_ca.key \
    -out ismailzai_ca.crt

Step two is more involved. First, each client will need to generate a CSR, which is a formal request to the CA. This can be as simple as the command below, which also generates a private key for the user (the CN is important and should uniquely identify the client):

openssl req -new \
    -subj "/CN=moismailzai" \
    -newkey rsa:4096 \
    -keyout user_mo.key \
    -out user_mo.csr

With the formal request now generated, it needs to be signed and authorized for client authentication by your CA (the subjectAltName is important and should uniquely identify the client):

openssl x509 -req -days 365 \
    -extensions v3_req -extfile <(printf "[v3_req]\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always,issuer\nbasicConstraints=critical,CA:false\nsubjectAltName=email:mo@ismailzai.com\nextendedKeyUsage=clientAuth\nkeyUsage=digitalSignature\n") \
    -CA ismailzai_ca.crt \
    -CAkey ismailzai_ca.key \
    -CAcreateserial \
    -in user_mo.csr \
    -out user_mo.crt

You now have a certificate that has been authorized by your CA for client authentication. The last part of step two is for both your CA certificate and the client certificate it just signed to be be imported to the client machine. First, combine the signed client certificate and private key into a PKCS #12 bundle:

openssl pkcs12 -export \
    -inkey user_mo.key \
    -in user_mo.crt \
    -out user_mo.p12

Finally, you'll need to:

  1. add your CA certificate as a trusted root CA on your operating system (Microsoft, Apple, Linux)
  2. add your client certificate to your browser (Firefox, Chrome).

With step two complete, all that's left is to configure the server. First, you'll need to copy your trusted root CA to the machine running your web server software. With that done, you just need to add a few lines to your Apache VHOST file, making sure that SSLCACertificateFile points to your certificate:

SSLCACertificateFile /etc/ssl/ismailzai_ca.crt
SSLVerifyClient require
SSLVerifyDepth 1
SSLOptions +StdEnvVars

The SSLVerifyDepth directive instructs Apache to only trust certificates signed directly by the CA certificate listed in SSLCACertificateFile. If you want to sign client certificates with an intermediate certificate instead, you will need to increase the verify depth to allow Apache to look up the chain.

It's worth reviewing the other SSLOptions that are available, but in my example, I've just enabled StdEnvVars, which instructs Apache to set a series of SSL_CLIENT_* server variables. Later, our application can parse these variables to see the metadata from the client certificate that was used to authenticate the client.

Restart the web server and navigate to your website using the configured client device: your browser should now prompt you to select a certificate. Once selected, you will be able to access the website as normal.

With the client validated, our application is ready to proceed with the normal authentication and authorization processes. In PHP, we can dump the $_SERVER variable and verify that the CN from the user's CSR and the subjectAltName from the signed client certificate are in the SSL_CLIENT_S_DN_CN and SSL_CLIENT_SAN_Email_0 keys, respectively.