I’m developing a Phoenix server that needs to terminate SSL for multiple domains with certificates acquired via Certbot & stored in database in order to support the custom domains feature in my app. I’m passing sni_fun
callback to https
keyword list in Phoenix like so:
config :my_app, MyAppWeb.Endpoint,
# ...
https: [
# ...
sni_fun: &MyAppWeb.Certs.sni_fun/1
]
which works perfectly fine as long as it relies on filesystem by passing certfile
, keyfile
and cacertfile
in the sni function like so:
def sni_fun(domain) do
domain = List.to_string(domain)
certs_dir = Path.join(:code.priv_dir(:my_app), "cert")
certfile = Path.join(certs_dir, "#{domain}.pem")
cacertfile = Path.join(certs_dir, "#{domain}_chain.pem")
keyfile = Path.join(certs_dir, "#{domain}_key.pem")
certs = [certfile: certfile, cacertfile: cacertfile, keyfile: keyfile]
get_and_write_certs(domain, certs)
certs
end
Notice that
certfile
,cacertfile
andkeyfile
map tocert.pem
,chain.pem
andprivkey.pem
obtained by certbot respectively.
But this means that I have to rely both on database and filesystem to deliver certificates. This bites when trying to figure out a proper & efficient caching strategy considering there’s already a cache in place. I’ve been trying to use equivalent cert
, cacerts
and key
options that appear both in erlang SSL docs and ranch docs, with this code being what I believe is the closest to SSL and ranch specs as well as notes in this thread about HTTPoison:
def sni_fun(domain) do
domain = List.to_string(domain)
{cert_pem_string, cacerts_pem_string, key_pem_string} = get_certs(domain)
cert = read_pem(cert_pem_string) |> hd() |> elem(1)
cacerts = read_pem(cacerts_pem_string) |> Enum.map(&elem(&1, 1))
key = read_pem(key_pem_string) |> hd()
[cert: cert, cacerts: cacerts, key: key]
end
defp read_pem(pem_string) do
pem_string
|> :public_key.pem_decode()
|> Enum.map(fn entry ->
entry = :public_key.pem_entry_decode(entry)
type = elem(entry, 0)
{type, :public_key.der_encode(type, entry)}
end)
end
This doesn’t work yielding following error in server log:
[info] TLS :server: In state :hello at tls_connection.erl:1359 generated SERVER ALERT: Fatal - Handshake Failure
- :malformed_handshake_data
Even though when IO.inspecting the values all seems legit and according to above specs:
[
cert: <<48, 130, 5, 99, 48, 130, 4, 75, 160, 3, 2, 1, 2, 2, 19, 0, 250, 215,
47, 160, 210, 189, 235, 118, 145, 90, 123, 29, 116, 249, 12, 148, 109, 65,
48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, ...>>,
cacerts: [
<<48, 130, 5, 91, 48, 130, 3, 67, 160, 3, 2, 1, 2, 2, 16, 77, 244, 43, 149,
209, 238, 155, 58, 76, 46, 179, 59, 141, 16, 93, 214, 48, 13, 6, 9, 42,
134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, ...>>,
<<48, 130, 5, 84, 48, 130, 4, 60, 160, 3, 2, 1, 2, 2, 17, 0, 237, 93, 91,
201, 109, 251, 223, 77, 62, 205, 106, 73, 141, 209, 179, 199, 48, 13, 6,
9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, ...>>
],
key: {:RSAPrivateKey,
<<48, 130, 4, 163, 2, 1, 0, 2, 130, 1, 1, 0, 172, 232, 46, 48, 27, 142, 154,
67, 197, 56, 70, 245, 47, 232, 199, 248, 192, 53, 199, 211, 94, 83, 73, 35,
24, 181, 123, 43, 194, 15, 191, 59, 16, ...>>},
]
Don’t worry, although trimmed it’s not a production certificate
I’ve also tried other variations e.g. by skipping the pem_entry_decode
+ der_encode
calls (since pem_decode
already seems to return valid DER encoded binary, just with extra element in entity tuple). Nothing seems to work unless returning to fs-based implementation…
So has anyone figured out any way to turn certificates (returned by Let’s Encrypt or not) into an in-memory representation that phoenix / cowboy / ranch / erlang ssl would consume properly? Or do you see any mistake that I’m making here? This could be useful for many other use cases like configuring certs in config/runtime.exs
from various secret stores without relying on filesystem.