diff options
Diffstat (limited to 'src/auth/mech-scram.c')
-rw-r--r-- | src/auth/mech-scram.c | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/src/auth/mech-scram.c b/src/auth/mech-scram.c new file mode 100644 index 0000000..a90d0d1 --- /dev/null +++ b/src/auth/mech-scram.c @@ -0,0 +1,550 @@ +/* + * SCRAM-SHA-1 SASL authentication, see RFC-5802 + * + * Copyright (c) 2011-2016 Florian Zeitz <florob@babelmonkeys.de> + * + * This software is released under the MIT license. + */ + +#include <limits.h> + +#include "auth-common.h" +#include "base64.h" +#include "buffer.h" +#include "hmac.h" +#include "sha1.h" +#include "sha2.h" +#include "randgen.h" +#include "safe-memset.h" +#include "str.h" +#include "strfuncs.h" +#include "strnum.h" +#include "password-scheme.h" +#include "mech.h" +#include "mech-scram.h" + +/* s-nonce length */ +#define SCRAM_SERVER_NONCE_LEN 64 + +struct scram_auth_request { + struct auth_request auth_request; + + pool_t pool; + + const struct hash_method *hash_method; + const char *password_scheme; + + /* sent: */ + const char *server_first_message; + const char *snonce; + + /* received: */ + const char *gs2_header; + const char *cnonce; + const char *client_first_message_bare; + const char *client_final_message_without_proof; + buffer_t *proof; + + /* stored */ + unsigned char *stored_key; + unsigned char *server_key; +}; + +static const char * +get_scram_server_first(struct scram_auth_request *request, + int iter, const char *salt) +{ + unsigned char snonce[SCRAM_SERVER_NONCE_LEN+1]; + string_t *str; + size_t i; + + /* RFC 5802, Section 7: + + server-first-message = + [reserved-mext ","] nonce "," salt "," + iteration-count ["," extensions] + + nonce = "r=" c-nonce [s-nonce] + + salt = "s=" base64 + + iteration-count = "i=" posit-number + ;; A positive number. + */ + + random_fill(snonce, sizeof(snonce)-1); + + /* make sure snonce is printable and does not contain ',' */ + for (i = 0; i < sizeof(snonce)-1; i++) { + snonce[i] = (snonce[i] % ('~' - '!')) + '!'; + if (snonce[i] == ',') + snonce[i] = '~'; + } + snonce[sizeof(snonce)-1] = '\0'; + request->snonce = p_strndup(request->pool, snonce, sizeof(snonce)); + + str = t_str_new(32 + strlen(request->cnonce) + sizeof(snonce) + + strlen(salt)); + str_printfa(str, "r=%s%s,s=%s,i=%d", request->cnonce, request->snonce, + salt, iter); + return str_c(str); +} + +static const char *get_scram_server_final(struct scram_auth_request *request) +{ + const struct hash_method *hmethod = request->hash_method; + struct hmac_context ctx; + const char *auth_message; + unsigned char server_signature[hmethod->digest_size]; + string_t *str; + + /* RFC 5802, Section 3: + + AuthMessage := client-first-message-bare + "," + + server-first-message + "," + + client-final-message-without-proof + ServerSignature := HMAC(ServerKey, AuthMessage) + */ + auth_message = t_strconcat(request->client_first_message_bare, ",", + request->server_first_message, ",", + request->client_final_message_without_proof, NULL); + + hmac_init(&ctx, request->server_key, hmethod->digest_size, hmethod); + hmac_update(&ctx, auth_message, strlen(auth_message)); + hmac_final(&ctx, server_signature); + + /* RFC 5802, Section 7: + + server-final-message = (server-error / verifier) + ["," extensions] + + verifier = "v=" base64 + ;; base-64 encoded ServerSignature. + + */ + str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(sizeof(server_signature))); + str_append(str, "v="); + base64_encode(server_signature, sizeof(server_signature), str); + + return str_c(str); +} + +static const char *scram_unescape_username(const char *in) +{ + string_t *out; + + /* RFC 5802, Section 5.1: + + The characters ',' or '=' in usernames are sent as '=2C' and '=3D' + respectively. If the server receives a username that contains '=' + not followed by either '2C' or '3D', then the server MUST fail the + authentication. + */ + + out = t_str_new(64); + for (; *in != '\0'; in++) { + i_assert(in[0] != ','); /* strsplit should have caught this */ + + if (in[0] == '=') { + if (in[1] == '2' && in[2] == 'C') + str_append_c(out, ','); + else if (in[1] == '3' && in[2] == 'D') + str_append_c(out, '='); + else + return NULL; + in += 2; + } else { + str_append_c(out, *in); + } + } + return str_c(out); +} + +static bool +parse_scram_client_first(struct scram_auth_request *request, + const unsigned char *data, size_t size, + const char **error_r) +{ + const char *login_username = NULL; + const char *data_cstr, *p; + const char *gs2_header, *gs2_cbind_flag, *authzid; + const char *cfm_bare, *username, *nonce; + const char *const *fields; + + data_cstr = gs2_header = t_strndup(data, size); + + /* RFC 5802, Section 7: + + client-first-message = gs2-header client-first-message-bare + gs2-header = gs2-cbind-flag "," [ authzid ] "," + + client-first-message-bare = [reserved-mext ","] + username "," nonce ["," extensions] + + extensions = attr-val *("," attr-val) + ;; All extensions are optional, + ;; i.e., unrecognized attributes + ;; not defined in this document + ;; MUST be ignored. + attr-val = ALPHA "=" value + */ + p = strchr(data_cstr, ','); + if (p == NULL) { + *error_r = "Invalid initial client message: " + "Missing first ',' in GS2 header"; + return FALSE; + } + gs2_cbind_flag = t_strdup_until(data_cstr, p); + data_cstr = p + 1; + + p = strchr(data_cstr, ','); + if (p == NULL) { + *error_r = "Invalid initial client message: " + "Missing second ',' in GS2 header"; + return FALSE; + } + authzid = t_strdup_until(data_cstr, p); + gs2_header = t_strdup_until(gs2_header, p + 1); + cfm_bare = p + 1; + + fields = t_strsplit(cfm_bare, ","); + if (str_array_length(fields) < 2) { + *error_r = "Invalid initial client message: " + "Missing nonce field"; + return FALSE; + } + username = fields[0]; + nonce = fields[1]; + + /* gs2-cbind-flag = ("p=" cb-name) / "n" / "y" + */ + switch (gs2_cbind_flag[0]) { + case 'p': + *error_r = "Channel binding not supported"; + return FALSE; + case 'y': + case 'n': + break; + default: + *error_r = "Invalid GS2 header"; + return FALSE; + } + + /* authzid = "a=" saslname + ;; Protocol specific. + */ + if (authzid[0] == '\0') + ; + else if (authzid[0] == 'a' && authzid[1] == '=') { + /* Unescape authzid */ + login_username = scram_unescape_username(authzid + 2); + + if (login_username == NULL) { + *error_r = "authzid escaping is invalid"; + return FALSE; + } + } else { + *error_r = "Invalid authzid field"; + return FALSE; + } + + /* reserved-mext = "m=" 1*(value-char) + */ + if (username[0] == 'm') { + *error_r = "Mandatory extension(s) not supported"; + return FALSE; + } + /* username = "n=" saslname + */ + if (username[0] == 'n' && username[1] == '=') { + /* Unescape username */ + username = scram_unescape_username(username + 2); + if (username == NULL) { + *error_r = "Username escaping is invalid"; + return FALSE; + } + if (!auth_request_set_username(&request->auth_request, + username, error_r)) + return FALSE; + } else { + *error_r = "Invalid username field"; + return FALSE; + } + if (login_username != NULL) { + if (!auth_request_set_login_username(&request->auth_request, + login_username, error_r)) + return FALSE; + } + + /* nonce = "r=" c-nonce [s-nonce] */ + if (nonce[0] == 'r' && nonce[1] == '=') + request->cnonce = p_strdup(request->pool, nonce+2); + else { + *error_r = "Invalid client nonce"; + return FALSE; + } + + request->gs2_header = p_strdup(request->pool, gs2_header); + request->client_first_message_bare = p_strdup(request->pool, cfm_bare); + return TRUE; +} + +static bool verify_credentials(struct scram_auth_request *request) +{ + const struct hash_method *hmethod = request->hash_method; + struct hmac_context ctx; + const char *auth_message; + unsigned char client_key[hmethod->digest_size]; + unsigned char client_signature[hmethod->digest_size]; + unsigned char stored_key[hmethod->digest_size]; + size_t i; + + /* RFC 5802, Section 3: + + AuthMessage := client-first-message-bare + "," + + server-first-message + "," + + client-final-message-without-proof + ClientSignature := HMAC(StoredKey, AuthMessage) + */ + auth_message = t_strconcat(request->client_first_message_bare, ",", + request->server_first_message, ",", + request->client_final_message_without_proof, NULL); + + hmac_init(&ctx, request->stored_key, hmethod->digest_size, hmethod); + hmac_update(&ctx, auth_message, strlen(auth_message)); + hmac_final(&ctx, client_signature); + + /* ClientProof := ClientKey XOR ClientSignature */ + const unsigned char *proof_data = request->proof->data; + for (i = 0; i < sizeof(client_signature); i++) + client_key[i] = proof_data[i] ^ client_signature[i]; + + /* StoredKey := H(ClientKey) */ + hash_method_get_digest(hmethod, client_key, sizeof(client_key), + stored_key); + + safe_memset(client_key, 0, sizeof(client_key)); + safe_memset(client_signature, 0, sizeof(client_signature)); + + return mem_equals_timing_safe(stored_key, request->stored_key, + sizeof(stored_key)); +} + +static void +credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + struct scram_auth_request *request = + (struct scram_auth_request *)auth_request; + const char *salt, *error; + unsigned int iter_count; + + switch (result) { + case PASSDB_RESULT_OK: + if (scram_scheme_parse(request->hash_method, + request->password_scheme, + credentials, size, &iter_count, &salt, + request->stored_key, request->server_key, + &error) < 0) { + e_info(auth_request->mech_event, + "%s", error); + auth_request_fail(auth_request); + break; + } + + request->server_first_message = p_strdup(request->pool, + get_scram_server_first(request, iter_count, salt)); + + auth_request_handler_reply_continue(auth_request, + request->server_first_message, + strlen(request->server_first_message)); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static bool +parse_scram_client_final(struct scram_auth_request *request, + const unsigned char *data, size_t size, + const char **error_r) +{ + const struct hash_method *hmethod = request->hash_method; + const char **fields, *cbind_input, *nonce_str; + unsigned int field_count; + string_t *str; + + /* RFC 5802, Section 7: + + client-final-message-without-proof = + channel-binding "," nonce ["," + extensions] + client-final-message = + client-final-message-without-proof "," proof + */ + fields = t_strsplit(t_strndup(data, size), ","); + field_count = str_array_length(fields); + if (field_count < 3) { + *error_r = "Invalid final client message"; + return FALSE; + } + + /* channel-binding = "c=" base64 + ;; base64 encoding of cbind-input. + + cbind-data = 1*OCTET + cbind-input = gs2-header [ cbind-data ] + ;; cbind-data MUST be present for + ;; gs2-cbind-flag of "p" and MUST be absent + ;; for "y" or "n". + */ + cbind_input = request->gs2_header; + str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(strlen(cbind_input))); + str_append(str, "c="); + base64_encode(cbind_input, strlen(cbind_input), str); + + if (strcmp(fields[0], str_c(str)) != 0) { + *error_r = "Invalid channel binding data"; + return FALSE; + } + + /* nonce = "r=" c-nonce [s-nonce] + ;; Second part provided by server. + c-nonce = printable + s-nonce = printable + */ + nonce_str = t_strconcat("r=", request->cnonce, request->snonce, NULL); + if (strcmp(fields[1], nonce_str) != 0) { + *error_r = "Wrong nonce"; + return FALSE; + } + + /* proof = "p=" base64 + */ + if (fields[field_count-1][0] == 'p') { + size_t len = strlen(&fields[field_count-1][2]); + + request->proof = buffer_create_dynamic(request->pool, + MAX_BASE64_DECODED_SIZE(len)); + if (base64_decode(&fields[field_count-1][2], len, NULL, + request->proof) < 0) { + *error_r = "Invalid base64 encoding"; + return FALSE; + } + if (request->proof->used != hmethod->digest_size) { + *error_r = "Invalid ClientProof length"; + return FALSE; + } + } else { + *error_r = "Invalid ClientProof"; + return FALSE; + } + + (void)str_array_remove(fields, fields[field_count-1]); + request->client_final_message_without_proof = + p_strdup(request->pool, t_strarray_join(fields, ",")); + + return TRUE; +} + +void mech_scram_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct scram_auth_request *request = + (struct scram_auth_request *)auth_request; + const char *error = NULL; + const char *server_final_message; + size_t len; + + if (request->client_first_message_bare == NULL) { + /* Received client-first-message */ + if (parse_scram_client_first(request, data, + data_size, &error)) { + auth_request_lookup_credentials( + &request->auth_request, + request->password_scheme, + credentials_callback); + return; + } + } else { + /* Received client-final-message */ + if (parse_scram_client_final(request, data, data_size, + &error)) { + if (!verify_credentials(request)) { + e_info(auth_request->mech_event, + AUTH_LOG_MSG_PASSWORD_MISMATCH); + } else { + server_final_message = + get_scram_server_final(request); + len = strlen(server_final_message); + auth_request_success(auth_request, + server_final_message, len); + return; + } + } + } + + if (error != NULL) + e_info(auth_request->mech_event, "%s", error); + auth_request_fail(auth_request); +} + +struct auth_request * +mech_scram_auth_new(const struct hash_method *hash_method, + const char *password_scheme) +{ + struct scram_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"scram_auth_request", 2048); + request = p_new(pool, struct scram_auth_request, 1); + request->pool = pool; + + request->hash_method = hash_method; + request->password_scheme = password_scheme; + + request->stored_key = p_malloc(pool, hash_method->digest_size); + request->server_key = p_malloc(pool, hash_method->digest_size); + + request->auth_request.pool = pool; + return &request->auth_request; +} + +static struct auth_request *mech_scram_sha1_auth_new(void) +{ + return mech_scram_auth_new(&hash_method_sha1, "SCRAM-SHA-1"); +} + +static struct auth_request *mech_scram_sha256_auth_new(void) +{ + return mech_scram_auth_new(&hash_method_sha256, "SCRAM-SHA-256"); +} + +const struct mech_module mech_scram_sha1 = { + "SCRAM-SHA-1", + + .flags = MECH_SEC_MUTUAL_AUTH, + .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + + mech_scram_sha1_auth_new, + mech_generic_auth_initial, + mech_scram_auth_continue, + mech_generic_auth_free +}; + +const struct mech_module mech_scram_sha256 = { + "SCRAM-SHA-256", + + .flags = MECH_SEC_MUTUAL_AUTH, + .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + + mech_scram_sha256_auth_new, + mech_generic_auth_initial, + mech_scram_auth_continue, + mech_generic_auth_free +}; |