* [PATCH RFC net-next 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule
2026-10-26 0:36 [PATCH RFC net-next 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
@ 2026-10-26 0:36 ` Martin Belanger via B4 Relay
2026-10-26 7:27 ` Hannes Reinecke
2026-10-26 0:36 ` [PATCH RFC net-next 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
` (3 subsequent siblings)
4 siblings, 1 reply; 9+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-26 0:36 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
From: Martin Belanger <martin.belanger@dell.com>
A TLS 1.3 KeyUpdate derives the next traffic secret, key and IV with
HKDF-Expand-Label (RFC 9846, section 7.2). Add the helper for the
kernel to do this itself.
Only the part of the key schedule after the handshake is needed. The
input is a traffic secret, which is already a pseudorandom key, so
HKDF-Extract is not needed. Each output is at most Hash.length bytes,
so each derivation is a single HMAC with the library SHA-256 or SHA-384
functions.
Assisted-by: LLM
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
net/tls/Kconfig | 2 ++
net/tls/Makefile | 2 +-
net/tls/tls.h | 2 ++
net/tls/tls_hkdf.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 87 insertions(+), 1 deletion(-)
diff --git a/net/tls/Kconfig b/net/tls/Kconfig
index 4f4d5973a28f..6183ebe954ee 100644
--- a/net/tls/Kconfig
+++ b/net/tls/Kconfig
@@ -8,6 +8,8 @@ config TLS
select CRYPTO
select CRYPTO_AES
select CRYPTO_GCM
+ select CRYPTO_LIB_SHA256
+ select CRYPTO_LIB_SHA512
select NET_SOCK_MSG
default n
help
diff --git a/net/tls/Makefile b/net/tls/Makefile
index 4c7d296081ee..6edcf1271b75 100644
--- a/net/tls/Makefile
+++ b/net/tls/Makefile
@@ -7,6 +7,6 @@ CFLAGS_trace.o := -I$(src)
obj-$(CONFIG_TLS) += tls.o
-tls-y := tls_main.o tls_sw.o tls_proc.o trace.o tls_strp.o
+tls-y := tls_main.o tls_sw.o tls_proc.o trace.o tls_strp.o tls_hkdf.o
tls-$(CONFIG_TLS_DEVICE) += tls_device.o tls_device_fallback.o
diff --git a/net/tls/tls.h b/net/tls/tls.h
index 60a37bdaaa25..b2061f6bf065 100644
--- a/net/tls/tls.h
+++ b/net/tls/tls.h
@@ -147,6 +147,8 @@ void tls_strp_abort_strp(struct tls_strparser *strp, int err);
int init_prot_info(struct tls_prot_info *prot,
const struct tls_crypto_info *crypto_info,
const struct tls_cipher_desc *cipher_desc);
+int tls_hkdf_expand_label(const u8 *secret, size_t secret_len,
+ const char *label, u8 *out, size_t out_len);
int tls_set_sw_offload(struct sock *sk, int tx,
struct tls_crypto_info *new_crypto_info);
void tls_update_rx_zc_capable(struct tls_context *tls_ctx);
diff --git a/net/tls/tls_hkdf.c b/net/tls/tls_hkdf.c
new file mode 100644
index 000000000000..49e33146ac7e
--- /dev/null
+++ b/net/tls/tls_hkdf.c
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Dell Technologies Inc. or its subsidiaries.
+ *
+ * TLS 1.3 HKDF-Expand-Label, RFC 9846 section 7.1.
+ *
+ * TLS 1.3 derives all of its secrets and keys with HKDF (RFC 5869). The
+ * TLS handshake runs in user space and gives the kernel one application
+ * traffic secret for each direction. For a KeyUpdate, the kernel derives
+ * the next traffic secret from the current one (section 7.2), and the
+ * new key and IV from the next traffic secret (section 7.3). Both steps
+ * use only HKDF-Expand-Label, so this file implements only that
+ * function, not the full key schedule of the handshake.
+ *
+ * The input is always a traffic secret. It is already a random key of
+ * the hash length, so HKDF-Extract is not needed. No output is longer
+ * than the hash, so HKDF-Expand is a single HMAC.
+ */
+
+#include <crypto/sha2.h>
+#include <linux/errno.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+
+#include "tls.h"
+
+#define TLS_HKDF_LABEL_PREFIX "tls13 "
+#define TLS_HKDF_LABEL_PREFIX_LEN (sizeof(TLS_HKDF_LABEL_PREFIX) - 1)
+#define TLS_HKDF_LABEL_MAX 32
+
+/**
+ * tls_hkdf_expand_label - HKDF-Expand-Label with an empty context
+ * @secret: the secret
+ * @secret_len: length of @secret, which selects the hash: SHA-256 for
+ * SHA256_DIGEST_SIZE, SHA-384 for SHA384_DIGEST_SIZE
+ * @label: the label, without the "tls13 " prefix
+ * @out: output buffer
+ * @out_len: number of bytes to derive, at most @secret_len
+ *
+ * Return: 0 on success, -EINVAL if a length is not valid.
+ */
+int tls_hkdf_expand_label(const u8 *secret, size_t secret_len,
+ const char *label, u8 *out, size_t out_len)
+{
+ u8 info[2 + 1 + TLS_HKDF_LABEL_PREFIX_LEN + TLS_HKDF_LABEL_MAX + 1 + 1];
+ size_t label_len = strlen(label);
+ u8 tmp[SHA384_DIGEST_SIZE];
+ size_t pos = 0;
+
+ if (secret_len != SHA256_DIGEST_SIZE &&
+ secret_len != SHA384_DIGEST_SIZE)
+ return -EINVAL;
+ if (!out_len || out_len > secret_len || label_len > TLS_HKDF_LABEL_MAX)
+ return -EINVAL;
+
+ /* struct {
+ * uint16 length;
+ * opaque label<7..255>;
+ * opaque context<0..255>;
+ * } HkdfLabel;
+ *
+ * followed by the HKDF-Expand counter for the first output block.
+ */
+ put_unaligned_be16(out_len, info);
+ pos += 2;
+ info[pos++] = TLS_HKDF_LABEL_PREFIX_LEN + label_len;
+ memcpy(info + pos, TLS_HKDF_LABEL_PREFIX, TLS_HKDF_LABEL_PREFIX_LEN);
+ pos += TLS_HKDF_LABEL_PREFIX_LEN;
+ memcpy(info + pos, label, label_len);
+ pos += label_len;
+ info[pos++] = 0; /* empty context */
+ info[pos++] = 1; /* counter */
+
+ if (secret_len == SHA256_DIGEST_SIZE)
+ hmac_sha256_usingrawkey(secret, secret_len, info, pos, tmp);
+ else
+ hmac_sha384_usingrawkey(secret, secret_len, info, pos, tmp);
+
+ memcpy(out, tmp, out_len);
+ memzero_explicit(tmp, sizeof(tmp));
+
+ return 0;
+}
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH RFC net-next 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule
2026-10-26 0:36 ` [PATCH RFC net-next 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
@ 2026-10-26 7:27 ` Hannes Reinecke
0 siblings, 0 replies; 9+ messages in thread
From: Hannes Reinecke @ 2026-10-26 7:27 UTC (permalink / raw)
To: martin.nvme.dev, John Fastabend, Jakub Kicinski, Sabrina Dubroca,
David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
On 10/26/26 1:36 AM, Martin Belanger via B4 Relay wrote:
> From: Martin Belanger <martin.belanger@dell.com>
>
> A TLS 1.3 KeyUpdate derives the next traffic secret, key and IV with
> HKDF-Expand-Label (RFC 9846, section 7.2). Add the helper for the
> kernel to do this itself.
>
> Only the part of the key schedule after the handshake is needed. The
> input is a traffic secret, which is already a pseudorandom key, so
> HKDF-Extract is not needed. Each output is at most Hash.length bytes,
> so each derivation is a single HMAC with the library SHA-256 or SHA-384
> functions.
>
> Assisted-by: LLM
> Signed-off-by: Martin Belanger <martin.belanger@dell.com>
> ---
> net/tls/Kconfig | 2 ++
> net/tls/Makefile | 2 +-
> net/tls/tls.h | 2 ++
> net/tls/tls_hkdf.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 87 insertions(+), 1 deletion(-)
>
And another HKDF implementation. Maybe it's time to split if off (again).
But anyway.
Reviewed-by: Hannes Reinecke <hare@kernel.org>
Cheers,
Hannes
--
Dr. Hannes Reinecke Kernel Storage Architect
hare@suse.de +49 911 74053 688
SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg
HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH RFC net-next 2/5] tls: add socket options to register the traffic secrets
2026-10-26 0:36 [PATCH RFC net-next 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
2026-10-26 0:36 ` [PATCH RFC net-next 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
@ 2026-10-26 0:36 ` Martin Belanger via B4 Relay
2026-10-26 7:37 ` Hannes Reinecke
2026-10-26 0:36 ` [PATCH RFC net-next 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
` (2 subsequent siblings)
4 siblings, 1 reply; 9+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-26 0:36 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
From: Martin Belanger <martin.belanger@dell.com>
kTLS holds the key and IV of each direction, but not the traffic secret
they were derived from. The derivation is one-way, so the kernel cannot
compute the next generation for a KeyUpdate on its own.
Add TLS_TX_TRAFFIC_SECRET and TLS_RX_TRAFFIC_SECRET. The owner of the
socket passes the TLS 1.3 application traffic secret of a direction after
installing its key. The cipher of the installed key selects the hash, so
the option takes only the secret. A socket without a registered secret
behaves as before.
The TX secret can be registered alone. The RX secret needs the TX secret
first: a received KeyUpdate can ask for one in reply, and since the
kernel consumes the record, only the kernel can send that reply.
Installing a new key with TLS_TX or TLS_RX removes the secrets that no
longer apply. The secrets are cleared when the context is freed and
cannot be read back.
Assisted-by: LLM
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
Documentation/networking/tls.rst | 26 ++++++++++++
include/net/tls.h | 13 ++++++
include/uapi/linux/tls.h | 2 +
net/tls/tls_main.c | 86 +++++++++++++++++++++++++++++++++++++++-
4 files changed, 126 insertions(+), 1 deletion(-)
diff --git a/Documentation/networking/tls.rst b/Documentation/networking/tls.rst
index 980c442d7161..2e011bd1ce4a 100644
--- a/Documentation/networking/tls.rst
+++ b/Documentation/networking/tls.rst
@@ -300,6 +300,32 @@ extra byte used by the ContentType field.
[1] https://datatracker.ietf.org/doc/html/rfc8449
+TLS_TX_TRAFFIC_SECRET and TLS_RX_TRAFFIC_SECRET
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``TLS_TX_TRAFFIC_SECRET`` registers the TLS 1.3 application traffic secret
+of the transmit direction, ``TLS_RX_TRAFFIC_SECRET`` that of the receive
+direction. With a registered secret, the kernel performs KeyUpdate for that
+direction itself (RFC 9846, section 4.7.3). The value is the secret itself.
+TLS 1.3 only, software path only.
+
+Install the key of the direction first, with the ``TLS_TX`` or ``TLS_RX``
+socket option described above. The cipher of that key selects the hash, and
+the secret must have the length of that hash: 32 bytes for SHA-256
+(AES-128-GCM, AES-128-CCM, ChaCha20-Poly1305) or 48 bytes for SHA-384
+(AES-256-GCM). Other ciphers return ``EOPNOTSUPP``.
+
+Each secret can be registered once. ``TLS_RX_TRAFFIC_SECRET`` needs
+``TLS_TX_TRAFFIC_SECRET`` first: a received KeyUpdate can ask for one in
+reply, and only the kernel sees it. ``TLS_TX_TRAFFIC_SECRET`` alone is
+allowed.
+
+If the application installs a new key itself with ``TLS_TX``, both secrets
+are removed. With ``TLS_RX``, the RX secret is removed. KeyUpdate for that
+direction is then left to the application again.
+
+The secrets cannot be read back.
+
Statistics
==========
diff --git a/include/net/tls.h b/include/net/tls.h
index e57bef58851e..aacc6d12226e 100644
--- a/include/net/tls.h
+++ b/include/net/tls.h
@@ -65,6 +65,9 @@ struct tls_rec;
#define TLS_AAD_SPACE_SIZE 13
+/* SHA-384, the longest hash of a TLS 1.3 cipher suite kTLS supports */
+#define TLS_MAX_TRAFFIC_SECRET_SIZE 48
+
#define TLS_MAX_IV_SIZE 16
#define TLS_MAX_SALT_SIZE 4
#define TLS_TAG_SIZE 16
@@ -224,6 +227,12 @@ struct tls_prot_info {
u16 tail_size;
};
+/* A TLS 1.3 application traffic secret. len is 0 if none is registered. */
+struct tls_traffic_secret {
+ u8 data[TLS_MAX_TRAFFIC_SECRET_SIZE];
+ u8 len;
+};
+
struct tls_context {
/* read-only cache line */
struct tls_prot_info prot_info;
@@ -266,6 +275,10 @@ struct tls_context {
union tls_crypto_context crypto_send;
union tls_crypto_context crypto_recv;
+ /* Registered for KeyUpdate, protected by the socket lock */
+ struct tls_traffic_secret tx_secret;
+ struct tls_traffic_secret rx_secret;
+
struct list_head list;
refcount_t refcount;
struct rcu_head rcu;
diff --git a/include/uapi/linux/tls.h b/include/uapi/linux/tls.h
index 1245ab38afc1..d1b136ecea96 100644
--- a/include/uapi/linux/tls.h
+++ b/include/uapi/linux/tls.h
@@ -42,6 +42,8 @@
#define TLS_TX_ZEROCOPY_RO 3 /* TX zerocopy (only sendfile now) */
#define TLS_RX_EXPECT_NO_PAD 4 /* Attempt opportunistic zero-copy */
#define TLS_TX_MAX_PAYLOAD_LEN 5 /* Maximum plaintext size */
+#define TLS_TX_TRAFFIC_SECRET 6 /* TLS 1.3 TX traffic secret */
+#define TLS_RX_TRAFFIC_SECRET 7 /* TLS 1.3 RX traffic secret */
/* Supported versions */
#define TLS_VERSION_MINOR(ver) ((ver) & 0xFF)
diff --git a/net/tls/tls_main.c b/net/tls/tls_main.c
index 8bdb78718a8d..1c6e993ec012 100644
--- a/net/tls/tls_main.c
+++ b/net/tls/tls_main.c
@@ -31,6 +31,7 @@
* SOFTWARE.
*/
+#include <crypto/sha2.h>
#include <linux/module.h>
#include <net/tcp.h>
@@ -325,6 +326,8 @@ void tls_ctx_free(struct sock *sk, struct tls_context *ctx)
memzero_explicit(&ctx->crypto_send, sizeof(ctx->crypto_send));
memzero_explicit(&ctx->crypto_recv, sizeof(ctx->crypto_recv));
+ memzero_explicit(&ctx->tx_secret, sizeof(ctx->tx_secret));
+ memzero_explicit(&ctx->rx_secret, sizeof(ctx->rx_secret));
mutex_destroy(&ctx->tx_lock);
if (sk)
@@ -757,8 +760,16 @@ static int do_tls_setsockopt_conf(struct sock *sk, sockptr_t optval,
ctx->rx_conf = conf;
update_sk_prot(sk, ctx);
- if (update)
+ if (update) {
+ /* The registered secret no longer matches the key. The RX
+ * secret also needs the TX one, see do_tls_setsockopt_secret().
+ */
+ if (tx)
+ memzero_explicit(&ctx->tx_secret,
+ sizeof(ctx->tx_secret));
+ memzero_explicit(&ctx->rx_secret, sizeof(ctx->rx_secret));
return 0;
+ }
if (tx) {
ctx->sk_write_space = sk->sk_write_space;
@@ -857,6 +868,72 @@ static int do_tls_setsockopt_tx_payload_len(struct sock *sk, sockptr_t optval,
return 0;
}
+/* Length of the TLS 1.3 traffic secret for @cipher_type, the size of
+ * its hash. 0 if KeyUpdate is not supported for the cipher.
+ */
+static int tls_traffic_secret_len(u16 cipher_type)
+{
+ switch (cipher_type) {
+ case TLS_CIPHER_AES_GCM_128:
+ case TLS_CIPHER_AES_CCM_128:
+ case TLS_CIPHER_CHACHA20_POLY1305:
+ return SHA256_DIGEST_SIZE;
+ case TLS_CIPHER_AES_GCM_256:
+ return SHA384_DIGEST_SIZE;
+ default:
+ return 0;
+ }
+}
+
+static int do_tls_setsockopt_secret(struct sock *sk, sockptr_t optval,
+ unsigned int optlen, int tx)
+{
+ struct tls_context *ctx = tls_get_ctx(sk);
+ struct tls_crypto_info *crypto_info;
+ struct tls_traffic_secret *secret;
+ int len;
+
+ if (tx) {
+ crypto_info = &ctx->crypto_send.info;
+ secret = &ctx->tx_secret;
+ } else {
+ crypto_info = &ctx->crypto_recv.info;
+ secret = &ctx->rx_secret;
+ }
+
+ /* The key must be installed first: its cipher selects the hash */
+ if (!TLS_CRYPTO_INFO_READY(crypto_info) ||
+ crypto_info->version != TLS_1_3_VERSION)
+ return -EINVAL;
+
+ if ((tx ? ctx->tx_conf : ctx->rx_conf) != TLS_SW)
+ return -EOPNOTSUPP;
+
+ len = tls_traffic_secret_len(crypto_info->cipher_type);
+ if (!len)
+ return -EOPNOTSUPP;
+
+ if (sockptr_is_null(optval) || optlen != len)
+ return -EINVAL;
+
+ /* A received KeyUpdate may ask for one in reply. The kernel
+ * consumes the record, so only the kernel can send the reply.
+ */
+ if (!tx && !ctx->tx_secret.len)
+ return -EINVAL;
+
+ if (secret->len)
+ return -EBUSY;
+
+ if (copy_from_sockptr(secret->data, optval, len)) {
+ memzero_explicit(secret->data, sizeof(secret->data));
+ return -EFAULT;
+ }
+ secret->len = len;
+
+ return 0;
+}
+
static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
unsigned int optlen)
{
@@ -883,6 +960,13 @@ static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
rc = do_tls_setsockopt_tx_payload_len(sk, optval, optlen);
release_sock(sk);
break;
+ case TLS_TX_TRAFFIC_SECRET:
+ case TLS_RX_TRAFFIC_SECRET:
+ lock_sock(sk);
+ rc = do_tls_setsockopt_secret(sk, optval, optlen,
+ optname == TLS_TX_TRAFFIC_SECRET);
+ release_sock(sk);
+ break;
default:
rc = -ENOPROTOOPT;
break;
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH RFC net-next 2/5] tls: add socket options to register the traffic secrets
2026-10-26 0:36 ` [PATCH RFC net-next 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
@ 2026-10-26 7:37 ` Hannes Reinecke
0 siblings, 0 replies; 9+ messages in thread
From: Hannes Reinecke @ 2026-10-26 7:37 UTC (permalink / raw)
To: martin.nvme.dev, John Fastabend, Jakub Kicinski, Sabrina Dubroca,
David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
On 10/26/26 1:36 AM, Martin Belanger via B4 Relay wrote:
> From: Martin Belanger <martin.belanger@dell.com>
>
> kTLS holds the key and IV of each direction, but not the traffic secret
> they were derived from. The derivation is one-way, so the kernel cannot
> compute the next generation for a KeyUpdate on its own.
>
> Add TLS_TX_TRAFFIC_SECRET and TLS_RX_TRAFFIC_SECRET. The owner of the
> socket passes the TLS 1.3 application traffic secret of a direction after
> installing its key. The cipher of the installed key selects the hash, so
> the option takes only the secret. A socket without a registered secret
> behaves as before.
>
> The TX secret can be registered alone. The RX secret needs the TX secret
> first: a received KeyUpdate can ask for one in reply, and since the
> kernel consumes the record, only the kernel can send that reply.
>
> Installing a new key with TLS_TX or TLS_RX removes the secrets that no
> longer apply. The secrets are cleared when the context is freed and
> cannot be read back.
>
> Assisted-by: LLM
> Signed-off-by: Martin Belanger <martin.belanger@dell.com>
> ---
> Documentation/networking/tls.rst | 26 ++++++++++++
> include/net/tls.h | 13 ++++++
> include/uapi/linux/tls.h | 2 +
> net/tls/tls_main.c | 86 +++++++++++++++++++++++++++++++++++++++-
> 4 files changed, 126 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/networking/tls.rst b/Documentation/networking/tls.rst
> index 980c442d7161..2e011bd1ce4a 100644
> --- a/Documentation/networking/tls.rst
> +++ b/Documentation/networking/tls.rst
> @@ -300,6 +300,32 @@ extra byte used by the ContentType field.
>
> [1] https://datatracker.ietf.org/doc/html/rfc8449
>
> +TLS_TX_TRAFFIC_SECRET and TLS_RX_TRAFFIC_SECRET
> +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> +
> +``TLS_TX_TRAFFIC_SECRET`` registers the TLS 1.3 application traffic secret
> +of the transmit direction, ``TLS_RX_TRAFFIC_SECRET`` that of the receive
> +direction. With a registered secret, the kernel performs KeyUpdate for that
> +direction itself (RFC 9846, section 4.7.3). The value is the secret itself.
> +TLS 1.3 only, software path only.
> +
> +Install the key of the direction first, with the ``TLS_TX`` or ``TLS_RX``
> +socket option described above. The cipher of that key selects the hash, and
> +the secret must have the length of that hash: 32 bytes for SHA-256
> +(AES-128-GCM, AES-128-CCM, ChaCha20-Poly1305) or 48 bytes for SHA-384
> +(AES-256-GCM). Other ciphers return ``EOPNOTSUPP``.
> +
> +Each secret can be registered once. ``TLS_RX_TRAFFIC_SECRET`` needs
> +``TLS_TX_TRAFFIC_SECRET`` first: a received KeyUpdate can ask for one in
> +reply, and only the kernel sees it. ``TLS_TX_TRAFFIC_SECRET`` alone is
> +allowed.
> +
> +If the application installs a new key itself with ``TLS_TX``, both secrets
> +are removed. With ``TLS_RX``, the RX secret is removed. KeyUpdate for that
> +direction is then left to the application again.
> +
> +The secrets cannot be read back.
> +
> Statistics
> ==========
>
> diff --git a/include/net/tls.h b/include/net/tls.h
> index e57bef58851e..aacc6d12226e 100644
> --- a/include/net/tls.h
> +++ b/include/net/tls.h
> @@ -65,6 +65,9 @@ struct tls_rec;
>
> #define TLS_AAD_SPACE_SIZE 13
>
> +/* SHA-384, the longest hash of a TLS 1.3 cipher suite kTLS supports */
> +#define TLS_MAX_TRAFFIC_SECRET_SIZE 48
> +
> #define TLS_MAX_IV_SIZE 16
> #define TLS_MAX_SALT_SIZE 4
> #define TLS_TAG_SIZE 16
> @@ -224,6 +227,12 @@ struct tls_prot_info {
> u16 tail_size;
> };
>
> +/* A TLS 1.3 application traffic secret. len is 0 if none is registered. */
> +struct tls_traffic_secret {
> + u8 data[TLS_MAX_TRAFFIC_SECRET_SIZE];
> + u8 len;
> +};
> +
> struct tls_context {
> /* read-only cache line */
> struct tls_prot_info prot_info;
> @@ -266,6 +275,10 @@ struct tls_context {
> union tls_crypto_context crypto_send;
> union tls_crypto_context crypto_recv;
>
> + /* Registered for KeyUpdate, protected by the socket lock */
> + struct tls_traffic_secret tx_secret;
> + struct tls_traffic_secret rx_secret;
> +
> struct list_head list;
> refcount_t refcount;
> struct rcu_head rcu;
> diff --git a/include/uapi/linux/tls.h b/include/uapi/linux/tls.h
> index 1245ab38afc1..d1b136ecea96 100644
> --- a/include/uapi/linux/tls.h
> +++ b/include/uapi/linux/tls.h
> @@ -42,6 +42,8 @@
> #define TLS_TX_ZEROCOPY_RO 3 /* TX zerocopy (only sendfile now) */
> #define TLS_RX_EXPECT_NO_PAD 4 /* Attempt opportunistic zero-copy */
> #define TLS_TX_MAX_PAYLOAD_LEN 5 /* Maximum plaintext size */
> +#define TLS_TX_TRAFFIC_SECRET 6 /* TLS 1.3 TX traffic secret */
> +#define TLS_RX_TRAFFIC_SECRET 7 /* TLS 1.3 RX traffic secret */
>
> /* Supported versions */
> #define TLS_VERSION_MINOR(ver) ((ver) & 0xFF)
> diff --git a/net/tls/tls_main.c b/net/tls/tls_main.c
> index 8bdb78718a8d..1c6e993ec012 100644
> --- a/net/tls/tls_main.c
> +++ b/net/tls/tls_main.c
> @@ -31,6 +31,7 @@
> * SOFTWARE.
> */
>
> +#include <crypto/sha2.h>
> #include <linux/module.h>
>
> #include <net/tcp.h>
> @@ -325,6 +326,8 @@ void tls_ctx_free(struct sock *sk, struct tls_context *ctx)
>
> memzero_explicit(&ctx->crypto_send, sizeof(ctx->crypto_send));
> memzero_explicit(&ctx->crypto_recv, sizeof(ctx->crypto_recv));
> + memzero_explicit(&ctx->tx_secret, sizeof(ctx->tx_secret));
> + memzero_explicit(&ctx->rx_secret, sizeof(ctx->rx_secret));
> mutex_destroy(&ctx->tx_lock);
>
> if (sk)
> @@ -757,8 +760,16 @@ static int do_tls_setsockopt_conf(struct sock *sk, sockptr_t optval,
> ctx->rx_conf = conf;
> update_sk_prot(sk, ctx);
>
> - if (update)
> + if (update) {
> + /* The registered secret no longer matches the key. The RX
> + * secret also needs the TX one, see do_tls_setsockopt_secret().
> + */
What is a bit unfortunate that we don't have a direct link between the
key and the traffic secret. So a user might set a key which is _not_
derived from the traffic secret, rendering the KeyUpdate non-workable.
Can we add a check here?
It should be possible to calculate the key from the traffic secret, no?
> + if (tx)
> + memzero_explicit(&ctx->tx_secret,
> + sizeof(ctx->tx_secret));
> + memzero_explicit(&ctx->rx_secret, sizeof(ctx->rx_secret));
> return 0;
> + }
>
> if (tx) {
> ctx->sk_write_space = sk->sk_write_space;
> @@ -857,6 +868,72 @@ static int do_tls_setsockopt_tx_payload_len(struct sock *sk, sockptr_t optval,
> return 0;
> }
>
> +/* Length of the TLS 1.3 traffic secret for @cipher_type, the size of
> + * its hash. 0 if KeyUpdate is not supported for the cipher.
> + */
> +static int tls_traffic_secret_len(u16 cipher_type)
> +{
> + switch (cipher_type) {
> + case TLS_CIPHER_AES_GCM_128:
> + case TLS_CIPHER_AES_CCM_128:
> + case TLS_CIPHER_CHACHA20_POLY1305:
> + return SHA256_DIGEST_SIZE;
> + case TLS_CIPHER_AES_GCM_256:
> + return SHA384_DIGEST_SIZE;
> + default:
> + return 0;
> + }
> +}
> +
> +static int do_tls_setsockopt_secret(struct sock *sk, sockptr_t optval,
> + unsigned int optlen, int tx)
> +{
> + struct tls_context *ctx = tls_get_ctx(sk);
> + struct tls_crypto_info *crypto_info;
> + struct tls_traffic_secret *secret;
> + int len;
> +
> + if (tx) {
> + crypto_info = &ctx->crypto_send.info;
> + secret = &ctx->tx_secret;
> + } else {
> + crypto_info = &ctx->crypto_recv.info;
> + secret = &ctx->rx_secret;
> + }
> +
> + /* The key must be installed first: its cipher selects the hash */
> + if (!TLS_CRYPTO_INFO_READY(crypto_info) ||
> + crypto_info->version != TLS_1_3_VERSION)
> + return -EINVAL;
> +
> + if ((tx ? ctx->tx_conf : ctx->rx_conf) != TLS_SW)
> + return -EOPNOTSUPP;
> +
> + len = tls_traffic_secret_len(crypto_info->cipher_type);
> + if (!len)
> + return -EOPNOTSUPP;
> +
> + if (sockptr_is_null(optval) || optlen != len)
> + return -EINVAL;
> +
> + /* A received KeyUpdate may ask for one in reply. The kernel
> + * consumes the record, so only the kernel can send the reply.
> + */
> + if (!tx && !ctx->tx_secret.len)
> + return -EINVAL;
Hmm? What is this? Doesn't that imply that you first have to set the
TX key before setting the RX key?
The documentation says we can do either way ...
> +
> + if (secret->len)
> + return -EBUSY;
> +
> + if (copy_from_sockptr(secret->data, optval, len)) {
> + memzero_explicit(secret->data, sizeof(secret->data));
> + return -EFAULT;
> + }
> + secret->len = len;
> +
> + return 0;
> +}
> +
> static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
> unsigned int optlen)
> {
> @@ -883,6 +960,13 @@ static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
> rc = do_tls_setsockopt_tx_payload_len(sk, optval, optlen);
> release_sock(sk);
> break;
> + case TLS_TX_TRAFFIC_SECRET:
> + case TLS_RX_TRAFFIC_SECRET:
> + lock_sock(sk);
> + rc = do_tls_setsockopt_secret(sk, optval, optlen,
> + optname == TLS_TX_TRAFFIC_SECRET);
> + release_sock(sk);
> + break;
> default:
> rc = -ENOPROTOOPT;
> break;
>
Cheers,
Hannes
--
Dr. Hannes Reinecke Kernel Storage Architect
hare@suse.de +49 911 74053 688
SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg
HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH RFC net-next 3/5] tls: update the RX key when a KeyUpdate is received
2026-10-26 0:36 [PATCH RFC net-next 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
2026-10-26 0:36 ` [PATCH RFC net-next 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
2026-10-26 0:36 ` [PATCH RFC net-next 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
@ 2026-10-26 0:36 ` Martin Belanger via B4 Relay
2026-10-26 7:53 ` Hannes Reinecke
2026-10-26 0:36 ` [PATCH RFC net-next 4/5] tls: send a KeyUpdate at the record limit and on request Martin Belanger via B4 Relay
2026-10-26 0:36 ` [PATCH RFC net-next 5/5] selftests/tls: add KeyUpdate tests Martin Belanger via B4 Relay
4 siblings, 1 reply; 9+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-26 0:36 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
From: Martin Belanger <martin.belanger@dell.com>
When a KeyUpdate arrives, kTLS blocks reads with -EKEYEXPIRED until
the user installs the next key. A kernel user such as nvme-tcp or
sunrpc never does, so the connection stalls.
For a socket with a registered RX secret, derive the next key and
install it when the KeyUpdate is decrypted. The record is not passed to
the user, so recvmsg(), splice() and read_sock() go on to the next
record. Without a registered secret the behavior is unchanged.
A KeyUpdate that is not alone in its record, has a wrong length or an
invalid request_update value ends the connection, as RFC 9846 section
4.7.3 requires. So does a peer that sends more than 8 KeyUpdates in one
second, the same limit as GnuTLS.
Assisted-by: LLM
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
include/net/tls.h | 7 +++
net/tls/Makefile | 3 +-
net/tls/tls.h | 2 +
net/tls/tls_keyupdate.c | 150 ++++++++++++++++++++++++++++++++++++++++++++++++
net/tls/tls_sw.c | 35 +++++++++--
5 files changed, 190 insertions(+), 7 deletions(-)
diff --git a/include/net/tls.h b/include/net/tls.h
index aacc6d12226e..a5bc28a7d1f5 100644
--- a/include/net/tls.h
+++ b/include/net/tls.h
@@ -63,6 +63,11 @@ struct tls_rec;
#define TLS_HANDSHAKE_KEYUPDATE 24 /* rfc8446 B.3: Key update */
+/* KeyUpdate: msg_type, 24-bit length, request_update (RFC 9846 4.7.3) */
+#define TLS_KEYUPDATE_MSG_SIZE 5
+#define TLS_KEY_UPDATE_NOT_REQUESTED 0
+#define TLS_KEY_UPDATE_REQUESTED 1
+
#define TLS_AAD_SPACE_SIZE 13
/* SHA-384, the longest hash of a TLS 1.3 cipher suite kTLS supports */
@@ -142,6 +147,8 @@ struct tls_sw_context_rx {
u8 zc_capable:1;
u8 reader_contended:1;
bool key_update_pending;
+ u8 key_update_count; /* received in this window */
+ unsigned long key_update_window; /* jiffies */
struct tls_strparser strp;
diff --git a/net/tls/Makefile b/net/tls/Makefile
index 6edcf1271b75..ce8ac6a59f8c 100644
--- a/net/tls/Makefile
+++ b/net/tls/Makefile
@@ -7,6 +7,7 @@ CFLAGS_trace.o := -I$(src)
obj-$(CONFIG_TLS) += tls.o
-tls-y := tls_main.o tls_sw.o tls_proc.o trace.o tls_strp.o tls_hkdf.o
+tls-y := tls_main.o tls_sw.o tls_proc.o trace.o tls_strp.o tls_hkdf.o \
+ tls_keyupdate.o
tls-$(CONFIG_TLS_DEVICE) += tls_device.o tls_device_fallback.o
diff --git a/net/tls/tls.h b/net/tls/tls.h
index b2061f6bf065..63ece8074985 100644
--- a/net/tls/tls.h
+++ b/net/tls/tls.h
@@ -149,6 +149,8 @@ int init_prot_info(struct tls_prot_info *prot,
const struct tls_cipher_desc *cipher_desc);
int tls_hkdf_expand_label(const u8 *secret, size_t secret_len,
const char *label, u8 *out, size_t out_len);
+int tls_keyupdate_rekey(struct sock *sk, int tx);
+int tls_keyupdate_rx(struct sock *sk, struct sk_buff *skb);
int tls_set_sw_offload(struct sock *sk, int tx,
struct tls_crypto_info *new_crypto_info);
void tls_update_rx_zc_capable(struct tls_context *tls_ctx);
diff --git a/net/tls/tls_keyupdate.c b/net/tls/tls_keyupdate.c
new file mode 100644
index 000000000000..543bf57599c7
--- /dev/null
+++ b/net/tls/tls_keyupdate.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Dell Technologies Inc. or its subsidiaries.
+ *
+ * TLS 1.3 KeyUpdate, RFC 9846 sections 4.7.3 and 7.2.
+ *
+ * A TLS 1.3 peer can change the key of the direction it sends in at any
+ * time. It sends a KeyUpdate message, then encrypts all later records
+ * with the next key. A sender must also change its key before the key
+ * has protected too many records (section 5.5).
+ *
+ * The next key is derived from the application traffic secret, not from
+ * the current key. User space gives the kernel only the key and IV, so
+ * the kernel cannot derive the next key on its own. Without the secret,
+ * a received KeyUpdate goes to the application, and the kernel stops
+ * decrypting until the application installs the next key with TLS_RX.
+ *
+ * With TLS_TX_TRAFFIC_SECRET or TLS_RX_TRAFFIC_SECRET, user space also
+ * gives the kernel the secret of that direction. The kernel then handles
+ * KeyUpdate itself. This file derives and installs the next key, and
+ * processes a received KeyUpdate. tls_sw.c sends KeyUpdate messages.
+ */
+
+#include <linux/jiffies.h>
+#include <linux/skbuff.h>
+#include <net/strparser.h>
+#include <net/tls.h>
+
+#include "tls.h"
+
+/* A peer that sends more KeyUpdates than this in one second ends the
+ * connection. Each one costs three HMACs and a new key, and carries no
+ * data.
+ */
+#define TLS_KEYUPDATE_RX_MAX_PER_SEC 8
+
+/**
+ * tls_keyupdate_rekey - install the next key of one direction
+ * @sk: socket
+ * @tx: non-zero for the transmit direction
+ *
+ * Derive the next application traffic secret from the registered one,
+ * then its key and IV, and install them. The record sequence number
+ * restarts at zero.
+ *
+ * Return: 0 on success, or a negative errno. On error the installed key
+ * and the registered secret do not change.
+ */
+int tls_keyupdate_rekey(struct sock *sk, int tx)
+{
+ struct tls_context *ctx = tls_get_ctx(sk);
+ struct tls_traffic_secret *secret;
+ const struct tls_cipher_desc *cipher_desc;
+ u8 iv[TLS_MAX_SALT_SIZE + TLS_MAX_IV_SIZE];
+ u8 next[TLS_MAX_TRAFFIC_SECRET_SIZE];
+ union tls_crypto_context tmp = {};
+ struct tls_crypto_info *info = &tmp.info;
+ int rc;
+
+ secret = tx ? &ctx->tx_secret : &ctx->rx_secret;
+ cipher_desc = get_cipher_desc(ctx->prot_info.cipher_type);
+ if (!cipher_desc || !secret->len ||
+ cipher_desc->salt + cipher_desc->iv > sizeof(iv))
+ return -EINVAL;
+
+ /* version and cipher_type; the rest of tmp stays zero */
+ *info = tx ? ctx->crypto_send.info : ctx->crypto_recv.info;
+
+ rc = tls_hkdf_expand_label(secret->data, secret->len, "traffic upd",
+ next, secret->len);
+ if (rc)
+ goto out;
+
+ rc = tls_hkdf_expand_label(next, secret->len, "key",
+ crypto_info_key(info, cipher_desc),
+ cipher_desc->key);
+ if (rc)
+ goto out;
+
+ rc = tls_hkdf_expand_label(next, secret->len, "iv", iv,
+ cipher_desc->salt + cipher_desc->iv);
+ if (rc)
+ goto out;
+
+ memcpy(crypto_info_salt(info, cipher_desc), iv, cipher_desc->salt);
+ memcpy(crypto_info_iv(info, cipher_desc), iv + cipher_desc->salt,
+ cipher_desc->iv);
+
+ rc = tls_set_sw_offload(sk, tx, info);
+ if (rc)
+ goto out;
+
+ memcpy(secret->data, next, secret->len);
+
+out:
+ memzero_explicit(next, sizeof(next));
+ memzero_explicit(iv, sizeof(iv));
+ memzero_explicit(&tmp, sizeof(tmp));
+
+ return rc;
+}
+
+/**
+ * tls_keyupdate_rx - process a received KeyUpdate
+ * @sk: socket
+ * @skb: the decrypted record, which starts with a KeyUpdate message
+ *
+ * Called with the socket locked, for a direction with a registered
+ * secret. The caller does not pass the record to the user.
+ *
+ * Return: 0 on success, or a negative errno if the connection must end.
+ */
+int tls_keyupdate_rx(struct sock *sk, struct sk_buff *skb)
+{
+ struct tls_context *ctx = tls_get_ctx(sk);
+ struct tls_sw_context_rx *rx_ctx = tls_sw_ctx_rx(ctx);
+ const struct strp_msg *rxm = strp_msg(skb);
+ u8 msg[TLS_KEYUPDATE_MSG_SIZE];
+ int rc;
+
+ /* A KeyUpdate must end its record (section 5.1). Another message
+ * in the same record is not supported.
+ */
+ if (rxm->full_len != sizeof(msg) ||
+ skb_copy_bits(skb, rxm->offset, msg, sizeof(msg)))
+ return -EBADMSG;
+
+ /* msg_type, a 24-bit length of 1, then request_update */
+ if (msg[1] || msg[2] || msg[3] != 1)
+ return -EBADMSG;
+ if (msg[4] != TLS_KEY_UPDATE_NOT_REQUESTED &&
+ msg[4] != TLS_KEY_UPDATE_REQUESTED)
+ return -EBADMSG;
+
+ if (!rx_ctx->key_update_count ||
+ time_after(jiffies, rx_ctx->key_update_window + HZ)) {
+ rx_ctx->key_update_window = jiffies;
+ rx_ctx->key_update_count = 0;
+ }
+ if (++rx_ctx->key_update_count > TLS_KEYUPDATE_RX_MAX_PER_SEC)
+ return -EBADMSG;
+
+ rc = tls_keyupdate_rekey(sk, 0);
+ if (rc) {
+ TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYERROR);
+ return rc;
+ }
+ TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYOK);
+
+ return 0;
+}
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index 312e51270f29..a314fa638dc3 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -1551,15 +1551,26 @@ static int tls_check_pending_rekey(struct sock *sk, struct tls_context *ctx,
if (hs_type == TLS_HANDSHAKE_KEYUPDATE) {
struct tls_sw_context_rx *rx_ctx = ctx->priv_ctx_rx;
- WRITE_ONCE(rx_ctx->key_update_pending, true);
TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYRECEIVED);
+
+ if (ctx->rx_secret.len) {
+ err = tls_keyupdate_rx(sk, skb);
+ if (err) {
+ tls_err_abort(sk, err);
+ return err;
+ }
+ return 1;
+ }
+
+ WRITE_ONCE(rx_ctx->key_update_pending, true);
}
return 0;
}
/* On decrypt failure the connection is aborted (sk_err set) before
- * returning a negative errno.
+ * returning a negative errno. Return 1 for a record the kernel consumed
+ * (a KeyUpdate): the caller frees it and does not pass it to the user.
*/
static int tls_rx_one_record(struct sock *sk, struct msghdr *msg,
struct tls_decrypt_arg *darg)
@@ -1883,6 +1894,11 @@ int tls_sw_recvmsg(struct sock *sk,
err = tls_rx_one_record(sk, msg, &darg);
if (err < 0)
goto recv_end;
+ if (err > 0) {
+ tls_rx_rec_done(ctx);
+ consume_skb(darg.skb);
+ continue;
+ }
async |= darg.async;
@@ -2019,9 +2035,8 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
if (err)
goto splice_read_end;
- if (!skb_queue_empty(&ctx->rx_list)) {
- skb = __skb_dequeue(&ctx->rx_list);
- } else {
+ skb = __skb_dequeue(&ctx->rx_list);
+ while (!skb) {
struct tls_decrypt_arg darg;
err = tls_rx_rec_wait(sk, flags & SPLICE_F_NONBLOCK,
@@ -2036,7 +2051,10 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
goto splice_read_end;
tls_rx_rec_done(ctx);
- skb = darg.skb;
+ if (err > 0)
+ consume_skb(darg.skb);
+ else
+ skb = darg.skb;
}
rxm = strp_msg(skb);
@@ -2118,6 +2136,11 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
err = tls_rx_one_record(sk, NULL, &darg);
if (err < 0)
goto read_sock_end;
+ if (err > 0) {
+ tls_rx_rec_done(ctx);
+ consume_skb(darg.skb);
+ continue;
+ }
released = tls_read_flush_backlog(sk, prot, INT_MAX,
0, decrypted,
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH RFC net-next 3/5] tls: update the RX key when a KeyUpdate is received
2026-10-26 0:36 ` [PATCH RFC net-next 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
@ 2026-10-26 7:53 ` Hannes Reinecke
0 siblings, 0 replies; 9+ messages in thread
From: Hannes Reinecke @ 2026-10-26 7:53 UTC (permalink / raw)
To: martin.nvme.dev, John Fastabend, Jakub Kicinski, Sabrina Dubroca,
David S. Miller, Eric Dumazet, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
On 10/26/26 1:36 AM, Martin Belanger via B4 Relay wrote:
> From: Martin Belanger <martin.belanger@dell.com>
>
> When a KeyUpdate arrives, kTLS blocks reads with -EKEYEXPIRED until
> the user installs the next key. A kernel user such as nvme-tcp or
> sunrpc never does, so the connection stalls.
>
> For a socket with a registered RX secret, derive the next key and
> install it when the KeyUpdate is decrypted. The record is not passed to
> the user, so recvmsg(), splice() and read_sock() go on to the next
> record. Without a registered secret the behavior is unchanged.
>
> A KeyUpdate that is not alone in its record, has a wrong length or an
> invalid request_update value ends the connection, as RFC 9846 section
> 4.7.3 requires. So does a peer that sends more than 8 KeyUpdates in one
> second, the same limit as GnuTLS.
>
> Assisted-by: LLM
> Signed-off-by: Martin Belanger <martin.belanger@dell.com>
> ---
> include/net/tls.h | 7 +++
> net/tls/Makefile | 3 +-
> net/tls/tls.h | 2 +
> net/tls/tls_keyupdate.c | 150 ++++++++++++++++++++++++++++++++++++++++++++++++
> net/tls/tls_sw.c | 35 +++++++++--
> 5 files changed, 190 insertions(+), 7 deletions(-)
>
> diff --git a/include/net/tls.h b/include/net/tls.h
> index aacc6d12226e..a5bc28a7d1f5 100644
> --- a/include/net/tls.h
> +++ b/include/net/tls.h
> @@ -63,6 +63,11 @@ struct tls_rec;
>
> #define TLS_HANDSHAKE_KEYUPDATE 24 /* rfc8446 B.3: Key update */
>
> +/* KeyUpdate: msg_type, 24-bit length, request_update (RFC 9846 4.7.3) */
> +#define TLS_KEYUPDATE_MSG_SIZE 5
> +#define TLS_KEY_UPDATE_NOT_REQUESTED 0
> +#define TLS_KEY_UPDATE_REQUESTED 1
> +
> #define TLS_AAD_SPACE_SIZE 13
>
> /* SHA-384, the longest hash of a TLS 1.3 cipher suite kTLS supports */
> @@ -142,6 +147,8 @@ struct tls_sw_context_rx {
> u8 zc_capable:1;
> u8 reader_contended:1;
> bool key_update_pending;
> + u8 key_update_count; /* received in this window */
> + unsigned long key_update_window; /* jiffies */
>
> struct tls_strparser strp;
>
> diff --git a/net/tls/Makefile b/net/tls/Makefile
> index 6edcf1271b75..ce8ac6a59f8c 100644
> --- a/net/tls/Makefile
> +++ b/net/tls/Makefile
> @@ -7,6 +7,7 @@ CFLAGS_trace.o := -I$(src)
>
> obj-$(CONFIG_TLS) += tls.o
>
> -tls-y := tls_main.o tls_sw.o tls_proc.o trace.o tls_strp.o tls_hkdf.o
> +tls-y := tls_main.o tls_sw.o tls_proc.o trace.o tls_strp.o tls_hkdf.o \
> + tls_keyupdate.o
>
> tls-$(CONFIG_TLS_DEVICE) += tls_device.o tls_device_fallback.o
> diff --git a/net/tls/tls.h b/net/tls/tls.h
> index b2061f6bf065..63ece8074985 100644
> --- a/net/tls/tls.h
> +++ b/net/tls/tls.h
> @@ -149,6 +149,8 @@ int init_prot_info(struct tls_prot_info *prot,
> const struct tls_cipher_desc *cipher_desc);
> int tls_hkdf_expand_label(const u8 *secret, size_t secret_len,
> const char *label, u8 *out, size_t out_len);
> +int tls_keyupdate_rekey(struct sock *sk, int tx);
> +int tls_keyupdate_rx(struct sock *sk, struct sk_buff *skb);
> int tls_set_sw_offload(struct sock *sk, int tx,
> struct tls_crypto_info *new_crypto_info);
> void tls_update_rx_zc_capable(struct tls_context *tls_ctx);
> diff --git a/net/tls/tls_keyupdate.c b/net/tls/tls_keyupdate.c
> new file mode 100644
> index 000000000000..543bf57599c7
> --- /dev/null
> +++ b/net/tls/tls_keyupdate.c
> @@ -0,0 +1,150 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/* Copyright (c) 2026 Dell Technologies Inc. or its subsidiaries.
> + *
> + * TLS 1.3 KeyUpdate, RFC 9846 sections 4.7.3 and 7.2.
> + *
> + * A TLS 1.3 peer can change the key of the direction it sends in at any
> + * time. It sends a KeyUpdate message, then encrypts all later records
> + * with the next key. A sender must also change its key before the key
> + * has protected too many records (section 5.5).
> + *
> + * The next key is derived from the application traffic secret, not from
> + * the current key. User space gives the kernel only the key and IV, so
> + * the kernel cannot derive the next key on its own. Without the secret,
> + * a received KeyUpdate goes to the application, and the kernel stops
> + * decrypting until the application installs the next key with TLS_RX.
> + *
> + * With TLS_TX_TRAFFIC_SECRET or TLS_RX_TRAFFIC_SECRET, user space also
> + * gives the kernel the secret of that direction. The kernel then handles
> + * KeyUpdate itself. This file derives and installs the next key, and
> + * processes a received KeyUpdate. tls_sw.c sends KeyUpdate messages.
> + */
> +
> +#include <linux/jiffies.h>
> +#include <linux/skbuff.h>
> +#include <net/strparser.h>
> +#include <net/tls.h>
> +
> +#include "tls.h"
> +
> +/* A peer that sends more KeyUpdates than this in one second ends the
> + * connection. Each one costs three HMACs and a new key, and carries no
> + * data.
> + */
> +#define TLS_KEYUPDATE_RX_MAX_PER_SEC 8
> +
> +/**
> + * tls_keyupdate_rekey - install the next key of one direction
> + * @sk: socket
> + * @tx: non-zero for the transmit direction
> + *
> + * Derive the next application traffic secret from the registered one,
> + * then its key and IV, and install them. The record sequence number
> + * restarts at zero.
> + *
> + * Return: 0 on success, or a negative errno. On error the installed key
> + * and the registered secret do not change.
> + */
> +int tls_keyupdate_rekey(struct sock *sk, int tx)
> +{
> + struct tls_context *ctx = tls_get_ctx(sk);
> + struct tls_traffic_secret *secret;
> + const struct tls_cipher_desc *cipher_desc;
> + u8 iv[TLS_MAX_SALT_SIZE + TLS_MAX_IV_SIZE];
> + u8 next[TLS_MAX_TRAFFIC_SECRET_SIZE];
> + union tls_crypto_context tmp = {};
> + struct tls_crypto_info *info = &tmp.info;
> + int rc;
> +
> + secret = tx ? &ctx->tx_secret : &ctx->rx_secret;
> + cipher_desc = get_cipher_desc(ctx->prot_info.cipher_type);
> + if (!cipher_desc || !secret->len ||
> + cipher_desc->salt + cipher_desc->iv > sizeof(iv))
> + return -EINVAL;
> +
> + /* version and cipher_type; the rest of tmp stays zero */
> + *info = tx ? ctx->crypto_send.info : ctx->crypto_recv.info;
> +
> + rc = tls_hkdf_expand_label(secret->data, secret->len, "traffic upd",
> + next, secret->len);
> + if (rc)
> + goto out;
> +
> + rc = tls_hkdf_expand_label(next, secret->len, "key",
> + crypto_info_key(info, cipher_desc),
> + cipher_desc->key);
> + if (rc)
> + goto out;
> +
> + rc = tls_hkdf_expand_label(next, secret->len, "iv", iv,
> + cipher_desc->salt + cipher_desc->iv);
> + if (rc)
> + goto out;
> +
> + memcpy(crypto_info_salt(info, cipher_desc), iv, cipher_desc->salt);
> + memcpy(crypto_info_iv(info, cipher_desc), iv + cipher_desc->salt,
> + cipher_desc->iv);
> +
> + rc = tls_set_sw_offload(sk, tx, info);
> + if (rc)
> + goto out;
> +
> + memcpy(secret->data, next, secret->len);
> +
> +out:
> + memzero_explicit(next, sizeof(next));
> + memzero_explicit(iv, sizeof(iv));
> + memzero_explicit(&tmp, sizeof(tmp));
> +
> + return rc;
> +}
> +
Hmm. So this calculates 'iv' and 'key' from the traffic secret.
But as it does, why can't we use it to calculate the _initial_ 'iv' and
'key' from the traffic secret?
IE wouldn't it make sense to allow for _just_ the (initial) traffic
secrets to be set, and calculate 'iv' and 'key' on the fly _always?
That would remove the possible admin error where the admin sets a
set of 'IV' and 'key' values which do _not_ match the traffic secrets.
Hmm?
> +/**
> + * tls_keyupdate_rx - process a received KeyUpdate
> + * @sk: socket
> + * @skb: the decrypted record, which starts with a KeyUpdate message
> + *
> + * Called with the socket locked, for a direction with a registered
> + * secret. The caller does not pass the record to the user.
> + *
> + * Return: 0 on success, or a negative errno if the connection must end.
> + */
> +int tls_keyupdate_rx(struct sock *sk, struct sk_buff *skb)
> +{
> + struct tls_context *ctx = tls_get_ctx(sk);
> + struct tls_sw_context_rx *rx_ctx = tls_sw_ctx_rx(ctx);
> + const struct strp_msg *rxm = strp_msg(skb);
> + u8 msg[TLS_KEYUPDATE_MSG_SIZE];
> + int rc;
> +
> + /* A KeyUpdate must end its record (section 5.1). Another message
> + * in the same record is not supported.
> + */
> + if (rxm->full_len != sizeof(msg) ||
> + skb_copy_bits(skb, rxm->offset, msg, sizeof(msg)))
> + return -EBADMSG;
> +
> + /* msg_type, a 24-bit length of 1, then request_update */
> + if (msg[1] || msg[2] || msg[3] != 1)
> + return -EBADMSG;
> + if (msg[4] != TLS_KEY_UPDATE_NOT_REQUESTED &&
> + msg[4] != TLS_KEY_UPDATE_REQUESTED)
> + return -EBADMSG;
> +
> + if (!rx_ctx->key_update_count ||
> + time_after(jiffies, rx_ctx->key_update_window + HZ)) {
> + rx_ctx->key_update_window = jiffies;
> + rx_ctx->key_update_count = 0;
> + }
> + if (++rx_ctx->key_update_count > TLS_KEYUPDATE_RX_MAX_PER_SEC)
> + return -EBADMSG;
> +
> + rc = tls_keyupdate_rekey(sk, 0);
> + if (rc) {
> + TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYERROR);
> + return rc;
> + }
> + TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYOK);
> +
> + return 0;
> +}
> diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
> index 312e51270f29..a314fa638dc3 100644
> --- a/net/tls/tls_sw.c
> +++ b/net/tls/tls_sw.c
> @@ -1551,15 +1551,26 @@ static int tls_check_pending_rekey(struct sock *sk, struct tls_context *ctx,
> if (hs_type == TLS_HANDSHAKE_KEYUPDATE) {
> struct tls_sw_context_rx *rx_ctx = ctx->priv_ctx_rx;
>
> - WRITE_ONCE(rx_ctx->key_update_pending, true);
> TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYRECEIVED);
> +
> + if (ctx->rx_secret.len) {
> + err = tls_keyupdate_rx(sk, skb);
> + if (err) {
> + tls_err_abort(sk, err);
> + return err;
> + }
> + return 1;
> + }
> +
> + WRITE_ONCE(rx_ctx->key_update_pending, true);
> }
>
> return 0;
> }
>
> /* On decrypt failure the connection is aborted (sk_err set) before
> - * returning a negative errno.
> + * returning a negative errno. Return 1 for a record the kernel consumed
> + * (a KeyUpdate): the caller frees it and does not pass it to the user.
> */
> static int tls_rx_one_record(struct sock *sk, struct msghdr *msg,
> struct tls_decrypt_arg *darg)
> @@ -1883,6 +1894,11 @@ int tls_sw_recvmsg(struct sock *sk,
> err = tls_rx_one_record(sk, msg, &darg);
> if (err < 0)
> goto recv_end;
> + if (err > 0) {
> + tls_rx_rec_done(ctx);
> + consume_skb(darg.skb);
> + continue;
> + }
>
> async |= darg.async;
>
> @@ -2019,9 +2035,8 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
> if (err)
> goto splice_read_end;
>
> - if (!skb_queue_empty(&ctx->rx_list)) {
> - skb = __skb_dequeue(&ctx->rx_list);
> - } else {
> + skb = __skb_dequeue(&ctx->rx_list);
> + while (!skb) {
> struct tls_decrypt_arg darg;
>
> err = tls_rx_rec_wait(sk, flags & SPLICE_F_NONBLOCK,
> @@ -2036,7 +2051,10 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
> goto splice_read_end;
>
> tls_rx_rec_done(ctx);
> - skb = darg.skb;
> + if (err > 0)
> + consume_skb(darg.skb);
> + else
> + skb = darg.skb;
> }
>
> rxm = strp_msg(skb);
> @@ -2118,6 +2136,11 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
> err = tls_rx_one_record(sk, NULL, &darg);
> if (err < 0)
> goto read_sock_end;
> + if (err > 0) {
> + tls_rx_rec_done(ctx);
> + consume_skb(darg.skb);
> + continue;
> + }
>
> released = tls_read_flush_backlog(sk, prot, INT_MAX,
> 0, decrypted,
>
The rest looks okay.
Cheers,
Hannes
--
Dr. Hannes Reinecke Kernel Storage Architect
hare@suse.de +49 911 74053 688
SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg
HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH RFC net-next 4/5] tls: send a KeyUpdate at the record limit and on request
2026-10-26 0:36 [PATCH RFC net-next 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
` (2 preceding siblings ...)
2026-10-26 0:36 ` [PATCH RFC net-next 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
@ 2026-10-26 0:36 ` Martin Belanger via B4 Relay
2026-10-26 0:36 ` [PATCH RFC net-next 5/5] selftests/tls: add KeyUpdate tests Martin Belanger via B4 Relay
4 siblings, 0 replies; 9+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-26 0:36 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
From: Martin Belanger <martin.belanger@dell.com>
RFC 9846 section 5.5 requires a key update before the key reaches its
usage limit, 2^24.5 full-size records for AES-GCM. The limit counts
records, and only the record layer knows where records start and end.
Section 4.7.3 also requires a reply to a KeyUpdate with
update_requested, sent before the next Application Data record. Both
need a KeyUpdate at an exact position in the record stream.
For a socket with a registered TX secret, send a KeyUpdate before the
next record when the key has protected 2^24 records, as GnuTLS does,
or when the peer asked for one. ChaCha20-Poly1305 has no record limit.
The KeyUpdate is sent as a handshake record, which closes the open data
record first. Pending encryptions are then completed, and only then is
the next key installed.
Add TLS_TX_KEY_UPDATE to send a KeyUpdate now, optionally asking the
peer to update too. It can only bring a KeyUpdate forward: the record
limit is enforced either way. Following section 4.7.3, a new request
to the peer waits for the answer to the previous one, and no more than
2^48-1 epochs are used.
Assisted-by: LLM
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
Documentation/networking/tls.rst | 17 +++++
include/net/tls.h | 5 +-
include/uapi/linux/tls.h | 1 +
net/tls/tls.h | 1 +
net/tls/tls_keyupdate.c | 5 ++
net/tls/tls_main.c | 32 +++++++++
net/tls/tls_sw.c | 149 ++++++++++++++++++++++++++++++++++++++-
7 files changed, 208 insertions(+), 2 deletions(-)
diff --git a/Documentation/networking/tls.rst b/Documentation/networking/tls.rst
index 2e011bd1ce4a..69752a6d67df 100644
--- a/Documentation/networking/tls.rst
+++ b/Documentation/networking/tls.rst
@@ -326,6 +326,23 @@ direction is then left to the application again.
The secrets cannot be read back.
+With a registered TX secret, the kernel sends a KeyUpdate before the key
+reaches its record limit (RFC 9846, section 5.5): after 2^24 records, except
+for ChaCha20-Poly1305, which has no limit. It also sends one before the next
+record when a received KeyUpdate asks for it. With a registered RX secret,
+the kernel processes a received KeyUpdate and does not pass the record to
+the application. A peer that sends more than 8 KeyUpdates in one second ends
+the connection.
+
+TLS_TX_KEY_UPDATE
+~~~~~~~~~~~~~~~~~
+
+TLS 1.3 only. Send a KeyUpdate now and install the next TX key. Needs a
+registered TX secret. The value is an ``int``: 0 to send a KeyUpdate, 1 to
+also ask the peer to update its key. Asking the peer needs a registered RX
+secret, and fails with ``EBUSY`` until the peer has answered the previous
+request.
+
Statistics
==========
diff --git a/include/net/tls.h b/include/net/tls.h
index a5bc28a7d1f5..d3123b17030c 100644
--- a/include/net/tls.h
+++ b/include/net/tls.h
@@ -282,9 +282,12 @@ struct tls_context {
union tls_crypto_context crypto_send;
union tls_crypto_context crypto_recv;
- /* Registered for KeyUpdate, protected by the socket lock */
+ /* KeyUpdate state, protected by the socket lock */
struct tls_traffic_secret tx_secret;
struct tls_traffic_secret rx_secret;
+ u64 tx_key_updates; /* KeyUpdates sent */
+ bool tx_key_update_reply; /* peer set update_requested */
+ bool tx_key_update_asked; /* we set it, no answer yet */
struct list_head list;
refcount_t refcount;
diff --git a/include/uapi/linux/tls.h b/include/uapi/linux/tls.h
index d1b136ecea96..c85408cb6ce7 100644
--- a/include/uapi/linux/tls.h
+++ b/include/uapi/linux/tls.h
@@ -44,6 +44,7 @@
#define TLS_TX_MAX_PAYLOAD_LEN 5 /* Maximum plaintext size */
#define TLS_TX_TRAFFIC_SECRET 6 /* TLS 1.3 TX traffic secret */
#define TLS_RX_TRAFFIC_SECRET 7 /* TLS 1.3 RX traffic secret */
+#define TLS_TX_KEY_UPDATE 8 /* Send a TLS 1.3 KeyUpdate now */
/* Supported versions */
#define TLS_VERSION_MINOR(ver) ((ver) & 0xFF)
diff --git a/net/tls/tls.h b/net/tls/tls.h
index 63ece8074985..d12c9278c814 100644
--- a/net/tls/tls.h
+++ b/net/tls/tls.h
@@ -151,6 +151,7 @@ int tls_hkdf_expand_label(const u8 *secret, size_t secret_len,
const char *label, u8 *out, size_t out_len);
int tls_keyupdate_rekey(struct sock *sk, int tx);
int tls_keyupdate_rx(struct sock *sk, struct sk_buff *skb);
+int tls_sw_key_update(struct sock *sk, int request);
int tls_set_sw_offload(struct sock *sk, int tx,
struct tls_crypto_info *new_crypto_info);
void tls_update_rx_zc_capable(struct tls_context *tls_ctx);
diff --git a/net/tls/tls_keyupdate.c b/net/tls/tls_keyupdate.c
index 543bf57599c7..851d5b54b336 100644
--- a/net/tls/tls_keyupdate.c
+++ b/net/tls/tls_keyupdate.c
@@ -146,5 +146,10 @@ int tls_keyupdate_rx(struct sock *sk, struct sk_buff *skb)
}
TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYOK);
+ /* Any KeyUpdate from the peer answers our update_requested */
+ ctx->tx_key_update_asked = false;
+ if (msg[4] == TLS_KEY_UPDATE_REQUESTED)
+ ctx->tx_key_update_reply = true;
+
return 0;
}
diff --git a/net/tls/tls_main.c b/net/tls/tls_main.c
index 1c6e993ec012..0bce7df279a5 100644
--- a/net/tls/tls_main.c
+++ b/net/tls/tls_main.c
@@ -934,6 +934,35 @@ static int do_tls_setsockopt_secret(struct sock *sk, sockptr_t optval,
return 0;
}
+static int do_tls_setsockopt_key_update(struct sock *sk, sockptr_t optval,
+ unsigned int optlen)
+{
+ struct tls_context *ctx = tls_get_ctx(sk);
+ int value, rc;
+
+ if (sockptr_is_null(optval) || optlen != sizeof(value))
+ return -EINVAL;
+
+ if (copy_from_sockptr(&value, optval, sizeof(value)))
+ return -EFAULT;
+
+ if (value != 0 && value != 1)
+ return -EINVAL;
+
+ rc = mutex_lock_interruptible(&ctx->tx_lock);
+ if (rc)
+ return rc;
+ lock_sock(sk);
+ if (ctx->tx_conf == TLS_SW)
+ rc = tls_sw_key_update(sk, value);
+ else
+ rc = -EINVAL;
+ release_sock(sk);
+ mutex_unlock(&ctx->tx_lock);
+
+ return rc;
+}
+
static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
unsigned int optlen)
{
@@ -967,6 +996,9 @@ static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
optname == TLS_TX_TRAFFIC_SECRET);
release_sock(sk);
break;
+ case TLS_TX_KEY_UPDATE:
+ rc = do_tls_setsockopt_key_update(sk, optval, optlen);
+ break;
default:
rc = -ENOPROTOOPT;
break;
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index a314fa638dc3..4acedddfb01e 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -40,6 +40,7 @@
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/splice.h>
+#include <linux/unaligned.h>
#include <crypto/aead.h>
#include <net/strparser.h>
@@ -1005,6 +1006,150 @@ static int tls_sw_sendmsg_locked(struct sock *sk, struct msghdr *msg,
return copied > 0 ? copied : ret;
}
+/* RFC 9846 section 5.5 allows 2^24.5 full-size records per AES-GCM key.
+ * Send the KeyUpdate a little earlier.
+ */
+#define TLS_KEYUPDATE_TX_RECORDS BIT_ULL(24)
+
+/* Section 4.7.3: the epoch must not exceed 2^48-1. The application
+ * traffic secrets start at epoch 3.
+ */
+#define TLS_KEYUPDATE_MAX (BIT_ULL(48) - 4)
+
+/* Send a KeyUpdate with the current key, then install the next key.
+ * Called with tx_lock held and the socket locked.
+ */
+static int tls_tx_key_update(struct sock *sk, u8 request, int flags)
+{
+ u8 body[TLS_KEYUPDATE_MSG_SIZE] = {
+ TLS_HANDSHAKE_KEYUPDATE, 0, 0, 1, request
+ };
+ struct tls_context *tls_ctx = tls_get_ctx(sk);
+ struct tls_sw_context_tx *ctx = tls_sw_ctx_tx(tls_ctx);
+ u8 record_type = TLS_RECORD_TYPE_HANDSHAKE;
+ u8 buf[CMSG_SPACE(sizeof(record_type))];
+ struct msghdr msg = {};
+ struct cmsghdr *cmsg;
+ struct kvec iov;
+ int ret;
+
+ memset(buf, 0, sizeof(buf));
+ msg.msg_control = buf;
+ msg.msg_controllen = sizeof(buf);
+ msg.msg_flags = flags & MSG_DONTWAIT;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_TLS;
+ cmsg->cmsg_type = TLS_SET_RECORD_TYPE;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(record_type));
+ memcpy(CMSG_DATA(cmsg), &record_type, sizeof(record_type));
+
+ iov.iov_base = body;
+ iov.iov_len = sizeof(body);
+ iov_iter_kvec(&msg.msg_iter, ITER_SOURCE, &iov, 1, sizeof(body));
+
+ /* A control record closes the open record first, so all data
+ * written so far goes out with the current key, ahead of the
+ * KeyUpdate.
+ */
+ ret = tls_sw_sendmsg_locked(sk, &msg, sizeof(body));
+ if (ret < 0)
+ return ret;
+
+ /* Nothing may be encrypted with the next key before the KeyUpdate
+ * is encrypted with the current one.
+ */
+ ret = tls_encrypt_async_wait(ctx);
+ if (!ret)
+ ret = tls_keyupdate_rekey(sk, 1);
+ if (ret) {
+ /* The peer expects the next key: the connection is lost */
+ TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSTXREKEYERROR);
+ tls_err_abort(sk, ret);
+ return ret;
+ }
+
+ TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSTXREKEYOK);
+ tls_ctx->tx_key_updates++;
+ if (request == TLS_KEY_UPDATE_NOT_REQUESTED)
+ tls_ctx->tx_key_update_reply = false;
+
+ return 0;
+}
+
+/* Send a KeyUpdate if one is due: in reply to the peer's
+ * update_requested (RFC 9846 section 4.7.3), or before the key reaches
+ * its record limit (section 5.5). ChaCha20-Poly1305 has no record limit:
+ * the sequence number would wrap first.
+ */
+static int tls_tx_key_update_if_due(struct sock *sk, int flags)
+{
+ struct tls_context *ctx = tls_get_ctx(sk);
+ bool limit;
+
+ if (likely(!ctx->tx_secret.len))
+ return 0;
+
+ limit = ctx->prot_info.cipher_type != TLS_CIPHER_CHACHA20_POLY1305 &&
+ get_unaligned_be64(ctx->tx.rec_seq) >= TLS_KEYUPDATE_TX_RECORDS;
+ if (!limit && !ctx->tx_key_update_reply)
+ return 0;
+
+ if (ctx->tx_key_updates >= TLS_KEYUPDATE_MAX) {
+ /* Ignore update_requested. At the record limit the
+ * connection must end.
+ */
+ ctx->tx_key_update_reply = false;
+ return limit ? -EKEYEXPIRED : 0;
+ }
+
+ return tls_tx_key_update(sk, TLS_KEY_UPDATE_NOT_REQUESTED, flags);
+}
+
+/**
+ * tls_sw_key_update - send a KeyUpdate now, for TLS_TX_KEY_UPDATE
+ * @sk: socket
+ * @request: non-zero to also ask the peer to update its key
+ *
+ * Called with tx_lock held and the socket locked.
+ *
+ * Return: 0 on success, or a negative errno.
+ */
+int tls_sw_key_update(struct sock *sk, int request)
+{
+ struct tls_context *ctx = tls_get_ctx(sk);
+ int ret;
+
+ if (!ctx->tx_secret.len)
+ return -EINVAL;
+
+ if (request) {
+ /* Only the kernel sees the answer */
+ if (!ctx->rx_secret.len)
+ return -EINVAL;
+ /* Section 4.7.3: wait for the answer to the previous one */
+ if (ctx->tx_key_update_asked)
+ return -EBUSY;
+ }
+
+ if (ctx->tx_key_updates >= TLS_KEYUPDATE_MAX - 1)
+ return -EKEYEXPIRED;
+
+ /* A reply must carry update_not_requested */
+ if (request && ctx->tx_key_update_reply) {
+ ret = tls_tx_key_update(sk, TLS_KEY_UPDATE_NOT_REQUESTED, 0);
+ if (ret)
+ return ret;
+ }
+
+ ret = tls_tx_key_update(sk, request ? TLS_KEY_UPDATE_REQUESTED :
+ TLS_KEY_UPDATE_NOT_REQUESTED, 0);
+ if (!ret && request)
+ ctx->tx_key_update_asked = true;
+
+ return ret;
+}
+
int tls_sw_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tls_context *tls_ctx = tls_get_ctx(sk);
@@ -1019,7 +1164,9 @@ int tls_sw_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
if (ret)
return ret;
lock_sock(sk);
- ret = tls_sw_sendmsg_locked(sk, msg, size);
+ ret = tls_tx_key_update_if_due(sk, msg->msg_flags);
+ if (!ret)
+ ret = tls_sw_sendmsg_locked(sk, msg, size);
release_sock(sk);
mutex_unlock(&tls_ctx->tx_lock);
return ret;
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH RFC net-next 5/5] selftests/tls: add KeyUpdate tests
2026-10-26 0:36 [PATCH RFC net-next 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
` (3 preceding siblings ...)
2026-10-26 0:36 ` [PATCH RFC net-next 4/5] tls: send a KeyUpdate at the record limit and on request Martin Belanger via B4 Relay
@ 2026-10-26 0:36 ` Martin Belanger via B4 Relay
4 siblings, 0 replies; 9+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-26 0:36 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Randy Dunlap, Shuah Khan
Cc: linux-kernel, netdev, linux-doc, linux-kselftest,
Hannes Reinecke, Chuck Lever, kernel-tls-handshake, linux-nvme,
Alistair Francis, Martin Belanger
From: Martin Belanger <martin.belanger@dell.com>
Test the in-kernel KeyUpdate on a bidirectional connection with both
traffic secrets registered on each socket:
- registration errors: wrong length, missing key, TLS 1.2, SM4, and
the RX secret before the TX secret;
- KeyUpdates in both directions with data around them;
- a data record, a KeyUpdate and another data record queued before
the first read;
- update_requested: the peer answers before its next data, and a
second request waits for the answer;
- splice() across a KeyUpdate;
- a key installed with TLS_TX removes the registered secrets;
- more than 8 KeyUpdates in one second, and an invalid
request_update value, end the connection.
Assisted-by: LLM
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
tools/testing/selftests/net/tls.c | 249 ++++++++++++++++++++++++++++++++++++++
1 file changed, 249 insertions(+)
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index 9d3cd4fff062..838030d09cd3 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -2390,6 +2390,255 @@ TEST_F(tls, rekey_poll_delay)
}
}
+/* In-kernel KeyUpdate: fd and cfd both register the TX and RX traffic
+ * secrets of a bidirectional connection. Returns false if the variant
+ * does not support it.
+ */
+static bool keyupdate_setup(struct __test_metadata *_metadata,
+ FIXTURE_DATA(tls) *self,
+ const FIXTURE_VARIANT(tls) *variant)
+{
+ struct tls_crypto_info_keys tls12;
+ unsigned char secret_a[48], secret_b[48];
+ int len;
+
+ if (self->notls || variant->tls_version != TLS_1_3_VERSION)
+ return false;
+
+ switch (variant->cipher_type) {
+ case TLS_CIPHER_AES_GCM_128:
+ case TLS_CIPHER_AES_CCM_128:
+ case TLS_CIPHER_CHACHA20_POLY1305:
+ len = 32;
+ break;
+ case TLS_CIPHER_AES_GCM_256:
+ len = 48;
+ break;
+ default:
+ return false;
+ }
+
+ /* cfd -> fd */
+ tls_crypto_info_init(variant->tls_version, variant->cipher_type,
+ &tls12, 1);
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX, &tls12, tls12.len), 0);
+ EXPECT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_TX, &tls12, tls12.len), 0);
+
+ memset(secret_a, 0xa5, sizeof(secret_a));
+ memset(secret_b, 0x5a, sizeof(secret_b));
+
+ /* the RX secret needs the TX secret first */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX_TRAFFIC_SECRET,
+ secret_b, len), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_TRAFFIC_SECRET,
+ secret_a, len), 0);
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX_TRAFFIC_SECRET,
+ secret_b, len), 0);
+ EXPECT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_TX_TRAFFIC_SECRET,
+ secret_b, len), 0);
+ EXPECT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_RX_TRAFFIC_SECRET,
+ secret_a, len), 0);
+
+ return true;
+}
+
+static void keyupdate_send(struct __test_metadata *_metadata, int fd,
+ int request)
+{
+ EXPECT_EQ(setsockopt(fd, SOL_TLS, TLS_TX_KEY_UPDATE,
+ &request, sizeof(request)), 0);
+}
+
+static void send_recv_str(struct __test_metadata *_metadata, int from, int to,
+ const char *str)
+{
+ int len = strlen(str) + 1;
+ char buf[64];
+
+ EXPECT_EQ(send(from, str, len, 0), len);
+ EXPECT_EQ(recv(to, buf, sizeof(buf), 0), len);
+ EXPECT_EQ(memcmp(buf, str, len), 0);
+}
+
+TEST_F(tls, keyupdate_secret_errors)
+{
+ unsigned char secret[48] = {};
+ int one = 1;
+
+ if (self->notls)
+ SKIP(return, "no TLS support");
+
+ /* no secret registered */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_KEY_UPDATE,
+ &one, sizeof(one)), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ if (variant->tls_version != TLS_1_3_VERSION) {
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_TRAFFIC_SECRET,
+ secret, 32), -1);
+ EXPECT_EQ(errno, EINVAL);
+ return;
+ }
+
+ if (variant->cipher_type == TLS_CIPHER_SM4_GCM ||
+ variant->cipher_type == TLS_CIPHER_SM4_CCM) {
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_TRAFFIC_SECRET,
+ secret, 32), -1);
+ EXPECT_EQ(errno, EOPNOTSUPP);
+ return;
+ }
+
+ /* the RX key is not installed on fd */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX_TRAFFIC_SECRET,
+ secret, 32), -1);
+ EXPECT_EQ(errno, EINVAL);
+
+ /* wrong length */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_TRAFFIC_SECRET,
+ secret, 16), -1);
+ EXPECT_EQ(errno, EINVAL);
+}
+
+TEST_F(tls, keyupdate)
+{
+ int i;
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ send_recv_str(_metadata, self->fd, self->cfd, "before");
+
+ /* stay below the limit of 8 received KeyUpdates per second */
+ for (i = 0; i < 4; i++) {
+ keyupdate_send(_metadata, self->fd, 0);
+ send_recv_str(_metadata, self->fd, self->cfd, "after");
+ keyupdate_send(_metadata, self->cfd, 0);
+ send_recv_str(_metadata, self->cfd, self->fd, "reverse");
+ }
+}
+
+TEST_F(tls, keyupdate_queued)
+{
+ const char *str = "queued behind a KeyUpdate";
+ int len = strlen(str) + 1;
+ char buf[64];
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ /* the record before the KeyUpdate, the KeyUpdate and the record
+ * after it are all queued before the first read
+ */
+ EXPECT_EQ(send(self->fd, str, len, MSG_MORE), len);
+ keyupdate_send(_metadata, self->fd, 0);
+ EXPECT_EQ(send(self->fd, str, len, 0), len);
+
+ EXPECT_EQ(recv(self->cfd, buf, len, MSG_WAITALL), len);
+ EXPECT_EQ(memcmp(buf, str, len), 0);
+ EXPECT_EQ(recv(self->cfd, buf, len, MSG_WAITALL), len);
+ EXPECT_EQ(memcmp(buf, str, len), 0);
+}
+
+TEST_F(tls, keyupdate_request)
+{
+ int one = 1;
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ keyupdate_send(_metadata, self->fd, 1);
+
+ /* no answer yet */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_KEY_UPDATE,
+ &one, sizeof(one)), -1);
+ EXPECT_EQ(errno, EBUSY);
+
+ /* cfd processes the request, then answers ahead of its data */
+ send_recv_str(_metadata, self->fd, self->cfd, "request");
+ send_recv_str(_metadata, self->cfd, self->fd, "answer");
+
+ /* answered */
+ keyupdate_send(_metadata, self->fd, 1);
+ send_recv_str(_metadata, self->fd, self->cfd, "request");
+ send_recv_str(_metadata, self->cfd, self->fd, "answer");
+}
+
+TEST_F(tls, keyupdate_splice)
+{
+ const char *str = "spliced after a KeyUpdate";
+ int len = strlen(str) + 1;
+ char buf[64];
+ int p[2];
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ ASSERT_GE(pipe(p), 0);
+
+ keyupdate_send(_metadata, self->fd, 0);
+ EXPECT_EQ(send(self->fd, str, len, 0), len);
+
+ EXPECT_EQ(splice(self->cfd, NULL, p[1], NULL, len, 0), len);
+ EXPECT_EQ(read(p[0], buf, len), len);
+ EXPECT_EQ(memcmp(buf, str, len), 0);
+
+ close(p[0]);
+ close(p[1]);
+}
+
+TEST_F(tls, keyupdate_manual_rekey)
+{
+ struct tls_crypto_info_keys tls12;
+ int zero = 0;
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ /* a key installed by the user removes the registered secrets */
+ tls_crypto_info_init(variant->tls_version, variant->cipher_type,
+ &tls12, 2);
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len), 0);
+
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_KEY_UPDATE,
+ &zero, sizeof(zero)), -1);
+ EXPECT_EQ(errno, EINVAL);
+}
+
+TEST_F(tls, keyupdate_rate_limit)
+{
+ char buf[64];
+ int i;
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ /* the ninth KeyUpdate in one second ends the connection */
+ for (i = 0; i < 9; i++)
+ keyupdate_send(_metadata, self->fd, 0);
+ EXPECT_EQ(send(self->fd, "x", 1, 0), 1);
+
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), 0), -1);
+ EXPECT_EQ(errno, EBADMSG);
+}
+
+TEST_F(tls, keyupdate_invalid)
+{
+ /* key_update, length 1, request_update 2 */
+ static const char bad[] = "\x18\x00\x00\x01\x02";
+ char buf[64];
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ EXPECT_EQ(tls_send_cmsg(self->fd, TLS_RECORD_TYPE_HANDSHAKE,
+ (char *)bad, 5, 0), 5);
+
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), 0), -1);
+ EXPECT_EQ(errno, EBADMSG);
+}
+
struct raw_rec {
unsigned int plain_len;
unsigned char plain_data[100];
--
2.53.0
^ permalink raw reply [flat|nested] 9+ messages in thread