| #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 "ssl_verify.h" |
| #include <inttypes.h> |
| |
| 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) + AUTH_TOKEN_SESSION_ID_LEN + 32) |
| |
| static struct key_type |
| auth_token_kt(void) |
| { |
| struct key_type kt = { 0 }; |
| /* 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 |
| add_session_token_env(struct tls_session *session, struct tls_multi *multi, |
| const struct user_pass *up) |
| { |
| if (!multi->opt.auth_token_generate) |
| { |
| return; |
| } |
| |
| int auth_token_state_flags = session->key[KS_PRIMARY].auth_token_state_flags; |
| |
| const char *state; |
| |
| if (!is_auth_token(up->password)) |
| { |
| state = "Initial"; |
| } |
| else if (auth_token_state_flags & AUTH_TOKEN_HMAC_OK) |
| { |
| switch (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 (auth_token_state_flags & AUTH_TOKEN_HMAC_OK |
| && !(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 + strlen(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) |
| { |
| write_pem_key_file(filename, auth_token_pem_name); |
| } |
| |
| void |
| auth_token_init_secret(struct key_ctx *key_ctx, const char *key_file, |
| bool 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); |
| |
| 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 |
| * 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_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); |
| |
| /* |
| * 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); |
| |
| /* |
| * 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 |
| */ |
| struct key_state *ks = &multi->session[TM_ACTIVE].key[KS_PRIMARY]; |
| if (ks->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) + 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))); |
| |
| 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 (%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) |
| { |
| 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 the size of the |
| * timestamp + hmac + session id |
| */ |
| 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 *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))); |
| 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 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)", |
| 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) |
| { |
| /* 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; |
| } |
| |
| 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; |
| } |
| } |