in the OpenVPN source distribution.
.\"*********************************************************
.TP
-.B \-\-auth\-gen\-token [lifetime]
+.B \-\-auth\-gen\-token [lifetime] [external-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
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]
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)
}
+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)
{
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
* 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)));
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)
{
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)",
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)));
{
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)",
{
msg(M_INFO, "--auth-token-gen: auth-token from client expired");
}
-
return ret;
}
*
* 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
* 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
*
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
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
"ifconfig_pool_netmask=",
"time_duration=",
"bytes_sent=",
- "bytes_received="
+ "bytes_received=",
+ "session_id=",
+ "session_state="
};
if (env_filter_level == 0)
&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))))
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;
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;
*/
#define KEY_SCAN_SIZE 3
-
/**
* Security parameter state for a single VPN tunnel.
* @ingroup control_processor
/**< Auth-token sent from client has valid hmac */
#define AUTH_TOKEN_EXPIRED (1<<1)
/**< Auth-token sent from client has expired */
+#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 */
* 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();
/* 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);
* 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
/* 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))
{
msg(D_TLS_ERRORS, "TLS Auth Error (%s): "
"could not create deferred auth control file", __func__);
- goto cleanup;
+ return retval;
}
#endif
msg(D_TLS_ERRORS, "TLS Auth Error (verify_user_pass_plugin): peer provided a blank username");
}
-cleanup:
return retval;
}
#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 */
/* 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);
*/
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
{
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
* 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);
}