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 _______________________________________________ Openvpn-devel mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/openvpn-devel
