Kernel TLS¶
概述¶
傳輸層安全協議 (TLS) 是一種執行在 TCP 之上的上層協議 (ULP)。 TLS 提供端到端的資料完整性和機密性。
使用者介面¶
建立 TLS 連線¶
首先建立一個新的 TCP 套接字,並設定 TLS ULP。
sock = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(sock, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));
設定 TLS ULP 允許我們設定/獲取 TLS 套接字選項。目前只有對稱加密在核心中處理。TLS 握手完成後,我們擁有將資料路徑移動到核心所需的所有引數。有一個單獨的套接字選項用於將傳送和接收移動到核心中。
/* From linux/tls.h */
struct tls_crypto_info {
unsigned short version;
unsigned short cipher_type;
};
struct tls12_crypto_info_aes_gcm_128 {
struct tls_crypto_info info;
unsigned char iv[TLS_CIPHER_AES_GCM_128_IV_SIZE];
unsigned char key[TLS_CIPHER_AES_GCM_128_KEY_SIZE];
unsigned char salt[TLS_CIPHER_AES_GCM_128_SALT_SIZE];
unsigned char rec_seq[TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE];
};
struct tls12_crypto_info_aes_gcm_128 crypto_info;
crypto_info.info.version = TLS_1_2_VERSION;
crypto_info.info.cipher_type = TLS_CIPHER_AES_GCM_128;
memcpy(crypto_info.iv, iv_write, TLS_CIPHER_AES_GCM_128_IV_SIZE);
memcpy(crypto_info.rec_seq, seq_number_write,
TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE);
memcpy(crypto_info.key, cipher_key_write, TLS_CIPHER_AES_GCM_128_KEY_SIZE);
memcpy(crypto_info.salt, implicit_iv_write, TLS_CIPHER_AES_GCM_128_SALT_SIZE);
setsockopt(sock, SOL_TLS, TLS_TX, &crypto_info, sizeof(crypto_info));
傳送和接收是分開設定的,但設定是相同的,使用 TLS_TX 或 TLS_RX。
傳送 TLS 應用程式資料¶
設定 TLS_TX 套接字選項後,透過此套接字傳送的所有應用程式資料都將使用 TLS 和套接字選項中提供的引數進行加密。 例如,我們可以傳送一個加密的 hello world 記錄,如下所示
const char *msg = "hello world\n";
send(sock, msg, strlen(msg));
如果可能,send() 資料將從提供給加密核心傳送緩衝區的使用者空間緩衝區直接加密。
sendfile 系統呼叫將透過最大長度 (2^14) 的 TLS 記錄傳送檔案的資料。
file = open(filename, O_RDONLY);
fstat(file, &stat);
sendfile(sock, file, &offset, stat.st_size);
除非傳遞了 MSG_MORE,否則在每次 send() 呼叫後都會建立併發送 TLS 記錄。 MSG_MORE 將延遲記錄的建立,直到未傳遞 MSG_MORE 或達到最大記錄大小。
核心需要為加密資料分配一個緩衝區。 此緩衝區在呼叫 send() 時分配,這樣整個 send() 呼叫將返回 -ENOMEM(或阻塞等待記憶體),或者加密將始終成功。 如果 send() 返回 -ENOMEM 並且套接字緩衝區中遺留了一些來自之前使用 MSG_MORE 呼叫的資料,則 MSG_MORE 資料將遺留在套接字緩衝區中。
接收 TLS 應用程式資料¶
設定 TLS_RX 套接字選項後,所有 recv 系列套接字呼叫都將使用提供的 TLS 引數進行解密。必須先接收到完整的 TLS 記錄才能進行解密。
char buffer[16384];
recv(sock, buffer, 16384);
如果使用者緩衝區足夠大,接收到的資料將直接解密到使用者緩衝區中,並且不會發生額外的分配。 如果使用者空間緩衝區太小,資料將在核心中解密並複製到使用者空間。
如果接收到的訊息中的 TLS 版本與 setsockopt 中傳遞的版本不匹配,則返回 EINVAL。
如果接收到的訊息太大,則返回 EMSGSIZE。
如果由於任何其他原因解密失敗,則返回 EBADMSG。
傳送 TLS 控制訊息¶
除了應用程式資料之外,TLS 還有控制訊息,例如警報訊息(記錄型別 21)和握手訊息(記錄型別 22)等。 可以透過 CMSG 提供 TLS 記錄型別,透過套接字傳送這些訊息。 例如,以下函式使用 @record_type 型別的記錄傳送 @length 位元組的 @data。
/* send TLS control message using record_type */
static int klts_send_ctrl_message(int sock, unsigned char record_type,
void *data, size_t length)
{
struct msghdr msg = {0};
int cmsg_len = sizeof(record_type);
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(cmsg_len)];
struct iovec msg_iov; /* Vector of data to send/receive into. */
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_TLS;
cmsg->cmsg_type = TLS_SET_RECORD_TYPE;
cmsg->cmsg_len = CMSG_LEN(cmsg_len);
*CMSG_DATA(cmsg) = record_type;
msg.msg_controllen = cmsg->cmsg_len;
msg_iov.iov_base = data;
msg_iov.iov_len = length;
msg.msg_iov = &msg_iov;
msg.msg_iovlen = 1;
return sendmsg(sock, &msg, 0);
}
控制訊息資料應以未加密的形式提供,並將由核心加密。
接收 TLS 控制訊息¶
TLS 控制訊息在使用者空間緩衝區中傳遞,訊息型別透過 cmsg 傳遞。 如果未提供 cmsg 緩衝區,則在接收到控制訊息時返回錯誤。 可以接收資料訊息而不設定 cmsg 緩衝區。
char buffer[16384];
char cmsg[CMSG_SPACE(sizeof(unsigned char))];
struct msghdr msg = {0};
msg.msg_control = cmsg;
msg.msg_controllen = sizeof(cmsg);
struct iovec msg_iov;
msg_iov.iov_base = buffer;
msg_iov.iov_len = 16384;
msg.msg_iov = &msg_iov;
msg.msg_iovlen = 1;
int ret = recvmsg(sock, &msg, 0 /* flags */);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg->cmsg_level == SOL_TLS &&
cmsg->cmsg_type == TLS_GET_RECORD_TYPE) {
int record_type = *((unsigned char *)CMSG_DATA(cmsg));
// Do something with record_type, and control message data in
// buffer.
//
// Note that record_type may be == to application data (23).
} else {
// Buffer contains application data.
}
recv 永遠不會從混合型別的 TLS 記錄返回資料。
TLS 1.3 金鑰更新¶
在 TLS 1.3 中,KeyUpdate 握手訊息表示傳送者正在更新其 TX 金鑰。 在 KeyUpdate 之後傳送的任何訊息都將使用新金鑰加密。 使用者空間庫可以使用 TLS_TX 和 TLS_RX 套接字選項將新金鑰傳遞給核心,就像初始金鑰一樣。 TLS 版本和密碼不能更改。
為了防止嘗試使用錯誤的金鑰解密傳入記錄,當核心接收到 KeyUpdate 訊息時,解密將暫停,直到使用 TLS_RX 套接字選項提供新金鑰。 在讀取 KeyUpdate 之後和提供新金鑰之前的任何讀取都將失敗並顯示 EKEYEXPIRED。 在提供新金鑰之前,poll() 不會報告來自套接字的任何讀取事件。 傳送端沒有暫停。
使用者空間應確保 crypto_info 已正確設定。 特別是,核心不會檢查金鑰/nonce 重用。
成功和失敗的金鑰更新的數量在 TlsTxRekeyOk, TlsRxRekeyOk, TlsTxRekeyError, TlsRxRekeyError 統計資訊中跟蹤。 TlsRxRekeyReceived 統計資訊計算已接收的 KeyUpdate 握手訊息的數量。
整合到使用者空間 TLS 庫中¶
在高層次上,核心 TLS ULP 是使用者空間 TLS 庫記錄層的替代品。
使用 ktls 作為記錄層的 OpenSSL 補丁集是 here。
一個例子 展示了使用 gnutls 在握手後直接呼叫 send。 由於它沒有實現完整的記錄層,因此不支援控制訊息。
可選最佳化¶
如果請求,TLS ULP 可以進行某些特定於條件的最佳化。 這些最佳化要麼不是普遍有益的,要麼可能會影響正確性,因此它們需要選擇加入。 所有選項都是透過 setsockopt() 按套接字設定的,並且可以透過 getsockopt() 和透過套接字診斷(ss)檢查其狀態。
TLS_TX_ZEROCOPY_RO¶
僅用於裝置解除安裝。 允許 sendfile() 資料直接傳輸到 NIC,而無需進行核心內副本。 啟用裝置解除安裝時,這允許真正的零複製行為。
應用程式必須確保資料在提交和傳輸完成之間沒有被修改。 換句話說,這主要適用於透過 sendfile() 在套接字上傳送的資料是隻讀的情況。
修改資料可能會導致不同版本的資料用於原始 TCP 傳輸和 TCP 重傳。 對於接收方,這將看起來 TLS 記錄已被篡改,並將導致記錄身份驗證失敗。
TLS_RX_EXPECT_NO_PAD¶
僅限 TLS 1.3。 期望傳送者不填充記錄。 這允許使用 TLS 1.3 將資料直接解密到使用者空間緩衝區中。
僅當遠端端受信任時,才能安全地啟用此最佳化,否則它是使 TLS 處理成本翻倍的攻擊媒介。
如果解密的記錄結果表明已填充或不是資料記錄,則會將其再次解密到沒有零複製的核心緩衝區中。 此類事件在 TlsDecryptRetry 統計資訊中計數。
統計¶
TLS 實現公開以下每個名稱空間的統計資訊(/proc/net/tls_stat)
TlsCurrTxSw,TlsCurrRxSw- 當前安裝的處理加密的主機上的 TX 和 RX 會話數TlsCurrTxDevice,TlsCurrRxDevice- 當前安裝的處理加密的 NIC 上的 TX 和 RX 會話數TlsTxSw,TlsRxSw- 使用主機加密開啟的 TX 和 RX 會話數TlsTxDevice,TlsRxDevice- 使用 NIC 加密開啟的 TX 和 RX 會話數TlsDecryptError- 記錄解密失敗(例如,由於不正確的身份驗證標記)TlsDeviceRxResync- 傳送到處理加密的 NIC 的 RX 重新同步數TlsDecryptRetry- 由於TLS_RX_EXPECT_NO_PAD錯誤預測而必須重新解密的 RX 記錄數。 請注意,此計數器也會為非資料記錄遞增。TlsRxNoPadViolation- 由於TLS_RX_EXPECT_NO_PAD錯誤預測而必須重新解密的 RX 資料記錄數。TlsTxRekeyOk,TlsRxRekeyOk- 現有會話上 TX 和 RX 的成功重新金鑰數TlsTxRekeyError,TlsRxRekeyError- 現有會話上 TX 和 RX 的失敗重新金鑰數TlsRxRekeyReceived- 已接收的 KeyUpdate 握手訊息數,需要使用者空間提供新的 RX 金鑰