查看源代码 示例
要查看 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
,并在许多协议中使用,例如 SMTP
、FTPS
和通过代理的 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/2
和 sign_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
选项,则会使用一对布隆过滤器(current 和 old)来记录传入的 ClientHello 消息(实际存储的是唯一的 binder 值)。current 布隆过滤器用于 WindowSize
秒来存储新元素。在时间窗口结束时,布隆过滤器会轮换(current 布隆过滤器变为 old,并将空的布隆过滤器设置为 current)。
当收到新的 ClientHello 时,无状态服务器中的防重放保护功能按以下步骤执行
- 报告的票据有效期(混淆的票据有效期)应小于票据的生存期。
- 实际票据有效期应小于票据的生存期(无状态会话票据包含服务器颁发票据时的时间戳)。
- 使用票据创建的 ClientHello 应在最近发送(新鲜度检查)。
- 如果以上所有检查都通过,则会检查 current 和 old 布隆过滤器,以检测是否已看到 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, [...]}}