[Openvpn-devel,RFC,net-next,v2,02/14] ovpn: enforce AES-GCM usage limits

Message ID 039bea1f79025e6374c6d3237f4f6e003060a996.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
  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 <ralf@mandelbit.com>
---
No functional changes since v1 https://lore.kernel.org/openvpn-devel/bee985b879f9e5b740921e448fa024500ac1340c.1782919654.git.ralf@mandelbit.com/

 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
  

Patch

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..2af2c3389852
--- /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 <ralf@mandelbit.com>
+ *		Antonio Quartulli <antonio@openvpn.net>
+ */
+
+#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)
+{
+	int ret = 0;
+	u64 total;
+
+	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[],