3  使用 SSL 应用程序 API

3 使用 SSL 应用程序 API

要查看 ssl 的相关版本信息,请调用 ssl:versions/0

要查看所有支持的密码套件,请调用 ssl:cipher_suites(all, 'tlsv1.3') 。连接可用的密码套件取决于 TLS 版本,以及 TLS-1.3 之前的证书。要查看默认密码套件列表,请将 all 更改为 default。请注意,TLS 1.3 和之前的版本没有共同的密码套件,要列出特定版本的密码套件,请使用 ssl:cipher_suites(exclusive, 'tlsv1.3') 。也可以指定您希望连接使用的特定密码套件。默认情况下,使用最强的可用密码套件。

以下部分展示了使用 Erlang shell 设置客户端/服务器连接的示例。sslsocket 的返回值缩写为 [...],因为它可能非常大,对于用户来说不透明,除了用于模式匹配的目的。

注意

请注意,服务器可以选择是否验证客户端证书,并且需要在两端进行额外的配置才能正常工作。示例中使用的证书和密钥是使用 OTP-25 中引入的 certs_keys 选项提供的。

 1 > ssl:start(), ssl:connect("google.com", 443, [{verify, verify_peer},
    {cacerts, public_key:cacerts_get()}]).
   {ok,{sslsocket, [...]}}

步骤 1:启动服务器端

1 server> ssl:start().
ok

步骤 2:使用备用证书,在本示例中,如果协商 TLS-1.3,则将优先使用 EDDSA 证书,而 RSA 证书将始终用于 TLS-1.2,因为它不支持 EDDSA 算法

2 server> {ok, ListenSocket} =
ssl:listen(9999, [{certs_keys, [#{certfile => "eddsacert.pem",
                                  keyfile => "eddsakey.pem"},
			        #{certfile => "rsacert.pem",
                                  keyfile => "rsakey.pem",
			          password => "foobar"}
			       ]},{reuseaddr, true}]).
{ok,{sslsocket, [...]}}

步骤 3:在 TLS 监听套接字上进行传输接受

3 server> {ok, TLSTransportSocket} = ssl:transport_accept(ListenSocket).
{ok,{sslsocket, [...]}}
注意

ssl:transport_accept/1 和 ssl:handshake/2 是独立的函数,以便可以在专用于处理连接的新 Erlang 进程中调用握手部分

步骤 4:启动客户端

1 client> ssl:start().
ok

请务必配置用于服务器证书验证的可信证书。

2 client> {ok, Socket} = ssl:connect("localhost", 9999,
      [{verify, verify_peer},
      {cacertfile, "cacerts.pem"}, {active, once}], infinity).
{ok,{sslsocket, [...]}}

步骤 5:执行 TLS 握手

4 server> {ok, Socket} = ssl:handshake(TLSTransportSocket).
{ok,{sslsocket, [...]}}
注意

真正的服务器应该使用 ssl:handshake/2,该函数具有超时机制,以避免 DoS 攻击。在本示例中,超时默认为无穷大。

步骤 6:通过 TLS 发送消息

5 server> ssl:send(Socket, "foo").
ok

步骤 7:刷新 shell 消息队列,以查看服务器端发送的消息是否被客户端接收

3 client> flush().
Shell got {ssl,{sslsocket,[...]},"foo"}
ok

将 TCP/IP 连接升级到 TLS 连接主要用于在希望首先进行未加密通信,然后使用 TLS 确保通信信道安全的情况下。请注意,客户端和服务器需要在进行通信的协议中同意进行升级。这种概念通常被称为 STARTLS,并在许多协议中使用,例如 SMTPFTPS 和通过代理的 HTTPS

警告

但是,最高安全建议正逐渐摒弃此类解决方案。

要升级到 TLS 连接

步骤 1:启动服务器端

1 server> ssl:start().
  ok

步骤 2:创建一个普通的 TCP 监听套接字,并确保 active 设置为 false,而不是设置为任何活动模式,否则 TLS 握手消息可能会传递到错误的进程。

2 server> {ok, ListenSocket} = gen_tcp:listen(9999, [{reuseaddr, true},
  {active, false}]).
  {ok, #Port<0.475>}

步骤 3:接受客户端连接

3 server> {ok, Socket} = gen_tcp:accept(ListenSocket).
  {ok, #Port<0.476>}

步骤 4:启动客户端

1 client> ssl:start().
  ok
2 client> {ok, Socket} = gen_tcp:connect("localhost", 9999,  [], infinity).

步骤 5:执行 TLS 握手

4 server> {ok, TLSSocket} = ssl:handshake(Socket, [{verify, verify_peer},
  {fail_if_no_peer_cert, true},
  {cacertfile, "cacerts.pem"},
  {certs_keys, [#{certfile => "cert.pem", keyfile => "key.pem"}]}]).
  {ok,{sslsocket,[...]}}

步骤 6:升级到 TLS 连接。客户端和服务器必须就升级达成一致。在客户端可以成功连接之前,服务器必须准备成为 TLS 服务器。

3 client>{ok, TLSSocket} = ssl:connect(Socket, [{verify, verify_peer},
  {cacertfile, "cacerts.pem"},
  {certs_keys, [#{certfile => "cert.pem", keyfile => "key.pem"}]}], infinity).
{ok,{sslsocket,[...]}}

步骤 7:通过 TLS 发送消息

4 client> ssl:send(TLSSocket, "foo").
      ok

步骤 8:在 TLS 套接字上设置 active once

5 server> ssl:setopts(TLSSocket, [{active, once}]).
      ok

步骤 9:刷新 shell 消息队列,以查看客户端发送的消息是否被服务器接收

5 server> flush().
      Shell got {ssl,{sslsocket,[...]},"foo"}
      ok

获取 TLS/DTLS 版本的默认密码套件列表。将 default 更改为 all 以获取所有可能的密码套件。

1>  Default = ssl:cipher_suites(default, 'tlsv1.2').
    [#{cipher => aes_256_gcm,key_exchange => ecdhe_ecdsa,
    mac => aead,prf => sha384}, ....]

在 OTP 20 中,最好删除所有使用 rsa 密钥交换的密码套件(在 21 中从默认值中删除)

2> NoRSA =
    ssl:filter_cipher_suites(Default,
                            [{key_exchange, fun(rsa) -> false;
			                        (_) -> true
			                    end}]).
    [...]

只选择一些套件

 3> Suites =
    ssl:filter_cipher_suites(Default,
                            [{key_exchange, fun(ecdh_ecdsa) -> true;
			                        (_) -> false
			                    end},
                             {cipher, fun(aes_128_cbc) -> true;
			                  (_) ->false
			              end}]).
    [#{cipher => aes_128_cbc,key_exchange => ecdh_ecdsa,
     mac => sha256,prf => sha256},
     #{cipher => aes_128_cbc,key_exchange => ecdh_ecdsa,mac => sha,
     prf => default_prf}]

通过将 prepend 更改为 append,使某些特定套件成为最优选的,或者最不优选的。

 4>ssl:prepend_cipher_suites(Suites, Default).
  [#{cipher => aes_128_cbc,key_exchange => ecdh_ecdsa,
     mac => sha256,prf => sha256},
   #{cipher => aes_128_cbc,key_exchange => ecdh_ecdsa,mac => sha,
     prf => default_prf},
   #{cipher => aes_256_cbc,key_exchange => ecdhe_ecdsa,
     mac => sha384,prf => sha384}, ...]

从 TLS-1.2 开始,签名算法(在 TLS-1.3 中称为签名方案)是可以协商的,因此也可以配置。这些算法/方案将用于协议消息和证书中的数字签名。

注意

TLS-1.3 方案具有原子名称,而 TLS-1.2 配置是包含一个哈希算法和一个签名算法的两个元素元组。当支持两个版本时,配置可以是这些元素的组合,因为可能会协商两个版本。所有 rsa_pss 基于方案都向后移植到 TLS-1.2,并且也可以在 TLS-1.2 配置中使用。在 TLS-1.2 中,服务器选择的签名算法也会受到所选密码套件的影响,而在 TLS-1.3 中则不会。

使用函数 ssl:signature_algs/2 可以检查系统可能配置的不同方面。例如,如果支持 TLS-1.3 和 TLS-1.2,则 OTP-26 和来自 OpenSSL 3.0.2 的 cryptolib 中的默认 signature_algorithm 列表将如下所示

 1>  ssl:signature_algs(default, 'tlsv1.3').
 %% TLS-1.3 schemes
 [eddsa_ed25519,eddsa_ed448,ecdsa_secp521r1_sha512,
  ecdsa_secp384r1_sha384,ecdsa_secp256r1_sha256,
  rsa_pss_pss_sha512,rsa_pss_pss_sha384,rsa_pss_pss_sha256,
  rsa_pss_rsae_sha512,rsa_pss_rsae_sha384,rsa_pss_rsae_sha256,
  %% Legacy schemes only valid for certificate signatures in TLS-1.3
  %% (would have a tuple name in TLS-1.2 only configuration)
  rsa_pkcs1_sha512,rsa_pkcs1_sha384,rsa_pkcs1_sha256
  %% TLS 1.2 algorithms
  {sha512,ecdsa},
  {sha384,ecdsa},
  {sha256,ecdsa}]

如果要添加对非默认支持算法的支持,应将它们附加到默认列表,因为配置按优先顺序排列,如下所示

    MySignatureAlgs = ssl:signature_algs(default, 'tlsv1.3') ++ [{sha, rsa}, {sha, dsa}],
    ssl:connect(Host,Port,[{signature_algs, MySignatureAlgs,...]}),
    ...

另请参见 ssl:signature_algs/2sign_algo()

Erlang ssl 应用程序能够使用 OpenSSL 引擎提供的私钥,使用以下机制

1> ssl:start().
ok

加载加密引擎,每个使用的引擎应执行一次。例如,动态加载名为 MyEngine 的引擎

2> {ok, EngineRef} =
crypto:engine_load(<<"dynamic">>,
[{<<"SO_PATH">>, "/tmp/user/engines/MyEngine"},<<"LOAD">>],
[]).
{ok,#Ref<0.2399045421.3028942852.173962>}

创建一个包含引擎信息和引擎使用的算法的映射

3> PrivKey =
 #{algorithm => rsa,
   engine => EngineRef,
   key_id => "id of the private key in Engine"}.

在 ssl key 选项中使用映射

4> {ok, SSLSocket} =
ssl:connect("localhost", 9999,
            [{cacertfile, "cacerts.pem"},
	    {certs_keys, [#{certfile => "cert.pem", key => PrivKey}]}
	    ], infinity).

另请参见 加密文档

NSS 密钥日志调试功能可供授权用户使用,例如,允许 Wireshark 解密 TLS 数据包。

服务器(使用 NSS 密钥日志记录)

    server() ->
        application:load(ssl),
        {ok, _} = application:ensure_all_started(ssl),
        Port = 11029,
        LOpts = [{certs_keys, [#{certfile => "cert.pem", keyfile => "key.pem"}]},
        {reuseaddr, true},
        {versions, ['tlsv1.2','tlsv1.3']},
        {keep_secrets, true} %% Enable NSS key log (debug option)
        ],
        {ok, LSock} = ssl:listen(Port, LOpts),
        {ok, ASock} = ssl:transport_accept(LSock),
        {ok, CSock} = ssl:handshake(ASock).

导出机密

      {ok, [{keylog, KeylogItems}]} = ssl:connection_information(CSock, [keylog]).
      file:write_file("key.log", [[KeylogItem,$\n] || KeylogItem <- KeylogItems]).

客户端可以通过在初始握手消息中发送会话 ID 来请求重用之前在该客户端和服务器之间建立的会话。服务器可以选择是否同意重用。如果同意,服务器将发回 ID;如果不同意,将发送新的 ID。ssl 应用程序有几种选项可用于处理会话重用。

在客户端,ssl 应用程序将保存会话数据,以尝试自动代表 Erlang 节点上的客户端进程重用会话。请注意,出于安全原因,只有已验证的会话才会保存,也就是说,会话恢复依赖于原始握手中的证书验证。为了最大限度地减少内存消耗,只有唯一的会话才会保存,除非为以下选项 {reuse_sessions, boolean() | save} 指定了特殊值 save,在这种情况下,将执行完整的握手,并且该特定会话将在握手返回之前保存。可以使用 ssl:connection_information/1 函数检索会话 ID 甚至包含会话数据的非透明二进制文件。可以使用 {reuse_session, SessionId} 显式重用已保存的会话(由 save 选项保证)。此外,客户端还可以重用 ssl 应用程序未保存的会话,方法是使用 {reuse_session, {SessionId, SessionData}}

注意

当使用显式会话重用时,客户端需要确保要重用的会话用于正确的服务器,并且已经过验证。

以下是一个客户端示例,为了便于阅读,将其分为多个步骤。

步骤 1 - 自动会话重用

1> ssl:start().
ok

2> {ok, C1} = ssl:connect("localhost", 9999, [{verify, verify_peer},
					      {versions, ['tlsv1.2']},
					      {cacertfile, "cacerts.pem"}]).
{ok,{sslsocket,{gen_tcp,#Port<0.7>,tls_connection,undefined}, ...}}

3> ssl:connection_information(C1, [session_id]).
{ok,[{session_id,<<95,32,43,22,35,63,249,22,26,36,106,
                   152,49,52,124,56,130,192,137,161,
                   146,145,164,232,...>>}]}

%% Reuse session if possible, note that if C2 is really fast the session
%% data might not be available for reuse.
4> {ok, C2} = ssl:connect("localhost", 9999, [{verify, verify_peer},
					      {versions, ['tlsv1.2']},
					      {cacertfile, "cacerts.pem"},
					      {reuse_sessions, true}]).
{ok,{sslsocket,{gen_tcp,#Port<0.8>,tls_connection,undefined}, ...]}}

%% C2 got same session ID as client one, session was automatically reused.
5> ssl:connection_information(C2, [session_id]).
{ok,[{session_id,<<95,32,43,22,35,63,249,22,26,36,106,
                   152,49,52,124,56,130,192,137,161,
                   146,145,164,232,...>>}]}

步骤 2 - 使用 save 选项

%% We want save this particular session for
%% reuse although it has the same basis as C1
6> {ok, C3} = ssl:connect("localhost", 9999, [{verify, verify_peer},
					      {versions, ['tlsv1.2']},
					      {cacertfile, "cacerts.pem"},
					      {reuse_sessions, save}]).
{ok,{sslsocket,{gen_tcp,#Port<0.9>,tls_connection,undefined}, ...]}}

%% A full handshake is performed and we get a new session ID
7> {ok, [{session_id, ID}]} = ssl:connection_information(C3, [session_id]).
{ok,[{session_id,<<91,84,27,151,183,39,84,90,143,141,
                   121,190,66,192,10,1,27,192,33,95,78,
                   8,34,180,...>>}]}

%% Use automatic session reuse
8> {ok, C4} = ssl:connect("localhost", 9999, [{verify, verify_peer},
					      {versions, ['tlsv1.2']},
					      {cacertfile, "cacerts.pem"},
					      {reuse_sessions, true}]).
{ok,{sslsocket,{gen_tcp,#Port<0.10>,tls_connection,
                        undefined}, ...]}}

%% The "saved" one happened to be selected, but this is not a guarantee
9> ssl:connection_information(C4, [session_id]).
{ok,[{session_id,<<91,84,27,151,183,39,84,90,143,141,
                   121,190,66,192,10,1,27,192,33,95,78,
                   8,34,180,...>>}]}

%% Make sure to reuse the "saved" session
10> {ok, C5} = ssl:connect("localhost", 9999, [{verify, verify_peer},
					       {versions, ['tlsv1.2']},
					       {cacertfile, "cacerts.pem"},
					       {reuse_session, ID}]).
{ok,{sslsocket,{gen_tcp,#Port<0.11>,tls_connection,
                        undefined}, ...]}}

11> ssl:connection_information(C5, [session_id]).
{ok,[{session_id,<<91,84,27,151,183,39,84,90,143,141,
                   121,190,66,192,10,1,27,192,33,95,78,
                   8,34,180,...>>}]}

步骤 3 - 显式会话重用

%% Perform a full handshake and the session will not be saved for reuse
12> {ok, C9} =
ssl:connect("localhost", 9999, [{verify, verify_peer},
				{versions, ['tlsv1.2']},
		                {cacertfile, "cacerts.pem"},
                                {reuse_sessions, false},
	                        {server_name_indication, disable}]).
{ok,{sslsocket,{gen_tcp,#Port<0.14>,tls_connection, ...}}

%% Fetch session ID and data for C9 connection
12> {ok, [{session_id, ID1}, {session_data, SessData}]} =
	ssl:connection_information(C9, [session_id, session_data]).
{ok,[{session_id,<<9,233,4,54,170,88,170,180,17,96,202,
                   85,85,99,119,47,9,68,195,50,120,52,
                   130,239,...>>},
     {session_data,<<131,104,13,100,0,7,115,101,115,115,105,
                     111,110,109,0,0,0,32,9,233,4,54,170,...>>}]}

%% Explicitly reuse the session from C9
13> {ok, C10} = ssl:connect("localhost", 9999, [{verify, verify_peer},
						{versions, ['tlsv1.2']},
						{cacertfile, "cacerts.pem"},
						{reuse_session, {ID1, SessData}}]).
{ok,{sslsocket,{gen_tcp,#Port<0.15>,tls_connection,
                        undefined}, ...}}

14> ssl:connection_information(C10, [session_id]).
{ok,[{session_id,<<9,233,4,54,170,88,170,180,17,96,202,
                   85,85,99,119,47,9,68,195,50,120,52,
                   130,239,...>>}]}

步骤 4 - 无法仅通过 ID 重用显式会话

%% Try to reuse the session from C9 using only the id
15> {ok, E} = ssl:connect("localhost", 9999, [{verify, verify_peer},
				              {versions, ['tlsv1.2']},
				              {cacertfile, "cacerts.pem"},
					      {reuse_session, ID1}]).
{ok,{sslsocket,{gen_tcp,#Port<0.18>,tls_connection,
                        undefined}, ...}}

%% This will fail (as it is not saved for reuse)
%% and a full handshake will be performed, we get a new id.
16>  ssl:connection_information(E, [session_id]).
{ok,[{session_id,<<87,46,43,126,175,68,160,153,37,29,
                   196,240,65,160,254,88,65,224,18,63,
                   18,17,174,39,...>>}]}

在服务器端,选项 {reuse_sessions, boolean()} 决定服务器是否保存会话数据并允许会话重用。选项 {reuse_session, fun()} 可以进一步自定义,该选项可以为会话重用引入本地策略。

TLS 1.3 引入了一种新的安全方式,可以使用会话票据恢复会话。会话票据是一个不透明的数据结构,在客户端尝试使用先前成功握手中的密钥材料恢复会话时,会在 ClientHello 的 pre_shared_key 扩展中发送该数据结构。

会话票据可以是有状态的或无状态的。有状态会话票据是数据库引用(会话票据存储),并与有状态服务器一起使用,而无状态票据是一个自加密和自认证的数据结构,包含加密密钥材料和状态数据,从而可以使用无状态服务器恢复会话。

有状态或无状态的选择取决于服务器的要求,因为会话票据对于客户端来说是不透明的。通常,有状态票据更小,并且服务器可以保证票据只使用一次。无状态票据包含额外数据,在服务器端需要较少的存储空间,但它们提供的反重放保证不同。另请参见 TLS 1.3 中的反重放保护

会话票据由服务器在新建立的 TLS 连接上发送。发送的票据数量及其生存期可以通过应用程序变量进行配置。另请参见 SSL 的配置

会话票据受应用程序流量密钥保护,而在无状态票据中,不透明的数据结构本身是自加密的。

带有自动和手动会话恢复的示例

      {ok, _} = application:ensure_all_started(ssl).
      LOpts = [{certs_keys, [#{certfile => "cert.pem",
                               keyfile => "key.pem"}]},
               {versions, ['tlsv1.2','tlsv1.3']},
               {session_tickets, stateless}].
      {ok, LSock} = ssl:listen(8001, LOpts).
      {ok, ASock} = ssl:transport_accept(LSock).

步骤 2(客户端):启动客户端并连接到服务器

      {ok, _} = application:ensure_all_started(ssl).
      COpts = [{cacertfile, "cert.pem"},
               {versions, ['tlsv1.2','tlsv1.3']},
               {log_level, debug},
               {session_tickets, auto}].
      ssl:connect("localhost", 8001, COpts).

步骤 3(服务器):启动 TLS 握手

      {ok, CSocket} = ssl:handshake(ASock).

使用完整握手建立连接。以下是交换消息的摘要。

      >>> TLS 1.3 Handshake, ClientHello ...
      <<< TLS 1.3 Handshake, ServerHello ...
      <<< Handshake, EncryptedExtensions ...
      <<< Handshake, Certificate ...
      <<< Handshake, CertificateVerify ...
      <<< Handshake, Finished ...
      >>> Handshake, Finished ...
      <<< Post-Handshake, NewSessionTicket ...

此时,客户端已存储接收到的会话票证,并在与同一服务器建立新连接时准备使用它们。

步骤 4(服务器): 接受服务器上的新连接

      {ok, ASock2} = ssl:transport_accept(LSock).

步骤 5(客户端): 建立新连接

      ssl:connect("localhost", 8001, COpts).

步骤 6(服务器): 开始握手

      {ok, CSock2} =ssl:handshake(ASock2).

第二个连接是使用先前握手中的密钥材料进行的会话恢复

      >>> TLS 1.3 Handshake, ClientHello ...
      <<< TLS 1.3 Handshake, ServerHello ...
      <<< Handshake, EncryptedExtensions ...
      <<< Handshake, Finished ...
      >>> Handshake, Finished ...
      <<< Post-Handshake, NewSessionTicket ...

也支持手动处理会话票证。在手动模式下,客户端负责处理接收到的会话票证。

步骤 7(服务器): 接受服务器上的新连接

      {ok, ASock3} = ssl:transport_accept(LSock).

步骤 8(客户端): 与服务器建立新连接

      {ok, _} = application:ensure_all_started(ssl).
      COpts2 = [{cacertfile, "cacerts.pem"},
                {versions, ['tlsv1.2','tlsv1.3']},
                {log_level, debug},
                {session_tickets, manual}].
      ssl:connect("localhost", 8001, COpts).

步骤 9(服务器): 开始握手

      {ok, CSock3} = ssl:handshake(ASock3).

握手完成后,用户进程会收到服务器发送的包含票证的消息。

步骤 10(客户端): 接收新的会话票证

      Ticket = receive {ssl, session_ticket, {_, TicketData}} -> TicketData end.

步骤 11(服务器): 接受服务器上的新连接

      {ok, ASock4} = ssl:transport_accept(LSock).

步骤 12(客户端): 使用步骤 10 中接收到的会话票证启动与服务器的新连接

      {ok, _} = application:ensure_all_started(ssl).
      COpts2 = [{cacertfile, "cert.pem"},
                {versions, ['tlsv1.2','tlsv1.3']},
                {log_level, debug},
                {session_tickets, manual},
                {use_ticket, [Ticket]}].
      ssl:connect("localhost", 8001, COpts).

步骤 13(服务器): 开始握手

      {ok, CSock4} = ssl:handshake(ASock4).

如果端点共享加密密钥(预共享密钥),TLS 1.3 允许客户端在第一次飞行时发送数据。这意味着如果客户端具有在先前成功的握手中接收到的有效会话票证,则可以发送提前数据。有关会话恢复的更多信息,请参见 TLS 1.3 中的会话票证和会话恢复.

提前数据的安全属性比其他类型的 TLS 数据弱。此数据不是前向安全的,并且容易受到重放攻击。有关可用的缓解策略,请参见 TLS 1.3 中的防重放保护.

在正常操作中,客户端不知道服务器实际实现了哪些(如果有)可用的缓解策略,因此只能发送他们认为可以安全重放的提前数据。例如,幂等的 HTTP 操作(例如 HEAD 和 GET)通常可以视为安全的,但即使它们也可能被大量重放利用,从而导致资源限制耗尽和其他类似问题。

使用自动和手动会话票证处理发送提前数据的示例

服务器

    early_data_server() ->
        application:load(ssl),
        {ok, _} = application:ensure_all_started(ssl),
        Port = 11029,
        LOpts = [{certs_keys, [#{certfile => "cert.pem", keyfile => "key.pem"}]},
        {reuseaddr, true},
        {versions, ['tlsv1.2','tlsv1.3']},
        {session_tickets, stateless},
        {early_data, enabled},
        ],
        {ok, LSock} = ssl:listen(Port, LOpts),
        %% Accept first connection
        {ok, ASock0} = ssl:transport_accept(LSock),
        {ok, CSock0} = ssl:handshake(ASock0),
        %% Accept second connection
        {ok, ASock1} = ssl:transport_accept(LSock),
        {ok, CSock1} = ssl:handshake(ASock1),
        Sock.

客户端(自动票证处理)

    early_data_auto() ->
        %% First handshake 1-RTT - get session tickets
	application:load(ssl),
	{ok, _} = application:ensure_all_started(ssl),
	Port = 11029,
	Data = <<"HEAD / HTTP/1.1\r\nHost: \r\nConnection: close\r\n">>,
	COpts0 = [{cacertfile, "cacerts.pem"},
	          {versions, ['tlsv1.2', 'tlsv1.3']},
	          {session_tickets, auto}],
        {ok, Sock0} = ssl:connect("localhost", Port, COpts0),

        %% Wait for session tickets
	timer:sleep(500),
	%% Close socket if server cannot handle multiple
        %% connections e.g. openssl s_server
	ssl:close(Sock0),

        %% Second handshake 0-RTT
	COpts1 = [{cacertfile,  "cacerts.pem"},
	          {versions, ['tlsv1.2', 'tlsv1.3']},
		  {session_tickets, auto},
		  {early_data, Data}],
        {ok, Sock} = ssl:connect("localhost", Port, COpts1),
	Sock.

客户端(手动票证处理)

    early_data_manual() ->
        %% First handshake 1-RTT - get session tickets
	application:load(ssl),
	{ok, _} = application:ensure_all_started(ssl),
	Port = 11029,
	Data = <<"HEAD / HTTP/1.1\r\nHost: \r\nConnection: close\r\n">>,
	COpts0 = [{cacertfile, "cacerts.pem"},
	          {versions, ['tlsv1.2', 'tlsv1.3']},
	          {session_tickets, manual}],
        {ok, Sock0} = ssl:connect("localhost", Port, COpts0),

        %% Wait for session tickets
	Ticket =
	    receive
	        {ssl, session_ticket, Ticket0} ->
		    Ticket0
            end,

       %% Close socket if server cannot handle multiple connections
       %% e.g. openssl s_server
       ssl:close(Sock0),

       %% Second handshake 0-RTT
       COpts1 = [{cacertfile, "cacerts.pem"},
                 {versions, ['tlsv1.2', 'tlsv1.3']},
		 {session_tickets, manual},
		 {use_ticket, [Ticket]},
		 {early_data, Data}],
       {ok, Sock} = ssl:connect("localhost", Port, COpts1),
       Sock.

TLS 1.3 协议本身不提供对 0-RTT 数据重放的保护,但描述了符合标准的服务器实现 SHOULD 实现的机制。SSL 应用程序中 TLS 1.3 的实现采用所有标准方法来防止潜在的威胁。

一次性票证

此机制可用于有状态会话票证。会话票证只能使用一次,后续使用同一票证会导致完全握手。有状态服务器通过维护一个包含未处理的有效票证数据库来强制执行此规则。

客户端问候记录

此机制可用于无状态会话票证。服务器会在给定的时间窗口内记录从 ClientHello(PSK 绑定器)派生的唯一值。票证的有效期通过使用“obsfuscated_ticket_age”和票证数据中加密的额外时间戳来验证。由于使用的存储允许误报,因此明显的重放将通过执行完全的 1-RTT 握手来回答。

新鲜度检查

此机制可用于无状态会话票证。由于票证数据包含嵌入式时间戳,因此服务器可以确定 ClientHello 是否是最近发送的,并接受 0-RTT 握手,否则会回退到完全的 1-RTT 握手。此机制与前一个机制紧密耦合,可以防止存储无限数量的 ClientHello。

当前实现使用一对布隆过滤器来实现最后两种机制。布隆过滤器是快速、内存高效、概率性的数据结构,可以判断元素是否可能在一个集合中或是否绝对不在集合中。

如果在服务器中定义了选项 anti_replay,则使用一对布隆过滤器(currentold)来记录传入的 ClientHello 消息(实际上存储的是唯一的绑定器值)。current 布隆过滤器用于 WindowSize 秒来存储新元素。在时间窗口结束时,布隆过滤器会旋转(current 布隆过滤器变为 old,并且将一个空的布隆过滤器设置为 current

无状态服务器中的防重放保护功能在接收到新的 ClientHello 时会按照以下步骤执行

  • 报告的票证有效期(obfuscated ticket age)应小于票证有效期。

  • 实际票证有效期应小于票证有效期(无状态会话票证包含服务器在颁发票证时的时间戳)。

  • 使用该票证创建的 ClientHello 应在最近发送(新鲜度检查)。

  • 如果所有上述检查都通过了,则检查 currentold 布隆过滤器以检测绑定器是否已被看到。由于它是一种概率性数据结构,可能会出现误报,并且会导致完全握手。

  • 如果未看到绑定器,则会验证绑定器。如果绑定器有效,则服务器将继续进行 0-RTT 握手。

使用 DTLS 的 API 与 TLS 基本相同。您需要将选项 {protocol, dtls} 添加到 connect 和 listen 函数。例如

 client> {ok, Socket} = ssl:connect("localhost", 9999, [{protocol, dtls},
{verify, verify_peer},{cacertfile, "cacerts.pem"}], infinity).
{ok,{sslsocket, [...]}}