Skip to content

Commit

Permalink
Trigger renegotiation of data key if getting close to the AEAD usage …
Browse files Browse the repository at this point in the history
…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 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 avoiding 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 effects that connection with a lot of small packets
(like TCP ACKs) mixed with large packets will be able to keep using the same
much longer until requiring a renegotiation.

This patch will set the limit where to trigger the renegotiation at 7/8
of the

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

Change-Id: Idccc93f303d29869ef4ed10b6bb56f37efedf623

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: I0d2c763fd1dcdacdd8993731fc4979e258d1da4e

Change-Id: I057f007577f10c6ac917ee4620ee3d2559187dc7
  • Loading branch information
schwabe committed Sep 30, 2024
1 parent 24223bf commit 5d24140
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 3 deletions.
43 changes: 43 additions & 0 deletions src/openvpn/crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "platform.h"

#include "memdbg.h"
#include <math.h>

/*
* Encryption and Compression Routines.
Expand Down Expand Up @@ -138,6 +139,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 blocked 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));
Expand Down Expand Up @@ -325,6 +331,38 @@ openvpn_encrypt(struct buffer *buf, struct buffer work,
}
}

int64_t
cipher_get_aead_limits(const char *ciphername)
{
if (!cipher_kt_mode_aead(ciphername))
{
return -1;
}

if (cipher_kt_name(ciphername) == cipher_kt_name("CHACHA20-POLY1305"))
{
return -1;
}

/* Assume all other ciphers require the limit */

/* We focus here on the equation
*
* q + s <= p^(1/2) * 2^(129/2) - 1
* q <= (p^(1/2) * 2^(129/2) - 1) / (L + 1)
*
* as is the one that is limiting us.
*
* With p = 2^-57 this becomes
*
* q + s <= (p^36 - 1)
*
*/
int64_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,
Expand Down Expand Up @@ -471,6 +509,11 @@ openvpn_decrypt_aead(struct buffer *buf, struct buffer work,
CRYPT_ERROR("cipher update failed");
}

/* update number of plaintext blocks decrypted. Use the x + (n-1)/n trick
* to round up the result to the number of blocked used. */
const int blocksize = AEAD_LIMIT_BLOCKSIZE;
opt->key_ctx_bi.decrypt.plaintext_blocks += (outlen + (blocksize - 1))/blocksize;

ASSERT(buf_inc_len(&work, outlen));
if (!cipher_ctx_final_check_tag(ctx->cipher, BPTR(&work) + outlen,
&outlen, tag_ptr, tag_size))
Expand Down
22 changes: 22 additions & 0 deletions src/openvpn/crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ struct key_ctx
* and the lower 32 bit of the IV are the packet id
*/
uint8_t implicit_iv[OPENVPN_MAX_IV_LENGTH];
/** Counters the number of plaintext encrypted using this cipher
* for AEAD blocks */
uint64_t plaintext_blocks;
};

#define KEY_DIRECTION_BIDIRECTIONAL 0 /* same keys for both directions */
Expand Down Expand Up @@ -601,6 +604,24 @@ 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 block + packets. Return -1 if ciphername is not an AEAD
* cipher or no limit (e.g. Chacha20-Poly1305) is needed.
*
* For reference see the OpenVPN RFC draft and
* https://www.ietf.org/archive/id/draft-irtf-cfrg-aead-limits-08.html
*/
int64_t
cipher_get_aead_limits(const char *ciphername);

/**
* Blocksize used for the AEAD limit caluclation
*
* Since cipher_ctx_block_size() is 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.
Expand Down Expand Up @@ -650,4 +671,5 @@ ovpn_expand_label(uint8_t *secret, size_t secret_len,
uint8_t *context, size_t context_len,
uint8_t *out, size_t out_len);


#endif /* CRYPTO_H */
63 changes: 60 additions & 3 deletions src/openvpn/ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ tls_limit_reneg_bytes(const char *ciphername, int64_t *reneg_bytes)
}
}

static int64_t
tls_get_limit_aead(const char *ciphername)
{
int64_t limit = cipher_get_aead_limits(ciphername);

if (limit == -1)
{
return 0;
}

/* set limit to 7/8 of the limit so the renogiation has can succeeds before
* we go over the limit */
limit = limit * 7/8;

msg(D_SHOW_KEYS, "Note: AEAD cipher %s will be limited to a sum of %"
PRIi64 " for block and packets before renegotiation",
ciphername, limit);
return limit;
}

void
tls_init_control_channel_frame_parameters(struct frame *frame, int tls_mtu)
{
Expand Down Expand Up @@ -1577,6 +1597,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;

Expand Down Expand Up @@ -2937,6 +2959,22 @@ tls_process_state(struct tls_multi *multi,
return true;
}

/**
* Checks if the limit is reached
*
* This method abstracts the calculation to make the calling function easier
* to read.
*/
static inline bool
aead_usage_limit_reached(const int64_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 + higest_pid > limit);
}

/**
* Determines if a renegotiation should be triggerred based on the various
* factors that can trigger one
Expand Down Expand Up @@ -2971,6 +3009,18 @@ should_trigger_renegotiation(const struct tls_session *session, const struct key
return true;
}

/* Check the AEAD usage limit of cleartext blocks + packets */
const struct key_ctx_bi *key_ctx_bi = &ks->crypto_options.key_ctx_bi;
const int64_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;
}
/*
Expand Down Expand Up @@ -3001,12 +3051,19 @@ tls_process(struct tls_multi *multi,
/* Should we trigger a soft reset? -- new key, keeps old key for a while */
if (ks->state >= S_GENERATED_KEYS
&& should_trigger_renegotiation(session, ks))
{
{
msg(D_TLS_DEBUG_LOW, "TLS: soft reset sec=%d/%d bytes=" counter_format
"/%lld pkts=" counter_format "/%lld",
"/%lld pkts=" counter_format "/%lld"
" 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);
}

Expand Down
3 changes: 3 additions & 0 deletions src/openvpn/ssl_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ struct tls_options
interval_t packet_timeout;
int64_t renegotiate_bytes;
int64_t renegotiate_packets;
/** This limit for AEAD cipher, this is the sum of packets + blocks
* that are allowed to be used */
int64_t aead_usage_limit;
interval_t renegotiate_seconds;

/* cert verification parms */
Expand Down
22 changes: 22 additions & 0 deletions tests/unit_tests/openvpn/test_crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <string.h>
#include <setjmp.h>
#include <cmocka.h>
#include <math.h>

#include "crypto.h"
#include "options.h"
Expand Down Expand Up @@ -448,6 +449,26 @@ 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"), -1);

int64_t aeslimit = cipher_get_aead_limits("AES-128-GCM");

assert_double_equal(aeslimit, pow(2, 36) -1, 0.1);

/* Check if this matches our exception for 1600 size packets */
double L = 101;
assert_double_equal(log2(aeslimit/L), 29.34, 0.01);

/* and for 9000 */
L = 563;
assert_double_equal(log2(aeslimit/ L), 26.86, 0.01);
}

void
crypto_test_hkdf_expand_testa1(void **state)
{
Expand Down Expand Up @@ -555,6 +576,7 @@ main(void)
cmocka_unit_test(crypto_test_hmac),
cmocka_unit_test(test_occ_mtu_calculation),
cmocka_unit_test(test_mssfix_mtu_calculation),
cmocka_unit_test(crypto_test_aead_limits),
cmocka_unit_test(crypto_test_hkdf_expand_testa1),
cmocka_unit_test(crypto_test_hkdf_expand_testa2),
cmocka_unit_test(crypto_test_hkdf_expand_testa3)
Expand Down

0 comments on commit 5d24140

Please sign in to comment.