[RFC ovpn net-next 09/14] ovpn: add epoch key derivation support

Ralf Lici <[email protected]>
Newsgroups gmane.network.openvpn.devel
Message-ID <2232f95098268c00dd6ce56e346e02fa0b111a17.1782919654.git.ralf@mandelbit.com>
Add the HKDF-Expand-Label helper used to derive per-epoch data keys and
implicit IVs from an imported epoch PRK. OpenVPN currently uses the
labels "datakey upd", "data_key" and "data_iv" for this derivation.

Epoch derivation uses HMAC-SHA256, so the PRK is fixed at 32 bytes. Keep
that size internal until the netlink key format is introduced.

Signed-off-by: Ralf Lici <[email protected]>
---
 drivers/net/Kconfig             |   2 +
 drivers/net/ovpn/Makefile       |   1 +
 drivers/net/ovpn/crypto_epoch.c | 200 ++++++++++++++++++++++++++++++++
 drivers/net/ovpn/crypto_epoch.h |  55 +++++++++
 4 files changed, 258 insertions(+)
 create mode 100644 drivers/net/ovpn/crypto_epoch.c
 create mode 100644 drivers/net/ovpn/crypto_epoch.h

diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
index ff79c466712d..cd4193ce51b4 100644
--- a/drivers/net/Kconfig
+++ b/drivers/net/Kconfig
@@ -109,6 +109,8 @@ config OVPN
 	select CRYPTO_AES
 	select CRYPTO_GCM
 	select CRYPTO_CHACHA20POLY1305
+	select CRYPTO_HMAC
+	select CRYPTO_SHA256
 	select STREAM_PARSER
 	help
 	  This module enhances the performance of the OpenVPN userspace software
diff --git a/drivers/net/ovpn/Makefile b/drivers/net/ovpn/Makefile
index 704af1deb7c1..9dc2906e7c22 100644
--- a/drivers/net/ovpn/Makefile
+++ b/drivers/net/ovpn/Makefile
@@ -10,6 +10,7 @@ obj-$(CONFIG_OVPN) := ovpn.o
 ovpn-y += bind.o
 ovpn-y += crypto.o
 ovpn-y += crypto_aead.o
+ovpn-y += crypto_epoch.o
 ovpn-y += crypto_key.o
 ovpn-y += main.o
 ovpn-y += io.o
diff --git a/drivers/net/ovpn/crypto_epoch.c b/drivers/net/ovpn/crypto_epoch.c
new file mode 100644
index 000000000000..46e1a0954b88
--- /dev/null
+++ b/drivers/net/ovpn/crypto_epoch.c
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0
+/*  OpenVPN data channel offload
+ *
+ *  Copyright (C) 2026 OpenVPN, Inc.
+ *
+ *  Author:	Ralf Lici <[email protected]>
+ *		Antonio Quartulli <[email protected]>
+ */
+
+#include <crypto/hash.h>
+#include <linux/unaligned.h>
+
+#include "crypto_epoch.h"
+#include "proto.h"
+
+#define OVPN_EPOCH_DATA_KEY_LABEL "data_key"
+#define OVPN_EPOCH_DATA_IV_LABEL "data_iv"
+#define OVPN_EPOCH_UPDATE_LABEL "datakey upd"
+#define OVPN_EPOCH_LABEL_PREFIX "ovpn "
+#define OVPN_EPOCH_INFO_MAX_SIZE 21
+
+#define OVPN_EPOCH_HASH_ALG "hmac(sha256)"
+
+static int ovpn_hkdf_expand(struct crypto_shash *shash, const u8 *info,
+			    size_t info_len, u8 *okm, size_t okm_len)
+{
+	unsigned int prev_len = 0, digest_len;
+	SHASH_DESC_ON_STACK(desc, shash);
+	u8 prev[OVPN_EPOCH_PRK_SIZE];
+	size_t copied = 0;
+	u8 counter = 1;
+	int ret = 0;
+
+	digest_len = crypto_shash_digestsize(shash);
+	if (WARN_ON_ONCE(digest_len != sizeof(prev)))
+		return -EINVAL;
+
+	desc->tfm = shash;
+
+	/* T(0) is the empty string */
+	while (copied < okm_len) {
+		size_t todo;
+
+		/* T(n) = HMAC-Hash(PRK, T(n-1) | info | n) */
+		ret = crypto_shash_init(desc);
+		if (ret)
+			goto out;
+		ret = crypto_shash_update(desc, prev, prev_len);
+		if (ret)
+			goto out;
+		ret = crypto_shash_update(desc, info, info_len);
+		if (ret)
+			goto out;
+		ret = crypto_shash_update(desc, &counter, sizeof(counter));
+		if (ret)
+			goto out;
+		ret = crypto_shash_final(desc, prev);
+		if (ret)
+			goto out;
+
+		prev_len = digest_len;
+		/* copy a full digest block or the final partial block */
+		todo = min_t(size_t, digest_len, okm_len - copied);
+		memcpy(okm + copied, prev, todo);
+		copied += todo;
+		counter++;
+	}
+
+out:
+	memzero_explicit(prev, sizeof(prev));
+	shash_desc_zero(desc);
+	return ret;
+}
+
+struct crypto_shash *ovpn_epoch_init_key(const u8 *key, size_t key_size)
+{
+	struct crypto_shash *shash;
+	int ret;
+
+	shash = crypto_alloc_shash(OVPN_EPOCH_HASH_ALG, 0, 0);
+	if (IS_ERR(shash))
+		return shash;
+
+	if (key_size != crypto_shash_digestsize(shash)) {
+		crypto_free_shash(shash);
+		return ERR_PTR(-EINVAL);
+	}
+
+	/* store the PRK as the shash key so it can be advanced in place */
+	ret = crypto_shash_setkey(shash, key, key_size);
+	if (ret) {
+		crypto_free_shash(shash);
+		return ERR_PTR(ret);
+	}
+
+	return shash;
+}
+
+static int ovpn_expand_label(struct crypto_shash *shash, const u8 *label,
+			     size_t label_len, u8 *okm, u16 okm_len)
+{
+	static const u8 label_prefix[] = OVPN_EPOCH_LABEL_PREFIX;
+	u8 prefix_len = sizeof(label_prefix) - 1, full_len;
+	u8 info[OVPN_EPOCH_INFO_MAX_SIZE];
+	u16 info_len;
+	int ret;
+
+	if (WARN_ON_ONCE(!label_len || label_len > 250))
+		return -EINVAL;
+
+	full_len = prefix_len + label_len;
+	info_len = sizeof(okm_len) + full_len + 2;
+	if (WARN_ON_ONCE(info_len > sizeof(info)))
+		return -EINVAL;
+
+	/* encode length, "ovpn " label and empty context */
+	put_unaligned_be16(okm_len, info);
+	info[2] = full_len;
+	memcpy(&info[3], label_prefix, prefix_len);
+	memcpy(&info[3 + prefix_len], label, label_len);
+	info[3 + full_len] = 0;
+
+	ret = ovpn_hkdf_expand(shash, info, info_len, okm, okm_len);
+	memzero_explicit(info, info_len);
+
+	return ret;
+}
+
+/**
+ * ovpn_epoch_iterate - advance an epoch PRK to the next epoch
+ * @epoch_key: PRK derivation state
+ *
+ * Epoch data keys are derived from a per-direction PRK. Advancing from E_N to
+ * E_N+1 uses OVPN-Expand-Label(E_N, "datakey upd", "", 32).
+ *
+ * Return: 0 on success or a negative error code otherwise.
+ */
+int ovpn_epoch_iterate(struct ovpn_epoch_key *epoch_key)
+{
+	u8 key[OVPN_EPOCH_PRK_SIZE];
+	int ret;
+
+	if (unlikely(epoch_key->epoch == OVPN_MAX_EPOCH))
+		return -ERANGE;
+
+	if (WARN_ON_ONCE(sizeof(key) !=
+			 crypto_shash_digestsize(epoch_key->shash)))
+		return -EINVAL;
+
+	ret = ovpn_expand_label(epoch_key->shash, OVPN_EPOCH_UPDATE_LABEL,
+				sizeof(OVPN_EPOCH_UPDATE_LABEL) - 1, key,
+				sizeof(key));
+	if (ret)
+		goto out;
+
+	/* install E_N+1 as the PRK for the next iteration */
+	ret = crypto_shash_setkey(epoch_key->shash, key, sizeof(key));
+	if (ret)
+		goto out;
+
+	/* expose the next epoch only after its PRK is installed */
+	epoch_key->epoch++;
+
+out:
+	memzero_explicit(key, sizeof(key));
+	return ret;
+}
+
+/**
+ * ovpn_epoch_derive_key - derive the concrete AEAD key for an epoch
+ * @epoch_key: PRK derivation state
+ * @cipher_key: destination for the AEAD cipher key
+ * @implicit_iv: destination for the AEAD implicit IV
+ *
+ * K_i is OVPN-Expand-Label(E_i, "data_key", "", key_size), while the implicit
+ * IV is OVPN-Expand-Label(E_i, "data_iv", "", OVPN_NONCE_SIZE).
+ *
+ * Return: 0 on success or a negative error code otherwise.
+ */
+int ovpn_epoch_derive_key(const struct ovpn_epoch_key *epoch_key,
+			  u8 cipher_key[], u8 implicit_iv[])
+{
+	int ret;
+
+	if (WARN_ON_ONCE(!epoch_key->cipher_key_len ||
+			 epoch_key->cipher_key_len > OVPN_EPOCH_PRK_SIZE))
+		return -EINVAL;
+
+	/* derive the concrete AEAD key for the current epoch */
+	ret = ovpn_expand_label(epoch_key->shash, OVPN_EPOCH_DATA_KEY_LABEL,
+				sizeof(OVPN_EPOCH_DATA_KEY_LABEL) - 1,
+				cipher_key, epoch_key->cipher_key_len);
+	if (ret)
+		return ret;
+
+	/* derive the implicit IV paired with that AEAD key */
+	return ovpn_expand_label(epoch_key->shash, OVPN_EPOCH_DATA_IV_LABEL,
+				 sizeof(OVPN_EPOCH_DATA_IV_LABEL) - 1,
+				 implicit_iv, OVPN_NONCE_SIZE);
+}
diff --git a/drivers/net/ovpn/crypto_epoch.h b/drivers/net/ovpn/crypto_epoch.h
new file mode 100644
index 000000000000..7da31c2acd44
--- /dev/null
+++ b/drivers/net/ovpn/crypto_epoch.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*  OpenVPN data channel offload
+ *
+ *  Copyright (C) 2026 OpenVPN, Inc.
+ *
+ *  Author:	Ralf Lici <[email protected]>
+ *		Antonio Quartulli <[email protected]>
+ */
+
+#ifndef _NET_OVPN_OVPNEPOCH_H_
+#define _NET_OVPN_OVPNEPOCH_H_
+
+#include <crypto/hash.h>
+#include <linux/limits.h>
+#include <linux/rcupdate.h>
+#include <linux/types.h>
+#include <uapi/linux/ovpn.h>
+
+#define OVPN_EPOCH_PRK_SIZE 32
+#define OVPN_MAX_EPOCH U16_MAX
+#define OVPN_EPOCH_FUTURE_KEYS_COUNT 16
+
+struct ovpn_key_ctx;
+
+/* crypto handle used for key derivation through HKDF-Expand-Label */
+struct ovpn_epoch_key {
+	u16 epoch;
+	unsigned int cipher_key_len;
+	struct crypto_shash *shash;
+};
+
+/* ring buffer of prederived future epoch data keys */
+struct ovpn_future_keys {
+	struct ovpn_key_ctx __rcu *keys[OVPN_EPOCH_FUTURE_KEYS_COUNT];
+	u16 head;
+	u16 tail;
+	bool full;
+};
+
+static inline u16
+ovpn_epoch_future_keys_count(const struct ovpn_future_keys *fk)
+{
+	if (fk->full)
+		return OVPN_EPOCH_FUTURE_KEYS_COUNT;
+
+	return (fk->head - fk->tail + OVPN_EPOCH_FUTURE_KEYS_COUNT) %
+	       OVPN_EPOCH_FUTURE_KEYS_COUNT;
+}
+
+struct crypto_shash *ovpn_epoch_init_key(const u8 *key, size_t key_size);
+int ovpn_epoch_iterate(struct ovpn_epoch_key *epoch_key);
+int ovpn_epoch_derive_key(const struct ovpn_epoch_key *epoch_key,
+			  u8 cipher_key[], u8 implicit_iv[]);
+
+#endif /* _NET_OVPN_OVPNEPOCH_H_ */
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.