otp icon indicating copy to clipboard operation
otp copied to clipboard

TLS handshake issue

Open itssundeep opened this issue 2 years ago • 9 comments

Describe the bug Fail to establish TLS connection when version is not specified, but works when version is set to 1.2 at the client side. This issue is found when server is started with verify_peer and the request for certificate returns empty list. Error on client side:

1> ssl:start().
ok
2> ssl:connect("localhost", 9999, [
2>  {certfile, "tls-gen/basic/result/client_certificate.pem"},
2>  {keyfile, "tls-gen/basic/result/client_key.pem"}
2>  ], infinity).
=WARNING REPORT==== 24-Jun-2022::10:24:37.745907 ===
Description: "Authenticity is not established by certificate path validation"
     Reason: "Option {verify, verify_peer} and cacertfile/cacerts is missing"

{ok,{sslsocket,{gen_tcp,#Port<0.5>,tls_connection,undefined},
               [<0.115.0>,<0.114.0>]}}
3> =NOTICE REPORT==== 24-Jun-2022::10:24:37.919848 ===
TLS client: In state connection received SERVER ALERT: Fatal - Certificate required

Client side error with log_level debug.

>>> Handshake, Certificate
[{certificate_request_context,<<>>},{certificate_list,[]}]
writing (30 bytes) TLS 1.2 Record Protocol, application_data
0000 - 17 03 03 00 19 57 1e 75  42 83 a8 79 40 b8 1a bf    .....W.uB..y@...
0010 - 63 af 5e f0 10 c4 cd 2f  6d e2 51 19 3f f7          c.^..../m.Q.?.
>>> Handshake, Finished
[{verify_data,<<133,128,28,173,99,120,66,202,147,101,220,174,217,147,30,69,
                44,180,236,191,254,162,174,41,16,136,150,72,113,139,73,47,
                224,94,213,71,59,81,69,24,45,119,68,15,31,3,195,92>>}]
writing (74 bytes) TLS 1.2 Record Protocol, application_data
0000 - 17 03 03 00 45 81 54 11  7b 5b af 36 c8 e2 85 3a    ....E.T.{[.6...:
0010 - f6 7b b4 5f cf ea 87 cc  79 c6 bb 71 74 51 ea 99    .{._....y..qtQ..
0020 - a1 d0 7b 60 c3 26 fd 0d  ee d7 91 60 5c d6 6f c6    ..{`.&.....`\.o.
0030 - 1b ea 8c 5d 5d 38 df 7f  02 dc 2c 49 84 aa b1 15    ...]]8....,I....
0040 - 49 7f 41 49 62 3c ba 63  72 fd                      I.AIb<.cr.
{ok,{sslsocket,{gen_tcp,#Port<0.5>,tls_connection,undefined},
               [<0.119.0>,<0.118.0>]}}
reading (24 bytes) TLS 1.2 Record Protocol, application_data
0000 - 17 03 03 00 13 f0 00 e4  0a 92 fd b3 5d bf cb e9    ............]...
0010 - cd 75 83 89 8d 83 87 42                             .u.....B
3> =NOTICE REPORT==== 24-Jun-2022::11:44:00.781779 ===
TLS client: In state connection received SERVER ALERT: Fatal - Certificate required

writing (24 bytes) TLS 1.2 Record Protocol, application_data
0000 - 17 03 03 00 13 7b c1 45  25 0b c5 92 5e 06 fb 6a    .....{.E%...^..j
0010 - 3b 81 43 9e e4 c2 d3 04                             ;.C.....

Error on server side:

Error: {'EXIT',
           {{badmatch,
                {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"}}}},
            [{erl_eval,expr,6,[{file,"erl_eval.erl"},{line,496}]},
             {erl_eval,exprs,6,[{file,"erl_eval.erl"},{line,136}]},
             {erl_eval,expr,6,[{file,"erl_eval.erl"},{line,480}]},
             {erl_eval,expr,6,[{file,"erl_eval.erl"},{line,492}]},
             {erl_eval,exprs,6,[{file,"erl_eval.erl"},{line,136}]},
             {shell,exprs,7,[{file,"shell.erl"},{line,691}]},
             {shell,eval_exprs,7,[{file,"shell.erl"},{line,647}]},
             {shell,eval_loop,3,[{file,"shell.erl"},{line,632}]}]}}
Restarting socket
=NOTICE REPORT==== 24-Jun-2022::10:24:37.909543 ===
TLS server: In state wait_cert at tls_handshake_1_3.erl:1469 generated SERVER ALERT: Fatal - Certificate required
 - certificate_required

To Reproduce

# generate certs and setup server.
ssl:start().
F = fun L() ->
    {ok, ListenSocket} = ssl:listen(9999,
        [
            {verify, verify_peer},
            {fail_if_no_peer_cert, true},
       	    {cacertfile, "tls-gen/basic/result/ca_certificate.pem"},
            {certs_keys, [#{certfile => "tls-gen/basic/result/server_certificate.pem", keyfile => "tls-gen/basic/result/server_key.pem"}]},
            {reuseaddr, true}
        ]
    ),
    R = fun L2() ->
        {ok, TLSTransportSocket} = ssl:transport_accept(ListenSocket),
        {ok, Socket} = ssl:handshake(TLSTransportSocket),
        io:format("~p", [{ssl:peercert(Socket), Socket}]),
        L2()
    end,
    Error = catch R(),
    io:format("Error: ~p \nRestarting socket\n", [Error]),
    ssl:close(ListenSocket),
    L()
end.
F().

Or if we use openssl to start the server.

openssl s_server -port 9999 -key tls-gen/basic/result/server_key.pem -cert tls-gen/basic/result/server_certificate.pem -Verify 1 -CAfile tls-gen/basic/result/ca_certificate.pem -debug
# start client
ssl:start().
ssl:connect("localhost", 9999, [
 {certfile, "tls-gen/basic/result/client_certificate.pem"},
 {keyfile, "tls-gen/basic/result/client_key.pem"}
 ], infinity).

Expected behavior I would expect connection setup to be successful.

Affected versions I tried to reproduce the issue on 25.0.2 on a mac, but the issue exists even on linux.

% erl
Erlang/OTP 25 [erts-13.0.2] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns] [dtrace]

Eshell V13.0.2  (abort with ^G)
1> crypto:info().
#{compile_type => normal,
  cryptolib_version_compiled => "OpenSSL 1.1.1o  3 May 2022",
  cryptolib_version_linked => "OpenSSL 1.1.1p  21 Jun 2022",
  link_type => dynamic,otp_crypto_version => "5.1.1"}
2> ssl:info().
** exception error: undefined function ssl:info/0
3> ssl:version().
** exception error: undefined function ssl:version/0
4> ssl:versions().
[{ssl_app,"10.8.2"},
 {supported,['tlsv1.3','tlsv1.2']},
 {supported_dtls,['dtlsv1.2']},
 {available,['tlsv1.3','tlsv1.2','tlsv1.1',tlsv1]},
 {available_dtls,['dtlsv1.2',dtlsv1]},
 {implemented,['tlsv1.3','tlsv1.2','tlsv1.1',tlsv1]},
 {implemented_dtls,['dtlsv1.2',dtlsv1]}]
5>
 >brew list erlang
/usr/local/Cellar/erlang/25.0.2/bin/ct_run
/usr/local/Cellar/erlang/25.0.2/bin/dialyzer
/usr/local/Cellar/erlang/25.0.2/bin/epmd
/usr/local/Cellar/erlang/25.0.2/bin/erl
/usr/local/Cellar/erlang/25.0.2/bin/erlc
/usr/local/Cellar/erlang/25.0.2/bin/escript
/usr/local/Cellar/erlang/25.0.2/bin/run_erl
/usr/local/Cellar/erlang/25.0.2/bin/to_erl
/usr/local/Cellar/erlang/25.0.2/bin/typer

Additional context It looks like this is a problem when server is configured to validate client certificate and client is sending an empty response

itssundeep avatar Jun 24 '22 17:06 itssundeep

Found the line that causes the issue: https://github.com/erlang/otp/commit/e165a3368da#diff-280aa9e67bfae54121bde3ed8b7e1eea6f845f68ca0600c96331fbacb315672bL3034-L3040

Previously, if no issuer found, it will try to use Session = Session0#session{sign_alg = SelectedSignAlg,own_certificates = EncodedChain,private_key = Key}, but after the change it will use Session0, so it won't send the client certificate in handshake if the client itself cannot path validate the client cert.

However I think client should still send the client cert even when cacert is missing. Server can still verify the client cert.

zzydxm avatar Jun 28 '22 16:06 zzydxm

We did keep that behavior for backward compatibility for pre-TLS-1.3 connections.

TLS-1.3 RFC has the following rules:

4.4.2.3. Client Certificate Selection

The following rules apply to certificates sent by the client:

  • The certificate type MUST be X.509v3 [RFC5280], unless explicitly negotiated otherwise (e.g., [RFC7250]).

  • If the "certificate_authorities" extension in the CertificateRequest message was present, at least one of the certificates in the certificate chain SHOULD be issued by one of the listed CAs.

  • The certificates MUST be signed using an acceptable signature algorithm, as described in Section 4.3.2. Note that this relaxes the constraints on certificate-signing algorithms found in prior versions of TLS.

  • If the CertificateRequest message contained a non-empty "oid_filters" extension, the end-entity certificate MUST match the extension OIDs that are recognized by the client, as described in Section 4.2.5.

So rule number two can not be followed unless the client can build the chain, which it really ought to be able to do. SHOULD is a strong recommendation. However, I agree that this clashes with maximum interop considerations, and the functionality that the chain can be reconstructed by the peer it could be acceptable. Maybe for TLS-1.3, this should be made configurable to make it a user tradeoff?! @u3s @dgud ?

IngelaAndin avatar Jun 30 '22 12:06 IngelaAndin

Or maybe only default to sending default "cert chain" when no cert chain could be constructed, that is only the entity cert will be sent!

IngelaAndin avatar Jul 01 '22 05:07 IngelaAndin

one of the certificates in the certificate chain SHOULD be issued by one of the listed CAs

Isn't it saying one of the client certificates should be issued by one of the listed CAs on server side? Although it seems a bit vague here.

zzydxm avatar Jul 05 '22 17:07 zzydxm

We're running in to the same issue in our test suite. As a workaround, we're forcing TLS1.2:

ssl_opts = [
  cacertfile: "...",
  certfile: "...",
  keyfile: "...",
  # workaround for https://github.com/erlang/otp/issues/6105, remove once the underlying issue is fixed
  versions: [:"tlsv1.2"]
]

HTTPoison.request!(method, url, body, headers, [ssl: ssl_opts])

reisub avatar Aug 01 '22 10:08 reisub

@zzydxm Yes, that is what it is saying. If the auth extension is sent then the client should verify that the chain it sends lives up to the requirements. But if the chain is missing we can not perform the check. So I think the compromise is to send the peer cert if the chain certs are missing as the server may then be able to verify the client if it owns the appropriate certs to recreate the chain that possiby is in its auth domain.

IngelaAndin avatar Aug 09 '22 10:08 IngelaAndin

@itssundeep @reisub will my PR solve your use case?

IngelaAndin avatar Aug 09 '22 15:08 IngelaAndin

ping!

IngelaAndin avatar Aug 15 '22 09:08 IngelaAndin

@IngelaAndin using code from your branch fixes the issue for our use case - thank you!

reisub avatar Aug 16 '22 07:08 reisub