Hy,

over the last days I have implemented SCRAM-SHA-1 in Dovecot's 2.1
branch. It does not do SCRAM-SHA-1-PLUS, but should be extendable enough
to introduce it later.

There are some checks for the message format which (assuming the client
acts correclty) are not strictly necessary during parsing. This is
partially in the hope that it might aid client implementers, partially
because it (IMHO) improves readability when checking against the RFC.

Also errors found in this way could be sent to the client, this is
however strictly OPTIONAL in the RFC, for now they are just logged.

Some of the variable names are rather long. This is in order to have
them match the terms introduced in the RFC, again I expect it to help
readability (maybe my recent Objective-C programming showing though).

I do feel somewhat insecure about my usage of some lib functions.
Hopefully no API has been abused too much.

I also note that there are a lot of fields in the scram_auth_request
struct. I think they are all there for a reason, however feel free to
prove me wrong.

Attached is a hg export.
It also includes a hmac-sha1 implementation, an adaption off of the
hmac-md5 implementation already in Dovecot. I guess those should
eventually be merged into a hash-independent hmac implementation, but I
figured this would have to do for now.

The implementation has been tested against GNU SASL and does appear to
work fine. (The command line was `gsasl -m SCRAM-SHA-1 -a user -p pass
--imap host` for those curious)

Regards
Florian "Florob" Zeitz
# HG changeset patch
# User Florian Zeitz <[email protected]>
# 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 <[email protected]>
+ * Copyright (c) 2011 Florian Zeitz <[email protected]>
+ *
+ * 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 <[email protected]>
# Date 1316132640 -7200
# Node ID e559c59fda1f93d2c1319f9f5d2d8b7d83cfc8ff
# 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,397 @@
+/*
+ * SCRAM-SHA-1 SASL authentication, see RFC-5802
+ *
+ * Copyright (c) 2011 Florian Zeitz <[email protected]>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "auth-common.h"
+#include "base64.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 *username;
+       char *gs2_cbind_flag;
+       char *cnonce;
+       char *snonce;
+       char *client_first_message_bare;
+       char *client_final_message_without_proof;
+       char *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((const char*)data, ",");
+
+       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);
+                       }
+               }
+
+               request->username = p_strdup(request->pool, str_c(username));
+       } 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];
+       string_t *str;
+       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];
+
+       str = t_str_new(MAX_BASE64_ENCODED_SIZE(sizeof(client_signature)));
+       base64_encode(client_signature, sizeof(client_signature), str);
+
+       safe_memset(client_key, 0, sizeof(client_key));
+       safe_memset(client_signature, 0, sizeof(client_signature));
+       safe_memset(stored_key, 0, sizeof(stored_key));
+
+       if (!strcmp(request->proof, str_c(str)))
+               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_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') {
+               request->proof = p_strdup(request->pool,
+                               &fields[field_count-1][2]);
+       } 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)
+                   && auth_request_set_username(auth_request,
+                           request->username, &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);

Reply via email to