Browse code

Trigger renegotiation of data key if getting close to the AEAD usage limit

This implements the limitation of AEAD key usage[1] with a confidentiality
margin of 2^-57, the same as TLS 1.3. In this implementation, unlike
TLS 1.3 that counts the number of records, we count the actual number of
packets and plaintext blocks. TLS 1.3 can reasonable assume that for
large data transfers, full records are used and therefore the maximum
record size of 2**14 (2*10 blocks) is used to calculate the number of
records before a new key needs to be used.

For a VPN like OpenVPN, the same calculation would either require using a
pessimistic assumption of using a MTU size of 65k which limits us to
2^24 packets, which equals only 24 GB with more common MTU/MSS of 1400
or requiring a dynamic calculation which includes the actual MTU that
we allow to send. For 1500 the calculation yields 2*29.4 which is a
quite significant higher number of packets (923 GB at 1400 MSS/MTU).

To avoid this dynamic calculation and also avoid needing to know the
MSS/MTU size in the crypto layer, this implementation foregoes the
simplification of counting just packets but will count blocks and packets
instead and determines the limit from that.

This also has the side effect that connections with a lot of small packets
(like TCP ACKs) mixed with large packets will be able to keep using the same
key much longer until requiring a renegotiation.

This patch will set the limit where to trigger the renegotiation at 7/8
of the recommended maximum value.

[1] https://www.ietf.org/archive/id/draft-irtf-cfrg-aead-limits-08.html

Testing instructions:

The easiest way to test if this patch works as
intended is to manually change the return value of cipher_get_aead_limits
to some silly low value like 2048. After a bit of VPN traffic, a soft
reset should occur that indicates being over the

TLS: soft reset sec=41/3600 bytes=59720/-1 pkts=78/0 aead_limit_send=1883/1792 aead_limit_recv=1937/1792

Here the send limit is over the limit (1792 = 2048 * 8/7).

Change-Id: I057f007577f10c6ac917ee4620ee3d2559187dc7
Signed-off-by: Arne Schwabe <arne@rfc2549.org>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <20241221153731.1755-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg30144.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>

Arne Schwabe authored on 2024/12/22 00:37:30
Showing 6 changed files
... ...
@@ -22,6 +22,14 @@ Support for tun/tap via unix domain socket and lwipovpn support
22 22
 
23 23
     For more details see [lwipovpn on Gihtub](https://github.com/OpenVPN/lwipovpn).
24 24
 
25
+Enforcement of AES-GCM usage limit
26
+    OpenVPN will now enforce the usage limits on AES-GCM with the same
27
+    confidentiality margin as TLS 1.3 does. This mean that renegotiation will
28
+    be triggered after roughly 2^28 to 2^31 packets depending of the packet
29
+    size. More details about usage limit of AES-GCM can be found here:
30
+
31
+    https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/
32
+
25 33
 Deprecated features
26 34
 -------------------
27 35
 ``secret`` support has been removed by default.
... ...
@@ -138,6 +138,11 @@ openvpn_encrypt_aead(struct buffer *buf, struct buffer work,
138 138
     ASSERT(cipher_ctx_update(ctx->cipher, BEND(&work), &outlen, BPTR(buf), BLEN(buf)));
139 139
     ASSERT(buf_inc_len(&work, outlen));
140 140
 
141
+    /* update number of plaintext blocks encrypted. Use the (x + (n-1))/n trick
142
+     * to round up the result to the number of blocks used */
143
+    const int blocksize = AEAD_LIMIT_BLOCKSIZE;
144
+    opt->key_ctx_bi.encrypt.plaintext_blocks += (outlen + (blocksize - 1))/blocksize;
145
+
141 146
     /* Flush the encryption buffer */
142 147
     ASSERT(cipher_ctx_final(ctx->cipher, BEND(&work), &outlen));
143 148
     ASSERT(buf_inc_len(&work, outlen));
... ...
@@ -325,6 +330,37 @@ openvpn_encrypt(struct buffer *buf, struct buffer work,
325 325
     }
326 326
 }
327 327
 
328
+uint64_t
329
+cipher_get_aead_limits(const char *ciphername)
330
+{
331
+    if (!cipher_kt_mode_aead(ciphername))
332
+    {
333
+        return 0;
334
+    }
335
+
336
+    if (cipher_kt_name(ciphername) == cipher_kt_name("CHACHA20-POLY1305"))
337
+    {
338
+        return 0;
339
+    }
340
+
341
+    /* Assume all other ciphers require the limit */
342
+
343
+    /* We focus here on the equation
344
+     *
345
+     *       q + s <= p^(1/2) * 2^(129/2) - 1
346
+     *
347
+     * as is the one that is limiting us.
348
+     *
349
+     *  With p = 2^-57 this becomes
350
+     *
351
+     *      q + s <= (2^36 - 1)
352
+     *
353
+     */
354
+    uint64_t rs = (1ull << 36) - 1;
355
+
356
+    return rs;
357
+}
358
+
328 359
 bool
329 360
 crypto_check_replay(struct crypto_options *opt,
330 361
                     const struct packet_id_net *pin, const char *error_prefix,
... ...
@@ -487,6 +523,12 @@ openvpn_decrypt_aead(struct buffer *buf, struct buffer work,
487 487
         goto error_exit;
488 488
     }
489 489
 
490
+
491
+    /* update number of plaintext blocks decrypted. Use the (x + (n-1))/n trick
492
+     * to round up the result to the number of blocks used. */
493
+    const int blocksize = AEAD_LIMIT_BLOCKSIZE;
494
+    opt->key_ctx_bi.decrypt.plaintext_blocks += (outlen + (blocksize - 1))/blocksize;
495
+
490 496
     *buf = work;
491 497
 
492 498
     gc_free(&gc);
... ...
@@ -177,6 +177,10 @@ struct key_ctx
177 177
     uint8_t implicit_iv[OPENVPN_MAX_IV_LENGTH];
178 178
     /**< The implicit part of the IV */
179 179
     size_t implicit_iv_len;     /**< The length of implicit_iv */
180
+    /** Counter for the number of plaintext block encrypted using this cipher
181
+     * with the current key in number of 128 bit blocks (only used for
182
+     * AEAD ciphers) */
183
+    uint64_t plaintext_blocks;
180 184
 };
181 185
 
182 186
 #define KEY_DIRECTION_BIDIRECTIONAL 0 /* same keys for both directions */
... ...
@@ -607,6 +611,25 @@ create_kt(const char *cipher, const char *md, const char *optname)
607 607
 }
608 608
 
609 609
 /**
610
+ * Check if the cipher is an AEAD cipher and needs to be limited to a certain
611
+ * number of number of blocks + packets. Return 0 if ciphername is not an AEAD
612
+ * cipher or no limit (e.g. Chacha20-Poly1305) is needed. (Or the limit is
613
+ * larger than 2^64)
614
+ *
615
+ * For reference see the OpenVPN RFC draft and
616
+ * https://www.ietf.org/archive/id/draft-irtf-cfrg-aead-limits-08.html
617
+ */
618
+uint64_t
619
+cipher_get_aead_limits(const char *ciphername);
620
+
621
+/**
622
+ * Blocksize used for the AEAD limit caluclation
623
+ *
624
+ * Since cipher_ctx_block_size() is not reliable and will return 1 in many
625
+ * cases use a hardcoded blocksize instead */
626
+#define     AEAD_LIMIT_BLOCKSIZE    16
627
+
628
+/**
610 629
  * Checks if the current TLS library supports the TLS 1.0 PRF with MD5+SHA1
611 630
  * that OpenVPN uses when TLS Keying Material Export is not available.
612 631
  *
... ...
@@ -614,4 +637,20 @@ create_kt(const char *cipher, const char *md, const char *optname)
614 614
  */
615 615
 bool check_tls_prf_working(void);
616 616
 
617
+/**
618
+ * Checks if the usage limit for an AEAD cipher is reached
619
+ *
620
+ * This method abstracts the calculation to make the calling function easier
621
+ * to read.
622
+ */
623
+static inline bool
624
+aead_usage_limit_reached(const uint64_t limit, const struct key_ctx *key_ctx,
625
+                         int64_t higest_pid)
626
+{
627
+    /* This is the  q + s <=  p^(1/2) * 2^(129/2) - 1 calculation where
628
+     * q is the number of protected messages (highest_pid)
629
+     * s Total plaintext length in all messages (in blocks) */
630
+    return (limit > 0 && key_ctx->plaintext_blocks + (uint64_t) higest_pid > limit);
631
+}
632
+
617 633
 #endif /* CRYPTO_H */
... ...
@@ -131,6 +131,26 @@ tls_limit_reneg_bytes(const char *ciphername, int64_t *reneg_bytes)
131 131
     }
132 132
 }
133 133
 
134
+static uint64_t
135
+tls_get_limit_aead(const char *ciphername)
136
+{
137
+    uint64_t limit = cipher_get_aead_limits(ciphername);
138
+
139
+    if (limit == 0)
140
+    {
141
+        return 0;
142
+    }
143
+
144
+    /* set limit to 7/8 of the limit so the renegotiation can succeed before
145
+     * we go over the limit */
146
+    limit = limit/8 * 7;
147
+
148
+    msg(D_SHOW_KEYS, "Note: AEAD cipher %s will trigger a renegotiation"
149
+        " at a sum of %" PRIi64 " blocks and packets.",
150
+        ciphername, limit);
151
+    return limit;
152
+}
153
+
134 154
 void
135 155
 tls_init_control_channel_frame_parameters(struct frame *frame, int tls_mtu)
136 156
 {
... ...
@@ -1579,6 +1599,8 @@ tls_session_generate_data_channel_keys(struct tls_multi *multi,
1579 1579
     tls_limit_reneg_bytes(session->opt->key_type.cipher,
1580 1580
                           &session->opt->renegotiate_bytes);
1581 1581
 
1582
+    session->opt->aead_usage_limit = tls_get_limit_aead(session->opt->key_type.cipher);
1583
+
1582 1584
     /* set the state of the keys for the session to generated */
1583 1585
     ks->state = S_GENERATED_KEYS;
1584 1586
 
... ...
@@ -2999,6 +3021,27 @@ should_trigger_renegotiation(const struct tls_session *session, const struct key
2999 2999
         return true;
3000 3000
     }
3001 3001
 
3002
+    /* Check the AEAD usage limit of cleartext blocks + packets.
3003
+     *
3004
+     *  Contrary to when epoch data mode is active, where only the sender side
3005
+     *  checks the limit, here we check both receive and send limit since
3006
+     *  we assume that only one side is aware of the limit.
3007
+     *
3008
+     *  Since if both sides were aware, then both sides will probably also
3009
+     *  switch to use epoch data channel instead, so this code is not
3010
+     *  in effect then.
3011
+     */
3012
+    const struct key_ctx_bi *key_ctx_bi = &ks->crypto_options.key_ctx_bi;
3013
+    const uint64_t usage_limit = session->opt->aead_usage_limit;
3014
+
3015
+    if (aead_usage_limit_reached(usage_limit, &key_ctx_bi->encrypt,
3016
+                                 ks->crypto_options.packet_id.send.id)
3017
+        || aead_usage_limit_reached(usage_limit, &key_ctx_bi->decrypt,
3018
+                                    ks->crypto_options.packet_id.rec.id))
3019
+    {
3020
+        return true;
3021
+    }
3022
+
3002 3023
     return false;
3003 3024
 }
3004 3025
 /*
... ...
@@ -3031,10 +3074,17 @@ tls_process(struct tls_multi *multi,
3031 3031
         && should_trigger_renegotiation(session, ks))
3032 3032
     {
3033 3033
         msg(D_TLS_DEBUG_LOW, "TLS: soft reset sec=%d/%d bytes=" counter_format
3034
-            "/%" PRIi64 " pkts=" counter_format "/%" PRIi64,
3034
+            "/%" PRIi64 " pkts=" counter_format "/%" PRIi64
3035
+            " aead_limit_send=%" PRIu64 "/%" PRIu64
3036
+            " aead_limit_recv=%" PRIu64 "/%" PRIu64,
3035 3037
             (int) (now - ks->established), session->opt->renegotiate_seconds,
3036 3038
             ks->n_bytes, session->opt->renegotiate_bytes,
3037
-            ks->n_packets, session->opt->renegotiate_packets);
3039
+            ks->n_packets, session->opt->renegotiate_packets,
3040
+            ks->crypto_options.key_ctx_bi.encrypt.plaintext_blocks + ks->n_packets,
3041
+            session->opt->aead_usage_limit,
3042
+            ks->crypto_options.key_ctx_bi.decrypt.plaintext_blocks + ks->n_packets,
3043
+            session->opt->aead_usage_limit
3044
+            );
3038 3045
         key_state_soft_reset(session);
3039 3046
     }
3040 3047
 
... ...
@@ -333,6 +333,9 @@ struct tls_options
333 333
     interval_t packet_timeout;
334 334
     int64_t renegotiate_bytes;
335 335
     int64_t renegotiate_packets;
336
+    /** limit for AEAD cipher, this is the sum of packets + blocks
337
+     * that are allowed to be used */
338
+    uint64_t aead_usage_limit;
336 339
     interval_t renegotiate_seconds;
337 340
 
338 341
     /* cert verification parms */
... ...
@@ -448,6 +448,29 @@ test_mssfix_mtu_calculation(void **state)
448 448
     gc_free(&gc);
449 449
 }
450 450
 
451
+void
452
+crypto_test_aead_limits(void **state)
453
+{
454
+    /* if ChaCha20-Poly1305 is not supported by the crypto library or in the
455
+     * current mode (FIPS), this will still return -1 */
456
+    assert_int_equal(cipher_get_aead_limits("CHACHA20-POLY1305"), 0);
457
+
458
+    int64_t aeslimit = cipher_get_aead_limits("AES-128-GCM");
459
+
460
+    assert_int_equal(aeslimit, (1ull << 36) - 1);
461
+
462
+    /* Check if this matches our exception for 1600 size packets assuming
463
+     * AEAD_LIMIT_BLOCKSIZE (128 bits/ 16 bytes). Gives us 100 blocks
464
+     * + 1 for the packet */
465
+    int64_t L = 101;
466
+    /* 2 ^ 29.34, using the result here to avoid linking to libm */
467
+    assert_int_equal(aeslimit / L, 680390858);
468
+
469
+    /* and for 9000, 2^26.86 */
470
+    L = 563;
471
+    assert_int_equal(aeslimit / L, 122059461);
472
+}
473
+
451 474
 int
452 475
 main(void)
453 476
 {
... ...
@@ -458,7 +481,8 @@ main(void)
458 458
         cmocka_unit_test(crypto_test_tls_prf),
459 459
         cmocka_unit_test(crypto_test_hmac),
460 460
         cmocka_unit_test(test_occ_mtu_calculation),
461
-        cmocka_unit_test(test_mssfix_mtu_calculation)
461
+        cmocka_unit_test(test_mssfix_mtu_calculation),
462
+        cmocka_unit_test(crypto_test_aead_limits)
462 463
     };
463 464
 
464 465
 #if defined(ENABLE_CRYPTO_OPENSSL)