内核 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);
TLS 记录在每次 send()
调用后创建并发送,除非传递了 MSG_MORE。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 库中¶
从高层次来看,内核 TLS ULP 是用户空间 TLS 库记录层的替代品。
使用 ktls 作为记录层的 OpenSSL 补丁集在这里。
使用 gnutls 在握手后直接调用发送的示例。由于它没有实现完整的记录层,因此不支持控制消息。
可选优化¶
如果请求,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 记录数。