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