核心 TLS 握手¶
概述¶
傳輸層安全 (TLS) 是一種執行在 TCP 上的上層協議 (ULP)。 TLS 除了對等身份驗證之外,還提供端到端的資料完整性和機密性。
核心的 kTLS 實現處理 TLS 記錄子協議,但不處理用於建立 TLS 會話的 TLS 握手子協議。 核心使用者可以使用此處描述的 API 來請求建立 TLS 會話。
在核心中提供握手服務有幾種可能的方法。 此處描述的 API 旨在隱藏這些實現的細節,以便核心 TLS 使用者無需瞭解握手是如何完成的。
使用者握手代理¶
在撰寫本文時,Linux 核心中沒有 TLS 握手實現。 為了提供握手服務,握手代理(通常在使用者空間中)在每個網路名稱空間中啟動,核心使用者可能需要在其中進行 TLS 握手。 握手代理監聽從核心傳送的指示握手請求正在等待的事件。
透過 netlink 操作將一個開啟的套接字傳遞給握手代理,這會在代理的檔案描述符表中建立一個套接字描述符。 如果握手成功完成,則握手代理會將套接字提升為使用 TLS ULP,並使用 SOL_TLS 套接字選項設定會話資訊。 握手代理透過第二個 netlink 操作將套接字返回給核心。
核心握手 API¶
核心 TLS 使用者透過呼叫 tls_client_hello() 函式之一,在開啟的套接字上啟動客戶端 TLS 握手。 首先,它填寫一個包含請求引數的結構。
struct tls_handshake_args {
struct socket *ta_sock;
tls_done_func_t ta_done;
void *ta_data;
const char *ta_peername;
unsigned int ta_timeout_ms;
key_serial_t ta_keyring;
key_serial_t ta_my_cert;
key_serial_t ta_my_privkey;
unsigned int ta_num_peerids;
key_serial_t ta_my_peerids[5];
};
@ta_sock 欄位引用一個開啟並連線的套接字。 使用者必須保持對套接字的引用,以防止握手進行期間套接字被銷燬。 使用者還必須在 sock->file 中例項化一個 struct file。
@ta_done 包含一個在握手完成時呼叫的回撥函式。 此函式的進一步解釋在下面的“握手完成”部分中。
使用者可以在 @ta_peername 欄位中提供一個以 NUL 結尾的主機名,該主機名作為 ClientHello 的一部分發送。 如果未提供 peername,則改為使用與伺服器 IP 地址關聯的 DNS 主機名。
使用者可以填寫 @ta_timeout_ms 欄位以強制服務握手代理在若干毫秒後退出。 這使得一旦核心和握手代理都關閉了它們的端點,套接字就可以完全關閉。
在發出握手請求之前,使用者透過例項化的金鑰將諸如 x.509 證書、私有證書金鑰和預共享金鑰之類的身份驗證材料提供給握手代理。 使用者可以在 @ta_keyring 欄位中提供一個連結到握手代理程序金鑰環的私有金鑰環,以防止其他子系統訪問這些金鑰。
要請求經過 x.509 身份驗證的 TLS 會話,使用者可以使用包含 x.509 證書和該證書私鑰的金鑰的序列號填寫 @ta_my_cert 和 @ta_my_privkey 欄位。 然後,它呼叫此函式
ret = tls_client_hello_x509(args, gfp_flags);
當握手請求正在進行時,該函式返回零。 返回零保證將為此套接字呼叫回撥函式 @ta_done。 如果無法啟動握手,該函式返回一個負 errno。 負 errno 保證不會在此套接字上呼叫回撥函式 @ta_done。
要使用預共享金鑰啟動客戶端 TLS 握手,請使用
ret = tls_client_hello_psk(args, gfp_flags);
但是,在這種情況下,使用者可以使用它希望提供的對等身份的金鑰的序列號填寫 @ta_my_peerids 陣列,並使用它已填寫的陣列條目的數量填寫 @ta_num_peerids 欄位。 其他欄位的填寫方式與上述相同。
要啟動匿名客戶端 TLS 握手,請使用
ret = tls_client_hello_anon(args, gfp_flags);
在這種型別的握手期間,握手代理不會向遠端端呈現任何對等身份資訊。 在握手期間僅執行伺服器身份驗證(即客戶端驗證伺服器的身份)。 因此,建立的會話僅使用加密。
核心伺服器的使用者使用
ret = tls_server_hello_x509(args, gfp_flags);
或
ret = tls_server_hello_psk(args, gfp_flags);
引數結構的填寫方式與上述相同。
如果使用者需要取消握手請求,例如,由於 ^C 或其他緊急事件,則使用者可以呼叫
bool tls_handshake_cancel(sock);
如果與 @sock 關聯的握手請求已被取消,則此函式返回 true。 不會呼叫使用者的握手完成回撥。 如果此函式返回 false,則已經呼叫了使用者的完成回撥。
握手完成¶
當握手代理完成處理後,它會通知核心使用者可以再次使用該套接字。 此時,將呼叫使用者握手完成回撥,該回調在 tls_handshake_args 結構中的 @ta_done 欄位中提供。
此函式的概要是
typedef void (*tls_done_func_t)(void *data, int status,
key_serial_t peerid);
使用者在 tls_handshake_args 結構的 @ta_data 欄位中提供一個 cookie,該 cookie 在此回撥的 @data 引數中返回。 使用者使用 cookie 將回調與等待握手完成的執行緒進行匹配。
握手的成功狀態透過 @status 引數返回
狀態 |
含義 |
|---|---|
0 |
TLS 會話建立成功 |
-EACCESS |
遠端對等方拒絕握手或身份驗證失敗 |
-ENOMEM |
臨時資源分配失敗 |
-EINVAL |
使用者提供了無效引數 |
-ENOKEY |
缺少身份驗證材料 |
-EIO |
發生意外故障 |
@peerid 引數包含包含遠端對等方身份的金鑰的序列號,如果會話未經過身份驗證,則包含值 TLS_NO_PEERID。
最佳實踐是,如果握手失敗,則立即關閉並銷燬套接字。
其他注意事項¶
在握手進行期間,核心使用者必須更改套接字的 sk_data_ready 回撥函式以忽略所有傳入資料。 一旦呼叫了握手完成回撥函式,就可以恢復正常的接收操作。
一旦建立了 TLS 會話,使用者必須為每個後續的 sock_recvmsg() 提供一個緩衝區,然後檢查作為控制訊息 (CMSG) 一部分的緩衝區。 每個控制訊息都指示接收到的訊息資料是 TLS 記錄資料還是會話元資料。
有關 kTLS 使用者如何在將套接字提升為使用 TLS ULP 後識別傳入的(解密的)應用程式資料、警報和握手資料包的詳細資訊,請參見 核心 TLS。