This is an automated email from the ASF dual-hosted git repository.
vatamane pushed a commit to branch jenkins-debug-failure
in repository https://gitbox.apache.org/repos/asf/couchdb.git
The following commit(s) were added to refs/heads/jenkins-debug-failure by this
push:
new 3a36bd67f Revert "Support safe secret rotation"
3a36bd67f is described below
commit 3a36bd67f9d38b339bd9be529f1c96d96420f3ce
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Thu Dec 18 12:42:47 2025 -0500
Revert "Support safe secret rotation"
This reverts commit 329920e902767850e1b8ebfb0b7effd7b093e31b.
---
src/couch/src/couch_httpd_auth.erl | 69 +++++-----
src/couch/src/couch_secondary_sup.erl | 1 -
src/couch/src/couch_secrets.erl | 193 ---------------------------
src/couch/test/eunit/couch_secrets_tests.erl | 81 -----------
src/docs/src/config/auth.rst | 12 --
5 files changed, 31 insertions(+), 325 deletions(-)
diff --git a/src/couch/src/couch_httpd_auth.erl
b/src/couch/src/couch_httpd_auth.erl
index 604c9dcfe..7c6a60d2b 100644
--- a/src/couch/src/couch_httpd_auth.erl
+++ b/src/couch/src/couch_httpd_auth.erl
@@ -209,15 +209,14 @@ proxy_auth_user(Req) ->
case chttpd_util:get_chttpd_auth_config("secret") of
undefined ->
Req#httpd{user_ctx = #user_ctx{name =
?l2b(UserName), roles = Roles}};
- _Secret ->
- Token =
- try
- binary:decode_hex(?l2b(header_value(Req,
XHeaderToken)))
- catch
- error:badarg ->
- undefined
- end,
- case couch_secrets:verify(UserName, Token) of
+ Secret ->
+ HashAlgorithms =
couch_util:get_config_hash_algorithms(),
+ Token = header_value(Req, XHeaderToken),
+ VerifyTokens = fun(HashAlg) ->
+ Hmac = couch_util:hmac(HashAlg, Secret,
UserName),
+
couch_passwords:verify(couch_util:to_hex(Hmac), Token)
+ end,
+ case lists:any(VerifyTokens, HashAlgorithms) of
true ->
Req#httpd{
user_ctx = #user_ctx{
@@ -356,30 +355,35 @@ cookie_authentication_handler(#httpd{mochi_req =
MochiReq} = Req, AuthModule) ->
end,
% Verify expiry and hash
CurrentTime = make_cookie_time(),
+ HashAlgorithms = couch_util:get_config_hash_algorithms(),
case chttpd_util:get_chttpd_auth_config("secret") of
undefined ->
couch_log:debug("cookie auth secret is not set", []),
Req;
- _SecretStr ->
+ SecretStr ->
+ Secret = ?l2b(SecretStr),
case AuthModule:get_user_creds(Req, User) of
nil ->
Req;
{ok, UserProps, _AuthCtx} ->
UserSalt = couch_util:get_value(<<"salt">>,
UserProps, <<"">>),
+ FullSecret = <<Secret/binary, UserSalt/binary>>,
Hash = ?l2b(HashStr),
+ VerifyHash = fun(HashAlg) ->
+ Hmac = couch_util:hmac(
+ HashAlg,
+ FullSecret,
+ lists:join(":", [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 list_to_integer(TimeStr, 16)) of
TimeStamp when CurrentTime < TimeStamp +
Timeout ->
- case
- couch_secrets:verify(
- lists:join(":", [User, TimeStr]),
- UserSalt,
- Hash
- )
- of
+ case lists:any(VerifyHash, HashAlgorithms)
of
true ->
TimeLeft = TimeStamp + Timeout -
CurrentTime,
couch_log:debug(
@@ -394,7 +398,7 @@ cookie_authentication_handler(#httpd{mochi_req = MochiReq}
= Req, AuthModule) ->
)
},
auth =
- {UserSalt, TimeLeft <
Timeout * 0.9}
+ {FullSecret, TimeLeft <
Timeout * 0.9}
};
_Else ->
Req
@@ -409,7 +413,7 @@ cookie_authentication_handler(#httpd{mochi_req = MochiReq}
= Req, AuthModule) ->
cookie_auth_header(#httpd{user_ctx = #user_ctx{name = null}}, _Headers) ->
[];
cookie_auth_header(
- #httpd{user_ctx = #user_ctx{name = User}, auth = {UserSalt, _SendCookie =
true}} =
+ #httpd{user_ctx = #user_ctx{name = User}, auth = {Secret, _SendCookie =
true}} =
Req,
Headers
) ->
@@ -426,20 +430,21 @@ cookie_auth_header(
if
AuthSession == undefined ->
TimeStamp = make_cookie_time(),
- [cookie_auth_cookie(Req, User, UserSalt, TimeStamp)];
+ [cookie_auth_cookie(Req, User, Secret, TimeStamp)];
true ->
[]
end;
cookie_auth_header(_Req, _Headers) ->
[].
-cookie_auth_cookie(Req, User, UserSalt, TimeStamp) ->
+cookie_auth_cookie(Req, User, Secret, TimeStamp) ->
SessionItems = [User, integer_to_list(TimeStamp, 16)],
- cookie_auth_cookie(Req, UserSalt, SessionItems).
+ cookie_auth_cookie(Req, Secret, SessionItems).
-cookie_auth_cookie(Req, UserSalt, SessionItems) when is_list(SessionItems) ->
+cookie_auth_cookie(Req, Secret, SessionItems) when is_list(SessionItems) ->
SessionData = lists:join(":", SessionItems),
- Hash = couch_secrets:sign(SessionData, UserSalt),
+ [HashAlgorithm | _] = couch_util:get_config_hash_algorithms(),
+ Hash = couch_util:hmac(HashAlgorithm, Secret, SessionData),
mochiweb_cookies:cookie(
"AuthSession",
couch_util:encodeBase64Url(lists:join(":", [SessionData, Hash])),
@@ -460,23 +465,11 @@ ensure_cookie_auth_secret() ->
undefined ->
NewSecret = ?b2l(couch_uuids:random()),
config:set("chttpd_auth", "secret", NewSecret),
- wait_for_secret(10),
NewSecret;
Secret ->
Secret
end.
-wait_for_secret(0) ->
- ok;
-wait_for_secret(N) ->
- case couch_secrets:secret_is_set() of
- true ->
- ok;
- false ->
- timer:sleep(50),
- wait_for_secret(N - 1)
- end.
-
% session handlers
% Login handler with user db
handle_session_req(Req) ->
@@ -521,11 +514,11 @@ handle_session_req(#httpd{method = 'POST', mochi_req =
MochiReq} = Req, AuthModu
Req, UserName, Password, UserProps, AuthModule, AuthCtx
),
% setup the session cookie
- ensure_cookie_auth_secret(),
+ Secret = ?l2b(ensure_cookie_auth_secret()),
UserSalt = couch_util:get_value(<<"salt">>, UserProps),
CurrentTime = make_cookie_time(),
Cookie = cookie_auth_cookie(
- Req, UserName, UserSalt, CurrentTime
+ Req, UserName, <<Secret/binary, UserSalt/binary>>, CurrentTime
),
% TODO document the "next" feature in Futon
{Code, Headers} =
diff --git a/src/couch/src/couch_secondary_sup.erl
b/src/couch/src/couch_secondary_sup.erl
index 766235d5d..8fc3c9a13 100644
--- a/src/couch/src/couch_secondary_sup.erl
+++ b/src/couch/src/couch_secondary_sup.erl
@@ -24,7 +24,6 @@ init([]) ->
],
Daemons =
[
- {couch_secrets, {couch_secrets, start_link, []}},
{query_servers, {couch_proc_manager, start_link, []}},
{vhosts, {couch_httpd_vhost, start_link, []}},
{uuids, {couch_uuids, start, []}},
diff --git a/src/couch/src/couch_secrets.erl b/src/couch/src/couch_secrets.erl
deleted file mode 100644
index 574db73a3..000000000
--- a/src/couch/src/couch_secrets.erl
+++ /dev/null
@@ -1,193 +0,0 @@
-% 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(couch_secrets).
-
--behaviour(gen_server).
--behaviour(config_listener).
-
--include_lib("couch/include/couch_db.hrl").
-
-%% public api
--export([sign/1, sign/2, verify/2, verify/3, secret_is_set/0]).
-
-%% gen_server functions
--export([
- start_link/0,
- init/1,
- handle_call/3,
- handle_cast/2,
- handle_continue/2,
- handle_info/2
-]).
-
-%% config_listener functions
--export([
- handle_config_change/5,
- handle_config_terminate/3
-]).
-
-sign(Message) ->
- sign(Message, <<>>).
-
-sign(Message, ExtraSecret) ->
- [HashAlgorithm | _] = couch_util:get_config_hash_algorithms(),
- case current_secret_from_ets() of
- undefined ->
- throw({internal_server_error, <<"cookie auth secret is not
set">>});
- CurrentSecret ->
- FullSecret = <<CurrentSecret/binary, ExtraSecret/binary>>,
- couch_util:hmac(HashAlgorithm, FullSecret, Message)
- end.
-
-verify(Message, ExpectedMAC) ->
- verify(Message, <<>>, ExpectedMAC).
-
-verify(Message, ExtraSecret, ExpectedMAC) ->
- FullSecrets = [<<Secret/binary, ExtraSecret/binary>> || Secret <-
all_secrets_from_ets()],
- AllAlgorithms = couch_util:get_config_hash_algorithms(),
- verify(Message, AllAlgorithms, FullSecrets, ExpectedMAC).
-
-verify(Message, AllAlgorithms, FullSecrets, ExpectedMAC) ->
- Algorithms = lists:filter(
- fun(Algorithm) ->
- #{size := Size} = crypto:hash_info(Algorithm),
- Size == byte_size(ExpectedMAC)
- end,
- AllAlgorithms
- ),
- VerifyFun = fun({Secret, Algorithm}) ->
- ActualMAC = couch_util:hmac(Algorithm, Secret, Message),
- crypto:hash_equals(ExpectedMAC, ActualMAC)
- end,
- lists:any(VerifyFun, [{S, A} || S <- FullSecrets, A <- Algorithms]).
-
-secret_is_set() ->
- current_secret_from_ets() /= undefined.
-
-start_link() ->
- gen_server:start_link({local, ?MODULE}, ?MODULE, nil, []).
-
-init(nil) ->
- ets:new(?MODULE, [named_table, {read_concurrency, true}]),
- true = ets:insert(?MODULE, {{node(), current},
current_secret_from_config()}),
- update_all_secrets(),
- erlang:send_after(5000, self(), cache_cleanup),
- ok = config:listen_for_changes(?MODULE, undefined),
- {ok, nil, {continue, get_secrets}}.
-
-handle_call({insert, {Node, current}, Secret}, _From, State) ->
- case current_secret_from_ets(Node) of
- undefined ->
- ets:insert(?MODULE, [{{Node, current}, Secret}]);
- OldSecret ->
- TimeoutSecs =
chttpd_util:get_chttpd_auth_config_integer("timeout", 600),
- ExpiresAt = erlang:system_time(second) + TimeoutSecs,
- ets:insert(?MODULE, [{{Node, current}, Secret}, {{Node,
ExpiresAt}, OldSecret}])
- end,
- update_all_secrets(),
- {reply, ok, State};
-handle_call({insert, Key, Secret}, _From, State) ->
- ets:insert(?MODULE, {Key, Secret}),
- update_all_secrets(),
- {reply, ok, State};
-handle_call(get_secrets, _From, State) ->
- Secrets = ets:match_object(?MODULE, {{node(), '_'}, '_'}),
- {reply, Secrets, State};
-handle_call(flush_cache, _From, State) ->
- %% used from tests to prevent spurious failures due to timing
- MatchSpec = [{{{'_', '$1'}, '_'}, [{is_integer, '$1'}], [true]}],
- NumDeleted = ets:select_delete(?MODULE, MatchSpec),
- if
- NumDeleted > 0 -> update_all_secrets();
- true -> ok
- end,
- {reply, NumDeleted, State};
-handle_call(_Msg, _From, State) ->
- {noreply, State}.
-
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-handle_continue(get_secrets, State) ->
- {Replies, _BadNodes} = gen_server:multi_call(nodes(), ?MODULE,
get_secrets),
- {_Nodes, Secrets} = lists:unzip(Replies),
- true = ets:insert(?MODULE, lists:flatten(Secrets)),
- update_all_secrets(),
- {noreply, State}.
-
-handle_info(restart_config_listener, State) ->
- ok = config:listen_for_changes(?MODULE, nil),
- update_current_secret(),
- {noreply, State};
-handle_info(cache_cleanup, State) ->
- erlang:send_after(5000, self(), cache_cleanup),
- Now = os:system_time(second),
- MatchSpec = [{{{'_', '$1'}, '_'}, [{is_integer, '$1'}, {'<', '$1', Now}],
[true]}],
- NumDeleted = ets:select_delete(?MODULE, MatchSpec),
- if
- NumDeleted > 0 -> update_all_secrets();
- true -> ok
- end,
- {noreply, State};
-handle_info(_Msg, State) ->
- {noreply, State}.
-
-handle_config_change("chttpd_auth", "secret", _, _, _) ->
- update_current_secret(),
- {ok, undefined};
-handle_config_change("couch_httpd_auth", "secret", _, _, _) ->
- update_current_secret(),
- {ok, undefined};
-handle_config_change(_, _, _, _, _) ->
- {ok, undefined}.
-
-handle_config_terminate(_, stop, _) ->
- ok;
-handle_config_terminate(_Server, _Reason, _State) ->
- erlang:send_after(3000, whereis(?MODULE), restart_config_listener).
-
-%% private functions
-
-update_current_secret() ->
- NewSecret = current_secret_from_config(),
- spawn(fun() ->
- gen_server:multi_call(nodes(), ?MODULE, {insert, {node(), current},
NewSecret}),
- gen_server:call(?MODULE, {insert, {node(), current}, NewSecret})
- end).
-
-update_all_secrets() ->
- AllSecrets = ets:match_object(?MODULE, {{'_', '_'}, '_'}),
- ets:insert(?MODULE, {all_secrets, lists:usort([V || {_K, V} <- AllSecrets,
is_binary(V)])}).
-
-current_secret_from_config() ->
- case chttpd_util:get_chttpd_auth_config("secret") of
- undefined ->
- undefined;
- Secret ->
- ?l2b(Secret)
- end.
-
-current_secret_from_ets() ->
- current_secret_from_ets(node()).
-
-current_secret_from_ets(Node) ->
- secret_from_ets({Node, current}).
-
-all_secrets_from_ets() ->
- secret_from_ets(all_secrets).
-
-secret_from_ets(Key) ->
- case ets:lookup(?MODULE, Key) of
- [{Key, Value}] -> Value;
- [] -> undefined
- end.
diff --git a/src/couch/test/eunit/couch_secrets_tests.erl
b/src/couch/test/eunit/couch_secrets_tests.erl
deleted file mode 100644
index 37305a63b..000000000
--- a/src/couch/test/eunit/couch_secrets_tests.erl
+++ /dev/null
@@ -1,81 +0,0 @@
-% 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(couch_secrets_tests).
-
--include_lib("couch/include/couch_eunit.hrl").
-
--define(DATA1, <<"data1">>).
--define(DATA2, <<"data2">>).
--define(BADMAC, <<"badmac">>).
--define(EXTRA1, <<"extra1">>).
--define(EXTRA2, <<"extra2">>).
--define(SECRET1, "secret1").
--define(SECRET2, "secret2").
-
-couch_secrets_test_() ->
- {setup, fun test_util:start_couch/0, fun test_util:stop_couch/1,
- {with, [
- fun error_if_no_secret/1,
- fun verify_works/1,
- fun verify_extra_secret_works/1,
- fun verify_old_secret_works/1,
- fun verify_old_secret_stops_working/1
- ]}}.
-
-error_if_no_secret(_Ctx) ->
- delete_secret(),
- ?assertThrow(
- {internal_server_error, <<"cookie auth secret is not set">>},
couch_secrets:sign(?DATA1)
- ).
-
-verify_works(_Ctx) ->
- set_secret(?SECRET1),
- MAC = couch_secrets:sign(?DATA1),
- ?assert(couch_secrets:verify(?DATA1, MAC)),
- ?assertNot(couch_secrets:verify(?DATA1, ?BADMAC)),
- ?assertNot(couch_secrets:verify(?DATA2, MAC)).
-
-verify_extra_secret_works(_Ctx) ->
- set_secret(?SECRET1),
- MAC = couch_secrets:sign(?DATA1, ?EXTRA1),
- ?assert(couch_secrets:verify(?DATA1, ?EXTRA1, MAC)),
- ?assertNot(couch_secrets:verify(?DATA1, ?EXTRA2, MAC)),
- ?assertNot(couch_secrets:verify(?DATA1, ?EXTRA1, ?BADMAC)),
- ?assertNot(couch_secrets:verify(?DATA2, ?EXTRA1, MAC)).
-
-verify_old_secret_works(_Ctx) ->
- set_secret(?SECRET1),
- MAC1 = couch_secrets:sign(?DATA1),
- set_secret(?SECRET2),
- MAC2 = couch_secrets:sign(?DATA1),
- ?assert(couch_secrets:verify(?DATA1, MAC1)),
- ?assert(couch_secrets:verify(?DATA1, MAC2)).
-
-verify_old_secret_stops_working(_Ctx) ->
- set_secret(?SECRET1),
- MAC1 = couch_secrets:sign(?DATA1),
- ?assert(couch_secrets:verify(?DATA1, MAC1)),
- set_secret(?SECRET2),
- MAC2 = couch_secrets:sign(?DATA1),
- ?assert(couch_secrets:verify(?DATA1, MAC2)),
- ?assert(gen_server:call(couch_secrets, flush_cache) > 0),
- ?assertNot(couch_secrets:verify(?DATA1, MAC1)),
- ?assert(couch_secrets:verify(?DATA1, MAC2)).
-
-delete_secret() ->
- config:delete("chttpd_auth", "secret"),
- config:delete("couch_httpd_auth", "secret").
-
-set_secret(Secret) ->
- config:set("chttpd_auth", "secret", Secret),
- timer:sleep(100).
diff --git a/src/docs/src/config/auth.rst b/src/docs/src/config/auth.rst
index fac69da7c..406ab4512 100644
--- a/src/docs/src/config/auth.rst
+++ b/src/docs/src/config/auth.rst
@@ -329,18 +329,6 @@ Authentication Configuration
[chttpd_auth]
secret = 92de07df7e7a3fe14808cef90a7cc0d91
- .. note::
- You can change the secret value at any time. New cookies will be
- signed with the new value and the previous value will be cached
- for the duration of the auth ``timeout`` parameter.
-
- The secret value should be set on all nodes at the same time.
CouchDB
- will tolerate a discrepancy, however, as each node sends its secret
- to the other nodes of the cluster.
-
- The easiest rotation method is to enable the config auto-reload
- feature then update the secret in the ``.ini`` file of each node.
-
.. config:option:: timeout :: Session timeout
.. versionchanged:: 3.2 moved from [couch_httpd_auth] to [chttpd_auth]
section