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 <a...@rfc2549.org>
> --- > 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 <anto...@openvpn.net> 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 Openvpn-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/openvpn-devel