Browse code

Implement --client-crresponse script options and plugin interface

This allows scripts and plugins to parse/react to a CR_RESPONSE message

Patch V2: doc fixes, do not put script under ENABLE_PLUGIN
Patch V3: rebase
Patch V4: fix else branch of the verify_crresponse_script function
Patch V5: unify message when unable to create/write crresponse file

Signed-off-by: Arne Schwabe <arne@rfc2549.org>
Acked-by: Heiko Hund <heiko@ist.eigentlich.net>
Message-Id: <20220824110930.73009-1-arne@rfc2549.org>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg25089.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>

Arne Schwabe authored on 2022/08/24 20:09:30
Showing 10 changed files
... ...
@@ -52,6 +52,11 @@ Script Order of Execution
52 52
    Executed in ``--mode server`` mode on new client connections, when the
53 53
    client is still untrusted.
54 54
 
55
+#. ``--client-crresponse``
56
+
57
+    Execute in ``--mode server`` whenever a client sends a
58
+    :code:`CR_RESPONSE` message
59
+
55 60
 SCRIPT HOOKS
56 61
 ------------
57 62
 
... ...
@@ -72,7 +77,7 @@ SCRIPT HOOKS
72 72
   double-quoted and/or escaped using a backslash, and should be separated
73 73
   by one or more spaces.
74 74
 
75
-  If ``method`` is set to :code:`via-env`, OpenVPN will call ``script``
75
+  If ``method`` is set to :code:`via-env`, OpenVPN will call ``cmd``
76 76
   with the environmental variables :code:`username` and :code:`password`
77 77
   set to the username/password strings provided by the client. *Beware*
78 78
   that this method is insecure on some platforms which make the environment
... ...
@@ -80,7 +85,7 @@ SCRIPT HOOKS
80 80
 
81 81
   If ``method`` is set to :code:`via-file`, OpenVPN will write the username
82 82
   and password to the first two lines of a temporary file. The filename
83
-  will be passed as an argument to ``script``, and the file will be
83
+  will be passed as an argument to ``cmd``, and the file will be
84 84
   automatically deleted by OpenVPN after the script returns. The location
85 85
   of the temporary file is controlled by the ``--tmp-dir`` option, and
86 86
   will default to the current directory if unspecified. For security,
... ...
@@ -123,6 +128,25 @@ SCRIPT HOOKS
123 123
   For a sample script that performs PAM authentication, see
124 124
   :code:`sample-scripts/auth-pam.pl` in the OpenVPN source distribution.
125 125
 
126
+--client-crresponse
127
+    Executed when the client sends a text based challenge response.
128
+
129
+    Valid syntax:
130
+    ::
131
+
132
+        client-crresponse cmd
133
+
134
+  OpenVPN will write the response of the client into a temporary file.
135
+  The filename will be passed as an argument to ``cmd``, and the file will be
136
+  automatically deleted by OpenVPN after the script returns.
137
+
138
+  The response is passed as is from the client. The script needs to check
139
+  itself if the input is valid, e.g. if the input is valid base64 encoding.
140
+
141
+  The script can either directly write the result of the verification to
142
+  :code:`auth_control_file or further defer it. See ``--auth-user-pass-verify``
143
+  for details.
144
+
126 145
 --client-connect cmd
127 146
   Run command ``cmd`` on client connection.
128 147
 
... ...
@@ -83,6 +83,10 @@ extern "C" {
83 83
  * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_CLIENT_CONNECT_V2
84 84
  * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_LEARN_ADDRESS
85 85
  *
86
+ * The OPENVPN_PLUGIN_CLIENT_CRRESPONSE function is called when the client sends
87
+ * the CR_RESPONSE message, this is *typically* after OPENVPN_PLUGIN_TLS_FINAL
88
+ * but may also occur much later.
89
+ *
86 90
  * [Client session ensues]
87 91
  *
88 92
  * For each "TLS soft reset", according to reneg-sec option (or similar):
... ...
@@ -128,7 +132,8 @@ extern "C" {
128 128
 #define OPENVPN_PLUGIN_ROUTE_PREDOWN            12
129 129
 #define OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER     13
130 130
 #define OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER_V2  14
131
-#define OPENVPN_PLUGIN_N                        15
131
+#define OPENVPN_PLUGIN_CLIENT_CRRESPONSE        15
132
+#define OPENVPN_PLUGIN_N                        16
132 133
 
133 134
 /*
134 135
  * Build a mask out of a set of plug-in types.
... ...
@@ -3046,6 +3046,7 @@ do_init_crypto_tls(struct context *c, const unsigned int flags)
3046 3046
 
3047 3047
     to.auth_user_pass_verify_script = options->auth_user_pass_verify_script;
3048 3048
     to.auth_user_pass_verify_script_via_file = options->auth_user_pass_verify_script_via_file;
3049
+    to.client_crresponse_script = options->client_crresponse_script;
3049 3050
     to.tmp_dir = options->tmp_dir;
3050 3051
     if (options->ccd_exclusive)
3051 3052
     {
... ...
@@ -1525,6 +1525,7 @@ show_p2mp_parms(const struct options *o)
1525 1525
     SHOW_STR(client_connect_script);
1526 1526
     SHOW_STR(learn_address_script);
1527 1527
     SHOW_STR(client_disconnect_script);
1528
+    SHOW_STR(client_crresponse_script);
1528 1529
     SHOW_STR(client_config_dir);
1529 1530
     SHOW_BOOL(ccd_exclusive);
1530 1531
     SHOW_STR(tmp_dir);
... ...
@@ -2686,6 +2687,10 @@ options_postprocess_verify_ce(const struct options *options,
2686 2686
         {
2687 2687
             msg(M_USAGE, "--client-connect requires --mode server");
2688 2688
         }
2689
+        if (options->client_crresponse_script)
2690
+        {
2691
+            msg(M_USAGE, "--client-crresponse requires --mode server");
2692
+        }
2689 2693
         if (options->client_disconnect_script)
2690 2694
         {
2691 2695
             msg(M_USAGE, "--client-disconnect requires --mode server");
... ...
@@ -7468,6 +7473,16 @@ add_option(struct options *options,
7468 7468
         set_user_script(options, &options->client_connect_script,
7469 7469
                         p[1], "client-connect", true);
7470 7470
     }
7471
+    else if (streq(p[0], "client-crresponse") && p[1])
7472
+    {
7473
+        VERIFY_PERMISSION(OPT_P_SCRIPT);
7474
+        if (!no_more_than_n_args(msglevel, p, 2, NM_QUOTE_HINT))
7475
+        {
7476
+            goto err;
7477
+        }
7478
+        set_user_script(options, &options->client_crresponse_script,
7479
+                        p[1], "client-crresponse", true);
7480
+    }
7471 7481
     else if (streq(p[0], "client-disconnect") && p[1])
7472 7482
     {
7473 7483
         VERIFY_PERMISSION(OPT_P_SCRIPT);
... ...
@@ -473,6 +473,7 @@ struct options
473 473
     const char *client_connect_script;
474 474
     const char *client_disconnect_script;
475 475
     const char *learn_address_script;
476
+    const char *client_crresponse_script;
476 477
     const char *client_config_dir;
477 478
     bool ccd_exclusive;
478 479
     bool disable;
... ...
@@ -102,7 +102,7 @@ plugin_type_name(const int type)
102 102
             return "PLUGIN_CLIENT_CONNECT";
103 103
 
104 104
         case OPENVPN_PLUGIN_CLIENT_CONNECT_V2:
105
-            return "PLUGIN_CLIENT_CONNECT";
105
+            return "PLUGIN_CLIENT_CONNECT_V2";
106 106
 
107 107
         case OPENVPN_PLUGIN_CLIENT_CONNECT_DEFER:
108 108
             return "PLUGIN_CLIENT_CONNECT_DEFER";
... ...
@@ -122,6 +122,9 @@ plugin_type_name(const int type)
122 122
         case OPENVPN_PLUGIN_ROUTE_PREDOWN:
123 123
             return "PLUGIN_ROUTE_PREDOWN";
124 124
 
125
+        case OPENVPN_PLUGIN_CLIENT_CRRESPONSE:
126
+            return "PLUGIN_CRRESPONSE";
127
+
125 128
         default:
126 129
             return "PLUGIN_???";
127 130
     }
... ...
@@ -229,6 +229,10 @@ receive_cr_response(struct context *c, const struct buffer *buffer)
229 229
 
230 230
     management_notify_client_cr_response(key_id, mda, es, m);
231 231
 #endif
232
+#if ENABLE_PLUGIN
233
+    verify_crresponse_plugin(c->c2.tls_multi, m);
234
+#endif
235
+    verify_crresponse_script(c->c2.tls_multi, m);
232 236
     msg(D_PUSH, "CR response was sent by client ('%s')", m);
233 237
 }
234 238
 
... ...
@@ -362,6 +362,7 @@ struct tls_options
362 362
 
363 363
     /* used for username/password authentication */
364 364
     const char *auth_user_pass_verify_script;
365
+    const char *client_crresponse_script;
365 366
     bool auth_user_pass_verify_script_via_file;
366 367
     const char *tmp_dir;
367 368
     const char *auth_user_pass_file;
... ...
@@ -1352,6 +1352,73 @@ done:
1352 1352
     return retval;
1353 1353
 }
1354 1354
 
1355
+#ifdef ENABLE_PLUGIN
1356
+void
1357
+verify_crresponse_plugin(struct tls_multi *multi, const char *cr_response)
1358
+{
1359
+    struct tls_session *session = &multi->session[TM_ACTIVE];
1360
+    setenv_str(session->opt->es, "crresponse", cr_response);
1361
+
1362
+    plugin_call(session->opt->plugins, OPENVPN_PLUGIN_CLIENT_CRRESPONSE, NULL,
1363
+                NULL, session->opt->es);
1364
+
1365
+    setenv_del(session->opt->es, "crresponse");
1366
+}
1367
+#endif
1368
+
1369
+void
1370
+verify_crresponse_script(struct tls_multi *multi, const char *cr_response)
1371
+{
1372
+
1373
+    struct tls_session *session = &multi->session[TM_ACTIVE];
1374
+
1375
+    if (!session->opt->client_crresponse_script)
1376
+    {
1377
+        return;
1378
+    }
1379
+    struct argv argv = argv_new();
1380
+    struct gc_arena gc = gc_new();
1381
+
1382
+    setenv_str(session->opt->es, "script_type", "client-crresponse");
1383
+
1384
+    /* Since cr response might be sensitive, like a stupid way to query
1385
+     * a password via 2FA, we pass it via file instead environment */
1386
+    const char *tmp_file = platform_create_temp_file(session->opt->tmp_dir, "cr", &gc);
1387
+    static const char *openerrmsg = "TLS CR Response Error: could not write "
1388
+                                    "crtext challenge response to file: %s";
1389
+
1390
+    if (tmp_file)
1391
+    {
1392
+        struct status_output *so = status_open(tmp_file, 0, -1, NULL,
1393
+                                               STATUS_OUTPUT_WRITE);
1394
+        status_printf(so, "%s", cr_response);
1395
+        if (!status_close(so))
1396
+        {
1397
+            msg(D_TLS_ERRORS, openerrmsg, tmp_file);
1398
+            tls_deauthenticate(multi);
1399
+            goto done;
1400
+        }
1401
+    }
1402
+    else
1403
+    {
1404
+        msg(D_TLS_ERRORS, openerrmsg, "creating file failed");
1405
+        tls_deauthenticate(multi);
1406
+        goto done;
1407
+    }
1408
+
1409
+    argv_parse_cmd(&argv, session->opt->client_crresponse_script);
1410
+    argv_printf_cat(&argv, "%s", tmp_file);
1411
+
1412
+
1413
+    if (!openvpn_run_script(&argv, session->opt->es, 0, "--client-crresponse"))
1414
+    {
1415
+        tls_deauthenticate(multi);
1416
+    }
1417
+done:
1418
+    argv_free(&argv);
1419
+    gc_free(&gc);
1420
+}
1421
+
1355 1422
 /*
1356 1423
  * Verify the username and password using a plugin
1357 1424
  */
... ...
@@ -177,6 +177,29 @@ bool cert_hash_compare(const struct cert_hash_set *chs1, const struct cert_hash_
177 177
 void verify_user_pass(struct user_pass *up, struct tls_multi *multi,
178 178
                       struct tls_session *session);
179 179
 
180
+
181
+
182
+/**
183
+ * Runs the --client-crresponse script if one is defined.
184
+ *
185
+ * As with the management interface the script is stateless in the sense that
186
+ * it does not directly participate in the authentication but rather should set
187
+ * the files for the deferred auth like the management commands.
188
+ *
189
+ */
190
+void
191
+verify_crresponse_script(struct tls_multi *multi, const char *cr_response);
192
+
193
+/**
194
+ * Call the plugin OPENVPN_PLUGIN_CLIENT_CRRESPONSE.
195
+ *
196
+ * As with the management interface calling the plugin is stateless in the sense
197
+ * that it does not directly participate in the authentication but rather
198
+ * should set the files for the deferred auth like the management commands.
199
+ */
200
+void
201
+verify_crresponse_plugin(struct tls_multi *multi, const char *cr_response);
202
+
180 203
 /**
181 204
  * Perform final authentication checks, including locking of the cn, the allowed
182 205
  * certificate hashes, and whether a client config entry exists in the