内核 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。