diff options
author | Ronny <ronny@apache.org> | 2022-08-24 18:45:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-24 18:45:32 +0200 |
commit | bc3242bc8cdf1b4e50da52c4d6c8ac2aeface4cb (patch) | |
tree | 9be685b76c6ae88cb9d49f416745b6a3c1a2c21e | |
parent | 133d6bb4007544d687f671c6ddfd96ae0d6153f0 (diff) | |
download | couchdb-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.ini | 8 | ||||
-rw-r--r-- | src/chttpd/test/eunit/chttpd_auth_hash_algorithms_tests.erl | 99 | ||||
-rw-r--r-- | 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. |