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

Reply via email to