檢視原始碼 範例
要查看 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
,並在許多協定中使用,例如透過 Proxy 的 SMTP
、FTPS
和 HTTPS
。
警告
但是,最大安全性建議正在遠離此類解決方案。
升級到 TLS 連線
步驟 1: 啟動伺服器端
1 server> ssl:start().
ok
步驟 2: 建立一個正常的 TCP 接聽通訊端,並確保 active
設定為 false
,且未設定為任何 active 模式,否則 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.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 中的預設 signature_algorithm 清單和來自 OpenSSL 3.0.2 的 cryptolib 會看起來像
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>}
建立一個包含引擎資訊和引擎所使用演算法的 map
3> PrivKey =
#{algorithm => rsa,
engine => EngineRef,
key_id => "id of the private key in Engine"}.
在 ssl 金鑰選項中使用 map
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 節點上的用戶端程序自動執行會話重複使用。請注意,基於安全原因,只會儲存已驗證的會話,也就是說,會話恢復依賴於在原始握手中執行的憑證驗證。為了最大程度地減少記憶體消耗,除非為以下選項 {reuse_sessions, boolean() | save}
指定特殊的 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 允許用戶端在第一次傳輸中發送資料。這表示如果用戶端具有先前成功握手中收到的有效會話票證,則可以發送早期資料。如需有關會話恢復的更多資訊,請參閱 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 資料重播的內在保護,但描述了符合規範的伺服器實作應實作的機制。SSL 應用程式中 TLS 1.3 的實作採用所有標準方法來防止潛在的威脅。
單次使用票證
此機制適用於有狀態的會話票證。會話票證只能使用一次,後續使用同一票證將導致完整的握手。有狀態的伺服器透過維護未處理的有效票證資料庫來強制執行此規則。
用戶端 Hello 記錄
此機制適用於無狀態的會話票證。伺服器在給定的時間視窗內記錄從 ClientHello (PSK binder) 衍生出的唯一值。票證的使用年限會透過「obsfuscated_ticket_age」和加密在票證資料中的額外時間戳記來驗證。由於使用中的資料儲存允許誤報,因此明顯的重播將透過執行完整的 1-RTT 握手來回應。
新鮮度檢查
此機制適用於無狀態的會話票證。由於票證資料具有內嵌時間戳記,伺服器可以確定 ClientHello 是否在最近發送,並接受 0-RTT 握手,否則會回退到完整的 1-RTT 握手。此機制與前一個機制緊密耦合,它可以防止儲存無限數量的 ClientHello。
目前的實作使用一對布隆過濾器來實作最後兩個機制。布隆過濾器是一種快速、記憶體效率高且機率性的資料結構,可以判斷元素是否可能在集合中,或是否絕對不在集合中。
如果在伺服器中定義了選項 anti_replay
,則會使用一對布隆過濾器(current 和 old)來記錄傳入的 ClientHello 訊息(實際上儲存的是唯一的綁定器值)。current 布隆過濾器用於 WindowSize
秒,以儲存新元素。在時間視窗結束時,布隆過濾器會旋轉(current 布隆過濾器會變成 old,並將空的布隆過濾器設定為 current)。
在收到新的 ClientHello 時,無狀態伺服器中的防重播保護功能會執行以下步驟
- 回報的票證使用年限(混淆的票證使用年限)應小於票證生命週期。
- 實際票證使用年限應小於票證生命週期(無狀態會話票證包含伺服器發出票證時的時間戳記)。
- 使用票證建立的 ClientHello 應在相對較近的時間內發送(新鮮度檢查)。
- 如果以上所有檢查都通過,則會檢查 current 和 old 布隆過濾器,以偵測是否已看到綁定器。由於是一種機率性的資料結構,因此可能會發生誤報,並且會觸發完整的握手。
- 如果未看到綁定器,則會驗證綁定器。如果綁定器有效,伺服器會繼續執行 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, [...]}}