From patchwork Thu Aug 8 04:51:29 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Arne Schwabe X-Patchwork-Id: 798 Return-Path: Delivered-To: patchwork@openvpn.net Delivered-To: patchwork@openvpn.net Received: from director7.mail.ord1d.rsapps.net ([172.30.191.6]) by backend30.mail.ord1d.rsapps.net with LMTP id uPR8EmA3TF0IOwAAIUCqbw for ; Thu, 08 Aug 2019 10:53:20 -0400 Received: from proxy14.mail.ord1d.rsapps.net ([172.30.191.6]) by director7.mail.ord1d.rsapps.net with LMTP id UKAIEWA3TF3DSAAAovjBpQ ; Thu, 08 Aug 2019 10:53:20 -0400 Received: from smtp39.gate.ord1d ([172.30.191.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) by proxy14.mail.ord1d.rsapps.net with LMTP id oFW4EGA3TF2BSQAAtEH5vw ; Thu, 08 Aug 2019 10:53:20 -0400 X-Spam-Threshold: 95 X-Spam-Score: 0 X-Spam-Flag: NO X-Virus-Scanned: OK X-Orig-To: openvpnslackdevel@openvpn.net X-Originating-Ip: [216.105.38.7] Authentication-Results: smtp39.gate.ord1d.rsapps.net; iprev=pass policy.iprev="216.105.38.7"; spf=pass smtp.mailfrom="openvpn-devel-bounces@lists.sourceforge.net" smtp.helo="lists.sourceforge.net"; dkim=fail (signature verification failed) header.d=sourceforge.net; dkim=fail (signature verification failed) header.d=sf.net; dmarc=none (p=nil; dis=none) header.from=rfc2549.org X-Suspicious-Flag: YES X-Classification-ID: 41eab1e0-b9ec-11e9-b2d0-525400a97bbc-1-1 Received: from [216.105.38.7] ([216.105.38.7:36634] helo=lists.sourceforge.net) by smtp39.gate.ord1d.rsapps.net (envelope-from ) (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) id E2/22-18032-E573C4D5; Thu, 08 Aug 2019 10:53:18 -0400 Received: from [127.0.0.1] (helo=sfs-ml-1.v29.lw.sourceforge.com) by sfs-ml-1.v29.lw.sourceforge.com with esmtp (Exim 4.90_1) (envelope-from ) id 1hvjld-0005yh-43; Thu, 08 Aug 2019 14:51:49 +0000 Received: from [172.30.20.202] (helo=mx.sourceforge.net) by sfs-ml-1.v29.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) (envelope-from ) id 1hvjlZ-0005xo-E4 for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:51:45 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=References:In-Reply-To:Message-Id:Date:Subject:To: From:Sender:Reply-To:Cc:MIME-Version:Content-Type:Content-Transfer-Encoding: Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=6eKMmtNzpoNxeKsk+mgG3mUmyX2no5VztXnJmaxN7p4=; b=KI6k0V+lpha8VEVWA3fbbmRR/T InNuOHSRX4PPmll5mgHjDhKS/cWYwpUByXqCX9GNtRjbmvM9p3dbkjSxSgqx7mCnCeEBhLeLfQie6 MPkCqm4ceNsRy0wKXVP6ZrXQnK4e0zpJE1Lliies2oh0etmsuXHo/ump0/ntQkSAvfNs=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=References:In-Reply-To:Message-Id:Date:Subject:To:From:Sender:Reply-To:Cc :MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=6eKMmtNzpoNxeKsk+mgG3mUmyX2no5VztXnJmaxN7p4=; b=cVUcWb8GFuktDTLyiW076bEJdr M3sUo7zWHrJm0GAofpr6E3QH9+6cb4fbxWKCRqEv10Sd2GuTNRiy0sLvgITdWUWwvZV9lOO0yALU/ zUeTXeKVd2pKDKOtssvO4jPBAJRB9GBouvkDACRZXv7ISEW+NyFlW5KwXGmAoee/ZDu4=; Received: from [192.26.174.232] (helo=mail.blinkt.de) by sfi-mx-4.v28.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) id 1hvjlV-00F5mD-Aq for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:51:45 +0000 Received: from kamera.blinkt.de ([2001:638:502:390:20c:29ff:fec8:535c]) by mail.blinkt.de with smtp (Exim 4.92 (FreeBSD)) (envelope-from ) id 1hvjlJ-000IgH-LM for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 16:51:29 +0200 Received: (nullmailer pid 1187 invoked by uid 10006); Thu, 08 Aug 2019 14:51:29 -0000 From: Arne Schwabe To: openvpn-devel@lists.sourceforge.net Date: Thu, 8 Aug 2019 16:51:29 +0200 Message-Id: <20190808145129.1142-1-arne@rfc2549.org> X-Mailer: git-send-email 2.17.1 In-Reply-To: <324fcf27-5a5a-62a7-f77d-cd279f46c712@sf.lists.topphemmelig.net> References: <324fcf27-5a5a-62a7-f77d-cd279f46c712@sf.lists.topphemmelig.net> X-Spam-Report: Spam Filtering performed by mx.sourceforge.net. See http://spamassassin.org/tag/ for more details. 0.0 URIBL_BLOCKED ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [URIs: makefile.am] 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level mail domains are different 0.0 SPF_NONE SPF: sender does not publish an SPF Record 0.0 SPF_HELO_NONE SPF: HELO does not publish an SPF Record 1.0 RDNS_NONE Delivered to internal network by a host with no rDNS X-Headers-End: 1hvjlV-00F5mD-Aq Subject: [Openvpn-devel] [PATCH v6 4/7] Rewrite auth-token-gen to be based on HMAC based tokens X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox The previous auth-token implementation had a serious problem, especially when paired with an unpatched OpenVPN client that keeps trying the auth-token (commit e61b401a). The auth-token-gen implementation forgot the auth-token on reconnect, this lead to reconnect with auth-token never working. This new implementation implements the auth-token in a stateles variant. By using HMAC to sign the auth-token the server can verify if a token has been authenticated and by checking the embedded timestamp in the token it can also verify that the auth-token is still valid. Using the new config directive auth-gen-token-secret instead of extending auth-gen-token (--auth-gen-token [lifetime] [secret-key]) was chosen to allow inlinening the secret key. Patch V2: cleaned up code, use refactored read_pem_key_file function Patch V3: clarify some design decision in the commit message Patch V4: Use ephermal_generate_key Patch V5: Use C99 PRIu64 instead of %lld int printf like statement, fix strict aliasing Acked-By: David Sommerseth --- doc/openvpn.8 | 25 ++++ src/openvpn/Makefile.am | 1 + src/openvpn/auth_token.c | 273 +++++++++++++++++++++++++++++++++++++++ src/openvpn/auth_token.h | 116 +++++++++++++++++ src/openvpn/init.c | 30 ++++- src/openvpn/openvpn.h | 1 + src/openvpn/options.c | 22 +++- src/openvpn/options.h | 4 + src/openvpn/push.c | 70 ++++++++-- src/openvpn/push.h | 8 ++ src/openvpn/ssl.c | 7 +- src/openvpn/ssl_common.h | 36 ++++-- src/openvpn/ssl_verify.c | 182 +++++++++++--------------- 13 files changed, 640 insertions(+), 135 deletions(-) create mode 100644 src/openvpn/auth_token.c create mode 100644 src/openvpn/auth_token.h diff --git a/doc/openvpn.8 b/doc/openvpn.8 index 4185ffed..a50d67d4 100644 --- a/doc/openvpn.8 +++ b/doc/openvpn.8 @@ -3720,6 +3720,9 @@ the token authentication internally and it will NOT do any additional authentications against configured external user/password authentication mechanisms. +The tokens implemented by this mechanism include a initial timestamp +and a renew timestamp and are secured by HMAC. + The .B lifetime argument defines how long the generated token is valid. The @@ -3730,6 +3733,16 @@ This feature is useful for environments which is configured to use One Time Passwords (OTP) as part of the user/password authentications and that authentication mechanism does not implement any auth\-token support. +.\"********************************************************* +.TP +.B \-\-auth\-gen\-token\-secret [file] +Specifies a file that hold a secret for the HMAC used in +.B \-\-auth\-gen\-token +If not present OpenVPN will generate a random secret on startup. This file +should be used if auth-token should valid after restarting a server or if +client should be able to roam between multiple OpenVPN server with their +auth\-token. + .\"********************************************************* .TP .B \-\-opt\-verify @@ -5775,6 +5788,17 @@ Servers can use .B \-\-tls\-crypt\-v2\-verify to specify a metadata verification command. .\"********************************************************* +.TP +.B \-\-genkey auth\-token [keyfile] +Generate a new secret that can be used +with +.B \-\-auth\-gen\-token\-secret + +.B Note: +this file should be kept secret to the server as anyone +that access to this file will be to generate auth tokens +that the OpenVPN server will accept as valid. +.\"********************************************************* .SS TUN/TAP persistent tunnel config mode: Available with Linux 2.4.7+. These options comprise a standalone mode of OpenVPN which can be used to create and delete persistent tunnels. @@ -6986,6 +7010,7 @@ X509_1_C=KG OpenVPN allows including files in the main configuration for the .B \-\-ca, \-\-cert, \-\-dh, \-\-extra\-certs, \-\-key, \-\-pkcs12, \-\-secret, .B \-\-crl\-verify, \-\-http\-proxy\-user\-pass, \-\-tls\-auth, +.B \-\-auth\-gen\-token\-secret .B \-\-tls\-crypt, and .B \-\-tls\-crypt-v2 diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am index 30caa01f..3754fd95 100644 --- a/src/openvpn/Makefile.am +++ b/src/openvpn/Makefile.am @@ -39,6 +39,7 @@ sbin_PROGRAMS = openvpn openvpn_SOURCES = \ argv.c argv.h \ + auth_token.c auth_token.h \ base64.c base64.h \ basic.h \ buffer.c buffer.h \ diff --git a/src/openvpn/auth_token.c b/src/openvpn/auth_token.c new file mode 100644 index 00000000..a6158d2a --- /dev/null +++ b/src/openvpn/auth_token.c @@ -0,0 +1,273 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#elif defined(_MSC_VER) +#include "config-msvc.h" +#endif + +#include "syshead.h" + +#include "base64.h" +#include "buffer.h" +#include "crypto.h" +#include "openvpn.h" +#include "ssl_common.h" +#include "auth_token.h" +#include "push.h" +#include "integer.h" +#include "ssl.h" +#include + +const char *auth_token_pem_name = "OpenVPN auth-token server key"; + + +/* Size of the data of the token (not b64 encoded and without prefix) */ +#define TOKEN_DATA_LEN (2 * sizeof(int64_t) + 32) + +static struct key_type +auth_token_kt(void) +{ + struct key_type kt; + /* We do not encrypt our session tokens */ + kt.cipher = NULL; + kt.digest = md_kt_get("SHA256"); + + if (!kt.digest) + { + msg(M_WARN, "ERROR: --tls-crypt requires HMAC-SHA-256 support."); + return (struct key_type) { 0 }; + } + + kt.hmac_length = md_kt_size(kt.digest); + + return kt; +} + + +void +auth_token_write_server_key_file(const char *filename) +{ + write_pem_key_file(filename, auth_token_pem_name); +} + +void +auth_token_init_secret(struct key_ctx *key_ctx, const char *key_file, + const char *key_inline) +{ + struct key_type kt = auth_token_kt(); + + struct buffer server_secret_key = alloc_buf(2048); + + bool key_loaded = false; + if (key_file) + { + key_loaded = read_pem_key_file(&server_secret_key, + auth_token_pem_name, + key_file, key_inline); + } + else + { + key_loaded = generate_ephemeral_key(&server_secret_key, + auth_token_pem_name); + } + + if (!key_loaded) + { + msg(M_FATAL, "ERROR: Cannot load auth-token secret"); + } + + struct key key; + + if (!buf_read(&server_secret_key, &key, sizeof(key))) + { + msg(M_FATAL, "ERROR: not enough data in auth-token secret"); + } + init_key_ctx(key_ctx, &key, &kt, false, "auth-token secret"); + + free_buf(&server_secret_key); +} + +void +generate_auth_token(const struct user_pass *up, struct tls_multi *multi) +{ + struct gc_arena gc = gc_new(); + + int64_t timestamp = htonll((uint64_t)now); + int64_t initial_timestamp = timestamp; + + hmac_ctx_t *ctx = multi->opt.auth_token_key.hmac; + ASSERT(hmac_ctx_size(ctx) == 256/8); + + if (multi->auth_token) + { + /* Just enough space to fit 8 bytes+ 1 extra to decode a non padded + * base64 string (multiple of 3 bytes). 9 bytes => 12 bytes base64 + * bytes + */ + char old_tstamp_decode[9]; + + /* + * reuse the same session id and timestamp and null terminate it at + * for base64 decode it only decodes the session id part of it + */ + char *old_tsamp_initial = multi->auth_token + strlen(SESSION_ID_PREFIX); + + old_tsamp_initial[12] = '\0'; + ASSERT(openvpn_base64_decode(old_tsamp_initial, old_tstamp_decode, 9) == 9); + initial_timestamp = *((uint64_t *)(old_tstamp_decode)); + + /* free the auth-token, we will replace it with a new one */ + free(multi->auth_token); + } + uint8_t hmac_output[256/8]; + + hmac_ctx_reset(ctx); + hmac_ctx_update(ctx, (uint8_t *) up->username, (int)strlen(up->username)); + hmac_ctx_update(ctx, (uint8_t *) &initial_timestamp, sizeof(initial_timestamp)); + hmac_ctx_update(ctx, (uint8_t *) ×tamp, sizeof(timestamp)); + hmac_ctx_final(ctx, hmac_output); + + /* Construct the unencoded session token */ + struct buffer token = alloc_buf_gc( + 2*sizeof(uint64_t) + 256/8, &gc); + + ASSERT(buf_write(&token, &initial_timestamp, sizeof(initial_timestamp))); + ASSERT(buf_write(&token, ×tamp, sizeof(timestamp))); + ASSERT(buf_write(&token, hmac_output, sizeof(hmac_output))); + + char *b64output; + openvpn_base64_encode(BPTR(&token), BLEN(&token), &b64output); + + struct buffer session_token = alloc_buf_gc( + strlen(SESSION_ID_PREFIX) + strlen(b64output) + 1, &gc); + + ASSERT(buf_write(&session_token, SESSION_ID_PREFIX, strlen(SESSION_ID_PREFIX))); + ASSERT(buf_write(&session_token, b64output, (int)strlen(b64output))); + ASSERT(buf_write_u8(&session_token, 0)); + + free(b64output); + + multi->auth_token = strdup((char *)BPTR(&session_token)); + + dmsg(D_SHOW_KEYS, "Generated token for client: %s", + multi->auth_token); + + gc_free(&gc); +} + +static bool +check_hmac_token(hmac_ctx_t *ctx, const uint8_t *b64decoded, const char *username) +{ + ASSERT(hmac_ctx_size(ctx) == 256/8); + + uint8_t hmac_output[256/8]; + + hmac_ctx_reset(ctx); + hmac_ctx_update(ctx, (uint8_t *) username, (int)strlen(username)); + hmac_ctx_update(ctx, b64decoded, TOKEN_DATA_LEN - 256/8); + hmac_ctx_final(ctx, hmac_output); + + const uint8_t *hmac = b64decoded + TOKEN_DATA_LEN - 256/8; + return memcmp_constant_time(&hmac_output, hmac, 32) == 0; +} + +unsigned int +verify_auth_token(struct user_pass *up, struct tls_multi *multi, + struct tls_session *session) +{ + /* + * Base64 is <= input and input is < USER_PASS_LEN, so using USER_PASS_LEN + * is safe here but a bit overkill + */ + uint8_t b64decoded[USER_PASS_LEN]; + int decoded_len = openvpn_base64_decode(up->password + strlen(SESSION_ID_PREFIX), + b64decoded, USER_PASS_LEN); + /* Ensure that the decoded data is at least the size of the + * timestamp + hmac */ + + if (decoded_len != TOKEN_DATA_LEN) + { + msg(M_WARN, "ERROR: --auth-token wrong size (%d!=%d)", + decoded_len, (int) TOKEN_DATA_LEN); + return 0; + } + + unsigned int ret = 0; + + + const uint8_t *tstamp_initial = b64decoded; + const uint8_t *tstamp = tstamp_initial + sizeof(int64_t); + + uint64_t timestamp = ntohll(*((uint64_t *) (tstamp))); + uint64_t timestamp_initial = ntohll(*((uint64_t *) (tstamp_initial))); + + hmac_ctx_t *ctx = multi->opt.auth_token_key.hmac; + if (check_hmac_token(ctx, b64decoded, up->username)) + { + ret |= AUTH_TOKEN_HMAC_OK; + } + else + { + msg(M_WARN, "--auth-token-gen: HMAC on token from client failed (%s)", + up->username); + return 0; + } + + /* Accept session tokens that not expired are in the acceptable range + * for renogiations */ + bool in_renog_time = now >= timestamp + && now < timestamp + 2 * session->opt->renegotiate_seconds; + + /* We could still have a client that does not update + * its auth-token, so also allow the initial auth-token */ + bool initialtoken = multi->auth_token_initial + && memcmp_constant_time(up->password, multi->auth_token_initial, + strlen(multi->auth_token_initial)) == 0; + + if (!in_renog_time && !initialtoken) + { + ret |= AUTH_TOKEN_EXPIRED; + } + + /* Sanity check the initial timestamp */ + if (timestamp < timestamp_initial) + { + msg(M_WARN, "Initial timestamp (%" PRIu64 " in token from client earlier than " + "current timestamp %" PRIu64 ". Broken/unsynchronised clock?", + timestamp_initial, timestamp); + ret |= AUTH_TOKEN_EXPIRED; + } + + if (multi->opt.auth_token_lifetime + && now > timestamp_initial + multi->opt.auth_token_lifetime) + { + ret |= AUTH_TOKEN_EXPIRED; + } + + if (ret & AUTH_TOKEN_EXPIRED) + { + msg(M_INFO, "--auth-token-gen: auth-token from client expired"); + } + + return ret; +} + +void +wipe_auth_token(struct tls_multi *multi) +{ + if (multi) + { + if (multi->auth_token) + { + secure_memzero(multi->auth_token, strlen(multi->auth_token)); + free(multi->auth_token); + } + if (multi->auth_token_initial) + { + secure_memzero(multi->auth_token_initial, + strlen(multi->auth_token_initial)); + free(multi->auth_token_initial); + } + multi->auth_token = NULL; + multi->auth_token_initial = NULL; + } +} diff --git a/src/openvpn/auth_token.h b/src/openvpn/auth_token.h new file mode 100644 index 00000000..600ac29f --- /dev/null +++ b/src/openvpn/auth_token.h @@ -0,0 +1,116 @@ +/* + * OpenVPN -- An application to securely tunnel IP networks + * over a single TCP/UDP port, with support for SSL/TLS-based + * session authentication and key exchange, + * packet encryption, packet authentication, and + * packet compression. + * + * Copyright (C) 2002-2018 OpenVPN Inc + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#ifndef AUTH_TOKEN_H +#define AUTH_TOKEN_H + +/** + * Generate an auth token based on username and timestamp + * + * The idea of auth token is to be stateless, so that we can verify use it + * even after we have forgotten about it or server has been restarted. + * + * To achieve this even though we cannot trust the client we use HMAC + * to be able to verify the information. + * + * Format of the auth-token (before base64 encode) + * + * uint64 timestamp (4 bytes)|uint64 timestamp (4 bytes)|sha256-hmac(32 bytes) + * + * The first timestamp is the time the token was initially created and is used to + * determine the maximum renewable time of the token. We always include this even + * if tokens do not expire (this value is not used) to keep the code cleaner. + * + * 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) + * + * The hmac is calculated over the username contactinated with the + * raw auth-token bytes to include authentication of the username in the token + * + * we prepend the session id with SESS_ID_ before sending it to the client + */ +void +generate_auth_token(const struct user_pass *up, struct tls_multi *multi); + +/** + * Verifies the auth token to be in the format that generate_auth_token + * create and checks if the token is valid. + * + * Also calls generate_auth_token to update the auth-token to extend + * its validity + */ +unsigned +verify_auth_token(struct user_pass *up, struct tls_multi *multi, + struct tls_session *session); + + + +/** + * Loads an HMAC secret from a file or if no file is present generates a + * epheremal secret for the run time of the server and stores it into ctx + */ +void +auth_token_init_secret(struct key_ctx *key_ctx, const char *key_file, + const char *key_inline); + + +/** + * Generate a auth-token server secret key, and write to file. + * + * @param filename Filename of the server key file to create. + */ +void auth_token_write_server_key_file(const char *filename); + + +/** + * Wipes the authentication token out of the memory, frees and cleans up + * related buffers and flags + * + * @param multi Pointer to a multi object holding the auth_token variables + */ +void wipe_auth_token(struct tls_multi *multi); + +/** + * The prefix given to auth tokens start with, this prefix is special + * cased to not show up in log files in OpenVPN 2 and 3 + * + * We also prefix this with _AT_ to only act on auth token generated by us. + */ +#define SESSION_ID_PREFIX "SESS_ID_AT_" + +/** + * Return if the password string has the format of a password. + * + * This fuction will always read as many bytes as SESSION_ID_PREFIX is longer + * the caller needs ensure that password memory is at least that long (true for + * calling with struct user_pass) + * @param password + * @return whether the password string starts with the session token prefix + */ +static inline bool +is_auth_token(const char *password) +{ + return (memcmp_constant_time(SESSION_ID_PREFIX, password, + strlen(SESSION_ID_PREFIX)) == 0); +} +#endif /* AUTH_TOKEN_H */ diff --git a/src/openvpn/init.c b/src/openvpn/init.c index b5a034dc..2efd0828 100644 --- a/src/openvpn/init.c +++ b/src/openvpn/init.c @@ -51,6 +51,7 @@ #include "ssl_verify.h" #include "tls_crypt.h" #include "forward.h" +#include "auth_token.h" #include "memdbg.h" @@ -1118,6 +1119,11 @@ do_genkey(const struct options *options) options->tls_crypt_v2_inline); return true; } + else if (options->genkey && options->genkey_type == GENKEY_AUTH_TOKEN) + { + auth_token_write_server_key_file(options->genkey_filename); + return true; + } else { return false; @@ -2534,7 +2540,6 @@ init_crypto_pre(struct context *c, const unsigned int flags) rand_ctx_enable_prediction_resistance(); } #endif - } /* @@ -2658,6 +2663,22 @@ do_init_tls_wrap_key(struct context *c) } +/* + * Initialise the auth-token key context + */ +static void +do_init_auth_token_key(struct context *c) +{ + if (!c->options.auth_token_generate) + { + return; + } + + auth_token_init_secret(&c->c1.ks.auth_token_key, + c->options.auth_token_secret_file, + c->options.auth_token_secret_file_inline); +} + /* * Initialize the persistent component of OpenVPN's TLS mode, * which is preserved across SIGUSR1 resets. @@ -2710,6 +2731,9 @@ do_init_crypto_tls_c1(struct context *c) /* initialize tls-auth/crypt/crypt-v2 key */ do_init_tls_wrap_key(c); + /* initialise auth-token crypto support */ + do_init_auth_token_key(c); + #if 0 /* was: #if ENABLE_INLINE_FILES -- Note that enabling this code will break restarts */ if (options->priv_key_file_inline) { @@ -2883,6 +2907,7 @@ do_init_crypto_tls(struct context *c, const unsigned int flags) to.auth_user_pass_file = options->auth_user_pass_file; to.auth_token_generate = options->auth_token_generate; to.auth_token_lifetime = options->auth_token_lifetime; + to.auth_token_key = c->c1.ks.auth_token_key; #endif to.x509_track = options->x509_track; @@ -4499,6 +4524,9 @@ inherit_context_child(struct context *dest, dest->c1.authname = src->c1.authname; dest->c1.keysize = src->c1.keysize; + /* inherit auth-token */ + dest->c1.ks.auth_token_key = src->c1.ks.auth_token_key; + /* options */ dest->options = src->options; options_detach(&dest->options); diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h index 29d21f0a..900db7e1 100644 --- a/src/openvpn/openvpn.h +++ b/src/openvpn/openvpn.h @@ -68,6 +68,7 @@ struct key_schedule struct key_ctx_bi tls_wrap_key; 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 2d3865a6..fbba93c9 100644 --- a/src/openvpn/options.c +++ b/src/openvpn/options.c @@ -1289,6 +1289,7 @@ show_p2mp_parms(const struct options *o) SHOW_BOOL(auth_user_pass_verify_script_via_file); SHOW_BOOL(auth_token_generate); SHOW_INT(auth_token_lifetime); + SHOW_STR(auth_token_secret_file); #if PORT_SHARE SHOW_STR(port_share_host); SHOW_STR(port_share_port); @@ -2336,7 +2337,11 @@ options_postprocess_verify_ce(const struct options *options, const struct connec { msg(M_USAGE, "--mode server requires --key-method 2"); } - + if (options->auth_token_generate && !options->renegotiate_seconds) + { + msg(M_USAGE, "--auth-gen-token needs a non-infinite " + "--renegotiate_seconds setting"); + } { const bool ccnr = (options->auth_user_pass_verify_script || PLUGIN_OPTION_LIST(options) @@ -6771,6 +6776,17 @@ add_option(struct options *options, options->auth_token_generate = true; options->auth_token_lifetime = p[1] ? positive_atoi(p[1]) : 0; } + else if (streq(p[0], "auth-gen-token-secret") && p[1] && (!p[2] + || (p[2] && streq(p[1], INLINE_FILE_TAG)))) + { + VERIFY_PERMISSION(OPT_P_GENERAL); + options->auth_token_secret_file = p[1]; + + if (streq(p[1], INLINE_FILE_TAG) && p[2]) + { + options->auth_token_secret_file_inline = p[2]; + } + } else if (streq(p[0], "client-connect") && p[1]) { VERIFY_PERMISSION(OPT_P_SCRIPT); @@ -7546,6 +7562,10 @@ add_option(struct options *options, options->genkey_extra_data = p[3]; } } + else if (streq(p[1], "auth-token")) + { + options->genkey_type = GENKEY_AUTH_TOKEN; + } else { msg(msglevel, "unknown --genkey type: %s", p[1]); diff --git a/src/openvpn/options.h b/src/openvpn/options.h index 63f0f4cb..d885f4d9 100644 --- a/src/openvpn/options.h +++ b/src/openvpn/options.h @@ -181,6 +181,7 @@ enum genkey_type { GENKEY_SECRET, GENKEY_TLS_CRYPTV2_CLIENT, GENKEY_TLS_CRYPTV2_SERVER, + GENKEY_AUTH_TOKEN }; /* Command line options */ @@ -469,6 +470,9 @@ struct options bool auth_user_pass_verify_script_via_file; bool auth_token_generate; unsigned int auth_token_lifetime; + const char *auth_token_secret_file; + const char *auth_token_secret_file_inline; + #if PORT_SHARE char *port_share_host; char *port_share_port; diff --git a/src/openvpn/push.c b/src/openvpn/push.c index 248c2e46..dc1a536a 100644 --- a/src/openvpn/push.c +++ b/src/openvpn/push.c @@ -331,6 +331,37 @@ send_push_request(struct context *c) } #if P2MP_SERVER +/** + * Prepare push option for auth-token + * @param tls_multi tls multi context of VPN tunnel + * @param gc gc arena for allocating push options + * @param push_list push list to where options are added + * + * @return true on success, false on failure. + */ +void +prepare_auth_token_push_reply(struct tls_multi *tls_multi, struct gc_arena *gc, + struct push_list *push_list) +{ + /* + * If server uses --auth-gen-token and we have an auth token + * to send to the client + */ + if (tls_multi->auth_token) + { + push_option_fmt(gc, push_list, M_USAGE, + "auth-token %s", + tls_multi->auth_token); + if (!tls_multi->auth_token_initial) + { + /* + * Save the initial auth token for clients that ignore + * the updates to the token + */ + tls_multi->auth_token_initial = strdup(tls_multi->auth_token); + } + } +} /** * Prepare push options, based on local options and available peer info. @@ -341,7 +372,7 @@ send_push_request(struct context *c) * * @return true on success, false on failure. */ -static bool +bool prepare_push_reply(struct context *c, struct gc_arena *gc, struct push_list *push_list) { @@ -389,6 +420,11 @@ prepare_push_reply(struct context *c, struct gc_arena *gc, tls_multi->use_peer_id = true; } } + /* + * If server uses --auth-gen-token and we have an auth token + * to send to the client + */ + prepare_auth_token_push_reply(tls_multi, gc, push_list); /* Push cipher if client supports Negotiable Crypto Parameters */ if (tls_peer_info_ncp_ver(peer_info) >= 2 && o->ncp_enabled) @@ -419,15 +455,6 @@ prepare_push_reply(struct context *c, struct gc_arena *gc, tls_poor_mans_ncp(o, tls_multi->remote_ciphername); } - /* If server uses --auth-gen-token and we have an auth token - * to send to the client - */ - if (false == tls_multi->auth_token_sent && NULL != tls_multi->auth_token) - { - push_option_fmt(gc, push_list, M_USAGE, - "auth-token %s", tls_multi->auth_token); - tls_multi->auth_token_sent = true; - } return true; } @@ -438,6 +465,7 @@ send_push_options(struct context *c, struct buffer *buf, { struct push_entry *e = push_list->head; + e = push_list->head; while (e) { if (e->enable) @@ -470,7 +498,27 @@ send_push_options(struct context *c, struct buffer *buf, return true; } -static bool +void +send_push_reply_auth_token(struct tls_multi *multi) +{ + struct gc_arena gc = gc_new(); + + + struct push_list push_list = {}; + prepare_auth_token_push_reply(multi, &gc, &push_list); + + /* prepare auth token should always add the auth-token option */ + struct push_entry *e = push_list.head; + ASSERT(e && e->enable); + + /* Construct a mimimal control channel push reply message */ + struct buffer buf = alloc_buf_gc(PUSH_BUNDLE_SIZE, &gc); + buf_printf(&buf, "%s, %s", push_reply_cmd, e->option); + send_control_channel_string_dowork(multi, BSTR(&buf), D_PUSH); + gc_free(&gc); +} + +bool send_push_reply(struct context *c, struct push_list *per_client_push_list) { struct gc_arena gc = gc_new(); diff --git a/src/openvpn/push.h b/src/openvpn/push.h index 5f6181e7..070782dd 100644 --- a/src/openvpn/push.h +++ b/src/openvpn/push.h @@ -69,6 +69,14 @@ void send_auth_failed(struct context *c, const char *client_reason); void send_restart(struct context *c, const char *kill_msg); +/** + * Sends a push reply message only containin the auth-token to update + * the auth-token on the client + * + * @param multi - The tls_multi structure belonging to the instance to push to + */ +void send_push_reply_auth_token(struct tls_multi *multi); + #endif #endif /* if P2MP */ #endif /* ifndef PUSH_H */ diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c index abc3c53b..0d79f91e 100644 --- a/src/openvpn/ssl.c +++ b/src/openvpn/ssl.c @@ -59,6 +59,7 @@ #include "ssl.h" #include "ssl_verify.h" #include "ssl_backend.h" +#include "auth_token.h" #include "memdbg.h" @@ -1368,11 +1369,7 @@ tls_multi_free(struct tls_multi *multi, bool clear) cert_hash_free(multi->locked_cert_hash_set); - if (multi->auth_token) - { - secure_memzero(multi->auth_token, AUTH_TOKEN_SIZE); - free(multi->auth_token); - } + wipe_auth_token(multi); free(multi->remote_ciphername); diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h index 0312c1f8..5e19ab56 100644 --- a/src/openvpn/ssl_common.h +++ b/src/openvpn/ssl_common.h @@ -298,7 +298,6 @@ struct tls_options /** TLS handshake wrapping state */ struct tls_wrap_ctx tls_wrap; - /* frame parameters for TLS control channel */ struct frame frame; /* used for username/password authentication */ @@ -306,10 +305,16 @@ struct tls_options bool auth_user_pass_verify_script_via_file; const char *tmp_dir; const char *auth_user_pass_file; - bool auth_token_generate; /**< Generate auth-tokens on successful user/pass auth, - * set via options->auth_token_generate. */ + +#ifdef P2MP_SERVER + bool auth_token_generate; /**< Generate auth-tokens on successful + * user/pass auth,seet via + * options->auth_token_generate. */ unsigned int auth_token_lifetime; + struct key_ctx auth_token_key; +#endif + /* use the client-config-dir as a positive authenticator */ const char *client_config_dir_exclusive; @@ -369,10 +374,6 @@ struct tls_options /** @} name Index of key_state objects within a tls_session structure */ /** @} addtogroup control_processor */ -#define AUTH_TOKEN_SIZE 32 /**< Size of server side generated auth tokens. - * 32 bytes == 256 bits - */ - /** * Security parameter state of a single session within a VPN tunnel. * @ingroup control_processor @@ -540,7 +541,21 @@ struct tls_multi * over control channel. */ char *peer_info; + char *auth_token; /**< If server sends a generated auth-token, + * this is the token to use for future + * user/pass authentications in this session. + */ + char *auth_token_initial; + /**< The first auth-token we sent to a client, for clients that do + * not update their auth-token (older OpenVPN3 core versions) + */ +#define AUTH_TOKEN_HMAC_OK (1<<0) + /**< Auth-token sent from client has valid hmac */ +#define AUTH_TOKEN_EXPIRED (1<<1) + /**< Auth-token sent from client has expired */ #endif + int auth_token_state_flags; + /**< The state of the auth-token sent from the client last time */ /* For P_DATA_V2 */ uint32_t peer_id; @@ -548,13 +563,6 @@ struct tls_multi char *remote_ciphername; /**< cipher specified in peer's config file */ - char *auth_token; /**< If server sends a generated auth-token, - * this is the token to use for future - * user/pass authentications in this session. - */ - time_t auth_token_tstamp; /**< timestamp of the generated token */ - bool auth_token_sent; /**< If server uses --auth-gen-token and - * token has been sent to client */ /* * Our session objects. */ diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c index a7f51751..b392b0ac 100644 --- a/src/openvpn/ssl_verify.c +++ b/src/openvpn/ssl_verify.c @@ -44,6 +44,8 @@ #ifdef ENABLE_CRYPTO_OPENSSL #include "ssl_verify_openssl.h" #endif +#include "auth_token.h" +#include "push.h" /** Maximum length of common name */ #define TLS_USERNAME_LEN 64 @@ -63,28 +65,6 @@ setenv_untrusted(struct tls_session *session) setenv_link_socket_actual(session->opt->es, "untrusted", &session->untrusted_addr, SA_IP_PORT); } - -/** - * Wipes the authentication token out of the memory, frees and cleans up related buffers and flags - * - * @param multi Pointer to a multi object holding the auth_token variables - */ -static void -wipe_auth_token(struct tls_multi *multi) -{ - if (multi) - { - if (multi->auth_token) - { - secure_memzero(multi->auth_token, AUTH_TOKEN_SIZE); - free(multi->auth_token); - } - multi->auth_token = NULL; - multi->auth_token_sent = false; - } -} - - /* * Remove authenticated state from all sessions in the given tunnel */ @@ -1253,6 +1233,7 @@ verify_user_pass_management(struct tls_session *session, const struct user_pass } #endif /* ifdef MANAGEMENT_DEF_AUTH */ + /* * Main username/password verification entry point */ @@ -1277,86 +1258,67 @@ verify_user_pass(struct user_pass *up, struct tls_multi *multi, string_mod_remap_name(up->username); string_mod(up->password, CC_PRINT, CC_CRLF, '_'); - /* If server is configured with --auth-gen-token and we have an - * authentication token for this client, this authentication + /* + * If auth token succeeds we skip the auth + * methods unless otherwise specified + */ + bool skip_auth = false; + + /* + * If server is configured with --auth-gen-token and the client sends + * something that looks like an authentication token, this * round will be done internally using the token instead of * calling any external authentication modules. */ - if (session->opt->auth_token_generate && multi->auth_token_sent - && NULL != multi->auth_token) + if (session->opt->auth_token_generate && is_auth_token(up->password)) { - unsigned int ssl_flags = session->opt->ssl_flags; - - /* Ensure that the username has not changed */ - if (!tls_lock_username(multi, up->username)) + multi->auth_token_state_flags = verify_auth_token(up, multi,session); + if (multi->auth_token_state_flags == AUTH_TOKEN_HMAC_OK) { - /* auth-token cleared in tls_lock_username() on failure */ - ks->authenticated = false; - return; + /* + * We do not want the EXPIRED flag here so check + * for equality with AUTH_TOKEN_HMAC_OK + */ + msg(M_WARN, "TLS: Username/auth-token authentication " + "succeeded for username '%s'", + up->username); + skip_auth = true; } - - /* If auth-token lifetime has been enabled, - * ensure the token has not expired - */ - if (session->opt->auth_token_lifetime > 0 - && (multi->auth_token_tstamp + session->opt->auth_token_lifetime) < now) + else { - msg(D_HANDSHAKE, "Auth-token for client expired\n"); wipe_auth_token(multi); ks->authenticated = false; + msg(M_WARN, "TLS: Username/auth-token authentication " + "failed for username '%s'", up->username); return; } + } + if (!skip_auth) + { - /* The core authentication of the token itself */ - if (memcmp_constant_time(multi->auth_token, up->password, - strlen(multi->auth_token)) != 0) + /* call plugin(s) and/or script */ +#ifdef MANAGEMENT_DEF_AUTH + if (man_def_auth == KMDA_DEF) { - ks->authenticated = false; - tls_deauthenticate(multi); - - msg(D_TLS_ERRORS, "TLS Auth Error: Auth-token verification " - "failed for username '%s' %s", up->username, - (ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) ? "[CN SET]" : ""); + man_def_auth = verify_user_pass_management(session, up); } - else +#endif + if (plugin_defined(session->opt->plugins, OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY)) { - ks->authenticated = true; - - if (ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) - { - set_common_name(session, up->username); - } - msg(D_HANDSHAKE, "TLS: Username/auth-token authentication " - "succeeded for username '%s' %s", - up->username, - (ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) ? "[CN SET]" : ""); + s1 = verify_user_pass_plugin(session, up); + } + if (session->opt->auth_user_pass_verify_script) + { + s2 = verify_user_pass_script(session, up); } - return; - } - - /* call plugin(s) and/or script */ -#ifdef MANAGEMENT_DEF_AUTH - if (man_def_auth == KMDA_DEF) - { - man_def_auth = verify_user_pass_management(session, up); - } -#endif - if (plugin_defined(session->opt->plugins, OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY)) - { - s1 = verify_user_pass_plugin(session, up); - } - if (session->opt->auth_user_pass_verify_script) - { - s2 = verify_user_pass_script(session, up); - } - /* check sizing of username if it will become our common name */ - if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) && strlen(up->username) > TLS_USERNAME_LEN) - { - msg(D_TLS_ERRORS, "TLS Auth Error: --username-as-common name specified and username is longer than the maximum permitted Common Name length of %d characters", TLS_USERNAME_LEN); - s1 = OPENVPN_PLUGIN_FUNC_ERROR; + /* check sizing of username if it will become our common name */ + if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) && strlen(up->username) > TLS_USERNAME_LEN) + { + msg(D_TLS_ERRORS, "TLS Auth Error: --username-as-common name specified and username is longer than the maximum permitted Common Name length of %d characters", TLS_USERNAME_LEN); + s1 = OPENVPN_PLUGIN_FUNC_ERROR; + } } - /* auth succeeded? */ if ((s1 == OPENVPN_PLUGIN_FUNC_SUCCESS #ifdef PLUGIN_DEF_AUTH @@ -1381,35 +1343,49 @@ verify_user_pass(struct user_pass *up, struct tls_multi *multi, ks->auth_deferred = true; } #endif + if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME)) + { + set_common_name(session, up->username); + } - if ((session->opt->auth_token_generate) && (NULL == multi->auth_token)) + if ((session->opt->auth_token_generate)) { - /* Server is configured with --auth-gen-token but no token has yet - * been generated for this client. Generate one and save it. + /* + * If we accepted a (not expired) token, i.e. + * initial auth via token on new connection, we need + * to store the auth-token in multi->auth_token, so + * the initial timestamp and session id can be extracted from it */ - uint8_t tok[AUTH_TOKEN_SIZE]; - - if (!rand_bytes(tok, AUTH_TOKEN_SIZE)) + if (multi->auth_token && (multi->auth_token_state_flags & AUTH_TOKEN_HMAC_OK) + && !(multi->auth_token_state_flags & AUTH_TOKEN_EXPIRED)) { - msg( M_FATAL, "Failed to get enough randomness for " - "authentication token"); + multi->auth_token = strdup(up->password); } - /* The token should be longer than the input when - * being base64 encoded + /* + * Server is configured with --auth-gen-token but no token has yet + * been generated for this client. Generate one and save it. */ - ASSERT(openvpn_base64_encode(tok, AUTH_TOKEN_SIZE, - &multi->auth_token) > AUTH_TOKEN_SIZE); - multi->auth_token_tstamp = now; - dmsg(D_SHOW_KEYS, "Generated token for client: %s", - multi->auth_token); + generate_auth_token(up, multi); } - - if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME)) + /* + * Auth token already sent to client, update auth-token on client. + * The initial auth-token is sent as part of the push message, for this + * update we need to schedule an extra push message. + */ + if (multi->auth_token_initial) { - set_common_name(session, up->username); + /* + * We do not explicitly schedule the sending of the + * control message here but control message are only + * postponed when the control channel is not yet fully + * established and furthermore since this is called in + * the middle of authentication, there are other messages + * (new data channel keys) that are sent anyway and will + * trigger schedueling + */ + send_push_reply_auth_token(multi); } - #ifdef ENABLE_DEF_AUTH msg(D_HANDSHAKE, "TLS: Username/Password authentication %s for username '%s' %s", ks->auth_deferred ? "deferred" : "succeeded", From patchwork Thu Aug 8 04:52:16 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Arne Schwabe X-Patchwork-Id: 799 Return-Path: Delivered-To: patchwork@openvpn.net Delivered-To: patchwork@openvpn.net Received: from director12.mail.ord1d.rsapps.net ([172.30.191.6]) by backend30.mail.ord1d.rsapps.net with LMTP id YBF/Hn43TF0IOwAAIUCqbw for ; Thu, 08 Aug 2019 10:53:50 -0400 Received: from proxy12.mail.ord1d.rsapps.net ([172.30.191.6]) by director12.mail.ord1d.rsapps.net with LMTP id +Bk0Hn43TF0/CAAAIasKDg ; Thu, 08 Aug 2019 10:53:50 -0400 Received: from smtp23.gate.ord1d ([172.30.191.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) by proxy12.mail.ord1d.rsapps.net with LMTP id WDqvHX43TF1DegAA7PHxkg ; Thu, 08 Aug 2019 10:53:50 -0400 X-Spam-Threshold: 95 X-Spam-Score: 0 X-Spam-Flag: NO X-Virus-Scanned: OK X-Orig-To: openvpnslackdevel@openvpn.net X-Originating-Ip: [216.105.38.7] Authentication-Results: smtp23.gate.ord1d.rsapps.net; iprev=pass policy.iprev="216.105.38.7"; spf=pass smtp.mailfrom="openvpn-devel-bounces@lists.sourceforge.net" smtp.helo="lists.sourceforge.net"; dkim=fail (signature verification failed) header.d=sourceforge.net; dkim=fail (signature verification failed) header.d=sf.net; dmarc=none (p=nil; dis=none) header.from=rfc2549.org X-Suspicious-Flag: YES X-Classification-ID: 536d94fa-b9ec-11e9-97a1-525400bfb165-1-1 Received: from [216.105.38.7] ([216.105.38.7:37348] helo=lists.sourceforge.net) by smtp23.gate.ord1d.rsapps.net (envelope-from ) (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) id 11/2F-32272-B773C4D5; Thu, 08 Aug 2019 10:53:48 -0400 Received: from [127.0.0.1] (helo=sfs-ml-1.v29.lw.sourceforge.com) by sfs-ml-1.v29.lw.sourceforge.com with esmtp (Exim 4.90_1) (envelope-from ) id 1hvjmS-0006Af-Gp; Thu, 08 Aug 2019 14:52:40 +0000 Received: from [172.30.20.202] (helo=mx.sourceforge.net) by sfs-ml-1.v29.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) (envelope-from ) id 1hvjmR-0006AO-HD for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:52:39 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=References:In-Reply-To:Message-Id:Date:Subject:Cc: To:From:Sender:Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding: Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=Uaj/NklQb4x9GTuJogdSQBmEvP/mSod30rxYB+44tYs=; b=C+kMigjxpSMsNUUyRhLqIaTMJy gHVwYPAvsNjA5gUCyMg5I04btySftGZBl8eJtpZI4XG3+9Hft5j8ZESDOLlbcUG3D//H6nvjPrvMX 8u1WAycN/wqtwf6zsYKvd9T9jTns6WLqupeFyqE4LztTH2vLkyxHMrExUnysVhJLUFwo=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=References:In-Reply-To:Message-Id:Date:Subject:Cc:To:From:Sender:Reply-To :MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=Uaj/NklQb4x9GTuJogdSQBmEvP/mSod30rxYB+44tYs=; b=guoXo2B/2+dHXCZLSsNuEFZYpW XOtudwsM6UCMefORUUoSAh2wyXQXgQDAiWdxbmNVbQrMHpUch2wXdgBOAjBboTQwOnSujpJW3CTBE RLzvIhOUW46Z0MPLqK2YEPd8U3LfUSGluFt/refJm/kB1h+/2uzRCfCAINWYVowuNnEE=; Received: from [192.26.174.232] (helo=mail.blinkt.de) by sfi-mx-4.v28.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) id 1hvjmL-00F5nB-5Z for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:52:39 +0000 Received: from kamera.blinkt.de ([2001:638:502:390:20c:29ff:fec8:535c]) by mail.blinkt.de with smtp (Exim 4.92 (FreeBSD)) (envelope-from ) id 1hvjm4-000IgW-84; Thu, 08 Aug 2019 16:52:16 +0200 Received: (nullmailer pid 1243 invoked by uid 10006); Thu, 08 Aug 2019 14:52:16 -0000 From: Arne Schwabe To: openvpn-devel@lists.sourceforge.net Date: Thu, 8 Aug 2019 16:52:16 +0200 Message-Id: <20190808145216.1198-1-arne@rfc2549.org> X-Mailer: git-send-email 2.17.1 In-Reply-To: <20190613134834.5709-4-arne@rfc2549.org> References: <20190613134834.5709-4-arne@rfc2549.org> X-Spam-Report: Spam Filtering performed by mx.sourceforge.net. See http://spamassassin.org/tag/ for more details. 0.0 URIBL_BLOCKED ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [URIs: openvpn.net] 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level mail domains are different 0.0 SPF_NONE SPF: sender does not publish an SPF Record 0.0 SPF_HELO_NONE SPF: HELO does not publish an SPF Record 1.0 RDNS_NONE Delivered to internal network by a host with no rDNS X-Headers-End: 1hvjmL-00F5nB-5Z Subject: [Openvpn-devel] [PATCH v6 5/7] Implement a permanent session id in auth-token X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Arne Schwabe MIME-Version: 1.0 Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox From: Arne Schwabe This allows an external authentication method (e.g. management interface) to track the connection and distinguish a reconnection from multiple connections. Addtionally this now also checks to workaround a problem with OpenVPN 3 core that sometimes uses a username hint from the config instead of using an empty username if the token would be valid with an empty username. Accepting such token can be only done explicitly when the external-auth keyword to auth-gen-token is present. Patch V2: Add Empty variants to work around behaviour in openvpn 3 Patch V3: document the behaviour of external-auth better in the man page, rename 'auth' parameter to 'external-auth' Patch V4: Rebase on current master Patch V6: Fix tls_lock_username rejecting clients with empty username when explicitly accepting them with external-auth --- doc/openvpn.8 | 37 +++++++++- src/openvpn/auth_token.c | 156 ++++++++++++++++++++++++++++++++++++--- src/openvpn/auth_token.h | 15 +++- src/openvpn/init.c | 1 + src/openvpn/manage.c | 4 +- src/openvpn/options.c | 14 +++- src/openvpn/options.h | 4 +- src/openvpn/ssl_common.h | 12 ++- src/openvpn/ssl_verify.c | 67 +++++++++++------ 9 files changed, 270 insertions(+), 40 deletions(-) diff --git a/doc/openvpn.8 b/doc/openvpn.8 index a50d67d4..19c386a0 100644 --- a/doc/openvpn.8 +++ b/doc/openvpn.8 @@ -3710,7 +3710,7 @@ For a sample script that performs PAM authentication, see in the OpenVPN source distribution. .\"********************************************************* .TP -.B \-\-auth\-gen\-token [lifetime] +.B \-\-auth\-gen\-token [lifetime] [auth] After successful user/password authentication, the OpenVPN server will with this option generate a temporary authentication token and push that to client. On the following @@ -3733,6 +3733,41 @@ This feature is useful for environments which is configured to use One Time Passwords (OTP) as part of the user/password authentications and that authentication mechanism does not implement any auth\-token support. + +When the external-auth keyword is present the normal authentication +method will always be called even if auth-token succeeds. Normally other +authentications method are skipped if auth-token verification suceeds or +fails. + +This option postpones this decision to the external authentication methods +and check the validity of the account and do other checks. + +In this mode the environment will have a session_id variable +that hold the session id from auth-gen-token. Also a environment +variable session_state is present. This variable tells whether the +auth-token has succeeded or not. It can have the following values: + + - Initial: No token from client. + - Authenticated: Token is valid and not expired + - Expired: Token is valid but has expired + - Invalid Token is invalid (failed HMAC or wrong length) + - AuthenticatedEmptyUser/ExpiredEmptyUser + The token is not valid with the username send from the client + but would be valid (or expired) if we assume an empty username was used + instead. These two cases are a workaround for behaviour in OpenVPN3. If + this workaround is not needed these two cases should be handled in the + same way as Invalid. + +.B Warning: +Use this feature only if you want your authentication method called on +every verification. Since the external authentication is called it needs +to also indicate a success or failure of the authentication. It is strongly +recommended to return an authentication failure in the case of the +Invalid/Expired auth-token with the external-auth option unless the client +could authenticate in another acceptable way (e.g. client certificate), +otherwise returning success will lead to authentication bypass (as does +returning success on a wrong password from a script). + .\"********************************************************* .TP .B \-\-auth\-gen\-token\-secret [file] diff --git a/src/openvpn/auth_token.c b/src/openvpn/auth_token.c index a6158d2a..5568a144 100644 --- a/src/openvpn/auth_token.c +++ b/src/openvpn/auth_token.c @@ -19,9 +19,13 @@ const char *auth_token_pem_name = "OpenVPN auth-token server key"; +#define AUTH_TOKEN_SESSION_ID_LEN 12 +#if AUTH_TOKEN_SESSION_ID_LEN % 3 +#error AUTH_TOKEN_SESSION_ID_LEN needs to be multiple a 3 +#endif /* Size of the data of the token (not b64 encoded and without prefix) */ -#define TOKEN_DATA_LEN (2 * sizeof(int64_t) + 32) +#define TOKEN_DATA_LEN (2 * sizeof(int64_t) + AUTH_TOKEN_SESSION_ID_LEN + 32) static struct key_type auth_token_kt(void) @@ -43,6 +47,85 @@ auth_token_kt(void) } +void +add_session_token_env(struct tls_session *session, struct tls_multi *multi, + const struct user_pass *up) +{ + if (!multi->opt.auth_token_generate) + { + return; + } + + + const char *state; + + if (!is_auth_token(up->password)) + { + state = "Initial"; + } + else if (multi->auth_token_state_flags & AUTH_TOKEN_HMAC_OK) + { + switch (multi->auth_token_state_flags & (AUTH_TOKEN_VALID_EMPTYUSER|AUTH_TOKEN_EXPIRED)) + { + case 0: + state = "Authenticated"; + break; + + case AUTH_TOKEN_EXPIRED: + state = "Expired"; + break; + + case AUTH_TOKEN_VALID_EMPTYUSER: + state = "AuthenticatedEmptyUser"; + break; + + case AUTH_TOKEN_VALID_EMPTYUSER | AUTH_TOKEN_EXPIRED: + state = "ExpiredEmptyUser"; + break; + + default: + /* Silence compiler warning, all four possible combinations are covered */ + ASSERT(0); + } + } + else + { + state = "Invalid"; + } + + setenv_str(session->opt->es, "session_state", state); + + /* We had a valid session id before */ + const char *session_id_source; + if (multi->auth_token_state_flags & AUTH_TOKEN_HMAC_OK + &!(multi->auth_token_state_flags & AUTH_TOKEN_EXPIRED)) + { + session_id_source = up->password; + } + else + { + /* + * No session before, generate a new session token for the new session + */ + if (!multi->auth_token) + { + generate_auth_token(up, multi); + } + session_id_source = multi->auth_token; + } + /* + * In the auth-token the auth token is already base64 encoded + * and being a multiple of 4 ensure that it a multiple of bytes + * in the encoding + */ + + char session_id[AUTH_TOKEN_SESSION_ID_LEN*2] = {0}; + memcpy(session_id, session_id_source + sizeof(SESSION_ID_PREFIX), + AUTH_TOKEN_SESSION_ID_LEN*8/6); + + setenv_str(session->opt->es, "session_id", session_id); +} + void auth_token_write_server_key_file(const char *filename) { @@ -97,6 +180,8 @@ generate_auth_token(const struct user_pass *up, struct tls_multi *multi) hmac_ctx_t *ctx = multi->opt.auth_token_key.hmac; ASSERT(hmac_ctx_size(ctx) == 256/8); + uint8_t sessid[AUTH_TOKEN_SESSION_ID_LEN]; + if (multi->auth_token) { /* Just enough space to fit 8 bytes+ 1 extra to decode a non padded @@ -109,27 +194,65 @@ generate_auth_token(const struct user_pass *up, struct tls_multi *multi) * reuse the same session id and timestamp and null terminate it at * for base64 decode it only decodes the session id part of it */ - char *old_tsamp_initial = multi->auth_token + strlen(SESSION_ID_PREFIX); + char *old_sessid = multi->auth_token + strlen(SESSION_ID_PREFIX); + char *old_tsamp_initial = old_sessid + AUTH_TOKEN_SESSION_ID_LEN*8/6; old_tsamp_initial[12] = '\0'; ASSERT(openvpn_base64_decode(old_tsamp_initial, old_tstamp_decode, 9) == 9); - initial_timestamp = *((uint64_t *)(old_tstamp_decode)); + + /* + * Avoid old gcc (4.8.x) complaining about strict aliasing + * by using a temporary variable instead of doing it in one + * line + */ + uint64_t *tstamp_ptr = (uint64_t *) old_tstamp_decode; + initial_timestamp = *tstamp_ptr; + + old_tsamp_initial[0] = '\0'; + ASSERT(openvpn_base64_decode(old_sessid, sessid, AUTH_TOKEN_SESSION_ID_LEN)==AUTH_TOKEN_SESSION_ID_LEN); + /* free the auth-token, we will replace it with a new one */ free(multi->auth_token); } + else if (!rand_bytes(sessid, AUTH_TOKEN_SESSION_ID_LEN)) + { + msg( M_FATAL, "Failed to get enough randomness for " + "authentication token"); + } + + /* Calculate the HMAC */ + /* We enforce up->username to be \0 terminated in ssl.c.. Allowing username + * with \0 in them is asking for troubles in so many ways anyway that we + * ignore that corner case here + */ uint8_t hmac_output[256/8]; hmac_ctx_reset(ctx); - hmac_ctx_update(ctx, (uint8_t *) up->username, (int)strlen(up->username)); + + /* + * If the token was only valid for the empty user, also generate + * a new token with the empty username since we do not want to loose + * the information that the username cannot be trusted + */ + if (multi->auth_token_state_flags & AUTH_TOKEN_VALID_EMPTYUSER) + { + hmac_ctx_update(ctx, (const uint8_t *) "", 0); + } + else + { + hmac_ctx_update(ctx, (uint8_t *) up->username, (int) strlen(up->username)); + } + hmac_ctx_update(ctx, sessid, AUTH_TOKEN_SESSION_ID_LEN); hmac_ctx_update(ctx, (uint8_t *) &initial_timestamp, sizeof(initial_timestamp)); hmac_ctx_update(ctx, (uint8_t *) ×tamp, sizeof(timestamp)); hmac_ctx_final(ctx, hmac_output); /* Construct the unencoded session token */ struct buffer token = alloc_buf_gc( - 2*sizeof(uint64_t) + 256/8, &gc); + 2*sizeof(uint64_t) + AUTH_TOKEN_SESSION_ID_LEN + 256/8, &gc); + ASSERT(buf_write(&token, sessid, sizeof(sessid))); ASSERT(buf_write(&token, &initial_timestamp, sizeof(initial_timestamp))); ASSERT(buf_write(&token, ×tamp, sizeof(timestamp))); ASSERT(buf_write(&token, hmac_output, sizeof(hmac_output))); @@ -148,12 +271,13 @@ generate_auth_token(const struct user_pass *up, struct tls_multi *multi) multi->auth_token = strdup((char *)BPTR(&session_token)); - dmsg(D_SHOW_KEYS, "Generated token for client: %s", - multi->auth_token); + dmsg(D_SHOW_KEYS, "Generated token for client: %s (%s)", + multi->auth_token, up->username); gc_free(&gc); } + static bool check_hmac_token(hmac_ctx_t *ctx, const uint8_t *b64decoded, const char *username) { @@ -181,9 +305,11 @@ verify_auth_token(struct user_pass *up, struct tls_multi *multi, uint8_t b64decoded[USER_PASS_LEN]; int decoded_len = openvpn_base64_decode(up->password + strlen(SESSION_ID_PREFIX), b64decoded, USER_PASS_LEN); - /* Ensure that the decoded data is at least the size of the - * timestamp + hmac */ + /* + * Ensure that the decoded data is the size of the + * timestamp + hmac + session id + */ if (decoded_len != TOKEN_DATA_LEN) { msg(M_WARN, "ERROR: --auth-token wrong size (%d!=%d)", @@ -193,8 +319,8 @@ verify_auth_token(struct user_pass *up, struct tls_multi *multi, unsigned int ret = 0; - - const uint8_t *tstamp_initial = b64decoded; + const uint8_t *sessid = b64decoded; + const uint8_t *tstamp_initial = sessid + AUTH_TOKEN_SESSION_ID_LEN; const uint8_t *tstamp = tstamp_initial + sizeof(int64_t); uint64_t timestamp = ntohll(*((uint64_t *) (tstamp))); @@ -205,6 +331,13 @@ verify_auth_token(struct user_pass *up, struct tls_multi *multi, { ret |= AUTH_TOKEN_HMAC_OK; } + else if (check_hmac_token(ctx, b64decoded, "")) + { + ret |= AUTH_TOKEN_HMAC_OK; + ret |= AUTH_TOKEN_VALID_EMPTYUSER; + /* overwrite the username of the client with the empty one */ + strcpy(up->username, ""); + } else { msg(M_WARN, "--auth-token-gen: HMAC on token from client failed (%s)", @@ -247,7 +380,6 @@ verify_auth_token(struct user_pass *up, struct tls_multi *multi, { msg(M_INFO, "--auth-token-gen: auth-token from client expired"); } - return ret; } diff --git a/src/openvpn/auth_token.h b/src/openvpn/auth_token.h index 600ac29f..c10afde9 100644 --- a/src/openvpn/auth_token.h +++ b/src/openvpn/auth_token.h @@ -34,7 +34,8 @@ * * Format of the auth-token (before base64 encode) * - * uint64 timestamp (4 bytes)|uint64 timestamp (4 bytes)|sha256-hmac(32 bytes) + * session id(12 bytes)|uint64 timestamp (4 bytes)| + * uint64 timestamp (4 bytes)|sha256-hmac(32 bytes) * * The first timestamp is the time the token was initially created and is used to * determine the maximum renewable time of the token. We always include this even @@ -44,6 +45,10 @@ * to determine if this token has been renewed in the acceptable time range * (2 * renogiation timeout) * + * The session 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/managment can track the + * session multiple reconnects/servers + * * The hmac is calculated over the username contactinated with the * raw auth-token bytes to include authentication of the username in the token * @@ -82,6 +87,14 @@ auth_token_init_secret(struct key_ctx *key_ctx, const char *key_file, void auth_token_write_server_key_file(const char *filename); +/** + * Put the session id, and auth token status into the environment + * if auth-token is enabled + * + */ +void add_session_token_env(struct tls_session *session, struct tls_multi *multi, + const struct user_pass *up); + /** * Wipes the authentication token out of the memory, frees and cleans up * related buffers and flags diff --git a/src/openvpn/init.c b/src/openvpn/init.c index 2efd0828..f395fe01 100644 --- a/src/openvpn/init.c +++ b/src/openvpn/init.c @@ -2907,6 +2907,7 @@ do_init_crypto_tls(struct context *c, const unsigned int flags) to.auth_user_pass_file = options->auth_user_pass_file; to.auth_token_generate = options->auth_token_generate; to.auth_token_lifetime = options->auth_token_lifetime; + to.auth_token_call_auth = options->auth_token_call_auth; to.auth_token_key = c->c1.ks.auth_token_key; #endif diff --git a/src/openvpn/manage.c b/src/openvpn/manage.c index 2d86dad4..1d97c2b6 100644 --- a/src/openvpn/manage.c +++ b/src/openvpn/manage.c @@ -2736,7 +2736,9 @@ env_filter_match(const char *env_str, const int env_filter_level) "ifconfig_pool_netmask=", "time_duration=", "bytes_sent=", - "bytes_received=" + "bytes_received=", + "session_id=", + "session_state=" }; if (env_filter_level == 0) diff --git a/src/openvpn/options.c b/src/openvpn/options.c index fbba93c9..752f5f2c 100644 --- a/src/openvpn/options.c +++ b/src/openvpn/options.c @@ -6770,11 +6770,23 @@ add_option(struct options *options, &options->auth_user_pass_verify_script, p[1], "auth-user-pass-verify", true); } - else if (streq(p[0], "auth-gen-token")) + else if (streq(p[0], "auth-gen-token") && !p[3]) { VERIFY_PERMISSION(OPT_P_GENERAL); options->auth_token_generate = true; options->auth_token_lifetime = p[1] ? positive_atoi(p[1]) : 0; + if (p[2]) + { + if (streq(p[2], "external-auth")) + { + options->auth_token_call_auth = true; + } + else + { + msg(msglevel, "Invalid argument to auth-gen-token: %s", p[2]); + } + } + } else if (streq(p[0], "auth-gen-token-secret") && p[1] && (!p[2] || (p[2] && streq(p[1], INLINE_FILE_TAG)))) diff --git a/src/openvpn/options.h b/src/openvpn/options.h index d885f4d9..8f722497 100644 --- a/src/openvpn/options.h +++ b/src/openvpn/options.h @@ -469,7 +469,9 @@ struct options const char *auth_user_pass_verify_script; bool auth_user_pass_verify_script_via_file; bool auth_token_generate; - unsigned int auth_token_lifetime; + bool auth_token_gen_secret_file; + bool auth_token_call_auth; + int auth_token_lifetime; const char *auth_token_secret_file; const char *auth_token_secret_file_inline; diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h index 5e19ab56..047aa59b 100644 --- a/src/openvpn/ssl_common.h +++ b/src/openvpn/ssl_common.h @@ -310,6 +310,7 @@ struct tls_options bool auth_token_generate; /**< Generate auth-tokens on successful * user/pass auth,seet via * options->auth_token_generate. */ + bool auth_token_call_auth; /**< always call normal authentication */ unsigned int auth_token_lifetime; struct key_ctx auth_token_key; @@ -470,7 +471,6 @@ struct tls_session */ #define KEY_SCAN_SIZE 3 - /** * Security parameter state for a single VPN tunnel. * @ingroup control_processor @@ -553,9 +553,17 @@ struct tls_multi /**< Auth-token sent from client has valid hmac */ #define AUTH_TOKEN_EXPIRED (1<<1) /**< Auth-token sent from client has expired */ -#endif +#define AUTH_TOKEN_VALID_EMPTYUSER (1<<2) + /**< + * Auth-token is only valid for an empty username + * and not the username actually supplied from the client + * + * OpenVPN 3 clients sometimes the empty username with a + * username hint from their config. + */ int auth_token_state_flags; /**< The state of the auth-token sent from the client last time */ +#endif /* For P_DATA_V2 */ uint32_t peer_id; diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c index b392b0ac..6535ad8d 100644 --- a/src/openvpn/ssl_verify.c +++ b/src/openvpn/ssl_verify.c @@ -1052,7 +1052,8 @@ tls_authenticate_key(struct tls_multi *multi, const unsigned int mda_key_id, con * Verify the user name and password using a script */ static bool -verify_user_pass_script(struct tls_session *session, const struct user_pass *up) +verify_user_pass_script(struct tls_session *session, struct tls_multi *multi, + const struct user_pass *up) { struct gc_arena gc = gc_new(); struct argv argv = argv_new(); @@ -1101,6 +1102,9 @@ verify_user_pass_script(struct tls_session *session, const struct user_pass *up) /* setenv client real IP address */ setenv_untrusted(session); + /* add auth-token environment */ + add_session_token_env(session, multi, up); + /* format command line */ argv_parse_cmd(&argv, session->opt->auth_user_pass_verify_script); argv_printf_cat(&argv, "%s", tmp_file); @@ -1134,7 +1138,8 @@ done: * Verify the username and password using a plugin */ static int -verify_user_pass_plugin(struct tls_session *session, const struct user_pass *up) +verify_user_pass_plugin(struct tls_session *session, struct tls_multi *multi, + const struct user_pass *up) { int retval = OPENVPN_PLUGIN_FUNC_ERROR; #ifdef PLUGIN_DEF_AUTH @@ -1154,6 +1159,8 @@ verify_user_pass_plugin(struct tls_session *session, const struct user_pass *up) /* setenv client real IP address */ setenv_untrusted(session); + /* add auth-token environment */ + add_session_token_env(session, multi, up); #ifdef PLUGIN_DEF_AUTH /* generate filename for deferred auth control file */ if (!key_state_gen_auth_control_file(ks, session->opt)) @@ -1197,7 +1204,9 @@ cleanup: #define KMDA_DEF 3 static int -verify_user_pass_management(struct tls_session *session, const struct user_pass *up) +verify_user_pass_management(struct tls_session *session, + struct tls_multi* multi, + const struct user_pass *up) { int retval = KMDA_ERROR; struct key_state *ks = &session->key[KS_PRIMARY]; /* primary key */ @@ -1215,6 +1224,11 @@ verify_user_pass_management(struct tls_session *session, const struct user_pass /* setenv client real IP address */ setenv_untrusted(session); + /* + * if we are using auth-gen-token, send also the session id of auth gen token to + * allow the management to figure out if it is a new session or a continued one + */ + add_session_token_env(session, multi, up); if (management) { management_notify_client_needing_auth(management, ks->mda_key_id, session->opt->mda_context, session->opt->es); @@ -1272,17 +1286,25 @@ verify_user_pass(struct user_pass *up, struct tls_multi *multi, */ if (session->opt->auth_token_generate && is_auth_token(up->password)) { - multi->auth_token_state_flags = verify_auth_token(up, multi,session); - if (multi->auth_token_state_flags == AUTH_TOKEN_HMAC_OK) + multi->auth_token_state_flags = verify_auth_token(up, multi, session); + if (session->opt->auth_token_call_auth) + { + /* + * we do not care about the result here because it is + * the responsibility of the external authentication to + * decide what to do with the result + */ + } + else if (multi->auth_token_state_flags == AUTH_TOKEN_HMAC_OK) { /* - * We do not want the EXPIRED flag here so check + * We do not want the EXPIRED or EMPTY USER flags here so check * for equality with AUTH_TOKEN_HMAC_OK */ msg(M_WARN, "TLS: Username/auth-token authentication " - "succeeded for username '%s'", + "succeeded for username '%s'", up->username); - skip_auth = true; + skip_auth = true; } else { @@ -1293,31 +1315,34 @@ verify_user_pass(struct user_pass *up, struct tls_multi *multi, return; } } + /* call plugin(s) and/or script */ if (!skip_auth) { - - /* call plugin(s) and/or script */ #ifdef MANAGEMENT_DEF_AUTH - if (man_def_auth == KMDA_DEF) + if (man_def_auth==KMDA_DEF) { - man_def_auth = verify_user_pass_management(session, up); + man_def_auth = verify_user_pass_management(session, multi, up); } #endif if (plugin_defined(session->opt->plugins, OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY)) { - s1 = verify_user_pass_plugin(session, up); + s1 = verify_user_pass_plugin(session, multi, up); } + if (session->opt->auth_user_pass_verify_script) { - s2 = verify_user_pass_script(session, up); + s2 = verify_user_pass_script(session, multi, up); } + } - /* check sizing of username if it will become our common name */ - if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) && strlen(up->username) > TLS_USERNAME_LEN) - { - msg(D_TLS_ERRORS, "TLS Auth Error: --username-as-common name specified and username is longer than the maximum permitted Common Name length of %d characters", TLS_USERNAME_LEN); - s1 = OPENVPN_PLUGIN_FUNC_ERROR; - } + /* check sizing of username if it will become our common name */ + if ((session->opt->ssl_flags & SSLF_USERNAME_AS_COMMON_NAME) && + strlen(up->username)>TLS_USERNAME_LEN) + { + msg(D_TLS_ERRORS, + "TLS Auth Error: --username-as-common name specified and username is longer than the maximum permitted Common Name length of %d characters", + TLS_USERNAME_LEN); + s1 = OPENVPN_PLUGIN_FUNC_ERROR; } /* auth succeeded? */ if ((s1 == OPENVPN_PLUGIN_FUNC_SUCCESS @@ -1357,7 +1382,7 @@ verify_user_pass(struct user_pass *up, struct tls_multi *multi, * the initial timestamp and session id can be extracted from it */ if (multi->auth_token && (multi->auth_token_state_flags & AUTH_TOKEN_HMAC_OK) - && !(multi->auth_token_state_flags & AUTH_TOKEN_EXPIRED)) + && !(multi->auth_token_state_flags & AUTH_TOKEN_EXPIRED)) { multi->auth_token = strdup(up->password); } From patchwork Thu Aug 8 04:53:39 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Arne Schwabe X-Patchwork-Id: 800 Return-Path: Delivered-To: patchwork@openvpn.net Delivered-To: patchwork@openvpn.net Received: from director10.mail.ord1d.rsapps.net ([172.30.191.6]) by backend30.mail.ord1d.rsapps.net with LMTP id EFJrKMI3TF0ocAAAIUCqbw for ; Thu, 08 Aug 2019 10:54:58 -0400 Received: from proxy12.mail.ord1d.rsapps.net ([172.30.191.6]) by director10.mail.ord1d.rsapps.net with LMTP id qMYOKMI3TF2QIgAApN4f7A ; Thu, 08 Aug 2019 10:54:58 -0400 Received: from smtp5.gate.ord1d ([172.30.191.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) by proxy12.mail.ord1d.rsapps.net with LMTP id 6Om6J8I3TF0CegAA7PHxkg ; Thu, 08 Aug 2019 10:54:58 -0400 X-Spam-Threshold: 95 X-Spam-Score: 0 X-Spam-Flag: NO X-Virus-Scanned: OK X-Orig-To: openvpnslackdevel@openvpn.net X-Originating-Ip: [216.105.38.7] Authentication-Results: smtp5.gate.ord1d.rsapps.net; iprev=pass policy.iprev="216.105.38.7"; spf=pass smtp.mailfrom="openvpn-devel-bounces@lists.sourceforge.net" smtp.helo="lists.sourceforge.net"; dkim=fail (signature verification failed) header.d=sourceforge.net; dkim=fail (signature verification failed) header.d=sf.net; dmarc=none (p=nil; dis=none) header.from=rfc2549.org X-Suspicious-Flag: YES X-Classification-ID: 7cb935c6-b9ec-11e9-be2b-525400d73c44-1-1 Received: from [216.105.38.7] ([216.105.38.7:39140] helo=lists.sourceforge.net) by smtp5.gate.ord1d.rsapps.net (envelope-from ) (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) id BC/0D-30897-1C73C4D5; Thu, 08 Aug 2019 10:54:57 -0400 Received: from [127.0.0.1] (helo=sfs-ml-1.v29.lw.sourceforge.com) by sfs-ml-1.v29.lw.sourceforge.com with esmtp (Exim 4.90_1) (envelope-from ) id 1hvjng-0006UE-OB; Thu, 08 Aug 2019 14:53:56 +0000 Received: from [172.30.20.202] (helo=mx.sourceforge.net) by sfs-ml-1.v29.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) (envelope-from ) id 1hvjng-0006Tw-6r for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:53:56 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=References:In-Reply-To:Message-Id:Date:Subject:Cc: To:From:Sender:Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding: Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=lcPc78P3fvSVVE9LhkII8arZiOsnyMed/Gec5/prxw4=; b=P1xZgdnIao4fA7ZZRBA1teLtIp J5R9Ko9w1rPEJLYYmv+MPtzzhb5Z6fy2XZQJ6LAM6CE9ISk4ygMKvlUE/cdHTsSgrkxnaI76ce+ys zDOCR7/h/nZDgvJMjZnvVlVCX5sjmF+Zz7S8yHNoj7FSiHAoMp/Rvs8Toc2RiMXIaP/Y=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=References:In-Reply-To:Message-Id:Date:Subject:Cc:To:From:Sender:Reply-To :MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=lcPc78P3fvSVVE9LhkII8arZiOsnyMed/Gec5/prxw4=; b=gnfUXzMJgJMtTnYNEHkCJFpNie RE/PJ7kYqtcO+mryJ6NpPLw4+4Zi+05zPC4E9fHi2j3bvQO/Evx9YNw6kW9JRi30yhXVFNK9fPEeo W1G4q2ifvTG0O8FqC5AXnj0/lXZyUqCwKvIJbBMKoms1gmfxZ9a80IKOdzK4bB9n2GNQ=; Received: from [192.26.174.232] (helo=mail.blinkt.de) by sfi-mx-3.v28.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) id 1hvjne-00FVV0-Mq for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:53:56 +0000 Received: from kamera.blinkt.de ([2001:638:502:390:20c:29ff:fec8:535c]) by mail.blinkt.de with smtp (Exim 4.92 (FreeBSD)) (envelope-from ) id 1hvjnP-000Igh-Mp; Thu, 08 Aug 2019 16:53:39 +0200 Received: (nullmailer pid 1310 invoked by uid 10006); Thu, 08 Aug 2019 14:53:39 -0000 From: Arne Schwabe To: openvpn-devel@lists.sourceforge.net Date: Thu, 8 Aug 2019 16:53:39 +0200 Message-Id: <20190808145339.1265-1-arne@rfc2549.org> X-Mailer: git-send-email 2.17.1 In-Reply-To: <414b5e76-c18d-5348-0d3c-84b121ca46bd@sf.lists.topphemmelig.net> References: <414b5e76-c18d-5348-0d3c-84b121ca46bd@sf.lists.topphemmelig.net> X-Spam-Report: Spam Filtering performed by mx.sourceforge.net. See http://spamassassin.org/tag/ for more details. 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level mail domains are different 0.0 URIBL_BLOCKED ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [URIs: openvpn.net] 0.0 SPF_NONE SPF: sender does not publish an SPF Record 0.0 SPF_HELO_NONE SPF: HELO does not publish an SPF Record 1.0 RDNS_NONE Delivered to internal network by a host with no rDNS X-Headers-End: 1hvjne-00FVV0-Mq Subject: [Openvpn-devel] [PATCH v6 6/7] Send indication that a session is expired to clients X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Arne Schwabe MIME-Version: 1.0 Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox From: Arne Schwabe This allows OpenVPN 3 core to fall back to the original authentication method. This commit changes man_def_auth_set_client_reason to auth_set_client_reason since it now used in more contexts. Also remove a FIXME about client_reason not being freed, as it is freed in tls_multi_free with auth_set_client_reason(multi, NULL); Patch V4: Rebase on master Acked-By: David Sommerseth --- src/openvpn/auth_token.c | 3 +++ src/openvpn/ssl.c | 6 ++---- src/openvpn/ssl_common.h | 10 +++++----- src/openvpn/ssl_verify.c | 8 ++++---- src/openvpn/ssl_verify.h | 15 ++++++++++----- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/openvpn/auth_token.c b/src/openvpn/auth_token.c index 5568a144..1edc8069 100644 --- a/src/openvpn/auth_token.c +++ b/src/openvpn/auth_token.c @@ -15,6 +15,7 @@ #include "push.h" #include "integer.h" #include "ssl.h" +#include "ssl_verify.h" #include const char *auth_token_pem_name = "OpenVPN auth-token server key"; @@ -378,6 +379,8 @@ verify_auth_token(struct user_pass *up, struct tls_multi *multi, if (ret & AUTH_TOKEN_EXPIRED) { + /* Tell client that the session token is expired */ + auth_set_client_reason(multi, "SESSION: token expired"); msg(M_INFO, "--auth-token-gen: auth-token from client expired"); } return ret; diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c index 0d79f91e..4455ebb8 100644 --- a/src/openvpn/ssl.c +++ b/src/openvpn/ssl.c @@ -1349,11 +1349,9 @@ tls_multi_free(struct tls_multi *multi, bool clear) ASSERT(multi); -#ifdef MANAGEMENT_DEF_AUTH - man_def_auth_set_client_reason(multi, NULL); - -#endif #if P2MP_SERVER + auth_set_client_reason(multi, NULL); + free(multi->peer_info); #endif diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h index 047aa59b..aad6af2f 100644 --- a/src/openvpn/ssl_common.h +++ b/src/openvpn/ssl_common.h @@ -526,16 +526,16 @@ struct tls_multi struct cert_hash_set *locked_cert_hash_set; #ifdef ENABLE_DEF_AUTH - /* - * An error message to send to client on AUTH_FAILED - */ - char *client_reason; - /* Time of last call to tls_authentication_status */ time_t tas_last; #endif #if P2MP_SERVER + /* + * An error message to send to client on AUTH_FAILED + */ + char *client_reason; + /* * A multi-line string of general-purpose info received from peer * over control channel. diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c index 6535ad8d..f61c621b 100644 --- a/src/openvpn/ssl_verify.c +++ b/src/openvpn/ssl_verify.c @@ -803,9 +803,8 @@ cleanup: #define ACF_FAILED 3 #endif -#ifdef MANAGEMENT_DEF_AUTH void -man_def_auth_set_client_reason(struct tls_multi *multi, const char *client_reason) +auth_set_client_reason(struct tls_multi* multi, const char* client_reason) { if (multi->client_reason) { @@ -814,11 +813,12 @@ man_def_auth_set_client_reason(struct tls_multi *multi, const char *client_reaso } if (client_reason && strlen(client_reason)) { - /* FIXME: Last alloc will never be freed */ multi->client_reason = string_alloc(client_reason, NULL); } } +#ifdef MANAGEMENT_DEF_AUTH + static inline unsigned int man_def_auth_test(const struct key_state *ks) { @@ -1022,7 +1022,7 @@ tls_authenticate_key(struct tls_multi *multi, const unsigned int mda_key_id, con if (multi) { int i; - man_def_auth_set_client_reason(multi, client_reason); + auth_set_client_reason(multi, client_reason); for (i = 0; i < KEY_SCAN_SIZE; ++i) { struct key_state *ks = multi->key_scan[i]; diff --git a/src/openvpn/ssl_verify.h b/src/openvpn/ssl_verify.h index 64f27efb..c54b89a6 100644 --- a/src/openvpn/ssl_verify.h +++ b/src/openvpn/ssl_verify.h @@ -224,18 +224,23 @@ struct x509_track #ifdef MANAGEMENT_DEF_AUTH bool tls_authenticate_key(struct tls_multi *multi, const unsigned int mda_key_id, const bool auth, const char *client_reason); -void man_def_auth_set_client_reason(struct tls_multi *multi, const char *client_reason); +#endif +#ifdef P2MP_SERVER +/** + * Sets the reason why authentication of a client failed. This be will send to the client + * when the AUTH_FAILED message is sent + * An example would be "SESSION: Token expired" + * @param multi The multi tls struct + * @param client_reason The string to send to the client as part of AUTH_FAILED + */ +void auth_set_client_reason(struct tls_multi* multi, const char* client_reason); #endif static inline const char * tls_client_reason(struct tls_multi *multi) { -#ifdef ENABLE_DEF_AUTH return multi->client_reason; -#else - return NULL; -#endif } /** Remove any X509_ env variables from env_set es */ From patchwork Thu Aug 8 04:54:12 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Arne Schwabe X-Patchwork-Id: 801 Return-Path: Delivered-To: patchwork@openvpn.net Delivered-To: patchwork@openvpn.net Received: from director7.mail.ord1d.rsapps.net ([172.30.191.6]) by backend30.mail.ord1d.rsapps.net with LMTP id wFqLHew3TF11OwAAIUCqbw for ; Thu, 08 Aug 2019 10:55:40 -0400 Received: from proxy8.mail.ord1d.rsapps.net ([172.30.191.6]) by director7.mail.ord1d.rsapps.net with LMTP id UConHew3TF0mSgAAovjBpQ ; Thu, 08 Aug 2019 10:55:40 -0400 Received: from smtp28.gate.ord1d ([172.30.191.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) by proxy8.mail.ord1d.rsapps.net with LMTP id MA/mG+w3TF0GYwAAGdz6CA ; Thu, 08 Aug 2019 10:55:40 -0400 X-Spam-Threshold: 95 X-Spam-Score: 0 X-Spam-Flag: NO X-Virus-Scanned: OK X-Orig-To: openvpnslackdevel@openvpn.net X-Originating-Ip: [216.105.38.7] Authentication-Results: smtp28.gate.ord1d.rsapps.net; iprev=pass policy.iprev="216.105.38.7"; spf=pass smtp.mailfrom="openvpn-devel-bounces@lists.sourceforge.net" smtp.helo="lists.sourceforge.net"; dkim=fail (signature verification failed) header.d=sourceforge.net; dkim=fail (signature verification failed) header.d=sf.net; dmarc=none (p=nil; dis=none) header.from=rfc2549.org X-Suspicious-Flag: YES X-Classification-ID: 94a01de4-b9ec-11e9-baf0-525400ea129b-1-1 Received: from [216.105.38.7] ([216.105.38.7:48650] helo=lists.sourceforge.net) by smtp28.gate.ord1d.rsapps.net (envelope-from ) (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) id B6/0F-04542-9E73C4D5; Thu, 08 Aug 2019 10:55:37 -0400 Received: from [127.0.0.1] (helo=sfs-ml-4.v29.lw.sourceforge.com) by sfs-ml-4.v29.lw.sourceforge.com with esmtp (Exim 4.90_1) (envelope-from ) id 1hvjoH-00024K-1o; Thu, 08 Aug 2019 14:54:33 +0000 Received: from [172.30.20.202] (helo=mx.sourceforge.net) by sfs-ml-4.v29.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) (envelope-from ) id 1hvjoF-000246-7i for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:54:31 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=References:In-Reply-To:Message-Id:Date:Subject:Cc: To:From:Sender:Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding: Content-ID:Content-Description:Resent-Date:Resent-From:Resent-Sender: Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=aiNyBJAijJd4ftTLE64WWfYOP8N9gtkk8KYQ7nwIvDQ=; b=V3KgiRednK8OjlKNb/jWNDG/Zt O7lrdXNiufIFpVxN6V1RVNOoPEgSPhm3WcQ3eJHNux01hu05YuJb7y3QMXHijrU5x4Wzx29lsX9Cq e8OgAfSYG1JhXEt7BhfUmwawPSazjB26hBvgI2zWkYwmS9nax0JSTP39pEipsY5kN8aI=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=References:In-Reply-To:Message-Id:Date:Subject:Cc:To:From:Sender:Reply-To :MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=aiNyBJAijJd4ftTLE64WWfYOP8N9gtkk8KYQ7nwIvDQ=; b=JcxKgIMcBRjNK2iCsBCGrq0bUC ynBwu4em6nrWLHc65W0BVR3VXtnzihfK0xixE/4UDyGKOqlujXYfY+StfTlesxY0af4j3rNusIPxY JVhZ0/Ksf3tuhH/OPaFlNdaRdV0ScxlvEta8nuB5ZHD2m6lh00bM9tYnG53BiyDu/D2k=; Received: from [192.26.174.232] (helo=mail.blinkt.de) by sfi-mx-4.v28.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.90_1) id 1hvjoD-00F5pF-4q for openvpn-devel@lists.sourceforge.net; Thu, 08 Aug 2019 14:54:31 +0000 Received: from kamera.blinkt.de ([2001:638:502:390:20c:29ff:fec8:535c]) by mail.blinkt.de with smtp (Exim 4.92 (FreeBSD)) (envelope-from ) id 1hvjnw-000Igr-KU; Thu, 08 Aug 2019 16:54:12 +0200 Received: (nullmailer pid 1366 invoked by uid 10006); Thu, 08 Aug 2019 14:54:12 -0000 From: Arne Schwabe To: openvpn-devel@lists.sourceforge.net Date: Thu, 8 Aug 2019 16:54:12 +0200 Message-Id: <20190808145412.1321-1-arne@rfc2549.org> X-Mailer: git-send-email 2.17.1 In-Reply-To: References: X-Spam-Report: Spam Filtering performed by mx.sourceforge.net. See http://spamassassin.org/tag/ for more details. 0.0 URIBL_BLOCKED ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [URIs: openvpn.net] 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level mail domains are different 0.0 SPF_NONE SPF: sender does not publish an SPF Record 0.0 SPF_HELO_NONE SPF: HELO does not publish an SPF Record 1.0 RDNS_NONE Delivered to internal network by a host with no rDNS 0.0 AWL AWL: Adjusted score from AWL reputation of From: address X-Headers-End: 1hvjoD-00F5pF-4q Subject: [Openvpn-devel] [PATCH v6 7/7] Implement unit tests for auth-gen-token X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Arne Schwabe MIME-Version: 1.0 Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox From: Arne Schwabe Patch V2: adapt unit tests to other V2 patches Patch V4: Resolve rebase conflicts Patch V5: Add \ lost in rebase that broke compilation --- tests/unit_tests/openvpn/Makefile.am | 18 +- tests/unit_tests/openvpn/test_auth_token.c | 375 +++++++++++++++++++++ 2 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/openvpn/test_auth_token.c diff --git a/tests/unit_tests/openvpn/Makefile.am b/tests/unit_tests/openvpn/Makefile.am index d015b293..60e84639 100644 --- a/tests/unit_tests/openvpn/Makefile.am +++ b/tests/unit_tests/openvpn/Makefile.am @@ -6,7 +6,7 @@ if HAVE_LD_WRAP_SUPPORT test_binaries += argv_testdriver buffer_testdriver endif -test_binaries += crypto_testdriver packet_id_testdriver +test_binaries += crypto_testdriver packet_id_testdriver auth_token_testdriver if HAVE_LD_WRAP_SUPPORT test_binaries += tls_crypt_testdriver endif @@ -94,3 +94,19 @@ networking_testdriver_SOURCES = test_networking.c mock_msg.c \ $(openvpn_srcdir)/packet_id.c \ $(openvpn_srcdir)/platform.c endif + +auth_token_testdriver_CFLAGS = @TEST_CFLAGS@ \ + -I$(openvpn_includedir) -I$(compat_srcdir) -I$(openvpn_srcdir) \ + $(OPTIONAL_CRYPTO_CFLAGS) +auth_token_testdriver_LDFLAGS = @TEST_LDFLAGS@ \ + $(OPTIONAL_CRYPTO_LIBS) + +auth_token_testdriver_SOURCES = test_auth_token.c mock_msg.c \ + $(openvpn_srcdir)/buffer.c \ + $(openvpn_srcdir)/crypto.c \ + $(openvpn_srcdir)/crypto_mbedtls.c \ + $(openvpn_srcdir)/crypto_openssl.c \ + $(openvpn_srcdir)/otime.c \ + $(openvpn_srcdir)/packet_id.c \ + $(openvpn_srcdir)/platform.c \ + $(openvpn_srcdir)/base64.c diff --git a/tests/unit_tests/openvpn/test_auth_token.c b/tests/unit_tests/openvpn/test_auth_token.c new file mode 100644 index 00000000..a3591b4a --- /dev/null +++ b/tests/unit_tests/openvpn/test_auth_token.c @@ -0,0 +1,375 @@ +/* + * OpenVPN -- An application to securely tunnel IP networks + * over a single UDP port, with support for SSL/TLS-based + * session authentication and key exchange, + * packet encryption, packet authentication, and + * packet compression. + * + * Copyright (C) 2016-2018 Fox Crypto B.V. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#elif defined(_MSC_VER) +#include "config-msvc.h" +#endif + +#include "syshead.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "auth_token.c" + +#include "mock_msg.h" + +struct test_context { + struct tls_multi multi; + struct key_type kt; + struct user_pass up; + struct tls_session session; +}; + +static const char *now0key0 = "SESS_ID_AT_0123456789abcdefAAAAAAAAAAAAAAAAAAAAAE5JsQJOVfo8jnI3RL3tBaR5NkE4yPfcylFUHmHSc5Bu"; + +static const char *zeroinline = "-----BEGIN OpenVPN auth-token server key-----\n" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n" + "-----END OpenVPN auth-token server key-----"; + +static const char *allx01inline = "-----BEGIN OpenVPN auth-token server key-----\n" + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\n" + "-----END OpenVPN auth-token server key-----"; + +static const char *random_key = "-----BEGIN OpenVPN auth-token server key-----\n" + "+mmmf7IQ5cymtMVjKYTWk8IOcYanRlpQmV9Tb3EjkHYxueBVDg3yqRgzeBlVGzNLD//rAPiOVhau\n" + "3NDBjNOQB8951bfs7Cc2mYfay92Bh2gRJ5XEM/DMfzCWN+7uU6NWoTTHr4FuojnIQtjtqVAj/JS9\n" + "w+dTSp/vYHl+c7uHd19uVRu/qLqV85+rm4tUGIjO7FfYuwyPqwmhuIsi3hs9QkSimh888FmBpoKY\n" + "/tbKVTJZmSERKti9KEwtV2eVAR0znN5KW7lCB3mHVAhN7bUpcoDjfCzYIFARxwswTFu9gFkwqUMY\n" + "I1KUOgIsVNs4llACioeXplYekWETR+YkJwDc/A==\n" + "-----END OpenVPN auth-token server key-----"; + +static const char *random_token = "SESS_ID_AT_ThhRItzOKNKrh3dfAAAAAFwzHpwAAAAAXDMenDdrq0RoH3dkA1f7O3wO+7kZcx2DusVZrRmFlWQM9HOb"; + + +static int +setup(void **state) +{ + struct test_context *ctx = calloc(1, sizeof(*ctx)); + *state = ctx; + + struct key key = { 0 }; + + ctx->kt = auth_token_kt(); + if (!ctx->kt.digest) + { + return 0; + } + ctx->multi.opt.auth_token_generate = true; + ctx->multi.opt.auth_token_lifetime = 3000; + + ctx->session.opt = calloc(1, sizeof(struct tls_options)); + ctx->session.opt->renegotiate_seconds = 120; + ctx->session.opt->auth_token_lifetime = 3000; + + strcpy(ctx->up.username, "test user name"); + strcpy(ctx->up.password, "ignored"); + + init_key_ctx(&ctx->multi.opt.auth_token_key, &key, &ctx->kt, false, "TEST"); + + now = 0; + return 0; +} + +static int +teardown(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + free_key_ctx(&ctx->multi.opt.auth_token_key); + wipe_auth_token(&ctx->multi); + + free(ctx->session.opt); + free(ctx); + + return 0; +} + +static void +auth_token_basic_test(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + generate_auth_token(&ctx->up, &ctx->multi); + strcpy(ctx->up.password, ctx->multi.auth_token); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); +} + +static void +auth_token_fail_invalid_key(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + generate_auth_token(&ctx->up, &ctx->multi); + strcpy(ctx->up.password, ctx->multi.auth_token); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + + /* Change auth-token key */ + struct key key; + memset(&key, '1', sizeof(key)); + free_key_ctx(&ctx->multi.opt.auth_token_key); + init_key_ctx(&ctx->multi.opt.auth_token_key, &key, &ctx->kt, false, "TEST"); + + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), 0); + + /* Load original test key again */ + memset(&key, 0, sizeof(key)); + free_key_ctx(&ctx->multi.opt.auth_token_key); + init_key_ctx(&ctx->multi.opt.auth_token_key, &key, &ctx->kt, false, "TEST"); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + +} + +static void +auth_token_test_timeout(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + now = 100000; + generate_auth_token(&ctx->up, &ctx->multi); + strcpy(ctx->up.password, ctx->multi.auth_token); + + /* No time has passed */ + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + + /* Token before validity, should be rejected */ + now = 100000 - 100; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED); + + /* Token still in validity, should be accepted */ + now = 100000 + 2*ctx->session.opt->renegotiate_seconds - 20; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + + /* Token past validity, should be rejected */ + now = 100000 + 2*ctx->session.opt->renegotiate_seconds + 20; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED); + + /* Check if the mode for a client that never updates its token works */ + ctx->multi.auth_token_initial = strdup(ctx->up.password); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + + /* But not when we reached our timeout */ + now = 100000 + ctx->session.opt->auth_token_lifetime + 1; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED); + + free(ctx->multi.auth_token_initial); + ctx->multi.auth_token_initial = NULL; + + /* regenerate the token util it hits the expiry */ + now = 100000; + while (now < 100000 + ctx->session.opt->auth_token_lifetime + 1) + { + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + generate_auth_token(&ctx->up, &ctx->multi); + strcpy(ctx->up.password, ctx->multi.auth_token); + now += ctx->session.opt->renegotiate_seconds; + } + + + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED); + ctx->multi.opt.auth_token_lifetime = 0; + + /* Non expiring token should be fine */ + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); +} + +static void +zerohmac(char *token) +{ + char *hmacstart = token + AUTH_TOKEN_SESSION_ID_LEN + + strlen(SESSION_ID_PREFIX) + 2*sizeof(uint64_t); + memset(hmacstart, 0x8d, strlen(hmacstart)); +} + +static void +auth_token_test_known_keys(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + now = 0; + /* Preload the session id so the same session id is used here */ + ctx->multi.auth_token = strdup(now0key0); + + /* Zero the hmac part to ensure we have a newly generated token */ + zerohmac(ctx->multi.auth_token); + + generate_auth_token(&ctx->up, &ctx->multi); + + assert_string_equal(now0key0, ctx->multi.auth_token); + + strcpy(ctx->up.password, ctx->multi.auth_token); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); +} + +static const char *lastsesion_statevalue; +void +setenv_str(struct env_set *es, const char *name, const char *value) +{ + if (streq(name, "session_state")) + { + lastsesion_statevalue = value; + } +} + +static void +auth_token_test_empty_user(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + CLEAR(ctx->up.username); + now = 0; + + generate_auth_token(&ctx->up, &ctx->multi); + strcpy(ctx->up.password, ctx->multi.auth_token); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK); + + now = 100000; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED); + strcpy(ctx->up.username, "test user name"); + + now = 0; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_VALID_EMPTYUSER); + + now = 100000; + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED|AUTH_TOKEN_VALID_EMPTYUSER); + + zerohmac(ctx->up.password); + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session), 0); +} + +static void +auth_token_test_env(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + ctx->multi.auth_token_state_flags = 0; + ctx->multi.auth_token = NULL; + add_session_token_env(&ctx->session, &ctx->multi, &ctx->up); + assert_string_equal(lastsesion_statevalue, "Initial"); + + ctx->multi.auth_token_state_flags = 0; + strcpy(ctx->up.password, now0key0); + add_session_token_env(&ctx->session, &ctx->multi, &ctx->up); + assert_string_equal(lastsesion_statevalue, "Invalid"); + + ctx->multi.auth_token_state_flags = AUTH_TOKEN_HMAC_OK; + add_session_token_env(&ctx->session, &ctx->multi, &ctx->up); + assert_string_equal(lastsesion_statevalue, "Authenticated"); + + ctx->multi.auth_token_state_flags = AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED; + add_session_token_env(&ctx->session, &ctx->multi, &ctx->up); + assert_string_equal(lastsesion_statevalue, "Expired"); + + ctx->multi.auth_token_state_flags = AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_VALID_EMPTYUSER; + add_session_token_env(&ctx->session, &ctx->multi, &ctx->up); + assert_string_equal(lastsesion_statevalue, "AuthenticatedEmptyUser"); + + ctx->multi.auth_token_state_flags = AUTH_TOKEN_HMAC_OK|AUTH_TOKEN_EXPIRED|AUTH_TOKEN_VALID_EMPTYUSER; + add_session_token_env(&ctx->session, &ctx->multi, &ctx->up); + assert_string_equal(lastsesion_statevalue, "ExpiredEmptyUser"); +} + +static void +auth_token_test_random_keys(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + now = 0x5c331e9c; + /* Preload the session id so the same session id is used here */ + ctx->multi.auth_token = strdup(random_token); + + free_key_ctx(&ctx->multi.opt.auth_token_key); + auth_token_init_secret(&ctx->multi.opt.auth_token_key, INLINE_FILE_TAG, random_key); + + /* Zero the hmac part to ensure we have a newly generated token */ + zerohmac(ctx->multi.auth_token); + + generate_auth_token(&ctx->up, &ctx->multi); + + assert_string_equal(random_token, ctx->multi.auth_token); + + strcpy(ctx->up.password, ctx->multi.auth_token); + assert_true(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session)); +} + + +static void +auth_token_test_key_load(void **state) +{ + struct test_context *ctx = (struct test_context *) *state; + + free_key_ctx(&ctx->multi.opt.auth_token_key); + auth_token_init_secret(&ctx->multi.opt.auth_token_key, INLINE_FILE_TAG, zeroinline); + strcpy(ctx->up.password, now0key0); + assert_true(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session)); + + free_key_ctx(&ctx->multi.opt.auth_token_key); + auth_token_init_secret(&ctx->multi.opt.auth_token_key, INLINE_FILE_TAG, allx01inline); + assert_false(verify_auth_token(&ctx->up, &ctx->multi, &ctx->session)); +} + +int +main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(auth_token_basic_test, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_fail_invalid_key, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_test_known_keys, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_test_empty_user, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_test_env, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_test_random_keys, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_test_key_load, setup, teardown), + cmocka_unit_test_setup_teardown(auth_token_test_timeout, setup, teardown), + }; + +#if defined(ENABLE_CRYPTO_OPENSSL) + OpenSSL_add_all_algorithms(); +#endif + + int ret = cmocka_run_group_tests_name("auth-token tests", tests, NULL, NULL); + + return ret; +} + +/* Dummy functions that do nothing to mock the functionality */ +void +send_push_reply_auth_token(struct tls_multi *multi) +{ +} + +void +auth_set_client_reason(struct tls_multi *multi, const char *reason) +{ + +}