* [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel
@ 2026-10-30 11:18 Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-30 11:18 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
kTLS does not currently handle TLS 1.3 KeyUpdate. It never sends a
KeyUpdate. And when it receives one, it blocks the socket until the
application installs the next key. kTLS also does not enforce the key
usage limit on transmit.
This applies to TLS libraries in user space as well as kernel users such
as NVMe/TCP or sunrpc:
- GnuTLS sends a KeyUpdate after 2^24 records when it runs its own
record layer. With kTLS, the record layer is handled in the kernel and
GnuTLS therefore never runs the check. OpenSSL never sends a KeyUpdate
on its own.
- NVMe/TCP and sunrpc don't have a mechanism to install a new key. A
conforming peer that sends a KeyUpdate stalls their connection.
RFC 9846 (July 2026), which obsoletes RFC 8446, makes the limit
mandatory (section 5.5): "Implementations MUST either close the
connection or do a key update as described in Section 4.7.3 prior to
reaching these limits." Long-lived connections with small records reach
the limit quickly. One NVMe/TCP queue sends at least one record per
command, and more with large I/O. Depending on the load and link speed,
the 2^24 limit can be reached in less than 10 minutes in many cases.
With this series, kTLS handles KeyUpdate in both directions for a socket
that registers its traffic secrets. A socket that does not register them
behaves as before. A user can opt in by passing the secrets after the
handshake. GnuTLS and OpenSSL install the kTLS keys themselves, so they
could register the secrets at the same time. tlshd gets the secrets from
GnuTLS through a callback it uses for QUIC.
Why the record layer
====================
KeyUpdate must be handled where the records are built.
The 2^24 limit is a record count, and only the record layer can see the
count precisely as it changes. A user sitting above kTLS can only read
the count with getsockopt(TLS_TX) after each write. A kTLS user writes
bytes, and the kernel splits and merges those bytes into records. I
counted send calls in an NVMe/TCP prototype trying to guesstimate the
2^24 limit. The count was correct with 4 KB I/O, but was 21 times too
low with 1 MB I/O. Each send call translates into a different number of
records. It's simply not possible to precisely guess the number of
records based on the number of send calls.
The KeyUpdate must also go at a precise place in the record stream
(section 4.7.3). It is encrypted with the old key, and every record
after it with the new key. When the peer sets update_requested, our
KeyUpdate must go out before the next Application Data record. The key
can only change between two records. Only net/tls has control over that.
Fixing this in one driver or library would leave other drivers or
libraries to re-implement their own (imperfect) solution.
The in-kernel QUIC series under review on netdev splits the work the
same way as this proposed fix. The handshake stays in user space, the
secrets are passed in with a socket option, and the kernel derives each
key update with HKDF-Expand-Label (quic_crypto_key_update()).
What the series does
====================
- Two new socket options, TLS_TX_TRAFFIC_SECRET and
TLS_RX_TRAFFIC_SECRET, are used to register the application traffic
secrets. The secret is passed as raw bytes. The kernel derives the
hash (SHA-256 or SHA-384) from the cipher installed with TLS_TX or
TLS_RX. The TX secret must be set first because the RX secret needs
the TX secret in case a received KeyUpdate asks for a reply.
- A third socket option, TLS_TX_KEY_UPDATE, can be used to send a
KeyUpdate now. It can also optionally ask the peer to update too.
- Receive: kTLS consumes the KeyUpdate, installs the next key and
continues. This works for recvmsg, splice and read_sock.
- Transmit: kTLS sends a KeyUpdate after 2^24 records, a margin below
the 2^24.5 limit of RFC 9846 section 5.5, and in reply to
update_requested. ChaCha20-Poly1305 has no record limit.
- kTLS aborts the connection when a peer sends more than 8 KeyUpdates in
one second, or an invalid KeyUpdate. GnuTLS uses the same rate limit.
- Software path only. Device offload is unchanged.
- SM4-GCM and SM4-CCM are not supported: the kernel has no HMAC-SM3.
Testing
=======
- NVMe/TCP host and target over loopback, with the record limit and the
rate limit lowered for the test: more than 1000 KeyUpdates in each
direction under I/O, with AES-128-GCM and AES-256-GCM.
- Interoperability with OpenSSL in user space, which derives the keys on
its own, including the reply to update_requested.
- New selftests in tools/testing/selftests/net/tls.c for every TLS 1.3
cipher kTLS supports. The full tls selftest passes.
The matching tlshd change for ktls-utils is a small patch to be
submitted at a later time if this series is accepted.
Questions
=========
This is an RFC for feedback on the design and the new socket options, in
particular:
1. I used one socket option per direction, like TLS_TX and TLS_RX. Is
that the right form? Would one option that sets secrets for both
directions be better?
2. When the application installs a key itself with TLS_TX or TLS_RX
after the secrets have already been registered, the series removes
the registered secrets (TLS_TX removes both, TLS_RX only the RX
secret) and gives KeyUpdate back to the application. Is that right,
or should the key change be refused while a secret is registered?
3. TLS_TX_KEY_UPDATE (patch 4) is not needed for the record limit. I can
move it to a separate series if you prefer a smaller first step.
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
Changes in v3:
- Patch 2: cite RFC 9846 section 7.3 for the key and IV derivation in
the comment of the secret check. (Hannes)
- Patch 3: count a KeyUpdate over the rate limit as TlsRxRekeyError
and log it, rate limited. The limit stays at 8 per second, the same
as in GnuTLS. (Hannes)
- Patch 4: rename tls_tx_key_update_if_due() to
tls_tx_maybe_update_key(). (Hannes)
- Patch 4: check for a due KeyUpdate before each record, not once per
sendmsg(). One call can write many records, and the reply to
update_requested must go before the next record. (Sashiko)
- Patch 4: align the control message buffer of the KeyUpdate record.
(Sashiko)
- Patch 5: add a test where one write crosses the record limit.
- Not changed: the lock and send error points of Sashiko on patch 4
are the same as for v1, see the v2 changes.
- The nvme-tcp fix for the stall on a KeyUpdate, "nvme-tcp: start
error recovery when read_sock fails", is in nvme-7.4 with Cc:
stable. (Chuck)
- Collect Reviewed-by from Hannes on patches 1, 2 and 5.
- Link to v2: https://patch.msgid.link/20261028-keyupdate-rfc-v2-0-eb5143b4c487@gmail.com
Changes in v2:
- Patch 2: check that the installed key and IV come from the
registered secret. (Hannes)
- Patch 3: free the KeyUpdate record when its processing fails.
(Sashiko)
- Patch 3: after a rekey, wake pollers only if a pending KeyUpdate held
them back. (Sashiko)
- Patch 4: keep MSG_NOSIGNAL when sending a KeyUpdate. setsockopt()
never raises SIGPIPE. (Sashiko)
- Patch 4: clear the KeyUpdate request state when the application
installs a key with TLS_TX or TLS_RX. (Sashiko)
- Patch 5: derive the test keys from the secrets with AF_ALG. Add tests
for the changes above.
- Not changed: if TCP fails to send the KeyUpdate after the record was
queued, TLS_TX_KEY_UPDATE can return 0, as sendmsg() returns the
queued length. The next call reports the error. (Sashiko)
- Link to v1: https://patch.msgid.link/20261025-keyupdate-rfc-v1-0-60b7756eccbd@gmail.com
---
Martin Belanger (5):
tls: add HKDF-Expand-Label for the TLS 1.3 key schedule
tls: add socket options to register the traffic secrets
tls: update the RX key when a KeyUpdate is received
tls: send a KeyUpdate at the record limit and on request
selftests/tls: add KeyUpdate tests
Documentation/networking/tls.rst | 43 ++++
include/net/tls.h | 23 ++
include/uapi/linux/tls.h | 3 +
net/tls/Kconfig | 2 +
net/tls/Makefile | 3 +-
net/tls/tls.h | 5 +
net/tls/tls_hkdf.c | 82 +++++++
net/tls/tls_keyupdate.c | 159 +++++++++++++
net/tls/tls_main.c | 181 ++++++++++++++-
net/tls/tls_sw.c | 207 ++++++++++++++++-
tools/testing/selftests/net/config | 4 +
tools/testing/selftests/net/tls.c | 461 +++++++++++++++++++++++++++++++++++++
12 files changed, 1164 insertions(+), 9 deletions(-)
---
base-commit: 1a2c35c49cc7c6be860038b8442b52424a8f123c
change-id: 20261025-keyupdate-rfc-94d3c7e84077
Best regards,
--
Martin Belanger <martin.nvme.dev@gmail.com>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH RFC net-next v3 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
@ 2026-10-30 11:18 ` Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-30 11:18 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
Reviewed-by: Hannes Reinecke <hare@kernel.org>
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] 6+ messages in thread
* [PATCH RFC net-next v3 2/5] tls: add socket options to register the traffic secrets
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
@ 2026-10-30 11:18 ` Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-30 11:18 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 secret must give the installed key and IV, or the option fails with
-EINVAL. A wrong secret would otherwise fail only at the first
KeyUpdate.
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
Reviewed-by: Hannes Reinecke <hare@kernel.org>
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 | 145 ++++++++++++++++++++++++++++++++++++++-
4 files changed, 185 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..c46dc399af16 100644
--- a/net/tls/tls_main.c
+++ b/net/tls/tls_main.c
@@ -31,6 +31,8 @@
* SOFTWARE.
*/
+#include <crypto/sha2.h>
+#include <crypto/utils.h>
#include <linux/module.h>
#include <net/tcp.h>
@@ -325,6 +327,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 +761,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 +869,130 @@ 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;
+ }
+}
+
+/* The installed key and IV must be the ones that @secret gives
+ * (RFC 9846 section 7.3):
+ *
+ * key = HKDF-Expand-Label(secret, "key", "", key_length)
+ * iv = HKDF-Expand-Label(secret, "iv", "", iv_length)
+ *
+ * crypto_info holds the iv as salt followed by iv. Each KeyUpdate
+ * derives the next key from the secret, so a wrong secret fails only
+ * at the first KeyUpdate, with no clear cause.
+ */
+static int tls_check_traffic_secret(struct tls_crypto_info *crypto_info,
+ const u8 *secret, int len)
+{
+ const struct tls_cipher_desc *cipher_desc;
+ u8 iv[TLS_MAX_SALT_SIZE + TLS_MAX_IV_SIZE];
+ u8 key[TLS_CIPHER_AES_GCM_256_KEY_SIZE];
+ int rc;
+
+ cipher_desc = get_cipher_desc(crypto_info->cipher_type);
+ if (!cipher_desc || cipher_desc->key > sizeof(key) ||
+ cipher_desc->salt + cipher_desc->iv > sizeof(iv))
+ return -EINVAL;
+
+ rc = tls_hkdf_expand_label(secret, len, "key", key, cipher_desc->key);
+ if (rc)
+ goto out;
+
+ rc = tls_hkdf_expand_label(secret, len, "iv", iv,
+ cipher_desc->salt + cipher_desc->iv);
+ if (rc)
+ goto out;
+
+ if (crypto_memneq(key, crypto_info_key(crypto_info, cipher_desc),
+ cipher_desc->key) ||
+ crypto_memneq(iv, crypto_info_salt(crypto_info, cipher_desc),
+ cipher_desc->salt) ||
+ crypto_memneq(iv + cipher_desc->salt,
+ crypto_info_iv(crypto_info, cipher_desc),
+ cipher_desc->iv))
+ rc = -EINVAL;
+
+out:
+ memzero_explicit(key, sizeof(key));
+ memzero_explicit(iv, sizeof(iv));
+
+ return rc;
+}
+
+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;
+ u8 data[TLS_MAX_TRAFFIC_SECRET_SIZE];
+ int len, rc;
+
+ 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(data, optval, len)) {
+ rc = -EFAULT;
+ goto out;
+ }
+
+ rc = tls_check_traffic_secret(crypto_info, data, len);
+ if (rc)
+ goto out;
+
+ memcpy(secret->data, data, len);
+ secret->len = len;
+
+out:
+ memzero_explicit(data, sizeof(data));
+
+ return rc;
+}
+
static int do_tls_setsockopt(struct sock *sk, int optname, sockptr_t optval,
unsigned int optlen)
{
@@ -883,6 +1019,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] 6+ messages in thread
* [PATCH RFC net-next v3 3/5] tls: update the RX key when a KeyUpdate is received
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
@ 2026-10-30 11:18 ` Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 4/5] tls: send a KeyUpdate at the record limit and on request Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 5/5] selftests/tls: add KeyUpdate tests Martin Belanger via B4 Relay
4 siblings, 0 replies; 6+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-30 11:18 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. That case is counted as
TlsRxRekeyError and logged, rate limited.
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 | 154 ++++++++++++++++++++++++++++++++++++++++++++++++
net/tls/tls_sw.c | 43 ++++++++++++--
5 files changed, 202 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..281c7b13eb82
--- /dev/null
+++ b/net/tls/tls_keyupdate.c
@@ -0,0 +1,154 @@
+// 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) {
+ TLS_INC_STATS(sock_net(sk), LINUX_MIB_TLSRXREKEYERROR);
+ net_warn_ratelimited("tls: peer sent more than %d KeyUpdates in one second\n",
+ 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..cc0177320e7e 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -1551,15 +1551,28 @@ 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);
+ /* callers free the record only on success */
+ consume_skb(skb);
+ 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 +1896,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 +2037,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 +2053,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 +2138,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,
@@ -2517,6 +2542,12 @@ static void tls_finish_key_update(struct sock *sk, struct tls_context *tls_ctx)
{
struct tls_sw_context_rx *ctx = tls_ctx->priv_ctx_rx;
+ /* Only a pending KeyUpdate holds poll() back. The in-kernel rekey
+ * runs while the KeyUpdate record is still queued.
+ */
+ if (!READ_ONCE(ctx->key_update_pending))
+ return;
+
WRITE_ONCE(ctx->key_update_pending, false);
/* wake-up pre-existing poll() */
ctx->saved_data_ready(sk);
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH RFC net-next v3 4/5] tls: send a KeyUpdate at the record limit and on request
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
` (2 preceding siblings ...)
2026-10-30 11:18 ` [PATCH RFC net-next v3 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
@ 2026-10-30 11:18 ` Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 5/5] selftests/tls: add KeyUpdate tests Martin Belanger via B4 Relay
4 siblings, 0 replies; 6+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-30 11:18 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 | 38 ++++++++-
net/tls/tls_sw.c | 164 ++++++++++++++++++++++++++++++++++++++-
7 files changed, 228 insertions(+), 3 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 281c7b13eb82..58cd426cc94b 100644
--- a/net/tls/tls_keyupdate.c
+++ b/net/tls/tls_keyupdate.c
@@ -150,5 +150,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 c46dc399af16..66bd5217aa67 100644
--- a/net/tls/tls_main.c
+++ b/net/tls/tls_main.c
@@ -765,10 +765,14 @@ static int do_tls_setsockopt_conf(struct sock *sk, sockptr_t optval,
/* The registered secret no longer matches the key. The RX
* secret also needs the TX one, see do_tls_setsockopt_secret().
*/
- if (tx)
+ if (tx) {
memzero_explicit(&ctx->tx_secret,
sizeof(ctx->tx_secret));
+ ctx->tx_key_update_reply = false;
+ }
memzero_explicit(&ctx->rx_secret, sizeof(ctx->rx_secret));
+ /* the answer now goes to user space */
+ ctx->tx_key_update_asked = false;
return 0;
}
@@ -993,6 +997,35 @@ static int do_tls_setsockopt_secret(struct sock *sk, sockptr_t optval,
return rc;
}
+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)
{
@@ -1026,6 +1059,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 cc0177320e7e..f46cfbd95a6f 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>
@@ -763,6 +764,8 @@ static int tls_sw_sendmsg_splice(struct sock *sk, struct msghdr *msg,
return 0;
}
+static int tls_tx_maybe_update_key(struct sock *sk, int flags);
+
static int tls_sw_sendmsg_locked(struct sock *sk, struct msghdr *msg,
size_t size)
{
@@ -805,6 +808,16 @@ static int tls_sw_sendmsg_locked(struct sock *sk, struct msghdr *msg,
goto send_end;
}
+ /* Check before each new record: one call can write many
+ * records, and a KeyUpdate reply must go before the next
+ * one. The KeyUpdate itself is a handshake record.
+ */
+ if (!ctx->open_rec && record_type == TLS_RECORD_TYPE_DATA) {
+ ret = tls_tx_maybe_update_key(sk, msg->msg_flags);
+ if (ret)
+ goto send_end;
+ }
+
if (ctx->open_rec)
rec = ctx->open_rec;
else
@@ -1005,6 +1018,153 @@ 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;
+ union {
+ struct cmsghdr cmsg;
+ u8 buf[CMSG_SPACE(sizeof(record_type))];
+ } ctrl = {};
+ struct msghdr msg = {};
+ struct cmsghdr *cmsg;
+ struct kvec iov;
+ int ret;
+
+ msg.msg_control = &ctrl;
+ msg.msg_controllen = sizeof(ctrl);
+ msg.msg_flags = flags & (MSG_DONTWAIT | MSG_NOSIGNAL);
+
+ 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_maybe_update_key(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,
+ MSG_NOSIGNAL);
+ if (ret)
+ return ret;
+ }
+
+ ret = tls_tx_key_update(sk, request ? TLS_KEY_UPDATE_REQUESTED :
+ TLS_KEY_UPDATE_NOT_REQUESTED, MSG_NOSIGNAL);
+ 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 +1179,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_maybe_update_key(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] 6+ messages in thread
* [PATCH RFC net-next v3 5/5] selftests/tls: add KeyUpdate tests
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
` (3 preceding siblings ...)
2026-10-30 11:18 ` [PATCH RFC net-next v3 4/5] tls: send a KeyUpdate at the record limit and on request Martin Belanger via B4 Relay
@ 2026-10-30 11:18 ` Martin Belanger via B4 Relay
4 siblings, 0 replies; 6+ messages in thread
From: Martin Belanger via B4 Relay @ 2026-10-30 11:18 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, the
RX secret before the TX secret, and a secret that does not give the
installed key;
- 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;
- one write that crosses the record limit gets a KeyUpdate in the
middle;
- a key installed with TLS_TX removes the registered secrets;
- after a key installed with TLS_RX, a new update_requested is
accepted;
- TLS_TX_KEY_UPDATE on a shut down socket does not raise SIGPIPE;
- more than 8 KeyUpdates in one second, and an invalid
request_update value, end the connection.
The keys are derived from the secrets with HKDF-Expand-Label, with the
HMAC of the kernel through AF_ALG.
Assisted-by: LLM
Reviewed-by: Hannes Reinecke <hare@kernel.org>
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
---
tools/testing/selftests/net/config | 4 +
tools/testing/selftests/net/tls.c | 461 +++++++++++++++++++++++++++++++++++++
2 files changed, 465 insertions(+)
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index cb8cb4696df7..0de4011cfabd 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -10,8 +10,12 @@ CONFIG_CAN_VXCAN=m
CONFIG_CGROUPS=y
CONFIG_CRYPTO_ARIA=y
CONFIG_CRYPTO_CHACHA20POLY1305=m
+CONFIG_CRYPTO_HMAC=y
CONFIG_CRYPTO_SHA1=y
+CONFIG_CRYPTO_SHA256=y
+CONFIG_CRYPTO_SHA512=y
CONFIG_CRYPTO_SM4_GENERIC=y
+CONFIG_CRYPTO_USER_API_HASH=y
CONFIG_DEBUG_INFO_BTF=y
CONFIG_DEBUG_INFO_BTF_MODULES=n
CONFIG_DUMMY=y
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index 9d3cd4fff062..abc543a5e3d9 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -11,6 +11,7 @@
#include <stdlib.h>
#include <unistd.h>
+#include <linux/if_alg.h>
#include <linux/tls.h>
#include <linux/tcp.h>
#include <linux/socket.h>
@@ -2390,6 +2391,466 @@ TEST_F(tls, rekey_poll_delay)
}
}
+#ifndef SOL_ALG
+#define SOL_ALG 279
+#endif
+
+/* RFC 9846 section 7.1, HKDF-Expand-Label with an empty context, for an
+ * output no longer than the hash. The HMAC comes from the kernel.
+ */
+static bool hkdf_expand_label(const unsigned char *secret, int secret_len,
+ const char *label, unsigned char *out,
+ int out_len)
+{
+ struct sockaddr_alg sa = {
+ .salg_family = AF_ALG,
+ .salg_type = "hash",
+ };
+ unsigned char info[2 + 1 + 255 + 1 + 1], md[48];
+ int tfm, op, n = 0, label_len = 6 + strlen(label);
+ bool ok = false;
+
+ strcpy((char *)sa.salg_name,
+ secret_len == 48 ? "hmac(sha384)" : "hmac(sha256)");
+
+ info[n++] = out_len >> 8;
+ info[n++] = out_len;
+ info[n++] = label_len;
+ memcpy(info + n, "tls13 ", 6);
+ memcpy(info + n + 6, label, label_len - 6);
+ n += label_len;
+ info[n++] = 0; /* context */
+ info[n++] = 1; /* HKDF-Expand counter */
+
+ tfm = socket(AF_ALG, SOCK_SEQPACKET, 0);
+ if (tfm < 0)
+ return false;
+ if (bind(tfm, (struct sockaddr *)&sa, sizeof(sa)) ||
+ setsockopt(tfm, SOL_ALG, ALG_SET_KEY, secret, secret_len))
+ goto out;
+
+ op = accept(tfm, NULL, 0);
+ if (op < 0)
+ goto out;
+ ok = write(op, info, n) == n && read(op, md, secret_len) == secret_len;
+ close(op);
+ if (ok)
+ memcpy(out, md, out_len);
+out:
+ close(tfm);
+ return ok;
+}
+
+/* Fill @keys with the key and IV that @secret gives, record number 0 */
+static bool keyupdate_keys(uint16_t cipher_type, const unsigned char *secret,
+ int secret_len, struct tls_crypto_info_keys *keys)
+{
+ unsigned char *key, *salt, *iv;
+ int key_len, salt_len, iv_len;
+ unsigned char nonce[12];
+
+ tls_crypto_info_init(TLS_1_3_VERSION, cipher_type, keys, 0);
+
+ switch (cipher_type) {
+ case TLS_CIPHER_AES_GCM_128:
+ key = keys->aes128.key;
+ salt = keys->aes128.salt;
+ iv = keys->aes128.iv;
+ key_len = sizeof(keys->aes128.key);
+ salt_len = sizeof(keys->aes128.salt);
+ iv_len = sizeof(keys->aes128.iv);
+ break;
+ case TLS_CIPHER_AES_GCM_256:
+ key = keys->aesgcm256.key;
+ salt = keys->aesgcm256.salt;
+ iv = keys->aesgcm256.iv;
+ key_len = sizeof(keys->aesgcm256.key);
+ salt_len = sizeof(keys->aesgcm256.salt);
+ iv_len = sizeof(keys->aesgcm256.iv);
+ break;
+ case TLS_CIPHER_AES_CCM_128:
+ key = keys->aesccm128.key;
+ salt = keys->aesccm128.salt;
+ iv = keys->aesccm128.iv;
+ key_len = sizeof(keys->aesccm128.key);
+ salt_len = sizeof(keys->aesccm128.salt);
+ iv_len = sizeof(keys->aesccm128.iv);
+ break;
+ case TLS_CIPHER_CHACHA20_POLY1305:
+ key = keys->chacha20.key;
+ salt = keys->chacha20.salt;
+ iv = keys->chacha20.iv;
+ key_len = sizeof(keys->chacha20.key);
+ salt_len = 0;
+ iv_len = sizeof(keys->chacha20.iv);
+ break;
+ default:
+ return false;
+ }
+
+ if (!hkdf_expand_label(secret, secret_len, "key", key, key_len) ||
+ !hkdf_expand_label(secret, secret_len, "iv", nonce,
+ salt_len + iv_len))
+ return false;
+
+ memcpy(salt, nonce, salt_len);
+ memcpy(iv, nonce + salt_len, iv_len);
+ return true;
+}
+
+/* In-kernel KeyUpdate: fd and cfd both register the TX and RX traffic
+ * secrets of a bidirectional connection. fd -> cfd starts at record
+ * number @seq. Returns false if the variant does not support it.
+ */
+static bool keyupdate_setup_seq(struct __test_metadata *_metadata,
+ FIXTURE_DATA(tls) *self,
+ const FIXTURE_VARIANT(tls) *variant,
+ uint64_t seq)
+{
+ struct tls_crypto_info_keys keys_a, keys_b;
+ unsigned char secret_a[48], secret_b[48];
+ unsigned char *rec_seq;
+ int len, i;
+
+ 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;
+ }
+
+ memset(secret_a, 0xa5, sizeof(secret_a));
+ memset(secret_b, 0x5a, sizeof(secret_b));
+
+ /* the keys must be the ones that the secrets give */
+ ASSERT_TRUE(keyupdate_keys(variant->cipher_type, secret_a, len,
+ &keys_a));
+ ASSERT_TRUE(keyupdate_keys(variant->cipher_type, secret_b, len,
+ &keys_b));
+
+ /* rec_seq is the last member of each crypto_info */
+ rec_seq = (unsigned char *)&keys_a + keys_a.len - 8;
+ for (i = 0; i < 8; i++)
+ rec_seq[i] = seq >> (56 - 8 * i);
+
+ /* fd -> cfd: replace the keys of the fixture */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX, &keys_a,
+ keys_a.len), 0);
+ EXPECT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_RX, &keys_a,
+ keys_a.len), 0);
+ /* cfd -> fd */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX, &keys_b,
+ keys_b.len), 0);
+ EXPECT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_TX, &keys_b,
+ keys_b.len), 0);
+
+ /* 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 bool keyupdate_setup(struct __test_metadata *_metadata,
+ FIXTURE_DATA(tls) *self,
+ const FIXTURE_VARIANT(tls) *variant)
+{
+ return keyupdate_setup_seq(_metadata, self, variant, 0);
+}
+
+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);
+
+ /* the installed key does not come from this secret */
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_TRAFFIC_SECRET, secret,
+ variant->cipher_type == TLS_CIPHER_AES_GCM_256 ?
+ 48 : 32), -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_manual_rekey_request)
+{
+ struct tls_crypto_info_keys keys;
+ unsigned char secret[48];
+ int len;
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ keyupdate_send(_metadata, self->fd, 1);
+
+ /* the answer to the request now goes to user space */
+ len = variant->cipher_type == TLS_CIPHER_AES_GCM_256 ? 48 : 32;
+ memset(secret, 0x3c, sizeof(secret));
+ ASSERT_TRUE(keyupdate_keys(variant->cipher_type, secret, len, &keys));
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX, &keys, keys.len), 0);
+
+ EXPECT_EQ(setsockopt(self->fd, SOL_TLS, TLS_RX_TRAFFIC_SECRET,
+ secret, len), 0);
+
+ keyupdate_send(_metadata, self->fd, 1);
+}
+
+TEST_F(tls, keyupdate_nosignal)
+{
+ int zero = 0;
+
+ if (!keyupdate_setup(_metadata, self, variant))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ /* The default action of SIGPIPE ends the test. The result is not
+ * checked: as for sendmsg(), the error can show up only on the
+ * next call.
+ */
+ EXPECT_EQ(shutdown(self->fd, SHUT_WR), 0);
+ setsockopt(self->fd, SOL_TLS, TLS_TX_KEY_UPDATE, &zero, sizeof(zero));
+}
+
+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_record_limit)
+{
+ struct tls_crypto_info_keys get;
+ char sbuf[1024], rbuf[1024];
+ socklen_t optlen;
+ unsigned char *rec_seq;
+ __u16 limit = 128;
+ int i;
+
+ if (variant->cipher_type == TLS_CIPHER_CHACHA20_POLY1305)
+ SKIP(return, "no record limit for ChaCha20-Poly1305");
+ if (!keyupdate_setup_seq(_metadata, self, variant, (1 << 24) - 2))
+ SKIP(return, "no in-kernel KeyUpdate for this variant");
+
+ ASSERT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN,
+ &limit, sizeof(limit)), 0);
+ for (i = 0; i < sizeof(sbuf); i++)
+ sbuf[i] = i;
+
+ /* one call writes 8 records or more, past the record limit */
+ EXPECT_EQ(send(self->fd, sbuf, sizeof(sbuf), 0), sizeof(sbuf));
+ EXPECT_EQ(recv(self->cfd, rbuf, sizeof(rbuf), MSG_WAITALL),
+ sizeof(rbuf));
+ EXPECT_EQ(memcmp(sbuf, rbuf, sizeof(sbuf)), 0);
+
+ /* the KeyUpdate in the middle of the call restarted at 0 */
+ tls_crypto_info_init(variant->tls_version, variant->cipher_type,
+ &get, 0);
+ optlen = get.len;
+ ASSERT_EQ(getsockopt(self->fd, SOL_TLS, TLS_TX, &get, &optlen), 0);
+ rec_seq = (unsigned char *)&get + optlen - 8;
+ for (i = 0; i < 5; i++)
+ EXPECT_EQ(rec_seq[i], 0);
+}
+
+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] 6+ messages in thread
end of thread, other threads:[~2026-10-30 11:18 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30 11:18 [PATCH RFC net-next v3 0/5] tls: perform TLS 1.3 KeyUpdate in the kernel Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 2/5] tls: add socket options to register the traffic secrets Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 3/5] tls: update the RX key when a KeyUpdate is received Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 4/5] tls: send a KeyUpdate at the record limit and on request Martin Belanger via B4 Relay
2026-10-30 11:18 ` [PATCH RFC net-next v3 5/5] selftests/tls: add KeyUpdate tests Martin Belanger via B4 Relay
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®