OCSP stapling for Erlang/OTP
Posted 2018-07-11 18:42:50.698413
The last few days I’ve been working on a small patch for Erlang/OTP 21 that adds support for server-side OCSP stapling to the ssl
application. It will take more work to get it into good enough shape for a PR, but for now I wanted to try it in the real world to see if I’m on the right track.
This post will show how OCSP stapling might work with Erlang TLS servers in general, and Phoenix in particular, if and when such a feature might be merged into a future OTP release. Feel free to follow along with the patched OTP linked to below, just don’t use it in production, please!
Staples?
OCSP stands for Online Certificate Status Protocol, a more modern approach to certificate revocation than unwieldy Certificate Revocation Lists (CRLs). Now some would argue that all certificate revocation is broken, and that neither CRLs nor OCSP are effective against man-in-the-middle attack with a compromised and revoked certificate, but let’s put that aside for now.
While more modern and efficient in some ways, OCSP has its own issues. A common criticism is the fact that it leaks personal information (browsing behaviour) to the issuing CA, through the OCSP status requests sent by the browsers. OCSP Stapling mitigates this concern: instead of the browser sending a status query from the user’s IP address, the server requests the OCSP response from the CA and sends it in-band as part of the TLS handshake.
Besides the privacy benefits, OCSP stapling also allows the server to cache the response and reuse it for all incoming connections. This eliminates the round-trip to to the OCSP server, reducing the load on the CA’s infrastructure and reducing the overall TLS session establishment latency.
Let’s have a look at the patch, and see what it would take to enable OCSP stapling in a Phoenix application.
The patch
OCSP stapling consists of two parts: the negotiation, done in the ClientHello and ServerHello handshake messages, and the CertificateStatus message that may be sent by the server. If the client includes a ‘status_request’ extension in its ClientHello, and the server confirms by including the extension in ServerHello, the CertificateStatus message is inserted between the Certificate and ServerKeyExchange messages.
In my current implementation (diff), the contents of the ‘status_request’ extension is pretty much opaque to the TLS implementation. Similarly, the CertificateStatus is generated by the application by wrapping the OCSP server’s response, and is relayed by the TLS server as-is. This means there is no need to include any OCSP-specific ASN.1 encoding/decoding in the ssl
application.
Setting the CertificateStatus response
The CertificateStatus response is set through the ssl_options
. When set, it is only sent if the client indicates support for OCSP stapling.
There are several functions and callbacks that can be used to set TLS socket options:
Listener socket creation
In Phoenix this would mean passing the option in the https
section of the endpoint configuration:
config :sample, SampleWeb.Endpoint,
https: [
port: 4001,
keyfile: "priv/cert/privkey.pem",
certfile: "priv/cert/cert.pem",
cacertfile: "priv/cert/chain.pem",
certificate_status: :ssl_handshake.certificate_status(1, ocsp_response),
# ...
However, since the OCSP response must be updated periodically this does not seem very practical, except for testing.
In ‘ssl_accept’
This requires a plain TCP socket acceptor, with the TLS options being set just before starting the handshake with :ssl.ssl_accept
. The latest OCSP response might be read from an ETS table. However, this is not how HTTPS endpoints work in Phoenix.
In ‘handshake_continue’
The new :ssl.handshake/[1,2,3]
in Erlang/OTP 21 allows last minute TLS options configuration based on the client’s capabilities, e.g. for dynamic ECDSA support. The patch adds a status_request
key to the map returned by :ssl.handshake
when the handshake: :hello
option is set. This allows the application to inspect the client request and decide on the appropriate response, to be passed to the :ssl.handshake_continue/[2,3]
function. However, as I wrote in that previous post, this API is not currently supported by Phoenix.
Using the ‘sni_fun’ callback
This is a bit of a hack, because the SNI callback is only called when the ‘server_name_indication’ was sent in the ClientHello message. There is no guarantee that a client including ‘status_request’ will also send ‘server_name_indication’, but in practice all modern browsers that support OCSP stapling will also send SNI. So for now this is what we’ll use.
Let’s add the callback to the Endpoint module:
defmodule SampleWeb.Endpoint do
# [...]
def ssloptions(_) do
case Sample.OCSPResponseCache.get() do
nil -> []
response -> [
certificate_status: :ssl_handshake.certificate_status(1, response)
]
end
end
end
We’ll look at the OCSPResponseCache module in a minute. For now, update the endpoint’s HTTPS configuration to include the sni_fun
option:
config :sample, SampleWeb.Endpoint,
https: [
port: 4001,
sni_fun: &SampleWeb.Endpoint.ssloptions/1,
# ...
Fetching the OCSP response
There is no OCSP client implementation in Erlang/Elixir that I’m aware of (yet). Fortunately we don’t have to implement the protocol to obtain a binary response message that we can use: all we need to do is make an HTTP request to a fixed URL at the OCSP server, and store the HTTP response body.
Here’s an implementation of the OCSPResponseCache module. Since it uses httpc
to make HTTP requests, add :inets
to your application’s extra_applications:
configuration in ‘mix.exs’. Add the OCSPResponseCache worker to your application’s supervisor, and update the OCSP server URL in the configuration file.
To build that URL, follow these instructions to prepare a binary OCSP request for your certificate’s OCSP endpoint and have OpenSSL save it to disk. Then base64-encode the result, URI-escape it and append it to the OCSP server URL. Try it out with ‘curl’ before your copy and paste the URL into the OCSPResponseCache module configuration.
Does it work?
Yes! You’re using it right now: this server is running the patched OTP version, with the configuration described above. Let’s see:
$ openssl s_client -connect blog.voltone.net:443 -servername blog.voltone.net -status -tlsextdebug
CONNECTED(00000005)
TLS server extension "status request" (id=5), len=0
TLS server extension "EC point formats" (id=11), len=2
0000 - 01 .
0002 - <SPACES/NULS>
TLS server extension "renegotiation info" (id=65281), len=1
0001 - <SPACES/NULS>
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify error:num=20:unable to get local issuer certificate
verify return:0
OCSP response:
======================================
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
Version: 1 (0x0)
Responder Id: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
Produced At: Jul 9 05:21:00 2018 GMT
Responses:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 7EE66AE7729AB3FCF8A220646C16A12D6071085D
Issuer Key Hash: A84A6A63047DDDBAE6D139B7A64565EFF3A8ECA1
Serial Number: 0482306BCF29584D9B058EC55503DC7B7E96
Cert Status: good
This Update: Jul 9 05:00:00 2018 GMT
Next Update: Jul 16 05:00:00 2018 GMT
Signature Algorithm: sha256WithRSAEncryption
[...]
Make sure to include the ‘-servername’ argument, otherwise the sni_fun
callback won’t be called, as explained above.
So, it works. Despite the issues with certificate revocation, I believe OCSP stapling is a useful feature. It improves in several ways on other revocation mechanisms, especially when dealing with constrained clients. And it’s a prerequisite for the OCSP Must-Staple certificate extension, which can actually help make certificate revocation work as it should have all along. But that’s something for a future post…
Back