]> git.ipfire.org Git - thirdparty/openvpn.git/commitdiff
Add option to check tls-crypt-v2 key timestamps
authorMax Fillinger <max@max-fillinger.net>
Wed, 19 Nov 2025 14:01:43 +0000 (15:01 +0100)
committerGert Doering <gert@greenie.muc.de>
Wed, 19 Nov 2025 15:28:00 +0000 (16:28 +0100)
This commit adds the option --tls-crypt-v2-max-age n. When a client key
is older than n days or has no timestamp, the server rejects it.

Based on work by Rein van Baaren for Sentyron.

Co-authored-by: Rein van Baaren <revaban04@proton.me>
Change-Id: I0579d18c784e2ac16973d5553992c28f281a0900
Signed-off-by: Max Fillinger <max@max-fillinger.net>
Acked-by: Arne Schwabe <arne-openvpn@rfc2549.org>
Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1304
Message-Id: <20251119140149.31867-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg34545.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
doc/man-sections/tls-options.rst
doc/tls-crypt-v2.txt
src/openvpn/init.c
src/openvpn/options.c
src/openvpn/options.h
src/openvpn/ssl_common.h
src/openvpn/tls_crypt.c

index db107e66021d0502b50cbc9d3891b060ba853b4f..63cb32f95ae9990fb94d686b4be74d60b80a55a1 100644 (file)
@@ -568,6 +568,10 @@ certificates and keys: https://github.com/OpenVPN/easy-rsa
   The command can reject the connection by exiting with a non-zero exit
   code.
 
+--tls-crypt-v2-max-age n
+  Reject tls-crypt-v2 client keys that are older than n days or have
+  no timestamp.
+
 --tls-exit
   Exit on TLS negotiation failure. This option can be useful when you only
   want to make one attempt at connecting, e.g. in a test or monitoring script.
index 7dcd0415fd5bdb0272aa3bda738bc430e8ccc454..c2e9debcc459e8a29c07dde0ed2af8a8fa44f955 100644 (file)
@@ -139,7 +139,10 @@ When setting up the openvpn connection:
    The message is dropped and no error response is sent when either 3.1, 3.2 or
    3.3 fails (DoS protection).
 
-4. Server optionally checks metadata using a --tls-crypt-v2-verify script
+4. The server optionally checks if the client key contains a timestamp that is
+   below a maximum age configured with the --tls-crypt-v2-max-age option.
+
+5. Server optionally checks metadata using a --tls-crypt-v2-verify script
 
    This allows early abort of connection, *before* we expose any of the
    notoriously dangerous TLS, X.509 and ASN.1 parsers and thereby reduces the
index 8d95d5c1f747e7302a1102662c9ebe4d9e2796ca..fc079e1124a05294404b4865c05844e7f954b420 100644 (file)
@@ -3418,6 +3418,7 @@ do_init_crypto_tls(struct context *c, const unsigned int flags)
         {
             to.tls_wrap.tls_crypt_v2_server_key = c->c1.ks.tls_crypt_v2_server_key;
             to.tls_crypt_v2_verify_script = c->options.tls_crypt_v2_verify_script;
+            to.tls_crypt_v2_max_age = c->options.tls_crypt_v2_max_age;
             if (options->ce.tls_crypt_v2_force_cookie)
             {
                 to.tls_wrap.opt.flags |= CO_FORCE_TLSCRYPTV2_COOKIE;
index 683543a8fad68eaebf3f58c31395cd81eea4fdec..4794315cc351be46a5d620f4d8f98854af9b16c5 100644 (file)
@@ -648,6 +648,8 @@ static const char usage_message[] =
     "                  fresh tls-crypt-v2 server key, and store to keyfile\n"
     "--tls-crypt-v2-verify cmd : Run command cmd to verify the metadata of the\n"
     "                  client-supplied tls-crypt-v2 client key\n"
+    "--tls-crypt-v2-max-age n : Only accept tls-crypt-v2 client keys that have a\n"
+    "                  timestamp which is at most n days old.\n"
     "--askpass [file]: Get PEM password from controlling tty before we daemonize.\n"
     "--auth-nocache  : Don't cache --askpass or --auth-user-pass passwords.\n"
     "--crl-verify crl ['dir']: Check peer certificate against a CRL.\n"
@@ -9079,6 +9081,14 @@ add_option(struct options *options, char *p[], bool is_inline, const char *file,
         VERIFY_PERMISSION(OPT_P_GENERAL);
         options->tls_crypt_v2_verify_script = p[1];
     }
+    else if (streq(p[0], "tls-crypt-v2-max-age") && p[1])
+    {
+        VERIFY_PERMISSION(OPT_P_GENERAL);
+        if (!atoi_constrained(p[1], &options->tls_crypt_v2_max_age, "tls-crypt-v2-max-age", 1, INT_MAX, msglevel))
+        {
+            goto err;
+        }
+    }
     else if (streq(p[0], "x509-track") && p[1] && !p[2])
     {
         VERIFY_PERMISSION(OPT_P_GENERAL);
index 9d2ff9fd93f671eabb738b0e41e2cc42bfa80dc2..42db9caec6b16db81994817660a20d76c847a8d6 100644 (file)
@@ -678,6 +678,8 @@ struct options
 
     const char *tls_crypt_v2_verify_script;
 
+    int tls_crypt_v2_max_age;
+
     /* Allow only one session */
     bool single_session;
 
index 23da8cf5d42e46c17c38d51e81b7937c70a33515..3129299b7f9c0ab2540f7f7cdb7574870b60396f 100644 (file)
@@ -383,6 +383,7 @@ struct tls_options
 
     bool tls_crypt_v2;
     const char *tls_crypt_v2_verify_script;
+    int tls_crypt_v2_max_age;
 
     /** TLS handshake wrapping state */
     struct tls_wrap_ctx tls_wrap;
index ab719b3826f4f390a5f3423c0f92b09a4790403c..318c939259df6478db9487de762885799ecb41cc 100644 (file)
@@ -29,6 +29,7 @@
 #include "argv.h"
 #include "base64.h"
 #include "crypto.h"
+#include "integer.h"
 #include "platform.h"
 #include "run_command.h"
 #include "session_id.h"
@@ -519,6 +520,34 @@ error_exit:
     return ret;
 }
 
+static bool
+tls_crypt_v2_check_client_key_age(const struct tls_wrap_ctx *ctx, int max_days)
+{
+    if (ctx->tls_crypt_v2_metadata.len < 1 + sizeof(int64_t))
+    {
+        msg(M_WARN, "ERROR: Client key metadata is too small to contain a timestamp.");
+        return false;
+    }
+
+    const uint8_t *metadata = ctx->tls_crypt_v2_metadata.data;
+    if (*metadata != TLS_CRYPT_METADATA_TYPE_TIMESTAMP)
+    {
+        msg(M_WARN, "ERROR: Client key does not have a timestamp.");
+        return false;
+    }
+
+    int64_t timestamp;
+    memcpy(&timestamp, metadata + 1, sizeof(int64_t));
+    timestamp = (int64_t)ntohll((uint64_t)timestamp);
+    int64_t max_age_in_seconds = max_days * 24 * 60 * 60;
+    if (now - timestamp > max_age_in_seconds)
+    {
+        msg(M_WARN, "ERROR: Client key is too old.");
+        return false;
+    }
+    return true;
+}
+
 static bool
 tls_crypt_v2_verify_metadata(const struct tls_wrap_ctx *ctx, const struct tls_options *opt)
 {
@@ -634,6 +663,12 @@ tls_crypt_v2_extract_client_key(struct buffer *buf, struct tls_wrap_ctx *ctx,
         return false;
     }
 
+    if (opt && opt->tls_crypt_v2_max_age > 0 && !tls_crypt_v2_check_client_key_age(ctx, opt->tls_crypt_v2_max_age))
+    {
+        secure_memzero(&ctx->original_wrap_keydata, sizeof(ctx->original_wrap_keydata));
+        return false;
+    }
+
     if (opt && opt->tls_crypt_v2_verify_script && !tls_crypt_v2_verify_metadata(ctx, opt))
     {
         secure_memzero(&ctx->original_wrap_keydata, sizeof(ctx->original_wrap_keydata));