summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRonny <ronny@apache.org>2022-08-24 18:45:32 +0200
committerGitHub <noreply@github.com>2022-08-24 18:45:32 +0200
commitbc3242bc8cdf1b4e50da52c4d6c8ac2aeface4cb (patch)
tree9be685b76c6ae88cb9d49f416745b6a3c1a2c21e
parent133d6bb4007544d687f671c6ddfd96ae0d6153f0 (diff)
downloadcouchdb-bc3242bc8cdf1b4e50da52c4d6c8ac2aeface4cb.tar.gz
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.
-rw-r--r--rel/overlay/etc/default.ini8
-rw-r--r--src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl99
-rw-r--r--src/couch/src/couch_httpd_auth.erl42
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.