Hi,
On 19/07/2021 15:31, Arne Schwabe wrote:
> This changes that auth_token_initial is set when the token is
> initially generated instead when pushing the token. Even I do not know
> anymore why I did it in this way in the first place. Also use
> multi->auth_token_initial as source for the sesssion ID since it should
> now always be available. Also set auth_token_initial directly to
> up->password once we verified that we have gotten a valid token from
> a client. This cleans ups the logic in generating the environment and
> makes the code flow clearer.
>
> Since the change makes auth_token_initial always available we need to add
> a check to only send a PUSH reply to update the token on renegotiations.
> The old code relied on multi->auth_token not being set in this case.
>
> This commit also removes the workaround for old OpenVPN clients. These
> were only available as commercial OpenVPN Connect client and not in use
> anymore.
>
> Furthermore, introduce a check if the session ID has changed during a session.
> Even though this is still a valid authentication changing to a different auth
> token mid session is highly irregular and should never occur naturally.
>
> Patch V2: rebase.
> Patch V3: fix formatting, clarifying commit message, remove initial
> token workaround for old v3.
> Patch v4: move sending the auth-token for renegotiations to a sane place
> and trigger it when the TLS session reaches its fully authenticated
> state.
> Patch v5: Move also setting auth_token_inital from up->password to a more
> logical place, general cleanups, add session id mismatch check
> Patch v6: Rework some comments and general cleanup of small things
>
> Signed-off-by: Arne Schwabe <[email protected]>
> ---
> doc/man-sections/server-options.rst | 4 +-
> src/openvpn/auth_token.c | 89 ++++++++++++++++------
> src/openvpn/auth_token.h | 11 ++-
> src/openvpn/push.c | 8 --
> src/openvpn/ssl.c | 7 +-
> src/openvpn/ssl_common.h | 9 ++-
> src/openvpn/ssl_verify.c | 31 +++-----
> tests/unit_tests/openvpn/test_auth_token.c | 49 +++++++++---
> 8 files changed, 140 insertions(+), 68 deletions(-)
>
> diff --git a/doc/man-sections/server-options.rst
> b/doc/man-sections/server-options.rst
> index 715473353..f1d2ec317 100644
> --- a/doc/man-sections/server-options.rst
> +++ b/doc/man-sections/server-options.rst
> @@ -35,7 +35,7 @@ fast hardware. SSL/TLS authentication must be used in this
> mode.
> token is reached or after not being renewed for more than 2 \*
> ``reneg-sec`` seconds. Clients will be sent renewed tokens on every TLS
> renogiation to keep the client's token updated. This is done to
> - invalidate a token if a client is disconnected for a sufficently long
> + invalidate a token if a client is disconnected for a sufficiently long
> time, while at the same time permitting much longer token lifetimes for
> active clients.
>
> @@ -46,7 +46,7 @@ fast hardware. SSL/TLS authentication must be used in this
> mode.
> When the :code:`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.
> + verification succeeds or fails.
>
> This option postpones this decision to the external authentication
> methods and checks the validity of the account and do other checks.
> diff --git a/src/openvpn/auth_token.c b/src/openvpn/auth_token.c
> index 0ea6d1832..d7c005256 100644
> --- a/src/openvpn/auth_token.c
> +++ b/src/openvpn/auth_token.c
> @@ -21,6 +21,8 @@
> const char *auth_token_pem_name = "OpenVPN auth-token server key";
>
> #define AUTH_TOKEN_SESSION_ID_LEN 12
> +
IF we want an empty line, I'd put this empty line after
AUTH_TOKEN_SESSION_ID_BASE64_LEN, but I leave this to Gert's taste.
> +#define AUTH_TOKEN_SESSION_ID_BASE64_LEN (AUTH_TOKEN_SESSION_ID_LEN * 8 / 6)
> #if AUTH_TOKEN_SESSION_ID_LEN % 3
> #error AUTH_TOKEN_SESSION_ID_LEN needs to be multiple a 3
> #endif
> @@ -109,11 +111,11 @@ add_session_token_env(struct tls_session *session,
> struct tls_multi *multi,
> /*
> * No session before, generate a new session token for the new
> session
> */
> - if (!multi->auth_token)
> + if (!multi->auth_token_initial)
> {
> generate_auth_token(up, multi);
> }
> - session_id_source = multi->auth_token;
> + session_id_source = multi->auth_token_initial;
> }
> /*
> * In the auth-token the auth token is already base64 encoded
> @@ -184,7 +186,7 @@ generate_auth_token(const struct user_pass *up, struct
> tls_multi *multi)
>
> uint8_t sessid[AUTH_TOKEN_SESSION_ID_LEN];
>
> - if (multi->auth_token)
> + if (multi->auth_token_initial)
> {
> /* 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
> @@ -192,13 +194,16 @@ generate_auth_token(const struct user_pass *up, struct
> tls_multi *multi)
> */
> 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_sessid = multi->auth_token + strlen(SESSION_ID_PREFIX);
> + /* Make a copy of the string to not modify multi->auth_token_initial
> */
> + char *initial_token_copy = string_alloc(multi->auth_token_initial,
> &gc);
> +
> + char *old_sessid = initial_token_copy + strlen(SESSION_ID_PREFIX);
> char *old_tsamp_initial = old_sessid + AUTH_TOKEN_SESSION_ID_LEN*8/6;
You've introduced the new constant but you're substituted it here ^ :-(
>
> + /*
> + * We null terminate the old token just after the session ID to let
> + * our base64 decode function only decode the session ID
> + */
> old_tsamp_initial[12] = '\0';
> ASSERT(openvpn_base64_decode(old_tsamp_initial, old_tstamp_decode,
> 9) == 9);
>
> @@ -211,11 +216,7 @@ generate_auth_token(const struct user_pass *up, struct
> tls_multi *multi)
> 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);
> + ASSERT(openvpn_base64_decode(old_sessid, sessid,
> AUTH_TOKEN_SESSION_ID_LEN) == AUTH_TOKEN_SESSION_ID_LEN);
> }
> else if (!rand_bytes(sessid, AUTH_TOKEN_SESSION_ID_LEN))
> {
> @@ -272,11 +273,22 @@ generate_auth_token(const struct user_pass *up, struct
> tls_multi *multi)
>
> free(b64output);
>
> + /* free the auth-token if defined, we will replace it with a new one */
> + free(multi->auth_token);
> multi->auth_token = strdup((char *)BPTR(&session_token));
>
> dmsg(D_SHOW_KEYS, "Generated token for client: %s (%s)",
> multi->auth_token, up->username);
>
> + if (!multi->auth_token_initial)
> + {
> + /*
> + * Save the initial auth token to continue using the same session ID
> + * and timestamp in updates
> + */
> + multi->auth_token_initial = strdup(multi->auth_token);
> + }
> +
> gc_free(&gc);
> }
>
> @@ -343,23 +355,17 @@ verify_auth_token(struct user_pass *up, struct
> tls_multi *multi,
> }
> else
> {
> - msg(M_WARN, "--auth-token-gen: HMAC on token from client failed
> (%s)",
> + msg(M_WARN, "--auth-gen-token: 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
> + bool in_renegotiation_time = now >= timestamp
> && now < timestamp + 2 *
> session->opt->renegotiate_seconds;
This second line should now be re-aligned..
>
> - /* 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)
> + if (!in_renegotiation_time)
> {
> ret |= AUTH_TOKEN_EXPIRED;
> }
> @@ -383,7 +389,20 @@ verify_auth_token(struct user_pass *up, struct tls_multi
> *multi,
> {
> /* 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");
> + msg(M_INFO, "--auth-gen-token: auth-token from client expired");
> + }
> +
> + /* Check that we do have the same session ID in the token as in our
> stored
> + * auth-token to ensure that it did not change.
> + * This also compares the prefix and session part of the
> + * tokens, which should be identical if the session ID stayed the same */
> + if (multi->auth_token_initial
> + && memcmp_constant_time(multi->auth_token_initial, up->password,
> + strlen(SESSION_ID_PREFIX) +
> AUTH_TOKEN_SESSION_ID_BASE64_LEN))
> + {
> + msg(M_WARN, "--auth-gen-token: session id in token changed
> (Rejecting "
> + "token.");
> + ret = 0;
> }
> return ret;
> }
> @@ -408,3 +427,27 @@ wipe_auth_token(struct tls_multi *multi)
> multi->auth_token_initial = NULL;
> }
> }
> +
> +void
> +resend_auth_token_renegotiation(struct tls_multi *multi, struct tls_session
> *session)
> +{
> + /*
> + * 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.
> + *
> + * Otherwise the auth-token get pushed out as part of the "normal"
> + * push-reply
> + */
> + bool is_renegotiation = session->key[KS_PRIMARY].key_id != 0;
> +
> + if (multi->auth_token_initial && is_renegotiation)
> + {
> + /*
> + * We do not explicitly reschedule the sending of the
> + * control message here. This might delay this reply
> + * a few seconds but this message is not time critical
> + */
> + send_push_reply_auth_token(multi);
> + }
> +}
> \ No newline at end of file
> diff --git a/src/openvpn/auth_token.h b/src/openvpn/auth_token.h
> index 73a00ddd7..de93a9413 100644
> --- a/src/openvpn/auth_token.h
> +++ b/src/openvpn/auth_token.h
> @@ -117,7 +117,7 @@ void wipe_auth_token(struct tls_multi *multi);
> /**
> * 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
> + * This function 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
> @@ -129,4 +129,13 @@ is_auth_token(const char *password)
> return (memcmp_constant_time(SESSION_ID_PREFIX, password,
> strlen(SESSION_ID_PREFIX)) == 0);
> }
> +/**
> + * Checks if a client should be sent a new auth token to update its
> + * current auth-token
> + * @param multi Pointer the multi object of the TLS session
> + * @param session Pointer to the TLS session itself
> + */
> +void
> +resend_auth_token_renegotiation(struct tls_multi *multi, struct tls_session
> *session);
> +
> #endif /* AUTH_TOKEN_H */
> diff --git a/src/openvpn/push.c b/src/openvpn/push.c
> index f4957f147..53cb7ca6f 100644
> --- a/src/openvpn/push.c
> +++ b/src/openvpn/push.c
> @@ -527,14 +527,6 @@ prepare_auth_token_push_reply(struct tls_multi
> *tls_multi, struct gc_arena *gc,
> 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);
> - }
> }
> }
>
> diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
> index 615ed69e5..e9b8d1b92 100644
> --- a/src/openvpn/ssl.c
> +++ b/src/openvpn/ssl.c
> @@ -3115,13 +3115,18 @@ tls_multi_process(struct tls_multi *multi,
>
> if (ks->state == S_ACTIVE && ks->authenticated == KS_AUTH_TRUE)
> {
> - /* This will move ks->state from S_ACTIVE to
> S_GENERATED_KEYS */
> + /* Session is now fully authenticated.
> + * tls_session_generate_data_channel_keys will move ks->state
> + * from S_ACTIVE to S_GENERATED_KEYS */
> if (!tls_session_generate_data_channel_keys(session))
> {
> msg(D_TLS_ERRORS, "TLS Error: generate_key_expansion
> failed");
> ks->authenticated = KS_AUTH_FALSE;
> ks->state = S_ERROR;
> }
> +
> + /* Update auth token on the client if needed */
> + resend_auth_token_renegotiation(multi, session);
> }
> }
> }
> diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h
> index 72eb55bd3..64c1d53f3 100644
> --- a/src/openvpn/ssl_common.h
> +++ b/src/openvpn/ssl_common.h
> @@ -95,7 +95,10 @@
> * completed while still within the
> * handshake window. Deferred auth and
> * client connect can still be pending. */
> -#define S_GENERATED_KEYS 7 /**< The data channel keys have been
> generated */
> +#define S_GENERATED_KEYS 7 /**< The data channel keys have been
> generated
> + * The TLS session is fully authenticated
> + * when reaching this state. */
> +
> /* Note that earlier versions also had a S_OP_NORMAL state that was
> * virtually identical with S_ACTIVE and the code still assumes everything
> * >= S_ACTIVE to be fully operational */
> @@ -596,8 +599,8 @@ struct tls_multi
> * 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)
> + /**< The first auth-token we sent to a client. We use this to remember
> + * the session ID and initial timestamp when generating new auth-token.
> */
> #define AUTH_TOKEN_HMAC_OK (1<<0)
> /**< Auth-token sent from client has valid hmac */
> diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c
> index bbb1878a3..913c95620 100644
> --- a/src/openvpn/ssl_verify.c
> +++ b/src/openvpn/ssl_verify.c
> @@ -1512,6 +1512,15 @@ verify_user_pass(struct user_pass *up, struct
> tls_multi *multi,
> if (session->opt->auth_token_generate && is_auth_token(up->password))
> {
> ks->auth_token_state_flags = verify_auth_token(up, multi, session);
> +
> + /* If this is the first time we see an auth-token in this multi
> session,
> + * save it as initial auth token. This ensures using the
> + * same session ID and initial timestamp in new tokens */
> + if (!multi->auth_token_initial)
> + {
> + multi->auth_token_initial = strdup(up->password);
> + }
> +
> if (session->opt->auth_token_call_auth)
> {
> /*
> @@ -1631,27 +1640,7 @@ verify_user_pass(struct user_pass *up, struct
> tls_multi *multi,
> */
> generate_auth_token(up, multi);
> }
> - /*
> - * 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.
> - *
> - * Otherwise the auth-token get pushed out as part of the "normal"
> - * push-reply
> - */
> - if (multi->auth_token_initial)
> - {
> - /*
> - * 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);
> - }
> +
> msg(D_HANDSHAKE, "TLS: Username/Password authentication %s for
> username '%s' %s",
> (ks->authenticated == KS_AUTH_DEFERRED) ? "deferred" :
> "succeeded",
> up->username,
> diff --git a/tests/unit_tests/openvpn/test_auth_token.c
> b/tests/unit_tests/openvpn/test_auth_token.c
> index 4030052e0..6bfad6efb 100644
> --- a/tests/unit_tests/openvpn/test_auth_token.c
> +++ b/tests/unit_tests/openvpn/test_auth_token.c
> @@ -174,7 +174,10 @@ auth_token_test_timeout(void **state)
>
> now = 100000;
> generate_auth_token(&ctx->up, &ctx->multi);
> +
> strcpy(ctx->up.password, ctx->multi.auth_token);
> + free(ctx->multi.auth_token_initial);
> + ctx->multi.auth_token_initial = NULL;
>
> /* No time has passed */
> assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, ctx->session),
> @@ -195,11 +198,6 @@ auth_token_test_timeout(void **state)
> 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),
> @@ -244,10 +242,10 @@ auth_token_test_known_keys(void **state)
>
> now = 0;
> /* Preload the session id so the same session id is used here */
> - ctx->multi.auth_token = strdup(now0key0);
> + ctx->multi.auth_token_initial = strdup(now0key0);
>
> /* Zero the hmac part to ensure we have a newly generated token */
> - zerohmac(ctx->multi.auth_token);
> + zerohmac(ctx->multi.auth_token_initial);
>
> generate_auth_token(&ctx->up, &ctx->multi);
>
> @@ -268,6 +266,38 @@ setenv_str(struct env_set *es, const char *name, const
> char *value)
> }
> }
>
> +void
> +auth_token_test_session_mismatch(void **state)
> +{
> + struct test_context *ctx = (struct test_context *) *state;
> +
> + /* Generate first auth token and check it is correct */
> + 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);
> +
> + char *token_sessiona = strdup(ctx->multi.auth_token);
> +
> + /* Generate second token */
> + wipe_auth_token(&ctx->multi);
> +
> + 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);
> +
> + assert_int_not_equal(0, memcmp(ctx->multi.auth_token_initial +
> strlen(SESSION_ID_PREFIX),
> + token_sessiona +
> strlen(SESSION_ID_PREFIX),
> + AUTH_TOKEN_SESSION_ID_BASE64_LEN));
> +
> + /* The first token is valid but should trigger the invalid response since
> + * the session id is not the same */
> + strcpy(ctx->up.password, token_sessiona);
> + assert_int_equal(verify_auth_token(&ctx->up, &ctx->multi, ctx->session),
> 0);
> + free(token_sessiona);
> +}
> +
> static void
> auth_token_test_empty_user(void **state)
> {
> @@ -341,13 +371,13 @@ auth_token_test_random_keys(void **state)
>
> now = 0x5c331e9c;
> /* Preload the session id so the same session id is used here */
> - ctx->multi.auth_token = strdup(random_token);
> + ctx->multi.auth_token_initial = strdup(random_token);
>
> free_key_ctx(&ctx->multi.opt.auth_token_key);
> auth_token_init_secret(&ctx->multi.opt.auth_token_key, random_key, true);
>
> /* Zero the hmac part to ensure we have a newly generated token */
> - zerohmac(ctx->multi.auth_token);
> + zerohmac(ctx->multi.auth_token_initial);
>
> generate_auth_token(&ctx->up, &ctx->multi);
>
> @@ -385,6 +415,7 @@ main(void)
> 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),
> + cmocka_unit_test_setup_teardown(auth_token_test_session_mismatch,
> setup, teardown)
> };
>
> #if defined(ENABLE_CRYPTO_OPENSSL)
>
Other than the few cosmetic issues mentioned above, this patch looks
good to me and passed my tests.
Acked-by: Antonio Quartulli <[email protected]>
I am not sure if a new patch is required to substitute the hardcoded
computation with the new constant. maybe Gert can do that :)
--
Antonio Quartulli
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel