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

Message ID 8074a8cd4c5c9ce1c0d0dabf0830514d20dd47a2.1782986577.git.ralf@mandelbit.com
State Changes Requested
Headers
Series ovpn: add epoch data channel keys |

Commit Message

Ralf Lici July 2, 2026, 11:52 a.m. UTC
  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 <ralf@mandelbit.com>
---
Changes since v1 https://lore.kernel.org/openvpn-devel/2232f95098268c00dd6ce56e346e02fa0b111a17.1782919654.git.ralf@mandelbit.com/
- Split PRK derivation from PRK installation so later refill code can
  derive scratch future-key state without mutating the active epoch key.

 drivers/net/Kconfig             |   2 +
 drivers/net/ovpn/Makefile       |   1 +
 drivers/net/ovpn/crypto_epoch.c | 223 ++++++++++++++++++++++++++++++++
 drivers/net/ovpn/crypto_epoch.h |  59 +++++++++
 4 files changed, 285 insertions(+)
 create mode 100644 drivers/net/ovpn/crypto_epoch.c
 create mode 100644 drivers/net/ovpn/crypto_epoch.h
  

Patch

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..bfdf030bb0fb
--- /dev/null
+++ b/drivers/net/ovpn/crypto_epoch.c
@@ -0,0 +1,223 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*  OpenVPN data channel offload
+ *
+ *  Copyright (C) 2026 OpenVPN, Inc.
+ *
+ *  Author:	Ralf Lici <ralf@mandelbit.com>
+ *		Antonio Quartulli <antonio@openvpn.net>
+ */
+
+#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, todo;
+	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) {
+		/* 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_derive_next_prk - derive the next epoch PRK
+ * @epoch_key: PRK derivation state
+ * @next_prk: destination for the next PRK
+ *
+ * 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_derive_next_prk(const struct ovpn_epoch_key *epoch_key,
+			       u8 next_prk[])
+{
+	if (unlikely(epoch_key->epoch == OVPN_MAX_EPOCH))
+		return -ERANGE;
+
+	if (WARN_ON_ONCE(OVPN_EPOCH_PRK_SIZE !=
+			 crypto_shash_digestsize(epoch_key->shash)))
+		return -EINVAL;
+
+	return ovpn_expand_label(epoch_key->shash, OVPN_EPOCH_UPDATE_LABEL,
+				 sizeof(OVPN_EPOCH_UPDATE_LABEL) - 1,
+				 next_prk, OVPN_EPOCH_PRK_SIZE);
+}
+
+int ovpn_epoch_set_prk(struct ovpn_epoch_key *epoch_key, const u8 prk[],
+		       u16 epoch)
+{
+	int ret;
+
+	ret = crypto_shash_setkey(epoch_key->shash, prk, OVPN_EPOCH_PRK_SIZE);
+	if (ret)
+		return ret;
+
+	epoch_key->epoch = epoch;
+
+	return 0;
+}
+
+/**
+ * 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;
+
+	ret = ovpn_epoch_derive_next_prk(epoch_key, key);
+	if (ret)
+		goto out;
+
+	/* expose the next epoch only after its PRK is installed */
+	ret = ovpn_epoch_set_prk(epoch_key, key, epoch_key->epoch + 1);
+
+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..1ffa6f55e729
--- /dev/null
+++ b/drivers/net/ovpn/crypto_epoch.h
@@ -0,0 +1,59 @@ 
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*  OpenVPN data channel offload
+ *
+ *  Copyright (C) 2026 OpenVPN, Inc.
+ *
+ *  Author:	Ralf Lici <ralf@mandelbit.com>
+ *		Antonio Quartulli <antonio@openvpn.net>
+ */
+
+#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_derive_next_prk(const struct ovpn_epoch_key *epoch_key,
+			       u8 next_prk[]);
+int ovpn_epoch_set_prk(struct ovpn_epoch_key *epoch_key, const u8 prk[],
+		       u16 epoch);
+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_ */