# HG changeset patch # User Florian Zeitz # Date 1316132569 -7200 # Node ID 9d3cb2ef74d8a235ff86b4f8d644a28a47c17a70 # Parent 61d3544f8fdf03558055d240f5815dc1eac7fb66 lib: Add hmac-sha1 adapted from hmac-md5 diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -43,6 +43,7 @@ hex-binary.c \ hex-dec.c \ hmac-md5.c \ + hmac-sha1.c \ home-expand.c \ hostpid.c \ imem.c \ diff --git a/src/lib/hmac-sha1.c b/src/lib/hmac-sha1.c new file mode 100644 --- /dev/null +++ b/src/lib/hmac-sha1.c @@ -0,0 +1,52 @@ +/* + * HMAC-SHA1 (RFC-2104) implementation. + * + * Copyright (c) 2004 Andrey Panin + * Copyright (c) 2011 Florian Zeitz + * + * This software is released under the MIT license. + */ + +#include "lib.h" +#include "hmac-sha1.h" +#include "safe-memset.h" + +void hmac_sha1_init(struct hmac_sha1_context *ctx, + const unsigned char *key, size_t key_len) +{ + int i; + unsigned char sha1key[20]; + unsigned char k_ipad[64]; + unsigned char k_opad[64]; + + if (key_len > 64) { + sha1_get_digest(key, key_len, sha1key); + key = sha1key; + key_len = 20; + } + + memcpy(k_ipad, key, key_len); + memset(k_ipad + key_len, 0, 64 - key_len); + memcpy(k_opad, k_ipad, 64); + + for (i = 0; i < 64; i++) { + k_ipad[i] ^= 0x36; + k_opad[i] ^= 0x5c; + } + + sha1_init(&ctx->ctx); + sha1_loop(&ctx->ctx, k_ipad, 64); + sha1_init(&ctx->ctxo); + sha1_loop(&ctx->ctxo, k_opad, 64); + + safe_memset(k_ipad, 0, 64); + safe_memset(k_opad, 0, 64); +} + +void hmac_sha1_final(struct hmac_sha1_context *ctx, unsigned char *digest) +{ + sha1_result(&ctx->ctx, digest); + + sha1_loop(&ctx->ctxo, digest, 20); + sha1_result(&ctx->ctxo, digest); +} diff --git a/src/lib/hmac-sha1.h b/src/lib/hmac-sha1.h new file mode 100644 --- /dev/null +++ b/src/lib/hmac-sha1.h @@ -0,0 +1,22 @@ +#ifndef HMAC_SHA1_H +#define HMAC_SHA1_H + +#include "sha1.h" + +struct hmac_sha1_context { + struct sha1_ctxt ctx, ctxo; +}; + +void hmac_sha1_init(struct hmac_sha1_context *ctx, + const unsigned char *key, size_t key_len); +void hmac_sha1_final(struct hmac_sha1_context *ctx, + unsigned char digest[SHA1_RESULTLEN]); + + +static inline void +hmac_sha1_update(struct hmac_sha1_context *ctx, const void *data, size_t size) +{ + sha1_loop(&ctx->ctx, data, size); +} + +#endif # HG changeset patch # User Florian Zeitz # Date 1316132640 -7200 # Node ID fea09a51a98b789cba58a0ca4f0e220f2ae1f646 # Parent 9d3cb2ef74d8a235ff86b4f8d644a28a47c17a70 auth: Implement the SCRAM-SHA-1 SASL mechanism diff --git a/src/auth/Makefile.am b/src/auth/Makefile.am --- a/src/auth/Makefile.am +++ b/src/auth/Makefile.am @@ -81,6 +81,7 @@ mech-gssapi.c \ mech-ntlm.c \ mech-otp.c \ + mech-scram-sha1.c \ mech-skey.c \ mech-rpa.c \ mech-apop.c \ diff --git a/src/auth/mech-scram-sha1.c b/src/auth/mech-scram-sha1.c new file mode 100644 --- /dev/null +++ b/src/auth/mech-scram-sha1.c @@ -0,0 +1,405 @@ +/* + * SCRAM-SHA-1 SASL authentication, see RFC-5802 + * + * Copyright (c) 2011 Florian Zeitz + * + * This software is released under the MIT license. + */ + +#include "auth-common.h" +#include "base64.h" +#include "buffer.h" +#include "hmac-sha1.h" +#include "randgen.h" +#include "safe-memset.h" +#include "str.h" +#include "strfuncs.h" +#include "mech.h" + +struct scram_auth_request { + struct auth_request auth_request; + + pool_t pool; + unsigned int authenticated:1; + + /* sent: */ + char *server_first_message; + unsigned char salt[16]; + unsigned char salted_password[SHA1_RESULTLEN]; + + /* received: */ + char *gs2_cbind_flag; + char *cnonce; + char *snonce; + char *client_first_message_bare; + char *client_final_message_without_proof; + buffer_t *proof; +}; + +static void Hi(const unsigned char *str, size_t str_size, + const unsigned char *salt, size_t salt_size, unsigned int i, + unsigned char result[SHA1_RESULTLEN]) +{ + struct hmac_sha1_context ctx; + unsigned char U[SHA1_RESULTLEN]; + size_t j, k; + + /* Calculate U1 */ + hmac_sha1_init(&ctx, str, str_size); + hmac_sha1_update(&ctx, salt, salt_size); + hmac_sha1_update(&ctx, "\0\0\0\1", 4); + hmac_sha1_final(&ctx, U); + + memcpy(result, U, SHA1_RESULTLEN); + + /* Calculate U2 to Ui and Hi*/ + for (j = 2; j <= i; j++) { + hmac_sha1_init(&ctx, str, str_size); + hmac_sha1_update(&ctx, U, sizeof(U)); + hmac_sha1_final(&ctx, U); + for (k = 0; k < SHA1_RESULTLEN; k++) + result[k] ^= U[k]; + } +} + +static const char *get_scram_server_first(struct scram_auth_request *request) +{ + unsigned char snonce[65]; + string_t *str; + size_t i; + + 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)); + + random_fill(request->salt, sizeof(request->salt)); + + str = t_str_new(MAX_BASE64_ENCODED_SIZE(sizeof(request->salt))); + base64_encode(request->salt, sizeof(request->salt), str); + + return t_strdup_printf("r=%s%s,s=%s,i=%i", request->cnonce, + request->snonce, str_c(str), 4096); +} + +static const char *get_scram_server_final(struct scram_auth_request *request) +{ + struct hmac_sha1_context ctx; + const char *auth_message; + unsigned char server_key[SHA1_RESULTLEN]; + unsigned char server_signature[SHA1_RESULTLEN]; + string_t *str; + + auth_message = t_strconcat(request->client_first_message_bare, ",", + request->server_first_message, ",", + request->client_final_message_without_proof, NULL); + + hmac_sha1_init(&ctx, request->salted_password, + sizeof(request->salted_password)); + hmac_sha1_update(&ctx, "Server Key", 10); + hmac_sha1_final(&ctx, server_key); + + safe_memset(request->salted_password, 0, + sizeof(request->salted_password)); + + hmac_sha1_init(&ctx, server_key, sizeof(server_key)); + hmac_sha1_update(&ctx, auth_message, strlen(auth_message)); + hmac_sha1_final(&ctx, server_signature); + + str = t_str_new(MAX_BASE64_ENCODED_SIZE(sizeof(server_signature))); + base64_encode(server_signature, sizeof(server_signature), str); + + return t_strdup_printf("v=%s", str_c(str)); +} + +static bool parse_scram_client_first(struct scram_auth_request *request, + const unsigned char *data, size_t size, + const char **error) +{ + const char *const *fields; + const char *p; + string_t *username; + + fields = t_strsplit(t_strndup(data, size), ","); + + if (str_array_length(fields) < 4) { + *error = "Invalid initial client message"; + return FALSE; + } + + switch (fields[0][0]) { + case 'p': + *error = "Channel binding not supported"; + return FALSE; + case 'y': + case 'n': + request->gs2_cbind_flag = p_strdup(request->pool, fields[0]); + break; + default: + *error = "Invalid GS2 header"; + return FALSE; + } + + if (fields[1][0] != '\0') { + *error = "authzid not supported"; + return FALSE; + } + + if (fields[2][0] == 'm') { + *error = "Mandatory extension(s) not supported"; + return FALSE; + } + + if (fields[2][0] == 'n') { + /* Unescape username */ + username = t_str_new(0); + + for (p = fields[2] + 2; *p != '\0'; p++) { + if (p[0] == '=') { + if (p[1] == '2' && p[2] == 'C') { + str_append_c(username, ','); + } else if (p[1] == '3' && p[2] == 'D') { + str_append_c(username, '='); + } else { + *error = "Username contains " + "forbidden character(s)"; + return FALSE; + } + p += 2; + } else if (p[0] == ',') { + *error = "Username contains " + "forbidden character(s)"; + return FALSE; + } else { + str_append_c(username, *p); + } + } + if (!auth_request_set_username(&request->auth_request, + str_c(username), error)) + return FALSE; + } else { + *error = "Invalid username"; + return FALSE; + } + + if (fields[3][0] == 'r') + request->cnonce = p_strdup(request->pool, fields[3]+2); + else { + *error = "Invalid client nonce"; + return FALSE; + } + + /* This works only without channel binding support, + otherwise the GS2 header doesn't have a fixed length */ + request->client_first_message_bare = + p_strndup(request->pool, data + 3, size - 3); + + return TRUE; +} + +static bool verify_credentials(struct scram_auth_request *request, + const unsigned char *credentials, size_t size) +{ + struct hmac_sha1_context ctx; + const char *auth_message; + unsigned char client_key[SHA1_RESULTLEN]; + unsigned char client_signature[SHA1_RESULTLEN]; + unsigned char stored_key[SHA1_RESULTLEN]; + size_t i; + + /* FIXME: credentials should be SASLprepped UTF8 data here */ + Hi(credentials, size, request->salt, sizeof(request->salt), 4096, + request->salted_password); + + hmac_sha1_init(&ctx, request->salted_password, + sizeof(request->salted_password)); + hmac_sha1_update(&ctx, "Client Key", 10); + hmac_sha1_final(&ctx, client_key); + + sha1_get_digest(client_key, sizeof(client_key), stored_key); + + auth_message = t_strconcat(request->client_first_message_bare, ",", + request->server_first_message, ",", + request->client_final_message_without_proof, NULL); + + hmac_sha1_init(&ctx, stored_key, sizeof(stored_key)); + hmac_sha1_update(&ctx, auth_message, strlen(auth_message)); + hmac_sha1_final(&ctx, client_signature); + + for (i = 0; i < sizeof(client_signature); i++) + client_signature[i] ^= client_key[i]; + + safe_memset(client_key, 0, sizeof(client_key)); + safe_memset(stored_key, 0, sizeof(stored_key)); + + if (!memcmp(client_signature, request->proof->data, + request->proof->used)) + return TRUE; + + return FALSE; +} + +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 *server_final_message; + + switch (result) { + case PASSDB_RESULT_OK: + if (!verify_credentials(request, credentials, size)) { + auth_request_log_info(auth_request, "scram-sha-1", + "password mismatch"); + auth_request_fail(auth_request); + } else { + request->authenticated = TRUE; + server_final_message = get_scram_server_final(request); + auth_request_handler_reply_continue(auth_request, + server_final_message, + strlen(server_final_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 ATTR_UNUSED, + const char **error) +{ + const char **fields; + unsigned int field_count; + const char *cbind_input; + string_t *str; + + fields = t_strsplit((const char*)data, ","); + field_count = str_array_length(fields); + + if (field_count < 3) { + *error = "Invalid final client message"; + return FALSE; + } + + cbind_input = t_strconcat(request->gs2_cbind_flag, ",,", NULL); + str = t_str_new(MAX_BASE64_ENCODED_SIZE(strlen(cbind_input))); + base64_encode(cbind_input, strlen(cbind_input), str); + + if (strcmp(fields[0], t_strconcat("c=", str_c(str), NULL))) { + *error = "Invalid channel binding data"; + return FALSE; + } + + if (strcmp(fields[1], t_strconcat("r=", request->cnonce, + request->snonce, NULL))) { + *error = "Wrong nonce"; + return FALSE; + } + + 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) + || (request->proof->used != SHA1_RESULTLEN)) { + *error = "Invalid base64 encoding " + "or length for ClientProof"; + return FALSE; + } + } else { + *error = "Invalid ClientProof"; + return FALSE; + } + + str_array_remove(fields, fields[field_count-1]); + request->client_final_message_without_proof = p_strdup(request->pool, + t_strarray_join(fields, ",")); + + auth_request_lookup_credentials(&request->auth_request, "PLAIN", + credentials_callback); + + return TRUE; +} + +static void mech_scram_sha1_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; + + if (request->authenticated) { + /* authentication is done, we were just waiting the last (empty) + client response */ + auth_request_success(auth_request, NULL, 0); + return; + } + + if (!request->client_first_message_bare) { + /* Received client-first-message */ + if (parse_scram_client_first(request, data, + data_size, &error)) { + request->server_first_message = p_strdup(request->pool, + get_scram_server_first(request)); + auth_request_handler_reply_continue(auth_request, + request->server_first_message, + strlen(request->server_first_message)); + return; + } + } else { + /* Received client-final-message */ + if (parse_scram_client_final(request, data, data_size, &error)) + return; + } + + if (error == NULL) + error = "authentication failed"; + + auth_request_log_info(auth_request, "scram-sha-1", "%s", error); + auth_request_fail(auth_request); +} + +static struct auth_request *mech_scram_sha1_auth_new(void) +{ + struct scram_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create("scram_sha1_auth_request", 2048); + request = p_new(pool, struct scram_auth_request, 1); + request->pool = pool; + + request->client_first_message_bare = NULL; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +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_sha1_auth_continue, + mech_generic_auth_free +}; diff --git a/src/auth/mech.c b/src/auth/mech.c --- a/src/auth/mech.c +++ b/src/auth/mech.c @@ -70,6 +70,7 @@ extern const struct mech_module mech_external; extern const struct mech_module mech_ntlm; extern const struct mech_module mech_otp; +extern const struct mech_module mech_scram_sha1; extern const struct mech_module mech_skey; extern const struct mech_module mech_rpa; extern const struct mech_module mech_anonymous; @@ -172,6 +173,7 @@ #endif } mech_register_module(&mech_otp); + mech_register_module(&mech_scram_sha1); mech_register_module(&mech_skey); mech_register_module(&mech_rpa); mech_register_module(&mech_anonymous);