diff --git a/Changes.rst b/Changes.rst index a6cc3be6cd4..34542a59324 100644 --- a/Changes.rst +++ b/Changes.rst @@ -22,6 +22,14 @@ Support for tun/tap via unix domain socket and lwipovpn support For more details see [lwipovpn on Gihtub](https://github.com/OpenVPN/lwipovpn). +Enforcement of AES-GCM usage limit + OpenVPN will now enforce the usage limits on AES-GCM with the same + confidentiality margin as TLS 1.3 does. This mean that renegotiation will + be triggered after roughly 2^28 to 2^31 packets depending of the packet + size. More details about usage limit of AES-GCM can be found here: + + https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/ + Deprecated features ------------------- ``secret`` support has been removed by default. diff --git a/src/openvpn/crypto.c b/src/openvpn/crypto.c index d548c8d4360..faf69fc3555 100644 --- a/src/openvpn/crypto.c +++ b/src/openvpn/crypto.c @@ -138,6 +138,11 @@ openvpn_encrypt_aead(struct buffer *buf, struct buffer work, ASSERT(cipher_ctx_update(ctx->cipher, BEND(&work), &outlen, BPTR(buf), BLEN(buf))); ASSERT(buf_inc_len(&work, outlen)); + /* update number of plaintext blocks encrypted. Use the (x + (n-1))/n trick + * to round up the result to the number of blocks used */ + const int blocksize = AEAD_LIMIT_BLOCKSIZE; + opt->key_ctx_bi.encrypt.plaintext_blocks += (outlen + (blocksize - 1))/blocksize; + /* Flush the encryption buffer */ ASSERT(cipher_ctx_final(ctx->cipher, BEND(&work), &outlen)); ASSERT(buf_inc_len(&work, outlen)); @@ -325,6 +330,37 @@ openvpn_encrypt(struct buffer *buf, struct buffer work, } } +uint64_t +cipher_get_aead_limits(const char *ciphername) +{ + if (!cipher_kt_mode_aead(ciphername)) + { + return 0; + } + + if (cipher_kt_name(ciphername) == cipher_kt_name("CHACHA20-POLY1305")) + { + return 0; + } + + /* Assume all other ciphers require the limit */ + + /* We focus here on the equation + * + * q + s <= p^(1/2) * 2^(129/2) - 1 + * + * as is the one that is limiting us. + * + * With p = 2^-57 this becomes + * + * q + s <= (2^36 - 1) + * + */ + uint64_t rs = (1ull << 36) - 1; + + return rs; +} + bool crypto_check_replay(struct crypto_options *opt, const struct packet_id_net *pin, const char *error_prefix, @@ -487,6 +523,12 @@ openvpn_decrypt_aead(struct buffer *buf, struct buffer work, goto error_exit; } + + /* update number of plaintext blocks decrypted. Use the (x + (n-1))/n trick + * to round up the result to the number of blocks used. */ + const int blocksize = AEAD_LIMIT_BLOCKSIZE; + opt->key_ctx_bi.decrypt.plaintext_blocks += (outlen + (blocksize - 1))/blocksize; + *buf = work; gc_free(&gc); diff --git a/src/openvpn/crypto.h b/src/openvpn/crypto.h index 04d7bb2ad08..4579b6589e7 100644 --- a/src/openvpn/crypto.h +++ b/src/openvpn/crypto.h @@ -177,6 +177,10 @@ struct key_ctx uint8_t implicit_iv[OPENVPN_MAX_IV_LENGTH]; /**< The implicit part of the IV */ size_t implicit_iv_len; /**< The length of implicit_iv */ + /** Counter for the number of plaintext block encrypted using this cipher + * with the current key in number of 128 bit blocks (only used for + * AEAD ciphers) */ + uint64_t plaintext_blocks; }; #define KEY_DIRECTION_BIDIRECTIONAL 0 /* same keys for both directions */ @@ -606,6 +610,25 @@ create_kt(const char *cipher, const char *md, const char *optname) return kt; } +/** + * Check if the cipher is an AEAD cipher and needs to be limited to a certain + * number of number of blocks + packets. Return 0 if ciphername is not an AEAD + * cipher or no limit (e.g. Chacha20-Poly1305) is needed. (Or the limit is + * larger than 2^64) + * + * For reference see the OpenVPN RFC draft and + * https://www.ietf.org/archive/id/draft-irtf-cfrg-aead-limits-08.html + */ +uint64_t +cipher_get_aead_limits(const char *ciphername); + +/** + * Blocksize used for the AEAD limit caluclation + * + * Since cipher_ctx_block_size() is not reliable and will return 1 in many + * cases use a hardcoded blocksize instead */ +#define AEAD_LIMIT_BLOCKSIZE 16 + /** * Checks if the current TLS library supports the TLS 1.0 PRF with MD5+SHA1 * that OpenVPN uses when TLS Keying Material Export is not available. @@ -614,4 +637,20 @@ create_kt(const char *cipher, const char *md, const char *optname) */ bool check_tls_prf_working(void); +/** + * Checks if the usage limit for an AEAD cipher is reached + * + * This method abstracts the calculation to make the calling function easier + * to read. + */ +static inline bool +aead_usage_limit_reached(const uint64_t limit, const struct key_ctx *key_ctx, + int64_t higest_pid) +{ + /* This is the q + s <= p^(1/2) * 2^(129/2) - 1 calculation where + * q is the number of protected messages (highest_pid) + * s Total plaintext length in all messages (in blocks) */ + return (limit > 0 && key_ctx->plaintext_blocks + (uint64_t) higest_pid > limit); +} + #endif /* CRYPTO_H */ diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c index fcfb3449f19..c77c4ed66c9 100644 --- a/src/openvpn/ssl.c +++ b/src/openvpn/ssl.c @@ -131,6 +131,26 @@ tls_limit_reneg_bytes(const char *ciphername, int64_t *reneg_bytes) } } +static uint64_t +tls_get_limit_aead(const char *ciphername) +{ + uint64_t limit = cipher_get_aead_limits(ciphername); + + if (limit == 0) + { + return 0; + } + + /* set limit to 7/8 of the limit so the renegotiation can succeed before + * we go over the limit */ + limit = limit/8 * 7; + + msg(D_SHOW_KEYS, "Note: AEAD cipher %s will trigger a renegotiation" + " at a sum of %" PRIi64 " blocks and packets.", + ciphername, limit); + return limit; +} + void tls_init_control_channel_frame_parameters(struct frame *frame, int tls_mtu) { @@ -1579,6 +1599,8 @@ tls_session_generate_data_channel_keys(struct tls_multi *multi, tls_limit_reneg_bytes(session->opt->key_type.cipher, &session->opt->renegotiate_bytes); + session->opt->aead_usage_limit = tls_get_limit_aead(session->opt->key_type.cipher); + /* set the state of the keys for the session to generated */ ks->state = S_GENERATED_KEYS; @@ -2999,6 +3021,27 @@ should_trigger_renegotiation(const struct tls_session *session, const struct key return true; } + /* Check the AEAD usage limit of cleartext blocks + packets. + * + * Contrary to when epoch data mode is active, where only the sender side + * checks the limit, here we check both receive and send limit since + * we assume that only one side is aware of the limit. + * + * Since if both sides were aware, then both sides will probably also + * switch to use epoch data channel instead, so this code is not + * in effect then. + */ + const struct key_ctx_bi *key_ctx_bi = &ks->crypto_options.key_ctx_bi; + const uint64_t usage_limit = session->opt->aead_usage_limit; + + if (aead_usage_limit_reached(usage_limit, &key_ctx_bi->encrypt, + ks->crypto_options.packet_id.send.id) + || aead_usage_limit_reached(usage_limit, &key_ctx_bi->decrypt, + ks->crypto_options.packet_id.rec.id)) + { + return true; + } + return false; } /* @@ -3031,10 +3074,17 @@ tls_process(struct tls_multi *multi, && should_trigger_renegotiation(session, ks)) { msg(D_TLS_DEBUG_LOW, "TLS: soft reset sec=%d/%d bytes=" counter_format - "/%" PRIi64 " pkts=" counter_format "/%" PRIi64, + "/%" PRIi64 " pkts=" counter_format "/%" PRIi64 + " aead_limit_send=%" PRIu64 "/%" PRIu64 + " aead_limit_recv=%" PRIu64 "/%" PRIu64, (int) (now - ks->established), session->opt->renegotiate_seconds, ks->n_bytes, session->opt->renegotiate_bytes, - ks->n_packets, session->opt->renegotiate_packets); + ks->n_packets, session->opt->renegotiate_packets, + ks->crypto_options.key_ctx_bi.encrypt.plaintext_blocks + ks->n_packets, + session->opt->aead_usage_limit, + ks->crypto_options.key_ctx_bi.decrypt.plaintext_blocks + ks->n_packets, + session->opt->aead_usage_limit + ); key_state_soft_reset(session); } diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h index 5840e2d73e7..ccbc053bdb9 100644 --- a/src/openvpn/ssl_common.h +++ b/src/openvpn/ssl_common.h @@ -333,6 +333,9 @@ struct tls_options interval_t packet_timeout; int64_t renegotiate_bytes; int64_t renegotiate_packets; + /** limit for AEAD cipher, this is the sum of packets + blocks + * that are allowed to be used */ + uint64_t aead_usage_limit; interval_t renegotiate_seconds; /* cert verification parms */ diff --git a/tests/unit_tests/openvpn/test_crypto.c b/tests/unit_tests/openvpn/test_crypto.c index fdc8fbdebae..ec8e661918e 100644 --- a/tests/unit_tests/openvpn/test_crypto.c +++ b/tests/unit_tests/openvpn/test_crypto.c @@ -448,6 +448,29 @@ test_mssfix_mtu_calculation(void **state) gc_free(&gc); } +void +crypto_test_aead_limits(void **state) +{ + /* if ChaCha20-Poly1305 is not supported by the crypto library or in the + * current mode (FIPS), this will still return -1 */ + assert_int_equal(cipher_get_aead_limits("CHACHA20-POLY1305"), 0); + + int64_t aeslimit = cipher_get_aead_limits("AES-128-GCM"); + + assert_int_equal(aeslimit, (1ull << 36) - 1); + + /* Check if this matches our exception for 1600 size packets assuming + * AEAD_LIMIT_BLOCKSIZE (128 bits/ 16 bytes). Gives us 100 blocks + * + 1 for the packet */ + int64_t L = 101; + /* 2 ^ 29.34, using the result here to avoid linking to libm */ + assert_int_equal(aeslimit / L, 680390858); + + /* and for 9000, 2^26.86 */ + L = 563; + assert_int_equal(aeslimit / L, 122059461); +} + int main(void) { @@ -458,7 +481,8 @@ main(void) cmocka_unit_test(crypto_test_tls_prf), cmocka_unit_test(crypto_test_hmac), cmocka_unit_test(test_occ_mtu_calculation), - cmocka_unit_test(test_mssfix_mtu_calculation) + cmocka_unit_test(test_mssfix_mtu_calculation), + cmocka_unit_test(crypto_test_aead_limits) }; #if defined(ENABLE_CRYPTO_OPENSSL)