Dual cert RSA/ECDSA server with Erlang/OTP 21
Posted 2018-07-03 18:55:58.000000
In my previous post about Erlang/OTP 21 I neglected to mention one change in the ssl
application:
OTP-15056 Application(s): ssl
Deprecate ssl:ssl_accept/[1,2,3] in favour of
ssl:handshake/[1,2,3]
At first glance this might look like a mere function rename, no big deal, but it turns out there is more to it than meets the eye. The new :ssl.handshake/[1,2,3]
functions support an option to introspect the capabilities of the client and make last-minute adjustments to the server TLS parameters before proceeding with the handshake. One thing this allows us to do is present an ECDSA certificate to clients that can handle it, while falling back to an RSA certificate for those that can’t.
Before diving in, I just want to make this clear: this is not going to work with current versions of Phoenix, Plug, Cowboy, and other servers. This is not a new ssl
socket option, it is a change in the way the API works. The old APIs are still there, and most applications will likely continue to use those old APIs for a while, until they drop support for pre-21 OTP versions. So with that out of the way, let’s get started…
Why (not yet) ECDSA?
Modern TLS clients have supported ECDSA certificates for a while now, many Certificate Authorities (CAs) will sign ECDSA certificates, and some operate ECDSA roots and intermediates. But most servers still cannot benefit from ECDSA, because switching from RSA to ECDSA could lead to compatibility issues with older clients.
The solution seems obvious: get both an RSA and an ECDSA certificate. However, until now this was not very practical: prior Erlang/OTP versions could not automatically fall back to the RSA certificate, so it was necessary to run separate RSA and ECDSA endpoints. These endpoints would have to run on different TCP ports or use different hostnames (using the ‘sni_fun’ callback).
Let’s look at how the new handshake
function allows the server to select the right certificate dynamically. Please make sure you are on Erlang/OTP 21 if you want to follow along.
Setting up
For starters, we need to generate some certificates. We could use OpenSSL for that, but I think I’ll use this opportunity to plug the X509 package I released yesterday :)
ext = [subject_alt_name: X509.Certificate.Extension.subject_alt_name(["localhost"])]
rsa_key = X509.PrivateKey.new_rsa(4096)
rsa_cert = X509.Certificate.self_signed(rsa_key, "/CN=Test RSA", extensions: ext)
rsa_opts = [
cert: X509.to_der(rsa_cert),
key: {:RSAPrivateKey, X509.to_der(rsa_key)}
]
ec_key = X509.PrivateKey.new_ec(:secp256r1)
ecdsa_cert = X509.Certificate.self_signed(ec_key, "/CN=Test ECDSA", extensions: ext)
ecdsa_opts = [
cert: X509.to_der(ecdsa_cert),
key: {:ECPrivateKey, X509.to_der(ec_key)}
]
The old way
Now let’s start a server on port 8001, using the RSA certificate, and handle an incoming connection with the old API. After the call to transport_accept/1
, initiate a connection with a TLS client in another window, e.g. openssl s_client -connect localhost:8001
:
Application.ensure_all_started(:ssl)
{:ok, server} = :ssl.listen(8001, rsa_opts)
{:ok, sock} = :ssl.transport_accept(server)
:ok = :ssl.ssl_accept(sock)
:ssl.connection_information(sock, [:cipher_suite])
:ssl.close(sock)
The return value from the connection_information/2
function should confirm that we used RSA authentication, probably with ECDHE key exchange (:ecdhe_rsa
).
The new API
Now let’s use the new API and see what it gives us. The new API requires that we use a gen_tcp
server, so we’ll start one on another TCP port:
{:ok, server} = :gen_tcp.listen(8003)
{:ok, sock} = :gen_tcp.accept(server)
{:ok, sock, hello} = :ssl.handshake(sock, handshake: :hello)
At this point the hello
variable contains the client’s capabilities, as extracted from the ClientHello message. For instance, hello.signature_algs
contains the supported signature algorithms (atoms for algorithms recognized by ssl
, numbers for unsupported algorithms).
To proceed with the handshake we’ll call the handshake_continue/2
function, in this case passing in the ECDSA certificate and key:
{:ok, sock} = :ssl.handshake_continue(sock, ecdsa_opts)
:ssl.connection_information(sock, [:cipher_suite])
:ssl.close(sock)
Putting it all together
Here’s a code fragment that actually selects the appropriate certificate for the current client:
# Assuming the gen_tcp server is already running
{:ok, sock} =
with {:ok, tcp_sock} <- :gen_tcp.accept(server),
{:ok, sock, hello} <- :ssl.handshake(tcp_sock, handshake: :hello) do
# Check for ECDSA and NIST P-256 curve support
if Enum.any?(hello.signature_algs, &match?({_, :ecdsa}, &1)) and
(:pubkey_cert_records.namedCurves(:secp256r1) in hello.elliptic_curves) do
:ssl.handshake_continue(sock, ecdsa_opts)
else
:ssl.handshake_continue(sock, rsa_opts)
end
end
:ssl.connection_information(sock, [:cipher_suite])
:ssl.close(sock)
Try it, first with openssl s_client -connect localhost:8003
and then with openssl s_client -connect localhost:8003 -sigalgs "RSA+SHA256"
. Note that you’ll need OpenSSL 1.x for that last command; on MacOS X, make sure you’ve got OpenSSL installed through Homebrew and use the binary in /usr/local/opt/openssl/bin/
.
Again, it will be a while before we will be able to take full advantage of this new capability in Phoenix/Plug, but it is nice to know that it is coming. And of course, if you’re writing your own servers using the ssl
APIs you can start using the new APIs today.
Back