I’m noodling on a toy project, and I’m trying to set up mutual TLS authentication with pinned self-signed certificates. I’m using a variation of the tak library, which parses the chain for a given cert, puts in its CAcerts, and does custom matching. I take that output and mix up the options to turn it into supporting multiple pins:
pin_certfiles_opts(FileNames) ->
pin_certfiles_opts(FileNames, [], []).
pin_certfiles_opts([], CAs, PinCerts) ->
[{cacerts, CAs},
{verify_fun, {fun verify_pins/3, PinCerts}},
{verify, verify_peer},
{fail_if_no_peer_cert, true}];
pin_certfiles_opts([FileName|FileNames], CAs, PinCerts) ->
%% Lift tak's own parsing, but then extract the option and allow setting
%% multiple certificates. We need this because we have many possibly valid
%% clients that can all be allowed to contact us as a server (whereas a
%% client may expect to only contact one valid server).
[{cacerts, [CADer]},
{verify_fun, {_PinFun, PinCert}} | _] = pin_certfile_opts(FileName),
pin_certfiles_opts(FileNames, [CADer|CAs], [PinCert|PinCerts]).
pin_certfile_opts(FileName) ->
%% Lift tak's own parsing of certs and chains.
{ok, Cert} = file:read_file(FileName),
tak:pem_to_ssl_options(Cert) ++
[{verify, verify_peer},
{fail_if_no_peer_cert, true}].
verify_pins(PinCert, valid_peer, PinCerts) ->
case lists:member(PinCert, PinCerts) of
true -> {valid, PinCerts};
false -> {fail, {peer_cert_unknown, subject(PinCert)}}
end;
verify_pins(_Cert, {extension, _}, PinCerts) ->
{unknown, PinCerts};
verify_pins(PinCert, {bad_cert, selfsigned_peer}, PinCerts) ->
case lists:member(PinCert, PinCerts) of
true -> {valid, PinCerts};
false -> {fail, {bad_cert, selfsigned_peer}}
end;
verify_pins(_Cert, {bad_cert, _} = Reason, _PinCerts) ->
{fail, Reason};
verify_pins(_Cert, valid, PinCerts) ->
{valid, PinCerts}.
This works fine when doing validation client-side:
- a pinned cert used by the server will make the handshake succeed
- an unpinned cert used by the server will make the handshake fail
However, when trying the opposite setup (the server tries to validate the client’s cert with a set of pins), I always get the following error:
{tls_alert,
{certificate_required,
"TLS server: In state wait_cert at tls_handshake_1_3.erl:1469 generated SERVER ALERT: Fatal - Certificate required\n certificate_required"}}
It seems that no matter what I do the server is not getting the required certificate.
The validation function is never called, and the TLS server dies in otp/tls_handshake_1_3.erl at afd664f7619f6c865a60f094f48c8f439f77bb81 · erlang/otp · GitHub .
If I configure in {versions, 'tlsv1.2'}
, everything passes. It’s specifically 1.3 that fails.
Any idea what changed in expectations on either the client or server side to trigger this in TLS 1.3? I’m on SSL 10.8 on erts-13.0 (OTP 25.0) – I haven’t tried 10.8.1 since the changelog didn’t seem relevant to this specific issue.
Erlang slack linked me to Nerves Hub connection fails after upgrading to 1.19.0 · Issue #160 · nerves-project/nerves_system_rpi4 · GitHub but specifically setting {certificate_authorities, false}
does not help anything either. Allow disabling certificate_authorities extension with peer-to-peer setup with TLS 1.3 · Issue #6106 · erlang/otp · GitHub is pending and I figure this could explain it, but I wanted to check to be sure.