[Openvpn-devel,3/3] Introduce dynamic tls-crypt for secure soft_reset/session renegotiation

Message ID 20220909195902.2011798-3-arne@rfc2549.org
State Superseded
Delegated to: Heiko Hund
Headers show
Series [Openvpn-devel,1/3] Allows renegotiation only to start if session is fully established | expand

Commit Message

Arne Schwabe Sept. 9, 2022, 9:59 a.m. UTC
Currently we have only one slot for renegotiation of the session/keys
If a replayed/faked packet is inserted by a malicous attacker, the
legimate peer cannot renegotiate anymore.

This commit introduces dynamic tls-crypt. When both peer support this
feature, both peer create a dynamic tls-crypt key using TLS EKM (export key
material) and will enforce using that key and tls-crypt for all
renegotiations.

Since one of tls-crypt/tls-crypt-v2 purpose is to provide poor man's post
quantum crypto guarantees, we have to ensure that the dynamic key tls-crypt
key that replace the original tls-crypt key is as strong as the orginal key
to avoid problems if there is a weak RNG or TLS EKM produces weak keys. We
ensure this but XORing the original key with the key from TLS EKM. If
tls-crypt/tls-cryptv2 is not active, we use just the key generated by
TLS EKM.

OpenVPN 2.x reserves the TM_ACTIVE session for renegotians. When a
SOFT_RESET_V1 packet is received, the active TLS session is moved from
KS_PRIMARY to KS_SECONDARY. If the SOFT_RESET_V1 came from a replay or a
was faked (no tls-auth/tls-crypt), the session is blocked until the TLS
renegotiation attempt times out, blocking the legimitate client.
Using a dynamic tls-crypt key here block any SOFT_RESET_V1 as replay and
fake packets will not have a matching authentication/encryption are
discarded.

HARD_RESET packets that are from a reconnecting peer are instead put in the
TM_UNTRUSTED/KS_PRIMARY slot until they are sufficiently verified, so the
dyanmic tls-crypt key is not used here. Replay/fake packets also do not
block the legimiate client.

The issue was initially reported by Fabio Streun <fabio.streun@inf.ethz.ch>

Signed-off-by: Arne Schwabe <arne@rfc2549.org>
---
 src/openvpn/crypto.c                      |  7 +-
 src/openvpn/crypto.h                      | 16 +++-
 src/openvpn/init.c                        |  8 +-
 src/openvpn/multi.c                       |  4 +
 src/openvpn/openvpn.h                     |  2 +
 src/openvpn/options.c                     |  4 +
 src/openvpn/push.c                        |  4 +
 src/openvpn/ssl.c                         | 30 ++++++--
 src/openvpn/ssl.h                         |  5 ++
 src/openvpn/ssl_backend.h                 |  1 +
 src/openvpn/ssl_common.h                  |  6 ++
 src/openvpn/ssl_ncp.c                     |  4 +
 src/openvpn/ssl_pkt.c                     |  2 +-
 src/openvpn/ssl_pkt.h                     | 21 +++++
 src/openvpn/tls_crypt.c                   | 93 ++++++++++++++++++++---
 src/openvpn/tls_crypt.h                   | 19 ++++-
 tests/unit_tests/openvpn/test_pkt.c       | 17 ++++-
 tests/unit_tests/openvpn/test_tls_crypt.c | 85 +++++++++++++++++++++
 18 files changed, 299 insertions(+), 29 deletions(-)

Comments

Heiko Hund Oct. 18, 2022, 1:36 a.m. UTC | #1
On Freitag, 9. September 2022 21:59:02 CEST Arne Schwabe wrote:
> --- a/src/openvpn/multi.c
> +++ b/src/openvpn/multi.c
> @@ -1803,6 +1803,10 @@ multi_client_set_protocol_options(struct context *c)
>      {
>          o->imported_protocol_flags |= CO_USE_TLS_KEY_MATERIAL_EXPORT;
>      }
> +    if (proto & IV_PROTO_SECURE_RENOG)

I think any occurrence of "renog" (i.e. short for renegotiation[s]) should be 
changed to "reneg" throughout this patch. See --reneg-x options. Could be just 
me, so a more opinions would be welcome.

> --- a/src/openvpn/options.c
> +++ b/src/openvpn/options.c
> @@ -8553,6 +8553,10 @@ add_option(struct options *options,
>              {
>                  options->imported_protocol_flags |=
> CO_USE_TLS_KEY_MATERIAL_EXPORT; }
> +            else if (streq(p[j], "secure-renog"))

Should be rewritten to use --protocol-flags instead.

> @@ -71,9 +71,77 @@ tls_crypt_init_key(struct key_ctx_bi *key, const char
> *key_file, msg(M_FATAL, "ERROR: --tls-crypt not supported");
>      }
>      crypto_read_openvpn_key(&kt, key, key_file, key_inline, key_direction,
> -                            "Control Channel Encryption", "tls-crypt");
> +                            "Control Channel Encryption", "tls-crypt",
> keydata); }
> 
> +/**
> + * Will produce dest = dest XOR data
> + */
> +static void
> +xor_key2_key(struct key2 *dest, const struct key2 *data)

I wonder if this would be more readable as
xor_key(struct key2 *key, const struct key2 *mask)

> +bool
> +tls_session_generate_secure_renegotition_key(struct tls_multi *multi,
> +                                             struct tls_session *session)

typo renegotiation_key

> +    struct key2 rengokeys;

typo renogkeys (renegkeys, IMHO)

> @@ -285,7 +354,7 @@ tls_crypt_v2_init_client_key(struct key_ctx_bi *key,
> struct buffer *wkc_buf, }
> 
>      tls_crypt_v2_load_client_key(key, &key2, false);
> -    secure_memzero(&key2, sizeof(key2));
> +    *original_key = key2;

We should do the zeroing in tls_session_generate_secure_renegotiation_key() 
shortly after we used it to XOR then. And maybe only delay it if we need to 
XOR anyways, could use original_key == NULL as indication.

> @@ -587,8 +655,8 @@ tls_crypt_v2_extract_client_key(struct buffer *buf,
>      ctx->cleanup_key_ctx = true;
>      ctx->opt.flags |= CO_PACKET_ID_LONG_FORM;
>      memset(&ctx->opt.key_ctx_bi, 0, sizeof(ctx->opt.key_ctx_bi));
> -    tls_crypt_v2_load_client_key(&ctx->opt.key_ctx_bi, &client_key, true);
> -    secure_memzero(&client_key, sizeof(client_key));
> +    tls_crypt_v2_load_client_key(&ctx->opt.key_ctx_bi,
> +                                 &ctx->original_tlscrypt_keydata, true);

Same as above for the server side. Could zero here immediately if 
original_tlscrypt_keydata == NULL
Arne Schwabe Oct. 18, 2022, 10:28 a.m. UTC | #2
> 
>> --- a/src/openvpn/options.c
>> +++ b/src/openvpn/options.c
>> @@ -8553,6 +8553,10 @@ add_option(struct options *options,
>>               {
>>                   options->imported_protocol_flags |=
>> CO_USE_TLS_KEY_MATERIAL_EXPORT; }
>> +            else if (streq(p[j], "secure-renog"))
> 
> Should be rewritten to use --protocol-flags instead.

It is already using protocol-flags. That is in the middle of 
protocol-flags in options.c

>>
>>       tls_crypt_v2_load_client_key(key, &key2, false);
>> -    secure_memzero(&key2, sizeof(key2));
>> +    *original_key = key2;
> 
> We should do the zeroing in tls_session_generate_secure_renegotiation_key()
> shortly after we used it to XOR then. And maybe only delay it if we need to
> XOR anyways, could use original_key == NULL as indication.

We cannot do that. The reason is that a client might reconnect to the 
same session (using the TM_UNTRUSTED session slot) or if we have a 
simple reconnect. And after the reconnect we need to calculate a new 
key. And anyway the data is memory anyway just implicit in the OpenSSL 
data structure (tls_wrap.opt.key_ctx_bi) that holds the HMAC/encryption 
key for the session just not explicitly.

> 
>> @@ -587,8 +655,8 @@ tls_crypt_v2_extract_client_key(struct buffer *buf,
>>       ctx->cleanup_key_ctx = true;
>>       ctx->opt.flags |= CO_PACKET_ID_LONG_FORM;
>>       memset(&ctx->opt.key_ctx_bi, 0, sizeof(ctx->opt.key_ctx_bi));
>> -    tls_crypt_v2_load_client_key(&ctx->opt.key_ctx_bi, &client_key, true);
>> -    secure_memzero(&client_key, sizeof(client_key));
>> +    tls_crypt_v2_load_client_key(&ctx->opt.key_ctx_bi,
>> +                                 &ctx->original_tlscrypt_keydata, true);
> 
> Same as above for the server side. Could zero here immediately if
> original_tlscrypt_keydata == NULL
> 

If we zero the key here, it will not be available when we want to 
calculate the secure renegotiation key with xor.

Arne

Patch

diff --git a/src/openvpn/crypto.c b/src/openvpn/crypto.c
index 9e10f64ee..4a8f514cd 100644
--- a/src/openvpn/crypto.c
+++ b/src/openvpn/crypto.c
@@ -1121,7 +1121,8 @@  void
 crypto_read_openvpn_key(const struct key_type *key_type,
                         struct key_ctx_bi *ctx, const char *key_file,
                         bool key_inline, const int key_direction,
-                        const char *key_name, const char *opt_name)
+                        const char *key_name, const char *opt_name,
+                        struct key2 *keydata)
 {
     struct key2 key2;
     struct key_direction_state kds;
@@ -1149,6 +1150,10 @@  crypto_read_openvpn_key(const struct key_type *key_type,
 
     /* initialize key in both directions */
     init_key_ctx_bi(ctx, &key2, key_direction, key_type, key_name);
+    if (keydata)
+    {
+        *keydata = key2;
+    }
     secure_memzero(&key2, sizeof(key2));
 }
 
diff --git a/src/openvpn/crypto.h b/src/openvpn/crypto.h
index 5ea889081..e7177ea43 100644
--- a/src/openvpn/crypto.h
+++ b/src/openvpn/crypto.h
@@ -234,7 +234,14 @@  struct crypto_options
      *   both sending and receiving
      *   directions. */
     struct packet_id packet_id; /**< Current packet ID state for both
-                                 *   sending and receiving directions. */
+                                 *   sending and receiving directions.
+                                 *
+                                 *   This contains the packet id that is
+                                 *   used for replay protection.
+                                 *
+                                 *   The packet id also used as the IV
+                                 *   for AEAD/OFB/CFG ciphers.
+                                 *   */
     struct packet_id_persist *pid_persist;
     /**< Persistent packet ID state for
      *   keeping state between successive
@@ -268,6 +275,10 @@  struct crypto_options
     /**< Bit-flag indicating that explicit exit notifies should be
      * sent via the control channel instead of using an OCC message
      */
+#define CO_USE_SECURE_RENEGOTIATION   (1<<7)
+    /**< Bit-flag indicating that renegotiations are using tls-crypt
+     *   with a TLS-EKM derived key.
+     */
 
     unsigned int flags;         /**< Bit-flags determining behavior of
                                  *   security operation functions. */
@@ -530,7 +541,8 @@  void key2_print(const struct key2 *k,
 void crypto_read_openvpn_key(const struct key_type *key_type,
                              struct key_ctx_bi *ctx, const char *key_file,
                              bool key_inline, const int key_direction,
-                             const char *key_name, const char *opt_name);
+                             const char *key_name, const char *opt_name,
+                             struct key2 *keydata);
 
 /*
  * Inline functions
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index 7ff7d545a..075c8d5a5 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -2783,7 +2783,7 @@  do_init_crypto_static(struct context *c, const unsigned int flags)
                                 options->shared_secret_file,
                                 options->shared_secret_file_inline,
                                 options->key_direction, "Static Key Encryption",
-                                "secret");
+                                "secret", NULL);
     }
     else
     {
@@ -2823,13 +2823,15 @@  do_init_tls_wrap_key(struct context *c)
                                 options->ce.tls_auth_file,
                                 options->ce.tls_auth_file_inline,
                                 options->ce.key_direction,
-                                "Control Channel Authentication", "tls-auth");
+                                "Control Channel Authentication", "tls-auth",
+                                NULL);
     }
 
     /* TLS handshake encryption+authentication (--tls-crypt) */
     if (options->ce.tls_crypt_file)
     {
         tls_crypt_init_key(&c->c1.ks.tls_wrap_key,
+                           &c->c1.ks.original_tlscrypt_keydata,
                            options->ce.tls_crypt_file,
                            options->ce.tls_crypt_file_inline,
                            options->tls_server);
@@ -2847,6 +2849,7 @@  do_init_tls_wrap_key(struct context *c)
         else
         {
             tls_crypt_v2_init_client_key(&c->c1.ks.tls_wrap_key,
+                                         &c->c1.ks.original_tlscrypt_keydata,
                                          &c->c1.ks.tls_crypt_v2_wkc,
                                          options->ce.tls_crypt_v2_file,
                                          options->ce.tls_crypt_v2_file_inline);
@@ -3147,6 +3150,7 @@  do_init_crypto_tls(struct context *c, const unsigned int flags)
         to.tls_wrap.opt.key_ctx_bi = c->c1.ks.tls_wrap_key;
         to.tls_wrap.opt.pid_persist = &c->c1.pid_persist;
         to.tls_wrap.opt.flags |= CO_PACKET_ID_LONG_FORM;
+        to.tls_wrap.original_tlscrypt_keydata = c->c1.ks.original_tlscrypt_keydata;
 
         if (options->ce.tls_crypt_v2_file)
         {
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 1d23c8237..b972c2ff4 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -1803,6 +1803,10 @@  multi_client_set_protocol_options(struct context *c)
     {
         o->imported_protocol_flags |= CO_USE_TLS_KEY_MATERIAL_EXPORT;
     }
+    if (proto & IV_PROTO_SECURE_RENOG)
+    {
+        o->imported_protocol_flags |= CO_USE_SECURE_RENEGOTIATION;
+    }
 #endif
 
     if (proto & IV_PROTO_CC_EXIT_NOTIFY)
diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h
index 00cd652fa..41391f9ce 100644
--- a/src/openvpn/openvpn.h
+++ b/src/openvpn/openvpn.h
@@ -65,6 +65,8 @@  struct key_schedule
     /* optional TLS control channel wrapping */
     struct key_type tls_auth_key_type;
     struct key_ctx_bi tls_wrap_key;
+    /** original tls-crypt preserved to xored into the tls_crypt renog key */
+    struct key2 original_tlscrypt_keydata;
     struct key_ctx tls_crypt_v2_server_key;
     struct buffer tls_crypt_v2_wkc;             /**< Wrapped client key */
     struct key_ctx auth_token_key;
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index 13b254186..cce48b5ff 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -8553,6 +8553,10 @@  add_option(struct options *options,
             {
                 options->imported_protocol_flags |= CO_USE_TLS_KEY_MATERIAL_EXPORT;
             }
+            else if (streq(p[j], "secure-renog"))
+            {
+                options->imported_protocol_flags |= CO_USE_SECURE_RENEGOTIATION;
+            }
 #endif
             else
             {
diff --git a/src/openvpn/push.c b/src/openvpn/push.c
index 989316130..9349c931a 100644
--- a/src/openvpn/push.c
+++ b/src/openvpn/push.c
@@ -665,6 +665,10 @@  prepare_push_reply(struct context *c, struct gc_arena *gc,
         push_option_fmt(gc, push_list, M_USAGE, "key-derivation tls-ekm");
     }
 
+    if (o->imported_protocol_flags & CO_USE_SECURE_RENEGOTIATION)
+    {
+        buf_printf(&proto_flags, " secure-renog");
+    }
 
     if (buf_len(&proto_flags) > 0)
     {
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index 36a236fe3..19bea7b24 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -362,7 +362,7 @@  calc_control_channel_frame_overhead(const struct tls_session *session)
     /* Message packet id */
     overhead += sizeof(packet_id_type);
 
-    if (session->tls_wrap.mode == TLS_WRAP_CRYPT)
+    if (session->tls_wrap.mode == TLS_WRAP_CRYPT || session->tls_wrap_renog.mode == TLS_WRAP_CRYPT)
     {
         overhead += tls_crypt_buf_overhead();
     }
@@ -1146,6 +1146,7 @@  tls_session_init(struct tls_multi *multi, struct tls_session *session)
     session->tls_wrap = session->opt->tls_wrap;
     session->tls_wrap.work = alloc_buf(BUF_SIZE(&session->opt->frame));
 
+
     /* initialize packet ID replay window for --tls-auth */
     packet_id_init(&session->tls_wrap.opt.packet_id,
                    session->opt->replay_window,
@@ -1191,6 +1192,7 @@  static void
 tls_session_free(struct tls_session *session, bool clear)
 {
     tls_wrap_free(&session->tls_wrap);
+    tls_wrap_free(&session->tls_wrap_renog);
 
     for (size_t i = 0; i < KS_SIZE; ++i)
     {
@@ -1708,6 +1710,7 @@  cleanup:
     return ret;
 }
 
+
 bool
 tls_session_update_crypto_params_do_work(struct tls_multi *multi,
                                          struct tls_session *session,
@@ -1755,6 +1758,17 @@  tls_session_update_crypto_params_do_work(struct tls_multi *multi,
         frame_print(frame_fragment, D_MTU_INFO, "Fragmentation MTU parms");
     }
 
+    if (session->key[KS_PRIMARY].key_id == 0
+        && session->opt->crypto_flags & CO_USE_SECURE_RENEGOTIATION)
+    {
+        /* If the secure renegotiation has been negotiated, and we are on the
+         * first session (key_id = 0), generate a tls-crypt key for following
+         * renegotiations */
+        if (!tls_session_generate_secure_renegotition_key(multi, session))
+        {
+            return false;
+        }
+    }
     return tls_session_generate_data_channel_keys(multi, session);
 }
 
@@ -2062,6 +2076,7 @@  push_peer_info(struct buffer *buf, struct tls_session *session)
 
 #ifdef HAVE_EXPORT_KEYING_MATERIAL
         iv_proto |= IV_PROTO_TLS_KEY_EXPORT;
+        iv_proto |= IV_PROTO_SECURE_RENOG;
 #endif
 
         buf_printf(&out, "IV_PROTO=%d\n", iv_proto);
@@ -3225,6 +3240,7 @@  tls_multi_process(struct tls_multi *multi,
                 }
                 else
                 {
+                    /* TODO: secure negotiation fix? */
                     reset_session(multi, session);
                 }
             }
@@ -3674,7 +3690,7 @@  tls_pre_decrypt(struct tls_multi *multi,
 
         /*
          * If --single-session, don't allow any hard-reset connection request
-         * unless it the first packet of the session.
+         * unless it is the first packet of the session.
          */
         if (multi->opt.single_session)
         {
@@ -3684,7 +3700,7 @@  tls_pre_decrypt(struct tls_multi *multi,
             goto error;
         }
 
-        if (!read_control_auth(buf, &session->tls_wrap, from,
+        if (!read_control_auth(buf, tls_session_get_tls_wrap(session, key_id), from,
                                session->opt))
         {
             goto error;
@@ -3739,8 +3755,8 @@  tls_pre_decrypt(struct tls_multi *multi,
          */
         if (op == P_CONTROL_SOFT_RESET_V1 && ks->state >= S_GENERATED_KEYS)
         {
-            if (!read_control_auth(buf, &session->tls_wrap, from,
-                                   session->opt))
+            if (!read_control_auth(buf, tls_session_get_tls_wrap(session, key_id),
+                                   from, session->opt))
             {
                 goto error;
             }
@@ -3761,8 +3777,8 @@  tls_pre_decrypt(struct tls_multi *multi,
                 do_burst = true;
             }
 
-            if (!read_control_auth(buf, &session->tls_wrap, from,
-                                   session->opt))
+            if (!read_control_auth(buf, tls_session_get_tls_wrap(session, key_id),
+                                   from, session->opt))
             {
                 goto error;
             }
diff --git a/src/openvpn/ssl.h b/src/openvpn/ssl.h
index 422e957ae..33e45a2da 100644
--- a/src/openvpn/ssl.h
+++ b/src/openvpn/ssl.h
@@ -43,6 +43,7 @@ 
 #include "ssl_common.h"
 #include "ssl_backend.h"
 #include "ssl_pkt.h"
+#include "tls_crypt.h"
 
 /* Used in the TLS PRF function */
 #define KEY_EXPANSION_ID "OpenVPN"
@@ -103,6 +104,9 @@ 
 /** Support for AUTH_FAIL,TEMP messages */
 #define IV_PROTO_AUTH_FAIL_TEMP  (1<<8)
 
+/** Support to secure renogiations with TLS-EKM dervied tls-crypt key */
+#define IV_PROTO_SECURE_RENOG    (1<<9)
+
 /* Default field in X509 to be username */
 #define X509_USERNAME_FIELD_DEFAULT "CN"
 
@@ -472,6 +476,7 @@  tls_wrap_free(struct tls_wrap_ctx *tls_wrap)
 
     free_buf(&tls_wrap->tls_crypt_v2_metadata);
     free_buf(&tls_wrap->work);
+    secure_memzero(&tls_wrap->original_tlscrypt_keydata, sizeof(tls_wrap->original_tlscrypt_keydata));
 }
 
 static inline bool
diff --git a/src/openvpn/ssl_backend.h b/src/openvpn/ssl_backend.h
index 215425d41..7f72dd486 100644
--- a/src/openvpn/ssl_backend.h
+++ b/src/openvpn/ssl_backend.h
@@ -391,6 +391,7 @@  void backend_tls_ctx_reload_crl(struct tls_root_ctx *ssl_ctx,
 
 #define EXPORT_KEY_DATA_LABEL       "EXPORTER-OpenVPN-datakeys"
 #define EXPORT_P2P_PEERID_LABEL     "EXPORTER-OpenVPN-p2p-peerid"
+#define EXPORT_SECURE_RENOG_LABEL   "EXPORTER-OpenVPN-secure-renegotiation"
 /**
  * Keying Material Exporters [RFC 5705] allows additional keying material to be
  * derived from existing TLS channel. This exported keying material can then be
diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h
index 9dcd447cb..6f7d7cae0 100644
--- a/src/openvpn/ssl_common.h
+++ b/src/openvpn/ssl_common.h
@@ -275,6 +275,8 @@  struct tls_wrap_ctx
     struct buffer tls_crypt_v2_metadata;     /**< Received from client */
     bool cleanup_key_ctx;                    /**< opt.key_ctx_bi is owned by
                                               *   this context */
+    struct key2 original_tlscrypt_keydata;
+    /**< original key data to be xored in to the  key for secure renegotiation */
 };
 
 /*
@@ -466,6 +468,10 @@  struct tls_session
     /* authenticate control packets */
     struct tls_wrap_ctx tls_wrap;
 
+    /* Specific tls-crypt for renegotiations, if this is valid,
+     * tls_wrap_renog.mode is TLS_WRAP_CRYPT, otherwise ignore it */
+    struct tls_wrap_ctx tls_wrap_renog;
+
     int initial_opcode;         /* our initial P_ opcode */
     struct session_id session_id; /* our random session ID */
 
diff --git a/src/openvpn/ssl_ncp.c b/src/openvpn/ssl_ncp.c
index fe8491925..013021d6d 100644
--- a/src/openvpn/ssl_ncp.c
+++ b/src/openvpn/ssl_ncp.c
@@ -453,6 +453,10 @@  p2p_ncp_set_options(struct tls_multi *multi, struct tls_session *session)
 
         }
     }
+    if (iv_proto_peer & IV_PROTO_SECURE_RENOG)
+    {
+        session->opt->crypto_flags |= CO_USE_SECURE_RENEGOTIATION;
+    }
 #endif /* if defined(HAVE_EXPORT_KEYING_MATERIAL) */
 }
 
diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
index 9b180cbf4..656a14923 100644
--- a/src/openvpn/ssl_pkt.c
+++ b/src/openvpn/ssl_pkt.c
@@ -193,7 +193,7 @@  write_control_auth(struct tls_session *session,
 
     msg(D_TLS_DEBUG, "%s(): %s", __func__, packet_opcode_name(opcode));
 
-    tls_wrap_control(&session->tls_wrap, header, buf, &session->session_id);
+    tls_wrap_control(tls_session_get_tls_wrap(session, ks->key_id), header, buf, &session->session_id);
 
     *to_link_addr = &ks->remote_addr;
 }
diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
index 9bb3ca958..3cef3ae15 100644
--- a/src/openvpn/ssl_pkt.h
+++ b/src/openvpn/ssl_pkt.h
@@ -273,6 +273,27 @@  packet_opcode_name(int op)
     }
 }
 
+/**
+ * Determines if the current session should use the renegotiation tls wrap
+ * struct instead the normal one and returns it
+ *
+ * @param session
+ * @param key_id    key_id of the received/or to be send packet
+ * @return
+ */
+static inline struct tls_wrap_ctx *
+tls_session_get_tls_wrap(struct tls_session *session, int key_id)
+{
+    if (key_id > 0 && session->tls_wrap_renog.mode == TLS_WRAP_CRYPT)
+    {
+        return &session->tls_wrap_renog;
+    }
+    else
+    {
+        return &session->tls_wrap;
+    }
+}
+
 /* initial packet id (instead of 0) that indicates that the peer supports
  * early protocol negotiation. This will make the packet id turn a bit faster
  * but the network time part of the packet id takes care of that. And
diff --git a/src/openvpn/tls_crypt.c b/src/openvpn/tls_crypt.c
index 2fc791119..cf3e68e5f 100644
--- a/src/openvpn/tls_crypt.c
+++ b/src/openvpn/tls_crypt.c
@@ -60,8 +60,8 @@  tls_crypt_buf_overhead(void)
 }
 
 void
-tls_crypt_init_key(struct key_ctx_bi *key, const char *key_file,
-                   bool key_inline, bool tls_server)
+tls_crypt_init_key(struct key_ctx_bi *key, struct key2 *keydata,
+                   const char *key_file, bool key_inline, bool tls_server)
 {
     const int key_direction = tls_server ?
                               KEY_DIRECTION_NORMAL : KEY_DIRECTION_INVERSE;
@@ -71,9 +71,77 @@  tls_crypt_init_key(struct key_ctx_bi *key, const char *key_file,
         msg(M_FATAL, "ERROR: --tls-crypt not supported");
     }
     crypto_read_openvpn_key(&kt, key, key_file, key_inline, key_direction,
-                            "Control Channel Encryption", "tls-crypt");
+                            "Control Channel Encryption", "tls-crypt", keydata);
 }
 
+/**
+ * Will produce dest = dest XOR data
+ */
+static void
+xor_key2_key(struct key2 *dest, const struct key2 *data)
+{
+    ASSERT(dest->n == 2 && data->n == 2);
+    for (int k = 0; k < 2; k++)
+    {
+        for (int j = 0; j < MAX_CIPHER_KEY_LENGTH; j++)
+        {
+            dest->keys[k].cipher[j] = dest->keys[k].cipher[j] ^ data->keys[k].cipher[j];
+        }
+
+        for (int j = 0; j < MAX_HMAC_KEY_LENGTH; j++)
+        {
+            dest->keys[k].hmac[j] = dest->keys[k].hmac[j] ^ data->keys[k].hmac[j];
+        }
+
+    }
+}
+
+bool
+tls_session_generate_secure_renegotition_key(struct tls_multi *multi,
+                                             struct tls_session *session)
+{
+    session->tls_wrap_renog.opt = session->tls_wrap.opt;
+    session->tls_wrap_renog.mode = TLS_WRAP_CRYPT;
+    session->tls_wrap_renog.cleanup_key_ctx = true;
+    session->tls_wrap_renog.work = alloc_buf(BUF_SIZE(&session->opt->frame));
+    session->tls_wrap_renog.opt.pid_persist = NULL;
+
+    packet_id_init(&session->tls_wrap_renog.opt.packet_id,
+                   session->opt->replay_window,
+                   session->opt->replay_time,
+                   "TLS_WRAP_RENOG", session->key_id);
+
+
+    struct key2 rengokeys;
+    if (!key_state_export_keying_material(session, EXPORT_SECURE_RENOG_LABEL,
+                                          strlen(EXPORT_SECURE_RENOG_LABEL),
+                                          rengokeys.keys, sizeof(rengokeys.keys)))
+    {
+        return false;
+    }
+    rengokeys.n = 2;
+
+    if (session->tls_wrap.mode == TLS_WRAP_CRYPT)
+    {
+        xor_key2_key(&rengokeys, &session->tls_wrap.original_tlscrypt_keydata);
+    }
+
+    const int key_direction = session->opt->server ?
+                              KEY_DIRECTION_NORMAL : KEY_DIRECTION_INVERSE;
+
+    struct key_direction_state kds;
+    key_direction_state_init(&kds, key_direction);
+
+    struct key_type kt = tls_crypt_kt();
+
+    init_key_ctx_bi(&session->tls_wrap_renog.opt.key_ctx_bi, &rengokeys, key_direction,
+                    &kt, "secure renegotiation");
+    secure_memzero(&rengokeys, sizeof(rengokeys));
+
+    return true;
+}
+
+
 bool
 tls_crypt_wrap(const struct buffer *src, struct buffer *dst,
                struct crypto_options *opt)
@@ -266,8 +334,9 @@  tls_crypt_v2_load_client_key(struct key_ctx_bi *key, const struct key2 *key2,
 }
 
 void
-tls_crypt_v2_init_client_key(struct key_ctx_bi *key, struct buffer *wkc_buf,
-                             const char *key_file, bool key_inline)
+tls_crypt_v2_init_client_key(struct key_ctx_bi *key, struct key2 *original_key,
+                             struct buffer *wkc_buf, const char *key_file,
+                             bool key_inline)
 {
     struct buffer client_key = alloc_buf(TLS_CRYPT_V2_CLIENT_KEY_LEN
                                          + TLS_CRYPT_V2_MAX_WKC_LEN);
@@ -285,7 +354,7 @@  tls_crypt_v2_init_client_key(struct key_ctx_bi *key, struct buffer *wkc_buf,
     }
 
     tls_crypt_v2_load_client_key(key, &key2, false);
-    secure_memzero(&key2, sizeof(key2));
+    *original_key = key2;
 
     *wkc_buf = client_key;
 }
@@ -570,15 +639,14 @@  tls_crypt_v2_extract_client_key(struct buffer *buf,
         return false;
     }
 
-    struct key2 client_key = { 0 };
     ctx->tls_crypt_v2_metadata = alloc_buf(TLS_CRYPT_V2_MAX_METADATA_LEN);
-    if (!tls_crypt_v2_unwrap_client_key(&client_key,
+    if (!tls_crypt_v2_unwrap_client_key(&ctx->original_tlscrypt_keydata,
                                         &ctx->tls_crypt_v2_metadata,
                                         wrapped_client_key,
                                         &ctx->tls_crypt_v2_server_key))
     {
         msg(D_TLS_ERRORS, "Can not unwrap tls-crypt-v2 client key");
-        secure_memzero(&client_key, sizeof(client_key));
+        secure_memzero(&ctx->original_tlscrypt_keydata, sizeof(ctx->original_tlscrypt_keydata));
         return false;
     }
 
@@ -587,8 +655,8 @@  tls_crypt_v2_extract_client_key(struct buffer *buf,
     ctx->cleanup_key_ctx = true;
     ctx->opt.flags |= CO_PACKET_ID_LONG_FORM;
     memset(&ctx->opt.key_ctx_bi, 0, sizeof(ctx->opt.key_ctx_bi));
-    tls_crypt_v2_load_client_key(&ctx->opt.key_ctx_bi, &client_key, true);
-    secure_memzero(&client_key, sizeof(client_key));
+    tls_crypt_v2_load_client_key(&ctx->opt.key_ctx_bi,
+                                 &ctx->original_tlscrypt_keydata, true);
 
     /* Remove client key from buffer so tls-crypt code can unwrap message */
     ASSERT(buf_inc_len(buf, -(BLEN(&wrapped_client_key))));
@@ -688,8 +756,9 @@  tls_crypt_v2_write_client_key_file(const char *filename,
     /* Sanity check: load client key (as "client") */
     struct key_ctx_bi test_client_key;
     struct buffer test_wrapped_client_key;
+    struct key2 keydata;
     msg(D_GENKEY, "Testing client-side key loading...");
-    tls_crypt_v2_init_client_key(&test_client_key, &test_wrapped_client_key,
+    tls_crypt_v2_init_client_key(&test_client_key, &keydata, &test_wrapped_client_key,
                                  client_file, client_inline);
     free_key_ctx_bi(&test_client_key);
 
diff --git a/src/openvpn/tls_crypt.h b/src/openvpn/tls_crypt.h
index 928ff5475..cf30768ad 100644
--- a/src/openvpn/tls_crypt.h
+++ b/src/openvpn/tls_crypt.h
@@ -110,12 +110,24 @@ 
  * @param key           The key context to initialize
  * @param key_file      The file to read the key from or the key itself if
  *                      key_inline is true.
+ * @param keydata       The keydata used to create key will be written here.
  * @param key_inline    True if key_file contains an inline key, False
  *                      otherwise.
  * @param tls_server    Must be set to true is this is a TLS server instance.
  */
-void tls_crypt_init_key(struct key_ctx_bi *key, const char *key_file,
-                        bool key_inline, bool tls_server);
+void tls_crypt_init_key(struct key_ctx_bi *key, struct key2 *keydata,
+                        const char *key_file, bool key_inline, bool tls_server);
+
+/**
+ * Generates a TLS Crypt to be used in the secure renegotiation using the
+ * TLS EKM exporter function.
+ * @param multi     multi session struct
+ * @param session   session that will be used for the TLS EKM exporter
+ * @return          true iff generating the key was successful
+ */
+bool
+tls_session_generate_secure_renegotition_key(struct tls_multi *multi,
+                                             struct tls_session *session);
 
 /**
  * Returns the maximum overhead (in bytes) added to the destination buffer by
@@ -171,6 +183,8 @@  void tls_crypt_v2_init_server_key(struct key_ctx *key_ctx, bool encrypt,
  *
  * @param key               Key structure to be initialized with the client
  *                          key.
+ * @param original_key      contains the key data, that has been used to
+ *                          initialise the key parameter
  * @param wrapped_key_buf   Returns buffer containing the wrapped key that will
  *                          be sent to the server when connecting.  Caller must
  *                          free this buffer when no longer needed.
@@ -180,6 +194,7 @@  void tls_crypt_v2_init_server_key(struct key_ctx *key_ctx, bool encrypt,
  *                          otherwise.
  */
 void tls_crypt_v2_init_client_key(struct key_ctx_bi *key,
+                                  struct key2 *original_key,
                                   struct buffer *wrapped_key_buf,
                                   const char *key_file, bool key_inline);
 
diff --git a/tests/unit_tests/openvpn/test_pkt.c b/tests/unit_tests/openvpn/test_pkt.c
index 2d771e301..d05f981f7 100644
--- a/tests/unit_tests/openvpn/test_pkt.c
+++ b/tests/unit_tests/openvpn/test_pkt.c
@@ -55,6 +55,16 @@  parse_line(const char *line, char **p, const int n, const char *file,
     return 0;
 }
 
+/* Define this function here as dummy since including the ssl_*.c files
+ * leads to having to include even more unrelated code */
+bool
+key_state_export_keying_material(struct tls_session *session,
+                                 const char *label, size_t label_size,
+                                 void *ekm, size_t ekm_size)
+{
+    ASSERT(0);
+}
+
 const char *
 print_link_socket_actual(const struct link_socket_actual *act, struct gc_arena *gc)
 {
@@ -183,7 +193,8 @@  init_tas_auth(int key_direction)
 
     crypto_read_openvpn_key(&tls_crypt_kt, &tas.tls_wrap.opt.key_ctx_bi,
                             static_key, true, key_direction,
-                            "Control Channel Authentication", "tls-auth");
+                            "Control Channel Authentication", "tls-auth",
+                            NULL);
 
     return tas;
 }
@@ -195,7 +206,9 @@  init_tas_crypt(bool server)
     tas.tls_wrap.mode = TLS_WRAP_CRYPT;
     tas.tls_wrap.opt.flags |= (CO_IGNORE_PACKET_ID|CO_PACKET_ID_LONG_FORM);
 
-    tls_crypt_init_key(&tas.tls_wrap.opt.key_ctx_bi, static_key, true, server);
+    tls_crypt_init_key(&tas.tls_wrap.opt.key_ctx_bi,
+                       &tas.tls_wrap.original_tlscrypt_keydata, static_key,
+                       true, server);
 
     return tas;
 }
diff --git a/tests/unit_tests/openvpn/test_tls_crypt.c b/tests/unit_tests/openvpn/test_tls_crypt.c
index 82bb0a266..1a46478d7 100644
--- a/tests/unit_tests/openvpn/test_tls_crypt.c
+++ b/tests/unit_tests/openvpn/test_tls_crypt.c
@@ -40,6 +40,18 @@ 
 
 #include "mock_msg.h"
 
+/* Define this function here as dummy since including the ssl_*.c files
+ * leads to having to include even more unrelated code */
+bool
+key_state_export_keying_material(struct tls_session *session,
+                                 const char *label, size_t label_size,
+                                 void *ekm, size_t ekm_size)
+{
+    memset(ekm, 0xba, ekm_size);
+    return true;
+}
+
+
 #define TESTBUF_SIZE            128
 
 /* Defines for use in the tests and the mock parse_line() */
@@ -141,6 +153,7 @@  struct test_tls_crypt_context {
     struct buffer unwrapped;
 };
 
+
 static int
 test_tls_crypt_setup(void **state)
 {
@@ -218,6 +231,75 @@  tls_crypt_loopback(void **state)
                         BLEN(&ctx->source));
 }
 
+
+/**
+ * Test generating secure renegotiation key
+ */
+static void
+test_tls_crypt_secure_renog_key(void **state)
+{
+    struct test_tls_crypt_context *ctx =
+        (struct test_tls_crypt_context *)*state;
+
+    struct gc_arena gc = gc_new();
+
+    struct tls_multi multi = { 0 };
+    struct tls_session session = { 0 };
+
+    struct tls_options tls_opt = { 0 };
+    tls_opt.replay_window = 32;
+    tls_opt.replay_time = 60;
+    tls_opt.frame.buf.payload_size = 512;
+    session.opt = &tls_opt;
+
+    tls_session_generate_secure_renegotition_key(&multi, &session);
+
+    struct tls_wrap_ctx *rctx = &session.tls_wrap_renog;
+
+    tls_crypt_wrap(&ctx->source, &rctx->work, &rctx->opt);
+    assert_int_equal(buf_len(&ctx->source) + 40, buf_len(&rctx->work));
+
+    uint8_t expected_ciphertext[] = {
+        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xe3, 0x19, 0x27, 0x7f, 0x1c, 0x8d, 0x6e, 0x6a,
+        0x77, 0x96, 0xa8, 0x55, 0x33, 0x7b, 0x9c, 0xfb, 0x56, 0xe1, 0xf1, 0x3a, 0x87, 0x0e, 0x66, 0x47,
+        0xdf, 0xa1, 0x95, 0xc9, 0x2c, 0x17, 0xa0, 0x15, 0xba, 0x49, 0x67, 0xa1, 0x1d, 0x55, 0xea, 0x1a,
+        0x06, 0xa7
+    };
+    assert_memory_equal(BPTR(&rctx->work), expected_ciphertext, buf_len(&rctx->work));
+    tls_wrap_free(&session.tls_wrap_renog);
+
+    /* Use previous tls-crypt key as 0x00, with xor we should have the same key
+     * and expect the same result */
+    session.tls_wrap.mode = TLS_WRAP_CRYPT;
+    memset(&session.tls_wrap.original_tlscrypt_keydata.keys, 0x00, sizeof(session.tls_wrap.original_tlscrypt_keydata.keys));
+    session.tls_wrap.original_tlscrypt_keydata.n = 2;
+
+    tls_session_generate_secure_renegotition_key(&multi, &session);
+    tls_crypt_wrap(&ctx->source, &rctx->work, &rctx->opt);
+    assert_int_equal(buf_len(&ctx->source) + 40, buf_len(&rctx->work));
+
+    assert_memory_equal(BPTR(&rctx->work), expected_ciphertext, buf_len(&rctx->work));
+    tls_wrap_free(&session.tls_wrap_renog);
+
+    /* XOR should not force a different key */
+    memset(&session.tls_wrap.original_tlscrypt_keydata.keys, 0x42, sizeof(session.tls_wrap.original_tlscrypt_keydata.keys));
+    tls_session_generate_secure_renegotition_key(&multi, &session);
+
+    tls_crypt_wrap(&ctx->source, &rctx->work, &rctx->opt);
+    assert_int_equal(buf_len(&ctx->source) + 40, buf_len(&rctx->work));
+
+    /* packet id at the start should be equal */
+    assert_memory_equal(BPTR(&rctx->work), expected_ciphertext, 8);
+
+    /* Skip packet id */
+    buf_advance(&rctx->work, 8);
+    assert_memory_not_equal(BPTR(&rctx->work), expected_ciphertext, buf_len(&rctx->work));
+    tls_wrap_free(&session.tls_wrap_renog);
+
+
+    gc_free(&gc);
+}
+
 /**
  * Check that zero-byte messages are successfully wrapped-and-unwrapped.
  */
@@ -632,6 +714,9 @@  main(void)
         cmocka_unit_test_setup_teardown(tls_crypt_v2_wrap_unwrap_dst_too_small,
                                         test_tls_crypt_v2_setup,
                                         test_tls_crypt_v2_teardown),
+        cmocka_unit_test_setup_teardown(test_tls_crypt_secure_renog_key,
+                                        test_tls_crypt_setup,
+                                        test_tls_crypt_teardown),
         cmocka_unit_test(test_tls_crypt_v2_write_server_key_file),
         cmocka_unit_test(test_tls_crypt_v2_write_client_key_file),
         cmocka_unit_test(test_tls_crypt_v2_write_client_key_file_metadata),