内核 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 的一部分发送。如果未提供对等名称,则改为使用与服务器 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 会话,使用者必须为控制消息 (CMSG) 提供缓冲区,然后检查它是每个后续 sock_recvmsg() 的一部分。每个控制消息都指示接收到的消息数据是 TLS 记录数据还是会话元数据。

有关 kTLS 使用者如何在套接字升级为使用 TLS ULP 后识别传入(解密)的应用程序数据、警报和握手数据包的详细信息,请参阅 内核 TLS