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

Message ID 20221212112746.1719605-1-arne@rfc2549.org
State Superseded
Headers show
Series [Openvpn-devel,v4] Introduce dynamic tls-crypt for secure soft_reset/session renegotiation | expand

Commit Message

Arne Schwabe Dec. 12, 2022, 11:27 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. This also add an additional protection layer for
renegotiations to be taken over by an illegimate client, binding the
renegotiations tightly to the original session. Especially when 2FA, webauth
or similar authentication is used, many third party setup ignore the need
to secure renegotiation with an auth-token.

Since one of tls-crypt/tls-crypt-v2 purposes 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. We also do not use hashing or anything else on the original key
before XOR to avoid any potential of a structure in the key or something
else that might weaken post-quantum use cases.

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. Here an attacker could theorectically send a
faked/replayed SOFT_RESET_V1 and firste packet containing the TLS client
hello. If this happens, 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 (and following
packets) 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.

This commit delays the purging of the original tls-crypt key data from
directly after passing it to crypto library to tls_wrap_free. We do this
to allow us mixing the new exported key with the original key. Since need
to be able to generate the secure renegotiation key, deleting
the key after generating a secure renegotiation key is not an option.
Even when the client does not support secure renegotiation, deleting the
key is not a good as the reconnecting client or (especially in p2p mode
with float) another client does the reconnect, we might need to generate a
secure renegotiation key. Delaying the deletion of the key has also little
effect as the key is still present in the OpenSSL/mbed TLS structures in
the tls_wrap structure, so only the number the keys is in memory would
be reduced.

Patch v2: fix spellings of reneg and renegotiations.
Patch v3: expand comment to original_tlscrypt_keydata and commit message,
          add Changes.rst
Patch v4: improve commit message, Changes.rst

Signed-off-by: Arne Schwabe <arne@rfc2549.org>
---
 Changes.rst                               |  6 ++
 src/openvpn/auth_token.h                  |  2 +-
 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                        |  5 ++
 src/openvpn/ssl.c                         | 25 ++++--
 src/openvpn/ssl.h                         |  5 ++
 src/openvpn/ssl_backend.h                 |  1 +
 src/openvpn/ssl_common.h                  | 13 ++++
 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 +++++++++++++++++++++
 20 files changed, 310 insertions(+), 29 deletions(-)

Comments

Frank Lichtenheld Jan. 9, 2023, 3:01 p.m. UTC | #1
On Mon, Dec 12, 2022 at 12:27:45PM +0100, Arne Schwabe wrote:
> 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

"peers"

> material) and will enforce using that key and tls-crypt for all
> renegotiations. This also add an additional protection layer for

General question about this feature:
We trigger using this key on key_id > 0, so if I understand the code
correctly, it will be used first when we want to renegotiate.
But will it then continued to be used? What exactly is the state after
the successful renegotiation?

> renegotiations to be taken over by an illegimate client, binding the
> renegotiations tightly to the original session. Especially when 2FA, webauth
> or similar authentication is used, many third party setup ignore the need
> to secure renegotiation with an auth-token.
> 
> Since one of tls-crypt/tls-crypt-v2 purposes 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

"if A or if B" is I think easier to read instead "if (A or B)" (in natural
language), so I would recommend "if TLS EKM".

> 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. We also do not use hashing or anything else on the original key
> before XOR to avoid any potential of a structure in the key or something
> else that might weaken post-quantum use cases.
> 
> OpenVPN 2.x reserves the TM_ACTIVE session for renegotians. When a

"renegotiations"

> SOFT_RESET_V1 packet is received, the active TLS session is moved from
> KS_PRIMARY to KS_SECONDARY. Here an attacker could theorectically send a
> faked/replayed SOFT_RESET_V1 and firste packet containing the TLS client

"first"

> hello. If this happens, 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 (and following

"blocks"

> packets) as replay and fake packets will not have a matching
> authentication/encryption are discarded.

"and will be 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.

"legitimate"

> This commit delays the purging of the original tls-crypt key data from
> directly after passing it to crypto library to tls_wrap_free. We do this
> to allow us mixing the new exported key with the original key. Since need

"we need"

> to be able to generate the secure renegotiation key, deleting
> the key after generating a secure renegotiation key is not an option.
> Even when the client does not support secure renegotiation, deleting the
> key is not a good as the reconnecting client or (especially in p2p mode

"a good option" ?

"as the" -> "since when the"

> with float) another client does the reconnect, we might need to generate a
> secure renegotiation key. Delaying the deletion of the key has also little
> effect as the key is still present in the OpenSSL/mbed TLS structures in
> the tls_wrap structure, so only the number the keys is in memory would

"only the number of times the key" ?

> be reduced.

[...]

> diff --git a/Changes.rst b/Changes.rst
> index fe91ece2e..d0b8ac21a 100644
> --- a/Changes.rst
> +++ b/Changes.rst
> @@ -111,6 +111,12 @@ Tun MTU can be pushed
>      directive ``--tun-mtu-max`` has been introduced to increase the maximum
>      pushable MTU size (defaults to 1600).
>  
> +Secure renegotiation
> +    When both peer are OpenVPN 2.6.0+, OpenVPN will use secure renegotiation

"peers"

> +    using a dynamically created tls-crypt key. This ensure that only the
> +    previously authenticated peer can do trigger renegotiation and complete
> +    renegotiations.
> +
>  Improved control channel packet size control (``max-packet-size``)
>      The size of control channel is no longer tied to
>      ``--link-mtu``/``--tun-mtu`` and can be set using ``--max-packet-size``.
> diff --git a/src/openvpn/auth_token.h b/src/openvpn/auth_token.h
> index 5e23d8c44..0a847ce56 100644
> --- a/src/openvpn/auth_token.h
> +++ b/src/openvpn/auth_token.h
> @@ -43,7 +43,7 @@
>   *
>   * The second timestamp is the time the token was renewed/regenerated and is used
>   * to determine if this token has been renewed in the acceptable time range
> - * (2 * renogiation timeout)
> + * (2 * renegoiation timeout)

"renegotiation"

>   *
>   * The session id is a random string of 12 byte (or 16 in base64) that is not
>   * used by OpenVPN itself but kept intact so that external logging/management
[...]
> diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h
> index c543cbf60..9df613179 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 renegotiation key */

"original tls-crypt 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/ssl.h b/src/openvpn/ssl.h
> index 55c672d44..1550fc79d 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 renegoiations with TLS-EKM dervied tls-crypt key */

"renegotiations"

"derived"

> +#define IV_PROTO_SECURE_RENEG    (1<<9)
> +
>  /* Default field in X509 to be username */
>  #define X509_USERNAME_FIELD_DEFAULT "CN"
>  
> @@ -476,6 +480,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_pkt.h b/src/openvpn/ssl_pkt.h
> index 9bb3ca958..dc3a53bf6 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

Since this seems to be where the actual magic happens, maybe we could
have a slightly more verbose comment here for people not as familiar
with the OpenVPN state machine?

Will the renegotiation key be refreshed as part of the renegotiation or
do all future renegotiations the same key?

> + *
> + * @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_reneg.mode == TLS_WRAP_CRYPT)
> +    {
> +        return &session->tls_wrap_reneg;
> +    }
> +    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.h b/src/openvpn/tls_crypt.h
> index 928ff5475..21b012ce4 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 Crypt key" ?

> + * 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_renegotiation_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

I think there should be no comma here.

> + *                          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);
>  
[...]

Regards,
Arne Schwabe Jan. 9, 2023, 4:36 p.m. UTC | #2
Am 09.01.23 um 16:01 schrieb Frank Lichtenheld:
> On Mon, Dec 12, 2022 at 12:27:45PM +0100, Arne Schwabe wrote:
>> 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
> 
> "peers"
> 
>> material) and will enforce using that key and tls-crypt for all
>> renegotiations. This also add an additional protection layer for
> 
> General question about this feature:
> We trigger using this key on key_id > 0, so if I understand the code
> correctly, it will be used first when we want to renegotiate.
> But will it then continued to be used? What exactly is the state after
> the successful renegotiation?

That is one of this hidden logic things of OpenVPN again. We ensure that 
the keyid will go to 1 instead 0 on key rollover, so only initial keyids 
are 0. So key-ids for renegotiations will be 1 to 7 and then rollover to 
1 instead of 0.


     /*
      * key_id increments to KEY_ID_MASK then recycles back to 1.
      * This way you know that if key_id is 0, it is the first key.
      */
     ++session->key_id;
     session->key_id &= P_KEY_ID_MASK;
     if (!session->key_id)
     {
         session->key_id = 1;
     }

>> +/**
>> + * Determines if the current session should use the renegotiation tls wrap
>> + * struct instead the normal one and returns it
> 
> Since this seems to be where the actual magic happens, maybe we could
> have a slightly more verbose comment here for people not as familiar
> with the OpenVPN state machine?
> 
> Will the renegotiation key be refreshed as part of the renegotiation or
> do all future renegotiations the same key?

Same key. The idea is to have a tls-crypt key that is only known between 
the two peers and not by anyone else. Rotating it here gives no benfit, 
so we keep the same keep to simplify the code.

I added the fact that we only one key in the description of 
thetls_session_generate_secure_renegotiation_key .

Arne
Frank Lichtenheld Jan. 10, 2023, 12:42 p.m. UTC | #3
On Mon, Jan 09, 2023 at 05:36:06PM +0100, Arne Schwabe wrote:
> Am 09.01.23 um 16:01 schrieb Frank Lichtenheld:
> > On Mon, Dec 12, 2022 at 12:27:45PM +0100, Arne Schwabe wrote:
> > > 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
> > 
> > "peers"
> > 
> > > material) and will enforce using that key and tls-crypt for all
> > > renegotiations. This also add an additional protection layer for
> > 
> > General question about this feature:
> > We trigger using this key on key_id > 0, so if I understand the code
> > correctly, it will be used first when we want to renegotiate.
> > But will it then continued to be used? What exactly is the state after
> > the successful renegotiation?
> 
> That is one of this hidden logic things of OpenVPN again. We ensure that the
> keyid will go to 1 instead 0 on key rollover, so only initial keyids are 0.
> So key-ids for renegotiations will be 1 to 7 and then rollover to 1 instead
> of 0.
> 
> 
>     /*
>      * key_id increments to KEY_ID_MASK then recycles back to 1.
>      * This way you know that if key_id is 0, it is the first key.
>      */
>     ++session->key_id;
>     session->key_id &= P_KEY_ID_MASK;
>     if (!session->key_id)
>     {
>         session->key_id = 1;
>     }

Okay, so it does roughly what I assumed it does. But strictly speaking this
is not a renegotiation key then. Once the first renegotiation happens the key
will be used for ALL control channel packets, is that correct?

So how does that avoid the replay attack? I mean it obviously avoids it for the
first renegotiation, but could you replay the first renegotiation afterwards?
Or does that not work due to increasing key_id/packet_id?

Regards,
Arne Schwabe Jan. 11, 2023, 10:40 a.m. UTC | #4
>>      /*
>>       * key_id increments to KEY_ID_MASK then recycles back to 1.
>>       * This way you know that if key_id is 0, it is the first key.
>>       */
>>      ++session->key_id;
>>      session->key_id &= P_KEY_ID_MASK;
>>      if (!session->key_id)
>>      {
>>          session->key_id = 1;
>>      }
> 
> Okay, so it does roughly what I assumed it does. But strictly speaking this
> is not a renegotiation key then. Once the first renegotiation happens the key
> will be used for ALL control channel packets, is that correct?
> 
> So how does that avoid the replay attack? I mean it obviously avoids it for the
> first renegotiation, but could you replay the first renegotiation afterwards?
> Or does that not work due to increasing key_id/packet_id?

We do initialise the replay protection on the tls-crypt keys. So an 
attacker can only replay packets that are already known to both peer and 
they will drop them. While you can get the same level of protection with 
tls-crypt (without dynamic key) using --replay-persist option, I don't 
think I have seen this option ever being used.

Arne

Patch

diff --git a/Changes.rst b/Changes.rst
index fe91ece2e..d0b8ac21a 100644
--- a/Changes.rst
+++ b/Changes.rst
@@ -111,6 +111,12 @@  Tun MTU can be pushed
     directive ``--tun-mtu-max`` has been introduced to increase the maximum
     pushable MTU size (defaults to 1600).
 
+Secure renegotiation
+    When both peer are OpenVPN 2.6.0+, OpenVPN will use secure renegotiation
+    using a dynamically created tls-crypt key. This ensure that only the
+    previously authenticated peer can do trigger renegotiation and complete
+    renegotiations.
+
 Improved control channel packet size control (``max-packet-size``)
     The size of control channel is no longer tied to
     ``--link-mtu``/``--tun-mtu`` and can be set using ``--max-packet-size``.
diff --git a/src/openvpn/auth_token.h b/src/openvpn/auth_token.h
index 5e23d8c44..0a847ce56 100644
--- a/src/openvpn/auth_token.h
+++ b/src/openvpn/auth_token.h
@@ -43,7 +43,7 @@ 
  *
  * The second timestamp is the time the token was renewed/regenerated and is used
  * to determine if this token has been renewed in the acceptable time range
- * (2 * renogiation timeout)
+ * (2 * renegoiation timeout)
  *
  * The session id is a random string of 12 byte (or 16 in base64) that is not
  * used by OpenVPN itself but kept intact so that external logging/management
diff --git a/src/openvpn/crypto.c b/src/openvpn/crypto.c
index d266716c7..d7e882ae0 100644
--- a/src/openvpn/crypto.c
+++ b/src/openvpn/crypto.c
@@ -1123,7 +1123,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;
@@ -1151,6 +1152,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 74b380327..d134a5d95 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -2807,7 +2807,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
     {
@@ -2847,13 +2847,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);
@@ -2871,6 +2873,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);
@@ -3188,6 +3191,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 0a23c2bcf..da037b983 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_RENEG)
+    {
+        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 c543cbf60..9df613179 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 renegotiation 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 e48e4b459..6023ff0a2 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -8611,6 +8611,10 @@  add_option(struct options *options,
             {
                 options->imported_protocol_flags |= CO_USE_TLS_KEY_MATERIAL_EXPORT;
             }
+            else if (streq(p[j], "secure-reneg"))
+            {
+                options->imported_protocol_flags |= CO_USE_SECURE_RENEGOTIATION;
+            }
 #endif
             else
             {
diff --git a/src/openvpn/push.c b/src/openvpn/push.c
index f8c747d44..2d131bb49 100644
--- a/src/openvpn/push.c
+++ b/src/openvpn/push.c
@@ -668,6 +668,11 @@  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-reneg");
+    }
+
     if (buf_len(&proto_flags) > 0)
     {
         push_option_fmt(gc, push_list, M_USAGE, "protocol-flags%s", buf_str(&proto_flags));
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index 9e5480528..734f396d0 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -1202,6 +1202,7 @@  static void
 tls_session_free(struct tls_session *session, bool clear)
 {
     tls_wrap_free(&session->tls_wrap);
+    tls_wrap_free(&session->tls_wrap_reneg);
 
     for (size_t i = 0; i < KS_SIZE; ++i)
     {
@@ -1767,6 +1768,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_renegotiation_key(multi, session))
+        {
+            return false;
+        }
+    }
     return tls_session_generate_data_channel_keys(multi, session);
 }
 
@@ -2074,6 +2086,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_RENEG;
 #endif
 
         buf_printf(&out, "IV_PROTO=%d\n", iv_proto);
@@ -3693,7 +3706,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)
         {
@@ -3703,7 +3716,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;
@@ -3758,8 +3771,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;
             }
@@ -3780,8 +3793,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 55c672d44..1550fc79d 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 renegoiations with TLS-EKM dervied tls-crypt key */
+#define IV_PROTO_SECURE_RENEG    (1<<9)
+
 /* Default field in X509 to be username */
 #define X509_USERNAME_FIELD_DEFAULT "CN"
 
@@ -476,6 +480,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..a4d8a7eea 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_RENEG_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 978a9fca0..2b9a0a0ae 100644
--- a/src/openvpn/ssl_common.h
+++ b/src/openvpn/ssl_common.h
@@ -275,6 +275,15 @@  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 */
+    /** original key data to be xored in to the key for secure renegotiation.
+     *
+     * We keep the original key data to ensure that the newly generated key
+     * for the secure renegotiation has the same level of quality by using
+     * xor with the original key. This gives us the same same entropy/randomness
+     * as the original tls-crypt key to ensure the post-quantum use case of
+     * tls-crypt still holds true
+     * */
+    struct key2 original_tlscrypt_keydata;
 };
 
 /*
@@ -468,6 +477,10 @@  struct tls_session
     /* authenticate control packets */
     struct tls_wrap_ctx tls_wrap;
 
+    /* Specific tls-crypt for renegotiations, if this is valid,
+     * tls_wrap_reneg.mode is TLS_WRAP_CRYPT, otherwise ignore it */
+    struct tls_wrap_ctx tls_wrap_reneg;
+
     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 b6884af96..8de35323c 100644
--- a/src/openvpn/ssl_ncp.c
+++ b/src/openvpn/ssl_ncp.c
@@ -461,6 +461,10 @@  p2p_ncp_set_options(struct tls_multi *multi, struct tls_session *session)
 
         }
     }
+    if (iv_proto_peer & IV_PROTO_SECURE_RENEG)
+    {
+        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 46bca21d8..7c891b528 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..dc3a53bf6 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_reneg.mode == TLS_WRAP_CRYPT)
+    {
+        return &session->tls_wrap_reneg;
+    }
+    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..e95de63b0 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 key = key XOR other
+ */
+static void
+xor_key2(struct key2 *key, const struct key2 *other)
+{
+    ASSERT(key->n == 2 && other->n == 2);
+    for (int k = 0; k < 2; k++)
+    {
+        for (int j = 0; j < MAX_CIPHER_KEY_LENGTH; j++)
+        {
+            key->keys[k].cipher[j] = key->keys[k].cipher[j] ^ other->keys[k].cipher[j];
+        }
+
+        for (int j = 0; j < MAX_HMAC_KEY_LENGTH; j++)
+        {
+            key->keys[k].hmac[j] = key->keys[k].hmac[j] ^ other->keys[k].hmac[j];
+        }
+
+    }
+}
+
+bool
+tls_session_generate_secure_renegotiation_key(struct tls_multi *multi,
+                                              struct tls_session *session)
+{
+    session->tls_wrap_reneg.opt = session->tls_wrap.opt;
+    session->tls_wrap_reneg.mode = TLS_WRAP_CRYPT;
+    session->tls_wrap_reneg.cleanup_key_ctx = true;
+    session->tls_wrap_reneg.work = alloc_buf(BUF_SIZE(&session->opt->frame));
+    session->tls_wrap_reneg.opt.pid_persist = NULL;
+
+    packet_id_init(&session->tls_wrap_reneg.opt.packet_id,
+                   session->opt->replay_window,
+                   session->opt->replay_time,
+                   "TLS_WRAP_RENEG", session->key_id);
+
+
+    struct key2 rengokeys;
+    if (!key_state_export_keying_material(session, EXPORT_SECURE_RENEG_LABEL,
+                                          strlen(EXPORT_SECURE_RENEG_LABEL),
+                                          rengokeys.keys, sizeof(rengokeys.keys)))
+    {
+        return false;
+    }
+    rengokeys.n = 2;
+
+    if (session->tls_wrap.mode == TLS_WRAP_CRYPT)
+    {
+        xor_key2(&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_reneg.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..21b012ce4 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_renegotiation_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 3bbd98973..e94b2852e 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)
 {
@@ -191,7 +201,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;
 }
@@ -203,7 +214,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..e5b189696 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_reneg_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_renegotiation_key(&multi, &session);
+
+    struct tls_wrap_ctx *rctx = &session.tls_wrap_reneg;
+
+    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_reneg);
+
+    /* 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_renegotiation_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_reneg);
+
+    /* 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_renegotiation_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_reneg);
+
+
+    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_reneg_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),