Open source software security

Utilizing Client Side x509 Cryptographic Certificates

21 August 2015

Transport Layer Security, or TLS, is a cryptographic protocol that allows for secure communications with web sites. HTTPS generally operates over TLS, on port 443. You can identify TLS connections by the lock icon in your browser URL bar and the "https" designation at the start of a website's URL.

TLS is designed to accomplish two things: protect communications from snooping, and validate the identity of the server you're communicating with. Understanding the first is simple - when a website delivers content over HTTPS all the messages to and from the browser are encrypted. The second feature, identity, is based on a system of public, distributed, trust and allows your browser to perform independent validation of security credentials presented by a site.

To understand how TLS works we need to distinguish between symmetric and asymmetric (or public key) encryption. Encryption consists of two parts: the algorithm and the key. Both forms of encryption utilize a shared algorithm between both ends of a conversation. In addition to a common algorithm, symmetric encryption uses a shared key. A message can be encrypted and decrypted with the same key, which all parties in a communication must know. Symmetric encryption is relatively fast but key management is an issue (i.e. protecting a key during the sharing phase or communicating a key to someone without it being intercepted in transit).

Public key cryptography, or asymmetric encryption, uses a common algorithm but uses two distinct keys, one to encrypt a message and one to decrypt a message. One key is designated "public" and can be shared freely, even observed by an attacker. The other key is "private" and is kept secret, typically never leaving a device. A public key can be used to encrypt a message that can only be decrypted with the private key. Thus, if Alice wants to send a secret message to Bob, Alice merely needs to encrypt the message with Bob's public key. Once encrypted the message can only be decrypted by the holder of Bob's private key. In a two way communication Alice and Bob exchange public keys, then Alice sends Bob messages encrypted with Bob's public key and Bob responds to Alice by encrypting messages with Alice's public key.

Asymmetric encryption includes the capability to digitally sign messages. Typically a message is composed, a hash digest is computed, then the hash is encrypted with the sender's private key, which produces the digital signature. When the message is delivered, the recipient can compute the hash digest of the message, decrypt the encrypted hash, or signature, using the sender's public key, and confirm that a) the contents of the message haven't changed in transit -and- b) that the message was sent by someone with access ot the sender's private key.

Asymmetric encryption is extremely powerful, but that power comes at a price. Asymmetric encryption is typically much slower than symmetric encryption and is much more computationally expensive. Thus it is preferable to use symmetric encryption when possible.

Unfortunately asymmetric encryption also presents one pernicious problem: identity. Because public keys are shared openly, it may be impossible to tell which public key is the correct public key for a specific recipient. For instance, if Bob creates and publishes a public key, and then Evil Eve creates a public key for "Bob" and publishes it, how can someone know which key is actually Bob's key?

The canonical solution to this identity issue is a third party trust. This is a mutually trusted third party that can vouch for the validity of public keys. Typically this assertion of trust is done by signing the public key using the public key of the trusted third party.

Modern computers ship with a root trust store that is used by browsers and other software. This trust store includes the public keys of common third party trust providers (companies like Verisign, Thawte, and others). You can search the web for easy ways to examine (and modify) your trust root.

Whenever your computer receives a new public key it checks for a signature on the key and verifies the signature using a public key from the computer's trust store. If the signature is from a third party that doesn't have a public key in the computer's trust store, or if the signature is invalid, the computer will generally present the user with a warning or abort the encrypted communication.

When your browser begins a TLS session, the server first presents it's public key to the client. The public key is actually a security certificate in x509 format (a specification for public key certificates). The certificate includes the server's name and is signed by a third party trust. The client verifies that the server name on the certificate matches the URL that the client is connecting to and also verifies the signing using the browser's trust store.

If the certificate passes the browser's checks then the browser calculates a symmetric key and encrypts it using the server's public key and sends this back to the server. The server, and client, then use this symmetric key to encrypt any further communications back and forth. This allows the two to communicate using the faster symmetric encryption and also verifies that the server holds the private key associated with the public key, because it is required to decrypt the initial message from the client.

In most cases servers will communicate with anyone. After all, public websites are meant to be accessible by any visitors. However, there are some resources that you may require limited access, such as administration consoles or editor functionality. In these cases a simple login is often implemented, but this solution is vulnerable to brute force password guessing attacks.

Servers can leverage the same public key cryptography to authenticate clients to restrict access to resources. Certain URL's can be protected by requiring a client to submit an x509 certificate that is validated by the server in the same way that the client validates the server key. This certificate must be signed by an CA that the server trusts, and may even require additional criteria (such as a specific common name, domain name, etc.).

Client side certificates utilize strong public key encryption to bolster security for resources. Client side certificates can be used in addition to more common security mechanisms, such as a login form or HTTP Basic Authentication, to provide additional defense in depth for a protected resource.

Client side certificates provide robust security, but there are limitations. The logistical hassle of generating certificates for clients, distributing them securely, and having them signed, is all problematic. For this reason the solution is mostly feasible in situations where the client is unable to provide further authentication, such as in situations where the client is a machine like an Internet of Things (IoT) device, or where the population of authorized users is relatively small. In either of these situations it makes clear sense to use client side certificates to provide further security authorization.

The linchpin of bidirectional trust between the client and the server is a common Certificate Authority. The shared CA allows the client and the server to trust identity assertions validated by the CA. In many environments a commercial third party trust provider is utilized. These companies sell security certificates that will validate using the built in trust store on most devices. This approach may be unnecessarily expensive, however, if an organization wishes to assert trust for clients and servers. It is easy to establish an internal Certificate Authority if you are able to modify the root trust stores on all clients and servers. By creating a public key for your new CA and placing it on all devices you can enable simple bidirectional trust. Of course, the ability to modify trust stores for all parties is a key consideration in the evaluation the viability of this approach.

You may wonder what the difference between a CA key and a site, or client's key is. The answer is an optional field in the x509 certificate that indicates that a specific key is granted the authority to sign other keys. This allows CA's to delegate authority to other intermediary CA's by signing public keys with the signing field.

The power to sign keys is strictly regulated by commercial CA's. However, we can create our own CA key with signing authority, then use that key to sign requests from clients. As long as our CA's public key appears in the root trust store of all parties involved in a communication they can validate certificates signed by our new CA.

Exercise: Building a CA

To begin we'll build a Certificate Authority. Assume that we're building this CA on a CentOS 7 machine that we have root access to. While CentOS and other Linux distributions will generally install with a certain amount of pre-built infrastructure around TLS certificates, in the form of directories and dummy certificates, we'll build our CA completely from scratch to avoid any confusion.

Remember that the CA certificate has the optional signing authority fields. It is important to keep the CA certificates distinct from any server certificates used to secure TLS communications since blending the two can cause confusion with client validation.

Log into your CentOS server as root and create a new directory in the /root directory to keep our CA certificates in using the following commands:

# cd /root
# mkdir ca
# cd ca

OpenSSL is the open source SSL library that we can use to create certificates. By default openssl commands will use configuration options in the openssl.cnf configuration file. We'll need to copy the server's configuration file to our working directory so that the defaults outlined in the file can be found when we use the openssl command. Do this using the command:

# cp /etc/pki/tls/openssl.cnf .

Next we'll create two directories to keep our public and private keys (certificates), certificate revocation lists (CRL's) and newly signed certificate signing requests in using the command:

# mkdir certs crl newcerts private

Finally we generate our CA's keypair. We'll use the RSA encryption algorithm, the triple DES cipher suite and a key size of 4096 bits using the command:

# openssl genrsa -des3 -out private/root-ca.key 4096

Next we need to create a certificate signing request (CSR) from our key pair so that we can self sign our public key. Do this using the command:

# openssl req -new -key private/root-ca.key -out root-ca.csr -config ./openssl.cnf

Finally we'll create our signing certificate and give it a one year lifespan using the command:

# openssl ca  -config ./openssl.cnf -create_serial -out root-ca.crt -days 365 -keyfile private/root-ca.key -selfsign -infiles root-ca.csr

Once this is complete your CA is ready to be used to sign certificates!

Exercise: Protecting a Web Directory

Once we have a CA we can begin to use the CA to sign certificates and establish bi-directional trust. One common use case for this security model is a web resource that you want to protect in such as way so as to provide encrypted communication, but also only allow access to holders of public keys signed by a mutually trusted CA.

We can set up a web directory, served by the Apache web server, in such a way so as to limit access exclusively to holders of trusted security certificates. To do this we'll first set up Apache to operate over HTTPS using a certificate signed by our CA.

To do this we first need to copy our root CA certificate into the directory used system wide to track certificates in /etc/pki/CA/certs. Do this with the command:

# cp /root/ca/root-ca.crt /etc/pki/CA/certs/

Next we need to adjust the Apache configuration to allow for the web server to optionally verify client certificates. You can do this by editing the /etc/httpd/conf.d/ssl.conf file and adding or uncommenting the line in the VirtualHost section:

SSLVerifyClient optional

Once this line is in the file we simply need to create a directory under the web root to protect using the command:

# mkdir /var/www/html/restricted

We need to update the httpd.conf file as well to add a Directory directive to ensure that the newly created web directory will override the optional SSL client verification and instead require clients to provide valid certificates to gain access. To do this edit the httpd.conf file and add the following lines:

<Directory "/var/www/html/restricted">
    SSLVerifyClient require
</Directory>

Finally we need to include an htaccess file in the web directory that can be used to restrict access to legitimate certificates - that is certificates that are signed by a trusted authority. We've added our CA certificate to our system's root trust store by adding it to /etc/pki/CA/certs so we can use it to sign certificates for clients. To add this restriction to our web directory create a new file at /var/www/html/restricted/.htaccess with the following contents:

SSLRequireSSL

Once this change is made restart the web server so that it requires certificates to access the directory and add a single file to the directory so we can ensure everything is working with the commands:

# systemctl restart httpd
# echo "hello world" > /var/www/html/restricted/index.html

You should be able to attempt to visit the website with a browser and get a warning that you're not allowed.

Forbidden due to client certificate restrictions

Exercise: Creating and Using a Client Certificate

Now all we need to do is create a client certificate. Ideally we'd do this on the client side so we're not transmitting private keys. For our example we'll create a client key on the same machine as the user 'user' to demonstrate.

First generate the certificate using the command:

$ cd /home/user
$ openssl genrsa -des3 -out user.client.key 4096

Next create the Certificate Signing Request (CSR) using the command:

$ openssl req -new -key user.client.key -out user.client.csr

Now we can sign this CSR using our CA as root thereby creating a complete x509 certificate with a one year expiration, with the commands:

# cd /root/ca
# openssl x509 -req -days 365 -in /home/user/user.client.csr -CA /root/ca/root-ca.crt -CAkey /root/ca/private/root-ca.key -set_serial 01 -out /home/user/user.client.crt
# chown user.user /home/user/user.client.crt

Finally we need to import the certificate into our browser (say Firefox), but to do this the certificate needs to be in a pkcs12 format. We can reformat the certificate as user with the command:

$ cd /home/user
$ openssl pkcs12 -export -clcerts -in user.client.crt -inkey user.client.key -out user.client.p12

Finally we import this certificate into our browser.

Importing the certificate into the browser

Once this id done we visit the restricted website to confirm that it prompts us to provide a cert and that everything is working properly!

Browser prompt to provide a client certificate