otp
otp copied to clipboard
TLS handshake issue
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
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.
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 ?
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!
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.
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])
@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.
@itssundeep @reisub will my PR solve your use case?
ping!
@IngelaAndin using code from your branch fixes the issue for our use case - thank you!