[PATCH RFC net-next 1/5] tls: add HKDF-Expand-Label for the TLS 1.3 key schedule

From: Martin Belanger via B4 Relay

Date: Sun Oct 25 2026 - 20:38:54 EST


From: Martin Belanger <martin.belanger@xxxxxxxx>

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@xxxxxxxx>
---
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