查看源代码 示例

要查看 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')。您还可以指定希望连接使用的特定密码套件。默认是使用最强的可用套件。

警告

强烈建议不要启用使用 RSA 作为密钥交换算法的密码套件(仅在 TLS-1.3 之前可用)。对于某些配置,可能存在软件预防措施,如果它们有效,则可以使用它们,但依赖它们起作用是危险的,并且还有许多更可靠的密码套件可以代替使用。

以下章节展示了如何使用 Erlang shell 设置客户端/服务器连接的小示例。sslsocket 的返回值缩写为 [...],因为它可能相当大,并且除了模式匹配之外,对用户是不透明的。

注意

请注意,客户端证书验证对于服务器是可选的,并且需要在双方进行额外的配置才能工作。示例中的证书和密钥使用 OTP 25 中引入的 certs_keys 中提供的 ssl:cert_key_conf/0 提供。

基本客户端

 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

升级示例 - 仅 TLS

将 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 版本的默认密码套件列表。将默认值更改为 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.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 中的默认 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 密钥选项中使用该映射

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

另请参阅 加密文档

NSS 密钥日志

授权用户可以使用 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]).

TLS 1.3 之前的会话重用

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

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

注意

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

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

步骤 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 中的会话票证和会话恢复

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 允许客户端在第一个 flight 上发送数据。这意味着,如果客户端具有在先前成功握手中收到的有效会话票据,则可以发送早期数据。有关会话恢复的更多信息,请参阅 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 中的防重放保护

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

单次使用票据

此机制适用于有状态会话票据。会话票据只能使用一次,随后使用同一票据会导致完整握手。有状态服务器通过维护未决有效票据的数据库来强制执行此规则。

客户端 Hello 记录

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

新鲜度检查

此机制适用于无状态会话票据。由于票据数据具有嵌入的时间戳,因此服务器可以确定 ClientHello 是否是在不久前发送的,并接受 0-RTT 握手,否则如果回退到完整的 1-RTT 握手。此机制与前一个机制紧密结合,它可以防止存储无限数量的 ClientHello。

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

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

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

  • 报告的票据有效期(混淆的票据有效期)应小于票据的生存期。
  • 实际票据有效期应小于票据的生存期(无状态会话票据包含服务器颁发票据时的时间戳)。
  • 使用票据创建的 ClientHello 应在最近发送(新鲜度检查)。
  • 如果以上所有检查都通过,则会检查 currentold 布隆过滤器,以检测是否已看到 binder。由于是概率数据结构,可能会出现误报,并且会触发完整的握手。
  • 如果未看到 binder,则会验证 binder。如果 binder 有效,则服务器将继续进行 0-RTT 握手。

使用 DTLS

使用 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, [...]}}