]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-auth: auth-scram-server - Add support for channel binding
authorStephan Bosch <stephan.bosch@open-xchange.com>
Sun, 5 Nov 2023 20:04:05 +0000 (21:04 +0100)
committerStephan Bosch <stephan.bosch@open-xchange.com>
Fri, 17 Jan 2025 17:36:39 +0000 (18:36 +0100)
src/lib-auth/auth-scram-server.c
src/lib-auth/auth-scram-server.h
src/lib-auth/auth-scram.h

index 8b7a10a32fc33c84dbe9431bedc066bc313eb783..b6b4062c8c642f10e85047ed3fbe82ba9e24afb4 100644 (file)
@@ -17,7 +17,6 @@
 #include "strfuncs.h"
 #include "strnum.h"
 
-#include "auth-scram.h"
 #include "auth-scram-server.h"
 
 /* s-nonce length */
@@ -37,6 +36,22 @@ auth_scram_server_set_login_username(struct auth_scram_server *server,
                                                   error_r);
 }
 
+static void
+auth_scram_server_start_channel_binding(struct auth_scram_server *server,
+                                       const char *type)
+{
+       i_assert(server->backend->start_channel_binding != NULL);
+       server->backend->start_channel_binding(server, type);
+}
+
+static int
+auth_scram_server_accept_channel_binding(struct auth_scram_server *server,
+                                        buffer_t **data_r)
+{
+       i_assert(server->backend->accept_channel_binding != NULL);
+       return server->backend->accept_channel_binding(server, data_r);
+}
+
 static int
 auth_scram_server_credentials_lookup(struct auth_scram_server *server)
 {
@@ -50,6 +65,7 @@ auth_scram_server_credentials_lookup(struct auth_scram_server *server)
        kdata->stored_key = p_malloc(pool, hmethod->digest_size);
        kdata->server_key = p_malloc(pool, hmethod->digest_size);
 
+       i_assert(server->backend->credentials_lookup != NULL);
        return server->backend->credentials_lookup(server, kdata);
 }
 
@@ -172,12 +188,45 @@ auth_scram_parse_client_first(struct auth_scram_server *server,
 
        /* gs2-cbind-flag  = ("p=" cb-name) / "n" / "y"
         */
+       enum auth_scram_cbind_server_support cbind_support =
+               server->set.cbind_support;
+
        switch (gs2_cbind_flag[0]) {
        case 'p':
-               *error_r = "Channel binding not supported";
-               return -1;
+               if (gs2_cbind_flag[1] != '=' || gs2_cbind_flag[2] == '\0') {
+                       *error_r = "Invalid GS2 header";
+                       return -1;
+               }
+               if (cbind_support == AUTH_SCRAM_CBIND_SERVER_SUPPORT_NONE) {
+                       *error_r = "Channel binding not supported";
+                       return -1;
+               }
+               auth_scram_server_start_channel_binding(server,
+                                                       &gs2_cbind_flag[2]);
+               break;
        case 'y':
+               /* RFC 5802, Section 6:
+
+                  If the flag is set to "y" and the server supports channel
+                  binding, the server MUST fail authentication. This is because
+                  if the client sets the channel binding flag to "y", then the
+                  client must have believed that the server did not support
+                  channel binding -- if the server did in fact support channel
+                  binding, then this is an indication that there has been a
+                  downgrade attack (e.g., an attacker changed the server's
+                  mechanism list to exclude the -PLUS suffixed SCRAM mechanism
+                  name(s)).
+               */
+               if (cbind_support != AUTH_SCRAM_CBIND_SERVER_SUPPORT_NONE) {
+                       *error_r = "Potential downgrade attack detected";
+                       return -1;
+               }
+               break;
        case 'n':
+               if (cbind_support == AUTH_SCRAM_CBIND_SERVER_SUPPORT_REQUIRED) {
+                       *error_r = "Channel binding required";
+                       return -1;
+               }
                break;
        default:
                *error_r = "Invalid GS2 header";
@@ -341,7 +390,9 @@ auth_scram_parse_client_final(struct auth_scram_server *server,
                              const char **error_r)
 {
        const struct hash_method *hmethod = server->set.hash_method;
-       const char **fields, *cbind_input, *nonce_str;
+       const char **fields, *nonce_str;
+       const void *cbind_input;
+       size_t cbind_input_size;
        unsigned int field_count;
        string_t *str;
 
@@ -369,10 +420,30 @@ auth_scram_parse_client_final(struct auth_scram_server *server,
                             ;; gs2-cbind-flag of "p" and MUST be absent
                             ;; for "y" or "n".
         */
-       cbind_input = server->gs2_header;
-       str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(strlen(cbind_input)));
+       if (server->gs2_header[0] != 'p') {
+               cbind_input = server->gs2_header;
+               cbind_input_size = strlen(server->gs2_header);
+       } else {
+               buffer_t *cbind_data;
+
+               if (auth_scram_server_accept_channel_binding(server,
+                                                            &cbind_data) < 0) {
+                       *error_r = "Channel binding failed";
+                       return -1;
+               }
+
+               size_t gs2_header_len = strlen(server->gs2_header);
+               buffer_t *cbind_buf;
+
+               cbind_buf = t_buffer_create(gs2_header_len + cbind_data->used);
+               buffer_append(cbind_buf, server->gs2_header, gs2_header_len);
+               buffer_append_buf(cbind_buf, cbind_data, 0, SIZE_MAX);
+               cbind_input = cbind_buf->data;
+               cbind_input_size = cbind_buf->used;
+       }
+       str = t_str_new(2 + MAX_BASE64_ENCODED_SIZE(cbind_input_size));
        str_append(str, "c=");
-       base64_encode(cbind_input, strlen(cbind_input), str);
+       base64_encode(cbind_input, cbind_input_size, str);
 
        if (!str_equals_timing_almost_safe(fields[0], str_c(str))) {
                *error_r = "Invalid channel binding data";
index 0e39a3fb4c794d47127e951b8023f8eee175ee6e..7c54f98d72c6ad41acff39e4245478e96d9bfc6a 100644 (file)
@@ -35,6 +35,9 @@ enum auth_scram_server_state {
 
 struct auth_scram_server_settings {
        const struct hash_method *hash_method;
+
+       /* The level of support for channel binding from the backend */
+       enum auth_scram_cbind_server_support cbind_support;
 };
 
 struct auth_scram_server_backend {
@@ -45,6 +48,14 @@ struct auth_scram_server_backend {
        bool (*set_login_username)(struct auth_scram_server *server,
                                   const char *username, const char **error_r);
 
+       /* Start channel binding in backend with provided type. */
+       void (*start_channel_binding)(struct auth_scram_server *server,
+                                     const char *type);
+       /* Accept channel binding from backend. Binding data is returned in
+          data_r. */
+       int (*accept_channel_binding)(struct auth_scram_server *server,
+                                     buffer_t **data_r);
+
        /* Instruct the backend to perform credentials lookup. The acquired
           credentials are to be assigned to the provided key_data struct
           eventually. If not immediately, the backend is supposed to call
index 0a6ba936d98cac81d2a846632d023e9b071e8e06..31d9501e35aa7c5f3959200fce7d2f1d40c35ec8 100644 (file)
@@ -7,6 +7,13 @@
 
 #define AUTH_SCRAM_DEFAULT_ITERATE_COUNT 4096
 
+/* The level of SCRAM channel binding support at the server */
+enum auth_scram_cbind_server_support {
+       AUTH_SCRAM_CBIND_SERVER_SUPPORT_NONE = 0,
+       AUTH_SCRAM_CBIND_SERVER_SUPPORT_AVAILABLE,
+       AUTH_SCRAM_CBIND_SERVER_SUPPORT_REQUIRED,
+};
+
 struct auth_scram_key_data {
        pool_t pool;
        const struct hash_method *hmethod;