AES-GCM has a finite per-key usage bound in terms of GCM block operations. ovpn already stops before packet-ID wrap, but it did not account for this tighter limit, so a large-volume key could exceed the AES-GCM usage budget without any kernel-side action.
Keep the configured cipher in the key slot and convert authenticated data and plaintext length to AES block units for AES-GCM. The block helper is cipher-specific rather than based on crypto_aead_blocksize, since GCM implementations can advertise a provider block size that is not the GCM accounting block. On TX, reserve the blocks with packet ID allocation and fail once the combined invocation/block budget would be exceeded, which drives the existing key-kill notification path. Also notify userspace once when TX or RX usage crosses 7/8 of the fixed hard limit, so userspace can rekey before the hard limit is reached. RX counts only authenticated, non-replayed packets and does not drop solely because the peer crossed a local usage threshold. Signed-off-by: Ralf Lici <[email protected]> --- drivers/net/ovpn/crypto.h | 5 ++ drivers/net/ovpn/crypto_aead.c | 13 +++- drivers/net/ovpn/crypto_limits.h | 125 +++++++++++++++++++++++++++++++ drivers/net/ovpn/io.c | 11 +++ drivers/net/ovpn/pktid.h | 57 ++++++++++++-- 5 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 drivers/net/ovpn/crypto_limits.h diff --git a/drivers/net/ovpn/crypto.h b/drivers/net/ovpn/crypto.h index 0e284fec3a75..09d3a945c5d9 100644 --- a/drivers/net/ovpn/crypto.h +++ b/drivers/net/ovpn/crypto.h @@ -10,6 +10,7 @@ #ifndef _NET_OVPN_OVPNCRYPTO_H_ #define _NET_OVPN_OVPNCRYPTO_H_ +#include "crypto_limits.h" #include "pktid.h" #include "proto.h" @@ -37,6 +38,8 @@ struct ovpn_peer_key_reset { struct ovpn_crypto_key_slot { u8 key_id; + enum ovpn_cipher_alg cipher_alg; + struct ovpn_limit usage_limit; struct crypto_aead *encrypt; struct crypto_aead *decrypt; @@ -44,7 +47,9 @@ struct ovpn_crypto_key_slot { u8 nonce_tail_recv[OVPN_NONCE_TAIL_SIZE]; struct ovpn_pktid_recv pid_recv ____cacheline_aligned_in_smp; + struct ovpn_key_usage usage_recv; struct ovpn_pktid_xmit pid_xmit ____cacheline_aligned_in_smp; + struct ovpn_key_usage usage_xmit; struct kref refcount; struct rcu_head rcu; }; diff --git a/drivers/net/ovpn/crypto_aead.c b/drivers/net/ovpn/crypto_aead.c index 86b69aeddcf7..9b10ae6f6002 100644 --- a/drivers/net/ovpn/crypto_aead.c +++ b/drivers/net/ovpn/crypto_aead.c @@ -139,6 +139,7 @@ int ovpn_aead_encrypt(struct ovpn_peer *peer, struct ovpn_crypto_key_slot *ks, struct sk_buff *skb) { const unsigned int tag_size = crypto_aead_authsize(ks->encrypt); + unsigned int plaintext_len; struct aead_request *req; struct sk_buff *trailer; struct scatterlist *sg; @@ -149,6 +150,7 @@ int ovpn_aead_encrypt(struct ovpn_peer *peer, struct ovpn_crypto_key_slot *ks, ovpn_skb_cb(skb)->peer = peer; ovpn_skb_cb(skb)->ks = ks; + plaintext_len = skb->len; /* Sample AEAD header format: * 48000001 00000005 7e7046bd 444a7e28 cc6387b1 64a4d6c1 380275a... @@ -205,7 +207,12 @@ int ovpn_aead_encrypt(struct ovpn_peer *peer, struct ovpn_crypto_key_slot *ks, /* obtain packet ID, which is used both as a first * 4 bytes of nonce and last 4 bytes of associated data. */ - ret = ovpn_pktid_xmit_next(&ks->pid_xmit, &pktid); + ret = ovpn_pktid_xmit_next(&ks->pid_xmit, &ks->usage_xmit, + &ks->usage_limit, + ovpn_aead_limit_blocks(ks->cipher_alg, + OVPN_AAD_SIZE, + plaintext_len), + &pktid); if (unlikely(ret < 0)) return ret; if (unlikely(ret > 0)) @@ -425,6 +432,10 @@ ovpn_aead_crypto_key_slot_new(const struct ovpn_key_config *kc) ks->decrypt = NULL; kref_init(&ks->refcount); ks->key_id = kc->key_id; + ks->cipher_alg = kc->cipher_alg; + ovpn_key_usage_limit_init(&ks->usage_limit, kc->cipher_alg); + ovpn_key_usage_init(&ks->usage_xmit); + ovpn_key_usage_init(&ks->usage_recv); ks->encrypt = ovpn_aead_init("encrypt", alg_name, kc->encrypt.cipher_key, diff --git a/drivers/net/ovpn/crypto_limits.h b/drivers/net/ovpn/crypto_limits.h new file mode 100644 index 000000000000..5b09b39f9a94 --- /dev/null +++ b/drivers/net/ovpn/crypto_limits.h @@ -0,0 +1,125 @@ +/* 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_CRYPTO_LIMITS_H_ +#define _NET_OVPN_CRYPTO_LIMITS_H_ + +#include <crypto/aes.h> +#include <linux/atomic.h> +#include <linux/limits.h> +#include <linux/math.h> +#include <linux/types.h> +#include <uapi/linux/ovpn.h> + +/* use the OpenVPN/SP 800-38D AES-GCM invocation limit */ +#define OVPN_AES_GCM_USAGE_LIMIT ((1ULL << 36) - 1) + +/* notify userspace at 7/8 of the AES-GCM hard limit */ +#define OVPN_AES_GCM_USAGE_NOTIFY (OVPN_AES_GCM_USAGE_LIMIT / 8 * 7) + +#define OVPN_KEY_USAGE_NOTIFY_BIT 0 + +struct ovpn_limit { + u64 soft; + u64 hard; +}; + +struct ovpn_key_usage { + atomic64_t blocks; + unsigned long flags; +}; + +static inline void ovpn_key_usage_init(struct ovpn_key_usage *usage) +{ + atomic64_set(&usage->blocks, 0); + usage->flags = 0; +} + +static inline void +ovpn_key_usage_limit_init(struct ovpn_limit *limit, + enum ovpn_cipher_alg cipher_alg) +{ + limit->soft = U64_MAX; + limit->hard = U64_MAX; + + switch (cipher_alg) { + case OVPN_CIPHER_ALG_AES_GCM: + limit->soft = OVPN_AES_GCM_USAGE_NOTIFY; + limit->hard = OVPN_AES_GCM_USAGE_LIMIT; + break; + case OVPN_CIPHER_ALG_CHACHA20_POLY1305: + default: + break; + } +} + +static inline bool ovpn_key_usage_over_limit(u64 limit, u64 pktid, u64 blocks) +{ + return pktid > limit || blocks > limit - pktid; +} + +static inline bool ovpn_key_usage_notify_once(struct ovpn_key_usage *usage) +{ + return !test_and_set_bit(OVPN_KEY_USAGE_NOTIFY_BIT, &usage->flags); +} + +static inline int +ovpn_key_usage_xmit(struct ovpn_key_usage *usage, + const struct ovpn_limit *limit, + u64 pktid, u64 blocks, bool pktid_notify) +{ + u64 total; + int ret = 0; + + total = atomic64_add_return(blocks, &usage->blocks); + + /* tx must stop before the hard limit is crossed */ + if (unlikely(ovpn_key_usage_over_limit(limit->hard, pktid, total))) + return -ERANGE; + + /* soft limits ask userspace to rekey before tx must stop */ + if (unlikely(pktid_notify || + ovpn_key_usage_over_limit(limit->soft, pktid, total)) && + ovpn_key_usage_notify_once(usage)) + ret = 1; + + return ret; +} + +static inline bool +ovpn_key_usage_recv(struct ovpn_key_usage *usage, + const struct ovpn_limit *limit, + u64 pktid, u64 blocks) +{ + u64 total; + + total = atomic64_add_return(blocks, &usage->blocks); + + /* rx threshold crossings are reported once without dropping + * the packet + */ + return unlikely(ovpn_key_usage_over_limit(limit->soft, pktid, total)) && + ovpn_key_usage_notify_once(usage); +} + +static inline u64 ovpn_aead_limit_blocks(enum ovpn_cipher_alg cipher_alg, + unsigned int aad, + unsigned int bytes) +{ + switch (cipher_alg) { + case OVPN_CIPHER_ALG_AES_GCM: + return DIV_ROUND_UP_ULL(aad, AES_BLOCK_SIZE) + + DIV_ROUND_UP_ULL(bytes, AES_BLOCK_SIZE); + case OVPN_CIPHER_ALG_CHACHA20_POLY1305: + default: + return 0; + } +} + +#endif /* _NET_OVPN_CRYPTO_LIMITS_H_ */ diff --git a/drivers/net/ovpn/io.c b/drivers/net/ovpn/io.c index a6b777a9c2d9..31c750b9481a 100644 --- a/drivers/net/ovpn/io.c +++ b/drivers/net/ovpn/io.c @@ -112,6 +112,7 @@ void ovpn_decrypt_post(void *data, int ret) struct sk_buff *skb = data; struct ovpn_socket *sock; struct ovpn_peer *peer; + u64 aead_blocks; __be16 proto; __be32 *pid; @@ -141,6 +142,16 @@ void ovpn_decrypt_post(void *data, int ret) goto drop; } + aead_blocks = ovpn_aead_limit_blocks(ks->cipher_alg, + OVPN_OPCODE_SIZE + + OVPN_NONCE_WIRE_SIZE, + skb->len - payload_offset); + if (unlikely(ovpn_pktid_recv_update_aead(&ks->pid_recv, + &ks->usage_recv, + &ks->usage_limit, + aead_blocks))) + ovpn_nl_key_swap_notify(peer, ks->key_id); + /* keep track of last received authenticated packet for keepalive */ WRITE_ONCE(peer->last_recv, ktime_get_real_seconds()); diff --git a/drivers/net/ovpn/pktid.h b/drivers/net/ovpn/pktid.h index 82f59256b4a3..a85a1d160150 100644 --- a/drivers/net/ovpn/pktid.h +++ b/drivers/net/ovpn/pktid.h @@ -10,6 +10,7 @@ #ifndef _NET_OVPN_OVPNPKTID_H_ #define _NET_OVPN_OVPNPKTID_H_ +#include "crypto_limits.h" #include "proto.h" /* If no packets received for this length of time, set a backtrack floor @@ -58,35 +59,75 @@ struct ovpn_pktid_recv { /** * ovpn_pktid_xmit_next - allocate a transmit packet ID * @pid: transmit packet ID state + * @usage: key usage state + * @limit: key usage limits + * @aead_blocks: AEAD usage blocks consumed by this packet * @pktid: location where the generated packet ID is stored * * The returned packet ID becomes part of the AEAD nonce, so the helper rejects - * the packet before the 32-bit packet-ID space wraps. + * the packet before the 32-bit packet-ID space wraps. It also reserves this + * packet's AEAD usage against the key and rejects the packet before the hard + * AES-GCM usage limit would be exceeded. * - * The packet-ID soft threshold does not reject the packet. It returns 1 once - * so the caller can notify userspace to rekey while packet-ID space remains. + * Soft thresholds do not reject the packet. They return 1 once so the caller + * can notify userspace to rekey while packet-ID space remains and before the + * hard AES-GCM usage limit is reached. * * Return: 1 if userspace should be notified, 0 if no notification is needed, * or a negative error code otherwise. */ -static inline int ovpn_pktid_xmit_next(struct ovpn_pktid_xmit *pid, u32 *pktid) +static inline int ovpn_pktid_xmit_next(struct ovpn_pktid_xmit *pid, + struct ovpn_key_usage *usage, + const struct ovpn_limit *limit, + u64 aead_blocks, u32 *pktid) { const u32 seq_num = atomic_fetch_add_unless(&pid->seq_num, 1, 0); - int ret = 0; + bool pktid_notify; + int ret; /* packet IDs are used to create cipher IVs and must not wrap */ if (unlikely(!seq_num)) return -ERANGE; - /* notify userspace before the packet ID space is close to wrapping */ - if (unlikely(seq_num == PKTID_XMIT_REKEY_NOTIFY)) - ret = 1; + pktid_notify = seq_num >= PKTID_XMIT_REKEY_NOTIFY; + ret = ovpn_key_usage_xmit(usage, limit, seq_num, aead_blocks, + pktid_notify); + if (unlikely(ret < 0)) + return ret; *pktid = seq_num; return ret; } +/** + * ovpn_pktid_recv_update_aead - account receive-side AEAD usage + * @pr: receive packet ID state + * @usage: key usage state + * @limit: key usage limits + * @aead_blocks: AEAD usage blocks consumed by this packet + * + * RX AEAD accounting is only informational for the local userspace process. + * The peer's packets that already passed authentication and replay checks are + * not dropped because the peer crossed a local usage threshold. + * + * Return: true if userspace should be notified, false otherwise. + */ +static inline bool +ovpn_pktid_recv_update_aead(struct ovpn_pktid_recv *pr, + struct ovpn_key_usage *usage, + const struct ovpn_limit *limit, + u64 aead_blocks) +{ + bool ret; + + spin_lock_bh(&pr->lock); + ret = ovpn_key_usage_recv(usage, limit, pr->id, aead_blocks); + spin_unlock_bh(&pr->lock); + + return ret; +} + /* Write 12-byte AEAD IV to dest */ static inline void ovpn_pktid_aead_write(const u32 pktid, const u8 nt[], -- 2.54.0 _______________________________________________ Openvpn-devel mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/openvpn-devel
