[Openvpn-devel,v3] Implement --client-crresponse script options and plugin interface

Message ID 20210518122635.2235658-1-arne@rfc2549.org
State Superseded
Headers show
Series [Openvpn-devel,v3] Implement --client-crresponse script options and plugin interface | expand

Commit Message

Arne Schwabe May 18, 2021, 2:26 a.m. UTC
This is allows scripts and pluginsto parse/react to a CR_RESPONSE message

Patch V2: doc fixes, do not put script under ENABLE_PLUGIN
Patch V3: rebase

Signed-off-by: Arne Schwabe <arne@rfc2549.org>
---
 doc/man-sections/script-options.rst | 28 ++++++++++++-
 include/openvpn-plugin.h.in         |  7 +++-
 src/openvpn/init.c                  |  1 +
 src/openvpn/options.c               | 15 +++++++
 src/openvpn/options.h               |  1 +
 src/openvpn/plugin.c                |  5 ++-
 src/openvpn/push.c                  |  4 ++
 src/openvpn/ssl_common.h            |  1 +
 src/openvpn/ssl_verify.c            | 65 +++++++++++++++++++++++++++++
 src/openvpn/ssl_verify.h            | 23 ++++++++++
 10 files changed, 146 insertions(+), 4 deletions(-)

Comments

Antonio Quartulli Aug. 9, 2021, 11:38 p.m. UTC | #1
Hi,

On 18/05/2021 14:26, Arne Schwabe wrote:
> This is allows scripts and pluginsto parse/react to a CR_RESPONSE message
> 
> Patch V2: doc fixes, do not put script under ENABLE_PLUGIN
> Patch V3: rebase
> 
> Signed-off-by: Arne Schwabe <arne@rfc2549.org>

I have a high level question about this patch.

At the moment there is no trace of CR_RESPONSE or request in the
OpenVPN2 code.

To the best of my knowledge that's because this protocol is entirely
handled outside of OpenVPN using INFO_PRE and custom control messages.

With this patch we are aiming at moving a piece of this "external" logic
inside OpenVPN.

Question 1:

What is the benefit of moving the CR response handling inside OpenVPN,
if there is no code to generate the request? Maybe there are advantages
that are just assumed?

Question 2:

If we implement handling CR-RESPONSE, do we want to implement such
handling in the future with every custom control message that may be
sent by the management interface?

Question 3:

IMHO, this is a very good case where OpenVPN can be transparent and let
the upper layer deal with it (i.e. via mgmt interface), why changing that?



Best Regards,
Antonio Quartulli Aug. 10, 2021, 12:03 a.m. UTC | #2
Hi,

On 10/08/2021 11:38, Antonio Quartulli wrote:
> Hi,
> 
> On 18/05/2021 14:26, Arne Schwabe wrote:
>> This is allows scripts and pluginsto parse/react to a CR_RESPONSE message
>>
>> Patch V2: doc fixes, do not put script under ENABLE_PLUGIN
>> Patch V3: rebase
>>
>> Signed-off-by: Arne Schwabe <arne@rfc2549.org>
> 
> I have a high level question about this patch.
> 
> At the moment there is no trace of CR_RESPONSE or request in the
> OpenVPN2 code.
> 
> To the best of my knowledge that's because this protocol is entirely
> handled outside of OpenVPN using INFO_PRE and custom control messages.
> 
> With this patch we are aiming at moving a piece of this "external" logic
> inside OpenVPN.
> 
> Question 1:
> 
> What is the benefit of moving the CR response handling inside OpenVPN,
> if there is no code to generate the request? Maybe there are advantages
> that are just assumed?
> 
> Question 2:
> 
> If we implement handling CR-RESPONSE, do we want to implement such
> handling in the future with every custom control message that may be
> sent by the management interface?
> 
> Question 3:
> 
> IMHO, this is a very good case where OpenVPN can be transparent and let
> the upper layer deal with it (i.e. via mgmt interface), why changing that?


I have to take this back.

OpenVPN already has some logic to parse the CR_RESPONSE and react to it
accordingly.

Therefore my comments above are wrong.

Please ignore my earlier message.

Best Regards,
Antonio Quartulli Aug. 10, 2021, 2:27 a.m. UTC | #3
Hi,

On 18/05/2021 14:26, Arne Schwabe wrote:
> This is allows scripts and pluginsto parse/react to a CR_RESPONSE message
> 
> Patch V2: doc fixes, do not put script under ENABLE_PLUGIN
> Patch V3: rebase
> 
> Signed-off-by: Arne Schwabe <arne@rfc2549.org>
> ---
>  doc/man-sections/script-options.rst | 28 ++++++++++++-
>  include/openvpn-plugin.h.in         |  7 +++-
>  src/openvpn/init.c                  |  1 +
>  src/openvpn/options.c               | 15 +++++++
>  src/openvpn/options.h               |  1 +
>  src/openvpn/plugin.c                |  5 ++-
>  src/openvpn/push.c                  |  4 ++
>  src/openvpn/ssl_common.h            |  1 +
>  src/openvpn/ssl_verify.c            | 65 +++++++++++++++++++++++++++++
>  src/openvpn/ssl_verify.h            | 23 ++++++++++
>  10 files changed, 146 insertions(+), 4 deletions(-)
> 
> diff --git a/doc/man-sections/script-options.rst b/doc/man-sections/script-options.rst
> index 22990f4f4..f48e5818d 100644
> --- a/doc/man-sections/script-options.rst
> +++ b/doc/man-sections/script-options.rst
> @@ -52,6 +52,11 @@ Script Order of Execution
>     Executed in ``--mode server`` mode on new client connections, when the
>     client is still untrusted.
>  
> +#. ``--client-crresponse``
> +
> +    Execute in ``--mode server`` whenever a client sends a
> +    :code:`CR_RESPONSE` message
> +
>  SCRIPT HOOKS
>  ------------
>  
> @@ -72,7 +77,7 @@ SCRIPT HOOKS
>    double-quoted and/or escaped using a backslash, and should be separated
>    by one or more spaces.
>  
> -  If ``method`` is set to :code:`via-env`, OpenVPN will call ``script``
> +  If ``method`` is set to :code:`via-env`, OpenVPN will call ``cmd``

this fix seems unrelated

>    with the environmental variables :code:`username` and :code:`password`
>    set to the username/password strings provided by the client. *Beware*
>    that this method is insecure on some platforms which make the environment
> @@ -80,7 +85,7 @@ SCRIPT HOOKS
>  
>    If ``method`` is set to :code:`via-file`, OpenVPN will write the username
>    and password to the first two lines of a temporary file. The filename
> -  will be passed as an argument to ``script``, and the file will be
> +  will be passed as an argument to ``cmd``, and the file will be

this fix seems unrelated

>    automatically deleted by OpenVPN after the script returns. The location
>    of the temporary file is controlled by the ``--tmp-dir`` option, and
>    will default to the current directory if unspecified. For security,
> @@ -123,6 +128,25 @@ SCRIPT HOOKS
>    For a sample script that performs PAM authentication, see
>    :code:`sample-scripts/auth-pam.pl` in the OpenVPN source distribution.
>  
> +--client-crresponse
> +    Executed when the client sends a text based challenge response.
> +
> +    Valid syntax:
> +    ::
> +
> +        client-crresponse cmd
> +
> +  OpenVPN will write the response of the client into a temporary file.
> +  The filename will be passed as an argument to ``cmd``, and the file will be
> +  automatically deleted by OpenVPN after the script returns.
> +
> +  The response is passed as is from the client. The script needs to check
> +  itself if the input is valid, e.g. if the input is valid base64 encoding.
> +
> +  The script can either directly write the result of the verification to
> +  :code:`auth_control_file or further defer it. See ``--auth-user-pass-verify``
> +  for details.
> +
>  --client-connect cmd
>    Run command ``cmd`` on client connection.
>  
> diff --git a/include/openvpn-plugin.h.in b/include/openvpn-plugin.h.in
> index 1f60f8861..550ce2746 100644
> --- a/include/openvpn-plugin.h.in
> +++ b/include/openvpn-plugin.h.in
> @@ -84,6 +84,10 @@ extern "C" {
>   * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_CLIENT_CONNECT_V2
>   * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_LEARN_ADDRESS
>   *
> + * The OPENVPN_PLUGIN_CLIENT_CRRESPONSE function is called when the client sends
> + * the CR_RESPONSE message, this is *typically* after OPENVPN_PLUGIN_TLS_FINAL
> + * but may also occur much later.
> + *
>   * [Client session ensues]
>   *
>   * For each "TLS soft reset", according to reneg-sec option (or similar):
> @@ -131,7 +135,8 @@ extern "C" {
>  #define OPENVPN_PLUGIN_ROUTE_PREDOWN            12
>  #define OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER     13
>  #define OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2  14
> -#define OPENVPN_PLUGIN_N                        15
> +#define OPENVPN_PLUGIN_CLIENT_CRRESPONSE        15
> +#define OPENVPN_PLUGIN_N                        16
>  
>  /*
>   * Build a mask out of a set of plug-in types.
> diff --git a/src/openvpn/init.c b/src/openvpn/init.c
> index 49c742928..ffb3c78d5 100644
> --- a/src/openvpn/init.c
> +++ b/src/openvpn/init.c
> @@ -2916,6 +2916,7 @@ do_init_crypto_tls(struct context *c, const unsigned int flags)
>  
>      to.auth_user_pass_verify_script = options->auth_user_pass_verify_script;
>      to.auth_user_pass_verify_script_via_file = options->auth_user_pass_verify_script_via_file;
> +    to.client_crresponse_script = options->client_crresponse_script;
>      to.tmp_dir = options->tmp_dir;
>      if (options->ccd_exclusive)
>      {
> diff --git a/src/openvpn/options.c b/src/openvpn/options.c
> index db460796a..3be0e2c35 100644
> --- a/src/openvpn/options.c
> +++ b/src/openvpn/options.c
> @@ -1327,6 +1327,7 @@ show_p2mp_parms(const struct options *o)
>      SHOW_STR(client_connect_script);
>      SHOW_STR(learn_address_script);
>      SHOW_STR(client_disconnect_script);
> +    SHOW_STR(client_crresponse_script);
>      SHOW_STR(client_config_dir);
>      SHOW_BOOL(ccd_exclusive);
>      SHOW_STR(tmp_dir);
> @@ -2470,6 +2471,10 @@ options_postprocess_verify_ce(const struct options *options,
>          {
>              msg(M_USAGE, "--client-connect requires --mode server");
>          }
> +        if (options->client_crresponse_script)
> +        {
> +            msg(M_USAGE, "--client-crresponse requires --mode server");
> +        }
>          if (options->client_disconnect_script)
>          {
>              msg(M_USAGE, "--client-disconnect requires --mode server");
> @@ -7082,6 +7087,16 @@ add_option(struct options *options,
>          set_user_script(options, &options->client_connect_script,
>                          p[1], "client-connect", true);
>      }
> +    else if (streq(p[0], "client-crresponse") && p[1])
> +    {
> +        VERIFY_PERMISSION(OPT_P_SCRIPT);
> +        if (!no_more_than_n_args(msglevel, p, 2, NM_QUOTE_HINT))
> +        {
> +            goto err;
> +        }
> +        set_user_script(options, &options->client_crresponse_script,
> +                        p[1], "client-crresponse", true);
> +    }
>      else if (streq(p[0], "client-disconnect") && p[1])
>      {
>          VERIFY_PERMISSION(OPT_P_SCRIPT);
> diff --git a/src/openvpn/options.h b/src/openvpn/options.h
> index 41e84f7e1..651594ae5 100644
> --- a/src/openvpn/options.h
> +++ b/src/openvpn/options.h
> @@ -449,6 +449,7 @@ struct options
>      const char *client_connect_script;
>      const char *client_disconnect_script;
>      const char *learn_address_script;
> +    const char *client_crresponse_script;
>      const char *client_config_dir;
>      bool ccd_exclusive;
>      bool disable;
> diff --git a/src/openvpn/plugin.c b/src/openvpn/plugin.c
> index 234e92b9c..466437284 100644
> --- a/src/openvpn/plugin.c
> +++ b/src/openvpn/plugin.c
> @@ -102,7 +102,7 @@ plugin_type_name(const int type)
>              return "PLUGIN_CLIENT_CONNECT";
>  
>          case OPENVPN_PLUGIN_CLIENT_CONNECT_V2:
> -            return "PLUGIN_CLIENT_CONNECT";
> +            return "PLUGIN_CLIENT_CONNECT_V2";

This seems unrelated, isn't it?

>  
>          case OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER:
>              return "PLUGIN_CLIENT_CONNECT_DEFER";
> @@ -125,6 +125,9 @@ plugin_type_name(const int type)
>          case OPENVPN_PLUGIN_ROUTE_PREDOWN:
>              return "PLUGIN_ROUTE_PREDOWN";
>  
> +        case OPENVPN_PLUGIN_CLIENT_CRRESPONSE:
> +            return "PLUGIN_CRRESPONSE";
> +
>          default:
>              return "PLUGIN_???";
>      }
> diff --git a/src/openvpn/push.c b/src/openvpn/push.c
> index 85f9488a5..88ed6b24e 100644
> --- a/src/openvpn/push.c
> +++ b/src/openvpn/push.c
> @@ -227,6 +227,10 @@ receive_cr_response(struct context *c, const struct buffer *buffer)
>  
>      management_notify_client_cr_response(key_id, mda, es, m);
>  #endif
> +#if ENABLE_PLUGIN
> +    verify_crresponse_plugin(c->c2.tls_multi, m);
> +#endif
> +    verify_crresponse_script(c->c2.tls_multi, m);
>      msg(D_PUSH, "CR response was sent by client ('%s')", m);
>  }
>  
> diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h
> index 61cae7419..2560cffd4 100644
> --- a/src/openvpn/ssl_common.h
> +++ b/src/openvpn/ssl_common.h
> @@ -330,6 +330,7 @@ struct tls_options
>  
>      /* used for username/password authentication */
>      const char *auth_user_pass_verify_script;
> +    const char *client_crresponse_script;
>      bool auth_user_pass_verify_script_via_file;
>      const char *tmp_dir;
>      const char *auth_user_pass_file;
> diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c
> index 0b41eea2d..839119df6 100644
> --- a/src/openvpn/ssl_verify.c
> +++ b/src/openvpn/ssl_verify.c
> @@ -1337,6 +1337,71 @@ done:
>      return retval;
>  }
>  
> +#ifdef ENABLE_PLUGIN
> +void
> +verify_crresponse_plugin(struct tls_multi *multi, const char *cr_response)
> +{
> +    struct tls_session *session = &multi->session[TM_ACTIVE];
> +    setenv_str(session->opt->es, "crresponse", cr_response);
> +
> +    plugin_call(session->opt->plugins, OPENVPN_PLUGIN_CLIENT_CRRESPONSE, NULL,
> +                NULL, session->opt->es);
> +
> +    setenv_del(session->opt->es, "crresponse");
> +}
> +#endif
> +
> +void
> +verify_crresponse_script(struct tls_multi *multi, const char *cr_response)
> +{
> +
> +    struct tls_session *session = &multi->session[TM_ACTIVE];
> +
> +    if (!session->opt->client_crresponse_script)
> +    {
> +        return;
> +    }
> +    struct argv argv = argv_new();
> +    struct gc_arena gc = gc_new();
> +
> +    setenv_str(session->opt->es, "script_type", "client-crresponse");
> +
> +    /* Since cr response might be sensitive, like a stupid way to query
> +     * a password via 2FA, we pass it via file instead environment */
> +    const char *tmp_file = platform_create_temp_file(session->opt->tmp_dir, "cr", &gc);
> +    if (tmp_file)
> +    {
> +        struct status_output *so = status_open(tmp_file, 0, -1, NULL,
> +                                               STATUS_OUTPUT_WRITE);
> +        status_printf(so, "%s", cr_response);
> +        if (!status_close(so))
> +        {
> +            msg(D_TLS_ERRORS, "TLS CR Response Error: could not write cr"
> +                              "responsed to file: %s",
> +                tmp_file);
> +            tls_deauthenticate(multi);
> +            goto done;
> +        }
> +    }
> +    else
> +    {
> +        msg(D_TLS_ERRORS, "TLS Auth Error: could not create write "
> +                          "username/password to temp file");
> +    }
> +
> +    argv_parse_cmd(&argv, session->opt->client_crresponse_script);
> +    argv_printf_cat(&argv, "%s", tmp_file);
> +
> +
> +    if (!openvpn_run_script(&argv, session->opt->es, 0, "--client-crresponse"))
> +    {
> +        tls_deauthenticate(multi);
> +    }
> +done:
> +    argv_free(&argv);
> +    gc_free(&gc);
> +}
> +
>  /*
>   * Verify the username and password using a plugin
>   */
> diff --git a/src/openvpn/ssl_verify.h b/src/openvpn/ssl_verify.h
> index 88c3d540a..a5421a9e3 100644
> --- a/src/openvpn/ssl_verify.h
> +++ b/src/openvpn/ssl_verify.h
> @@ -206,6 +206,29 @@ tls_common_name_hash(const struct tls_multi *multi, const char **cn, uint32_t *c
>  void verify_user_pass(struct user_pass *up, struct tls_multi *multi,
>                        struct tls_session *session);
>  
> +
> +
> +/**
> + * Runs the --client-crresponse script if one is defined.
> + *
> + * As with the management interface the script is stateless in the sense that
> + * it does not directly participate in the authentication but rather should set
> + * the files for the deferred auth like the management commands.
> + *
> + */
> +void
> +verify_crresponse_script(struct tls_multi *multi, const char *cr_response);
> +
> +/**
> + * Call the plugin OPENVPN_PLUGIN_CLIENT_CRRESPONSE.
> + *
> + * As with the management interface calling the plugin is stateless in the sense
> + * that it does not directly participate in the authentication but rather
> + * should set the files for the deferred auth like the management commands.
> + */
> +void
> +verify_crresponse_plugin(struct tls_multi *multi, const char *cr_response);
> +
>  /**
>   * Perform final authentication checks, including locking of the cn, the allowed
>   * certificate hashes, and whether a client config entry exists in the
> 


The code looks reasonable and I Can't spot anything wrong, other than
what I noted above.

On v2 David already conducted tests and ACK'd this patch, therefore I'd
just consider his reply still valid, since this is just a rebase.

Best Regards,
Heiko Hund Aug. 15, 2022, 5:14 a.m. UTC | #4
On Dienstag, 18. Mai 2021 14:26:35 CEST Arne Schwabe wrote:
> This is allows scripts and pluginsto parse/react to a CR_RESPONSE message

This commit message needs a makeover, I think.

> -  If ``method`` is set to :code:`via-env`, OpenVPN will call ``script``
> +  If ``method`` is set to :code:`via-env`, OpenVPN will call ``cmd``

These drive-by fixes Antonio spotted make sense and are not intrusive enough 
that I care.

> +verify_crresponse_script(struct tls_multi *multi, const char *cr_response)
[...]
> +    const char *tmp_file = platform_create_temp_file(session->opt->tmp_dir,
> "cr", &gc); 
> +    if (tmp_file)
> +    {
> +        struct status_output *so = status_open(tmp_file, 0, -1, NULL,
> +                                               STATUS_OUTPUT_WRITE);
> +        status_printf(so, "%s", cr_response);
> +        if (!status_close(so))
> +        {
> +            msg(D_TLS_ERRORS, "TLS CR Response Error: could not write cr"
> +                              "responsed to file: %s",
> +                tmp_file);
> +            tls_deauthenticate(multi);
> +            goto done;
> +        }
> +    }
> +    else
> +    {
> +        msg(D_TLS_ERRORS, "TLS Auth Error: could not create write "
> +                          "username/password to temp file");
> +    }

This else branch should be the same as the "if (!status_close(so))" one above 
I think, as you don't want to call the script without a valid tempfile. 
Besides that the error message is copy/paste wrong anyway. So, maybe introduce 
a bool and do the error handling in one place, might help future copy/pastes 
as well.

Patch

diff --git a/doc/man-sections/script-options.rst b/doc/man-sections/script-options.rst
index 22990f4f4..f48e5818d 100644
--- a/doc/man-sections/script-options.rst
+++ b/doc/man-sections/script-options.rst
@@ -52,6 +52,11 @@  Script Order of Execution
    Executed in ``--mode server`` mode on new client connections, when the
    client is still untrusted.
 
+#. ``--client-crresponse``
+
+    Execute in ``--mode server`` whenever a client sends a
+    :code:`CR_RESPONSE` message
+
 SCRIPT HOOKS
 ------------
 
@@ -72,7 +77,7 @@  SCRIPT HOOKS
   double-quoted and/or escaped using a backslash, and should be separated
   by one or more spaces.
 
-  If ``method`` is set to :code:`via-env`, OpenVPN will call ``script``
+  If ``method`` is set to :code:`via-env`, OpenVPN will call ``cmd``
   with the environmental variables :code:`username` and :code:`password`
   set to the username/password strings provided by the client. *Beware*
   that this method is insecure on some platforms which make the environment
@@ -80,7 +85,7 @@  SCRIPT HOOKS
 
   If ``method`` is set to :code:`via-file`, OpenVPN will write the username
   and password to the first two lines of a temporary file. The filename
-  will be passed as an argument to ``script``, and the file will be
+  will be passed as an argument to ``cmd``, and the file will be
   automatically deleted by OpenVPN after the script returns. The location
   of the temporary file is controlled by the ``--tmp-dir`` option, and
   will default to the current directory if unspecified. For security,
@@ -123,6 +128,25 @@  SCRIPT HOOKS
   For a sample script that performs PAM authentication, see
   :code:`sample-scripts/auth-pam.pl` in the OpenVPN source distribution.
 
+--client-crresponse
+    Executed when the client sends a text based challenge response.
+
+    Valid syntax:
+    ::
+
+        client-crresponse cmd
+
+  OpenVPN will write the response of the client into a temporary file.
+  The filename will be passed as an argument to ``cmd``, and the file will be
+  automatically deleted by OpenVPN after the script returns.
+
+  The response is passed as is from the client. The script needs to check
+  itself if the input is valid, e.g. if the input is valid base64 encoding.
+
+  The script can either directly write the result of the verification to
+  :code:`auth_control_file or further defer it. See ``--auth-user-pass-verify``
+  for details.
+
 --client-connect cmd
   Run command ``cmd`` on client connection.
 
diff --git a/include/openvpn-plugin.h.in b/include/openvpn-plugin.h.in
index 1f60f8861..550ce2746 100644
--- a/include/openvpn-plugin.h.in
+++ b/include/openvpn-plugin.h.in
@@ -84,6 +84,10 @@  extern "C" {
  * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_CLIENT_CONNECT_V2
  * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_LEARN_ADDRESS
  *
+ * The OPENVPN_PLUGIN_CLIENT_CRRESPONSE function is called when the client sends
+ * the CR_RESPONSE message, this is *typically* after OPENVPN_PLUGIN_TLS_FINAL
+ * but may also occur much later.
+ *
  * [Client session ensues]
  *
  * For each "TLS soft reset", according to reneg-sec option (or similar):
@@ -131,7 +135,8 @@  extern "C" {
 #define OPENVPN_PLUGIN_ROUTE_PREDOWN            12
 #define OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER     13
 #define OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2  14
-#define OPENVPN_PLUGIN_N                        15
+#define OPENVPN_PLUGIN_CLIENT_CRRESPONSE        15
+#define OPENVPN_PLUGIN_N                        16
 
 /*
  * Build a mask out of a set of plug-in types.
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index 49c742928..ffb3c78d5 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -2916,6 +2916,7 @@  do_init_crypto_tls(struct context *c, const unsigned int flags)
 
     to.auth_user_pass_verify_script = options->auth_user_pass_verify_script;
     to.auth_user_pass_verify_script_via_file = options->auth_user_pass_verify_script_via_file;
+    to.client_crresponse_script = options->client_crresponse_script;
     to.tmp_dir = options->tmp_dir;
     if (options->ccd_exclusive)
     {
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index db460796a..3be0e2c35 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -1327,6 +1327,7 @@  show_p2mp_parms(const struct options *o)
     SHOW_STR(client_connect_script);
     SHOW_STR(learn_address_script);
     SHOW_STR(client_disconnect_script);
+    SHOW_STR(client_crresponse_script);
     SHOW_STR(client_config_dir);
     SHOW_BOOL(ccd_exclusive);
     SHOW_STR(tmp_dir);
@@ -2470,6 +2471,10 @@  options_postprocess_verify_ce(const struct options *options,
         {
             msg(M_USAGE, "--client-connect requires --mode server");
         }
+        if (options->client_crresponse_script)
+        {
+            msg(M_USAGE, "--client-crresponse requires --mode server");
+        }
         if (options->client_disconnect_script)
         {
             msg(M_USAGE, "--client-disconnect requires --mode server");
@@ -7082,6 +7087,16 @@  add_option(struct options *options,
         set_user_script(options, &options->client_connect_script,
                         p[1], "client-connect", true);
     }
+    else if (streq(p[0], "client-crresponse") && p[1])
+    {
+        VERIFY_PERMISSION(OPT_P_SCRIPT);
+        if (!no_more_than_n_args(msglevel, p, 2, NM_QUOTE_HINT))
+        {
+            goto err;
+        }
+        set_user_script(options, &options->client_crresponse_script,
+                        p[1], "client-crresponse", true);
+    }
     else if (streq(p[0], "client-disconnect") && p[1])
     {
         VERIFY_PERMISSION(OPT_P_SCRIPT);
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index 41e84f7e1..651594ae5 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -449,6 +449,7 @@  struct options
     const char *client_connect_script;
     const char *client_disconnect_script;
     const char *learn_address_script;
+    const char *client_crresponse_script;
     const char *client_config_dir;
     bool ccd_exclusive;
     bool disable;
diff --git a/src/openvpn/plugin.c b/src/openvpn/plugin.c
index 234e92b9c..466437284 100644
--- a/src/openvpn/plugin.c
+++ b/src/openvpn/plugin.c
@@ -102,7 +102,7 @@  plugin_type_name(const int type)
             return "PLUGIN_CLIENT_CONNECT";
 
         case OPENVPN_PLUGIN_CLIENT_CONNECT_V2:
-            return "PLUGIN_CLIENT_CONNECT";
+            return "PLUGIN_CLIENT_CONNECT_V2";
 
         case OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER:
             return "PLUGIN_CLIENT_CONNECT_DEFER";
@@ -125,6 +125,9 @@  plugin_type_name(const int type)
         case OPENVPN_PLUGIN_ROUTE_PREDOWN:
             return "PLUGIN_ROUTE_PREDOWN";
 
+        case OPENVPN_PLUGIN_CLIENT_CRRESPONSE:
+            return "PLUGIN_CRRESPONSE";
+
         default:
             return "PLUGIN_???";
     }
diff --git a/src/openvpn/push.c b/src/openvpn/push.c
index 85f9488a5..88ed6b24e 100644
--- a/src/openvpn/push.c
+++ b/src/openvpn/push.c
@@ -227,6 +227,10 @@  receive_cr_response(struct context *c, const struct buffer *buffer)
 
     management_notify_client_cr_response(key_id, mda, es, m);
 #endif
+#if ENABLE_PLUGIN
+    verify_crresponse_plugin(c->c2.tls_multi, m);
+#endif
+    verify_crresponse_script(c->c2.tls_multi, m);
     msg(D_PUSH, "CR response was sent by client ('%s')", m);
 }
 
diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h
index 61cae7419..2560cffd4 100644
--- a/src/openvpn/ssl_common.h
+++ b/src/openvpn/ssl_common.h
@@ -330,6 +330,7 @@  struct tls_options
 
     /* used for username/password authentication */
     const char *auth_user_pass_verify_script;
+    const char *client_crresponse_script;
     bool auth_user_pass_verify_script_via_file;
     const char *tmp_dir;
     const char *auth_user_pass_file;
diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c
index 0b41eea2d..839119df6 100644
--- a/src/openvpn/ssl_verify.c
+++ b/src/openvpn/ssl_verify.c
@@ -1337,6 +1337,71 @@  done:
     return retval;
 }
 
+#ifdef ENABLE_PLUGIN
+void
+verify_crresponse_plugin(struct tls_multi *multi, const char *cr_response)
+{
+    struct tls_session *session = &multi->session[TM_ACTIVE];
+    setenv_str(session->opt->es, "crresponse", cr_response);
+
+    plugin_call(session->opt->plugins, OPENVPN_PLUGIN_CLIENT_CRRESPONSE, NULL,
+                NULL, session->opt->es);
+
+    setenv_del(session->opt->es, "crresponse");
+}
+#endif
+
+void
+verify_crresponse_script(struct tls_multi *multi, const char *cr_response)
+{
+
+    struct tls_session *session = &multi->session[TM_ACTIVE];
+
+    if (!session->opt->client_crresponse_script)
+    {
+        return;
+    }
+    struct argv argv = argv_new();
+    struct gc_arena gc = gc_new();
+
+    setenv_str(session->opt->es, "script_type", "client-crresponse");
+
+    /* Since cr response might be sensitive, like a stupid way to query
+     * a password via 2FA, we pass it via file instead environment */
+    const char *tmp_file = platform_create_temp_file(session->opt->tmp_dir, "cr", &gc);
+    if (tmp_file)
+    {
+        struct status_output *so = status_open(tmp_file, 0, -1, NULL,
+                                               STATUS_OUTPUT_WRITE);
+        status_printf(so, "%s", cr_response);
+        if (!status_close(so))
+        {
+            msg(D_TLS_ERRORS, "TLS CR Response Error: could not write cr"
+                              "responsed to file: %s",
+                tmp_file);
+            tls_deauthenticate(multi);
+            goto done;
+        }
+    }
+    else
+    {
+        msg(D_TLS_ERRORS, "TLS Auth Error: could not create write "
+                          "username/password to temp file");
+    }
+
+    argv_parse_cmd(&argv, session->opt->client_crresponse_script);
+    argv_printf_cat(&argv, "%s", tmp_file);
+
+
+    if (!openvpn_run_script(&argv, session->opt->es, 0, "--client-crresponse"))
+    {
+        tls_deauthenticate(multi);
+    }
+done:
+    argv_free(&argv);
+    gc_free(&gc);
+}
+
 /*
  * Verify the username and password using a plugin
  */
diff --git a/src/openvpn/ssl_verify.h b/src/openvpn/ssl_verify.h
index 88c3d540a..a5421a9e3 100644
--- a/src/openvpn/ssl_verify.h
+++ b/src/openvpn/ssl_verify.h
@@ -206,6 +206,29 @@  tls_common_name_hash(const struct tls_multi *multi, const char **cn, uint32_t *c
 void verify_user_pass(struct user_pass *up, struct tls_multi *multi,
                       struct tls_session *session);
 
+
+
+/**
+ * Runs the --client-crresponse script if one is defined.
+ *
+ * As with the management interface the script is stateless in the sense that
+ * it does not directly participate in the authentication but rather should set
+ * the files for the deferred auth like the management commands.
+ *
+ */
+void
+verify_crresponse_script(struct tls_multi *multi, const char *cr_response);
+
+/**
+ * Call the plugin OPENVPN_PLUGIN_CLIENT_CRRESPONSE.
+ *
+ * As with the management interface calling the plugin is stateless in the sense
+ * that it does not directly participate in the authentication but rather
+ * should set the files for the deferred auth like the management commands.
+ */
+void
+verify_crresponse_plugin(struct tls_multi *multi, const char *cr_response);
+
 /**
  * Perform final authentication checks, including locking of the cn, the allowed
  * certificate hashes, and whether a client config entry exists in the