Skip to content

Instantly share code, notes, and snippets.

@MichaelViveros
Created October 22, 2020 23:26
Show Gist options
  • Save MichaelViveros/4cfce1a2ac4f0585fc6b6f06cca07b94 to your computer and use it in GitHub Desktop.
Save MichaelViveros/4cfce1a2ac4f0585fc6b6f06cca07b94 to your computer and use it in GitHub Desktop.

Mutual TLS in Elixir Part 2: Testing and Intermediate CAs

Part 2 will cover testing client authentication and setting up intermediate CAs

In Part 1, we got HTTPoison working to simply and securely send a client certificate as part of Mutual TLS. This covered the basic use case of sending a client certificate to a server.

To test our code, we used server.cryptomix.com which is a test site that requests a client certificate but it doesn't actually verify the client certificate. This isn't realistic since in practice, if the client certificate is expired or if it isn't signed by a trusted CA, the server will return an error.

Additionally, our dummy certificate from Part 1 was not very realistic either. It was signed directly by our dummy root CA. This is a common practice when you're using a homegrown CA internal to your company to do mTLS between your micro-services. But if you're doing mTLS externally with your customers, it's more common for your certificate to be issued from a public CA (Ex. DigiCert).

Part 2 of this series of blog posts will go over those advanced use cases of properly testing client authentication and setting up client certificates that are signed by an intermediate CA.

We will use the same dummy client certificate from Part 1. You can clone https://github.com/MichaelViveros/blog_mtls_elixir to get a template project with that certificate and the necessary dependencies. https://gist.github.com/80f55cd8eb2e19f56122099a66ef5e34

Also feel free to use your own client certificate if you have one.

Testing Client Authentication

There is no existing test site that verifies client certificates so I made an Apache docker image to do this locally. That image will run an Apache server that does two levels of client certificate verification:

  • standard verification - is the certificate expired? signed by a trusted CA?
  • custom verification - is the certificate for a specfic, expected client?

For the custom verification, we can set the ALLOWED_CLIENT_S_DN environment variable to our client certificate's domain name (dummy-mtls-client.com). The server will check the client certificate's domain name against this environment variable.

In a new terminal window, you can pull the image and start the server: https://gist.github.com/6705a0f91fc9e9e1e0b9dbb6c6d4e2a2

Note that we connect a volume from our ./certs directory (containing our dummy certificates) to the Apache server where:

  • server.crt / server.key contain the server certificate/key that the Apache server will present
  • ca.crt contains the list of trusted CAs that the Apache server uses for client verification

ca.crt just contains one certificate for our root CA which the client certificate is signed by. Our server only expects one client to connect to it so it only has one trusted CA but it's also common for servers to accept multiple clients. In that case, you could put Mozilla's canonical list of trusted CAs into ca.crt (like we did in Part 1 with CAStore).

Note that server.crt's common name is dummy-mtls-server.com and clients will expect the server's hostname to match the certficate's common name. Since we're running the server locally, we can update /etc/hosts to map dummy-mtls-server.com to 127.0.0.1. Then we can make requests to dummy-mtls-server.com and the hostname will match the certificte's common name. https://gist.github.com/8e2c7ba2bfc11df559907b9da3c92a39

Here's the code from Part 1 that makes requests with a client certificate:

TODO: insert link to corresponding gist

If you haven't completed Part 1, you'll have to add the CAStore dependency to mix.exs. https://gist.github.com/5f99027ece293c8eda84ee746a5ec524

Let's make a request to the local test server: https://gist.github.com/29483a3a5e132bbe2c94030b5cd1fc9f

Returns: https://gist.github.com/15c10c39879503f7712ad488ed056e5a

Uh-oh. We get an :unknown_ca error. The client doesn't recognize the server certificate's CA. This is because the client is using CAStore's list of canonicaly trusted public CAs but the server certificate is signed by our dummy root CA.

We can fix this by pointing cacertfile to ca.crt. https://gist.github.com/6e3ea6ace4cc08dc229fac69dd319ad3

Now the request to the test server works. https://gist.github.com/958ff76eca42fd97a12da0e39d7c58f3

Returns: https://gist.github.com/114c06a74df63a040b8584a9631d2bb0

Now we know that client authentication works! The test server was able to verify our client certificate and return a 200.

Intermediate CAs

Our client certificate is directly signed by our root CA. The certificate chain looks like: https://gist.github.com/9f37625c4b59ee1bebf71a59bf60a021

But it's common for certificates issued by public CAs to not be signed directly by the trusted root CA. They are signed by an intermediate CA, who in turn has a certificate signed by the root CA. This longer certificate chain is an industry standard which limits the number of root CAs in circulation. The less root CAs there are, the less potential CAs there are which could potentially be hacked.

To mimic this, we can use client2.crt which is signed by an intermediate CA. The certificate chain looks like: https://gist.github.com/12d8b86174d0045f39a4dc6be6c25899

Let's update our code to use client2.crt. https://gist.github.com/138d610c8965e467cbd66c193a887ddb

We'll have to restart the test server so it can expect client2.crt's domain name, dummy-mtls-client-2.com. You can either stop it with ctrl+c or run docker stop. https://gist.github.com/3fbd954975ad9a5cbedd6e9a0cbb27e9

Run the test server again with ALLOWED_CLIENT_S_DN pointing to dummy-mtls-client-2.com: https://gist.github.com/4e1ea038711a59b9b54a09a5813283a6

Make a request to the test server: https://gist.github.com/d8760a2a3ece4118fb13ef8815fffbe8

Returns: https://gist.github.com/dd5d85680a2e226a7c97db70bfdec6d9

Dang it, we got an :unknown_ca error again. The server doesn't recognize the client certificate's CA. The server is only allowing certificates signed by the dummy root CA. but our new client certificate is signed by the dummy intermediate CA.

To fix this, we can update the code to send the intermediate CA certificate alongside the client certificate. Then the server will be able to follow the chain from the client to the intermediate CA to the trusted root CA.

We can actually use one of our existing config options to do this: cacertfile. This option gets used for two things:

  • specifying the trusted CAs that the client will check the server certificate against (this is how we're currently using cacertfile)
  • specifying intermediate certificates in the client certificate chain that the client will send to the server

See the Erlang ssl docs for more info.

We'll have to include both ca.crt and intermediate_ca.crt in cacertfile:

  • ca.crt will allow us to trust the server
  • intermediate_ca.crt will allow the server to trust us

The root_and_intermediate.crt file contains both these certificates.

Here's the updated code: https://gist.github.com/68b6f701367af48559257a7c4a198245

Sending a request to the test server: https://gist.github.com/ab7f3e10bb0bba676dea9bb31d30762a

Returns: https://gist.github.com/17437063e969c80be70032cab38adf0a

Nice! The server was able to successfuly verify our client certificate which was signed by an intermediate CA.

Conclusion

By using an Apache docker image, we were able to verify client authentication works. Then we setup a client certificate signed by an intermediate CA to test out a longer certificate chain (which is more common when using certificates issued by public CAs).

Here's the code that uses the first client certificate (which is directly signed by the root CA) to test client authentication with the Apache test server: https://gist.github.com/6df84bd646f3e2e701da8fe93ed0849e

Here's the code to use the second client certificate (which is signed by an intermediate CA): https://gist.github.com/1d6edeecfecc001fafa4f768c97ce98d

Part 1 went over basic use cases to get mTLS working simply and securely. Part 2 went over more advanced use cases to test out client authentication and intermediate CAs. Now you should be able to get mTLS working whatever way you need it, best of luck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment