Building mutual authentication systems with openresty

"If the user trusts the CA and can verify the CA's signature, then he can also assume that a certain public key does indeed belong to whoever is identified in the certificate."

Mutual authentication systems are designed to authenticate the two communicating parties to each other. In most scenarios an ssl certificate authenticates the server. Whereas the client is authenticated by the means of a username and password. SSL certificates however, can also be used to authenticate the client. The easiest way to set up a certificate based authentication is to create our own certificate authority. But before we do that lets quickly address the most important question regarding the CA

Why do we even need a CA?

When you get a certificate from a CA what becomes most important is the fact that its authenticity is protected. In other words a CA digitally signs the certificate with its private key. A public (unsigned) certificate can be hijacked and used maliciously. But a digitally signed certificate can't be reproduced since the hijacker does not have a private key to sign the certificate with.

To state simply if you have a CA on your server and you can ensure the protection of the CA's private key any certificate that is signed by the CA comes with a guarantee that it has been issued by a trusted source. An unsigned certificate has no such guarantee.

Now when it comes to server side certificates this guarantee is provided by organizations like GeoTrust, go daddy, letsencrypt etc. This ensures that when our server is accessed by clients like web browser, which already come prepackaged with the public certificates of these organizations, the signature of the server certificate can be verified and the end user can safely assume that his connection is secure.

We are free to create our own CA. It won't be recognized by client tools like web browsers but it provides more than enough trust for using it in, say, backend api services as long as the private key is protected. Enough with the theory though. It's time for some action.

I like my sugar sweet

To create a certificate authority all we need is a couple of lines of openssl magic

--For generating the CA Key--
-- openssl  genrsa  -des3 -passout pass:abba -out ../utils/test_certs/ca.key 4096

--For generating the CA certificate--

--openssl req -new -x509 -days 365 -key ../utils/test_certs/ca.key  -passin pass:abba -out ../utils/test_certs/ca.cert -subj "/C=IN/ST=HR/L=GGN/O=wrinq/"

And that's basically all there is to it. Let's quickly go through the parameters used to generate the key and the certificate.

The `-passout` parameter specifies a password to encrypt the private key with. It ensures that even if the private key gets stolen to attacker is not able to do much with it. The password can also be supplied via a file using the notation `-passout file:path`. Supplying the password via a file has the advantage it it can't be read by the utilities like `ps`.

Similarly the -passin parameter specifies the password to decrypt the file. Just like `-passout` it may be supplied in a file to prevent it from being read from utilities like `ps` .

The `out` parameter accepts the path to which the output file is written.

The `subj` allows you to pass the Distinguished Name (DN) as command line arguments instead of being prompted on the terminal.

If the commands are executed successfully you should see the "ca.cert" and the "ca.key" files in the "test_certs" directory.So much for the generation of the Certificate Authority. Now we may create client certificates and have them signed by our CA.

The steps involved for creation of a client certificate are not all that different from the creation of a CA certificate.

#create a private key for the client	      
openssl  genrsa  -des3 -passout pass:abba -out utils/test_certs/client_new.key

#create a certificate signing request using the clients private key
openssl req -new -key utils/test_certs/client_new.key -out utils/test_certs/client_new.csr -passin pass:abba -subj "/C=IN/ST=HR/L=GGN/O=wrinq/"

#create the client certificate from the CSR and digitally sign it with the CA certificate that we created previously

openssl x509 -req -days 365 -in utils/test_certs/client_new.csr -CA utils/test_certs/ca.cert -CAkey utils/test_certs/ca.key -passin pass:abba -set_serial 01 -out utils/test_certs/shail.cert

The only difference between creating a client certificate and creating a CA is that, while creating a client, we introduce an additional step where it's digitally signed by our CA. And since we've already discussed the importance of signing it with a CA lets move look at the directives needed to configure nginx to accept client certificates.

Nginx configuration for client side certificates

The the ssl configuration for nginx is simple enough

## server certificate configuration		  
ssl_certificate      ./utils/test_certs/test_cert.pem;
ssl_certificate_key  ./utils/test_certs/test_key.pem;

## CA certificate configuration
ssl_client_certificate      ./utils/test_certs/ca.cert;
ssl_verify_client	    on;

## Other ssl directives
ssl_session_timeout  5m;
ssl_protocols  SSLv2 SSLv3 TLSv1 TLSv1.2 TLSv1.1;
ssl_prefer_server_ciphers   on;


The ssl configuration can be divided into three distinct sections. The `ssl_certificate` and the `ssl_certificate_key` directives specify the path for certificate and key files for the server. These directives allow the client to authenticate the server.

The `ssl_client_certificate` directive allows our server to verify the authenticity of the client by ensuring that a certificate send by the client request is digitally signed by our CA.

There are some other ssl directives for configuring the timeout, ciphers to be used etc but they are not pertinent to the discussion.

With the following configuration in place:-

  1. A client will be forced to send a certificate with the connection request
  2. Any request without the certificate will be denied
  3. The certificate will only be accepted if it's signed by our certificate authority. Any unsigned certificate or a certificate that has expired will be explicitly rejected.

Additional processing on parsed certificate parameters

Nginx gives you access to certain parameters that allow you to determine the status of client certificate. They are documented on the nginx's official website. Among the listed variables I found the `ssl_client_s_dn` variable to be most useful because it gives you the distinguished name in a parsed format like so:-


You can parse this information and perform additional processing on the parameters. It's pretty simple if you are using openresty (which you should be since its the best thing in the world). Here's all that you need to do:

                  local dn = ngx.var.ssl_client_s_dn

This one line of code opens up all sorts of possibilities for us. One that I particularly like is to have a database entry on the email field of the distinguished name. This way we ensure that we issue only one certificate per email address (which by the way is a standard practice among the big CAs) and if we have to look up additional details for person to whom the certificate belongs we can simply look up the corresponding email. The certificate ensures the authenticity or the request and the rest of the details are kept safe in the database.

Shouldn't a serial number be used to uniquely identify a certificate?

A serial number uniquely identifies a certificate issued by a CA. That is two certificates issued by two different CA's can have same serial numbers. In our case, since we are our own CA, a serial number seems like a good choice .But our goal here is not to uniquely identify a certificate. Rather to uniquely identify a certificate holder. A valid email account with an organization is a good indicator of authenticity of a certificate holder. A serial number is just a random string that conveys no useful information regarding "the face behind" the certificate. However do experiment for yourself and see what parameter best fits your needs.

Testing our MAS server

There is only so much that can be written about the concepts and strategy. A program is meant to be run. So without wasting anymore words lets see how our code behaves in run time. Clone this repository and follow the instructions in the README file.

You should be able to verify that:-

  1. All requests without a certificate are rejected
  2. All the requests containing a certificate not signed by the CA are rejected
  3. All the requests with an expired or a blacklisted certificate are rejected.

The concept of mutual authentication systems is underutilized only because most of the client side certificates don't work with the browsers. However in the day and age of microservices the client certificates provide a very convent way to authenticate your consumers.

-Akshat Jiwan Sharma

comments powered by Disqus