You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2022/09/01 16:05:54 UTC

[couchdb] 25/31: Upgrade hash algorithm for cookie auth (#4140)

This is an automated email from the ASF dual-hosted git repository.

rnewson pushed a commit to branch raft_storemodule
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit e762b10a20c932d987688d5e73a18aa1d51d9947
Author: Ronny <ro...@apache.org>
AuthorDate: Wed Aug 24 18:45:32 2022 +0200

    Upgrade hash algorithm for cookie auth (#4140)
    
    Introduce a new config setting "hash_algorithms".
    
    The values of the new config parameter is a list of comma-separated values of Erlang hash algorithms.
    
    An example:
    
    hash_algorithms = sha256, sha, md5
    
    This line will use and generate new cookies with the sha256 hash algorithm and accept/verify cookies with the given hash algorithms sha256, sha and md5.
---
 rel/overlay/etc/default.ini                        |  8 ++
 .../eunit/chttpd_auth_hash_algorithms_tests.erl    | 99 ++++++++++++++++++++++
 src/couch/src/couch_httpd_auth.erl                 | 42 ++++++++-
 3 files changed, 146 insertions(+), 3 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index b88dbcbce..15cd0d4bd 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -267,6 +267,14 @@ bind_address = 127.0.0.1
 ; Set the SameSite cookie property for the auth cookie. If empty, the SameSite property is not set.
 ;same_site =
 
+; Set the HMAC algorithm used by cookie authentication
+; Possible values: sha,sha224,sha256,sha384,sha512,sha3_224,sha3_256,sha3_384,sha3_512,
+;                  blake2b,blake2s,md4,md5,ripemd160
+; New cookie sessions are generated with the first hash algorithm.
+; All values can be used to decode the session.
+; Default: sha256, sha
+hash_algorithms = sha256, sha
+
 ; [chttpd_auth_cache]
 ; max_lifetime = 600000
 ; max_objects = 
diff --git a/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl b/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl
new file mode 100644
index 000000000..3d872aa46
--- /dev/null
+++ b/src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl
@@ -0,0 +1,99 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+-module(chttpd_auth_hash_algorithms_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("chttpd/test/eunit/chttpd_test.hrl").
+
+-define(ADM_USER, "adm_user").
+-define(ADM_PASS, "adm_pass").
+-define(ALLOWED_HASHES, "sha256, sha512, sha, blake2s").
+-define(DISALLOWED_HASHES, "md4, md5, ripemd160").
+
+hash_algorithms_test_() ->
+    {
+        "Testing hash algorithms for cookie auth",
+        {
+            setup,
+            fun setup/0,
+            fun teardown/1,
+            with([
+                ?TDEF(test_hash_algorithms_should_work),
+                ?TDEF(test_hash_algorithms_should_fail)
+            ])
+        }
+    }.
+
+% Test utility functions
+setup() ->
+    Ctx = test_util:start_couch([chttpd]),
+    Hashed = couch_passwords:hash_admin_password(?ADM_PASS),
+    NewSecret = ?b2l(couch_uuids:random()),
+    config:set("admins", ?ADM_USER, ?b2l(Hashed), false),
+    config:set("chttpd_auth", "secret", NewSecret, false),
+    config:set("chttpd", "require_valid_user", "true", false),
+    config:set("chttpd_auth", "hash_algorithms", ?ALLOWED_HASHES, false),
+    AllowedHashes = re:split(config:get("chttpd_auth", "hash_algorithms"), "\\s*,\\s*", [
+        trim, {return, binary}
+    ]),
+    DisallowedHashes = re:split(?DISALLOWED_HASHES, "\\s*,\\s*", [trim, {return, binary}]),
+    {Ctx, {AllowedHashes, DisallowedHashes}}.
+
+teardown({Ctx, _}) ->
+    config:delete("chttpd_auth", "hash_algorithms", false),
+    config:delete("chttpd", "require_valid_user", false),
+    config:delete("chttpd_auth", "secret", false),
+    config:delete("admins", ?ADM_USER, false),
+    test_util:stop_couch(Ctx).
+
+% Helper functions
+base_url() ->
+    Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
+    Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)),
+    "http://" ++ Addr ++ ":" ++ Port.
+
+make_auth_session_string(HashAlgorithm, User, Secret, TimeStamp) ->
+    SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
+    Hash = couch_util:hmac(HashAlgorithm, Secret, SessionData),
+    "AuthSession=" ++ couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)).
+
+get_user_props(User) ->
+    couch_auth_cache:get_user_creds(User).
+
+get_full_secret(User) ->
+    {ok, UserProps, _AuthCtx} = get_user_props(User),
+    UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<"">>),
+    Secret = ?l2b(chttpd_util:get_chttpd_auth_config("secret")),
+    <<Secret/binary, UserSalt/binary>>.
+
+% Test functions
+test_hash_algorithm([], _) ->
+    ok;
+test_hash_algorithm([DefaultHashAlgorithm | DecodingHashAlgorithmsList] = _, Status) ->
+    CurrentTime = couch_httpd_auth:make_cookie_time(),
+    Cookie = make_auth_session_string(
+        erlang:binary_to_existing_atom(DefaultHashAlgorithm),
+        ?ADM_USER,
+        get_full_secret(?ADM_USER),
+        CurrentTime
+    ),
+    {ok, ReqStatus, _, _} = test_request:request(get, base_url(), [{cookie, Cookie}]),
+    ?assertEqual(Status, ReqStatus),
+    test_hash_algorithm(DecodingHashAlgorithmsList, Status).
+
+test_hash_algorithms_should_work({_, {AllowedHashes, _}} = _) ->
+    test_hash_algorithm(AllowedHashes, 200).
+
+test_hash_algorithms_should_fail({_, {_, DisallowedHashes}} = _) ->
+    test_hash_algorithm(DisallowedHashes, 401).
diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl
index a5a876b18..e2cb02f8c 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -16,6 +16,8 @@
 
 -include_lib("couch/include/couch_db.hrl").
 
+-define(DEFAULT_HASH_ALGORITHM, sha256).
+
 -export([party_mode_handler/1]).
 
 -export([
@@ -296,6 +298,7 @@ cookie_authentication_handler(#httpd{mochi_req = MochiReq} = Req, AuthModule) ->
                 end,
             % Verify expiry and hash
             CurrentTime = make_cookie_time(),
+            HashAlgorithms = get_config_hash_algorithms(),
             case chttpd_util:get_chttpd_auth_config("secret") of
                 undefined ->
                     couch_log:debug("cookie auth secret is not set", []),
@@ -308,15 +311,18 @@ cookie_authentication_handler(#httpd{mochi_req = MochiReq} = Req, AuthModule) ->
                         {ok, UserProps, _AuthCtx} ->
                             UserSalt = couch_util:get_value(<<"salt">>, UserProps, <<"">>),
                             FullSecret = <<Secret/binary, UserSalt/binary>>,
-                            ExpectedHash = couch_util:hmac(sha, FullSecret, User ++ ":" ++ TimeStr),
                             Hash = ?l2b(HashStr),
+                            VerifyHash = fun(HashAlg) ->
+                                Hmac = couch_util:hmac(HashAlg, FullSecret, User ++ ":" ++ TimeStr),
+                                couch_passwords:verify(Hmac, Hash)
+                            end,
                             Timeout = chttpd_util:get_chttpd_auth_config_integer(
                                 "timeout", 600
                             ),
                             couch_log:debug("timeout ~p", [Timeout]),
                             case (catch erlang:list_to_integer(TimeStr, 16)) of
                                 TimeStamp when CurrentTime < TimeStamp + Timeout ->
-                                    case couch_passwords:verify(ExpectedHash, Hash) of
+                                    case lists:any(VerifyHash, HashAlgorithms) of
                                         true ->
                                             TimeLeft = TimeStamp + Timeout - CurrentTime,
                                             couch_log:debug(
@@ -367,7 +373,8 @@ cookie_auth_header(_Req, _Headers) ->
 
 cookie_auth_cookie(Req, User, Secret, TimeStamp) ->
     SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
-    Hash = couch_util:hmac(sha, Secret, SessionData),
+    [HashAlgorithm | _] = get_config_hash_algorithms(),
+    Hash = couch_util:hmac(HashAlgorithm, Secret, SessionData),
     mochiweb_cookies:cookie(
         "AuthSession",
         couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
@@ -695,3 +702,32 @@ authentication_warning(#httpd{mochi_req = Req}, User) ->
         "~p: Authentication failed for user ~s from ~s",
         [?MODULE, User, Peer]
     ).
+
+verify_hash_names(HashAlgorithms, SupportedHashFun) ->
+    verify_hash_names(HashAlgorithms, SupportedHashFun, []).
+verify_hash_names([], _, HashNames) ->
+    lists:reverse(HashNames);
+verify_hash_names([H | T], SupportedHashFun, HashNames) ->
+    try
+        HashAtom = binary_to_existing_atom(H),
+        Result =
+            case lists:member(HashAtom, SupportedHashFun) of
+                true -> [HashAtom | HashNames];
+                false -> HashNames
+            end,
+        verify_hash_names(T, SupportedHashFun, Result)
+    catch
+        error:badarg ->
+            couch_log:warning("~p: Hash algorithm ~s is not valid.", [?MODULE, H]),
+            verify_hash_names(T, SupportedHashFun, HashNames)
+    end.
+
+-spec get_config_hash_algorithms() -> list(atom()).
+get_config_hash_algorithms() ->
+    SupportedHashAlgorithms = crypto:supports(hashs),
+    HashAlgorithmsStr = chttpd_util:get_chttpd_auth_config("hash_algorithms", "sha256, sha"),
+    HashAlgorithms = re:split(HashAlgorithmsStr, "\\s*,\\s*", [trim, {return, binary}]),
+    case verify_hash_names(HashAlgorithms, SupportedHashAlgorithms) of
+        [] -> [?DEFAULT_HASH_ALGORITHM];
+        VerifiedHashNames -> VerifiedHashNames
+    end.