Server-side TLS Cert validation woes in TLS 1.3

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.

2 Likes

This is a long shot, but check out this issue…

…and this fix:

Give Erlang 25.0.2 a try.

EDIT: hm I see where you have already looked at the 10.8.1 release notes.

1 Like

Yeah I can confirm that the behaviour is unchanged in OTP-25.0.2, just tried it to be 100% certain.

1 Like

This appears fixed in OTP-26.rc-1 once I took the time to remove the {fail_if_no_peer_cert, true} option from the client-side (which used not to matter in previous versions). My mutual auth with pinned certs now works with exclusive TLS1.3 options.

4 Likes