Merge d38dbaad98 into merged_master (Bitcoin PR bitcoin/bitcoin#28167)

This commit is contained in:
ikripaka 2026-03-03 11:10:05 +00:00
commit cf5c455fa1
No known key found for this signature in database
8 changed files with 132 additions and 11 deletions

View file

@ -35,8 +35,10 @@ it will use a special cookie file for authentication. The cookie is generated wi
content when the daemon starts, and deleted when it exits. Read access to this file
controls who can access it through RPC.
By default the cookie is stored in the data directory, but it's location can be overridden
with the option '-rpccookiefile'.
By default the cookie is stored in the data directory, but its location can be
overridden with the option `-rpccookiefile`. Default file permissions for the
cookie are "owner" (i.e. user read/writeable) via default application-wide file
umask of `0077`, but these can be overridden with the `-rpccookieperms` option.
This allows for running bitcoind without having to do any manual configuration.

View file

@ -11,6 +11,8 @@
#include <netaddress.h>
#include <rpc/protocol.h>
#include <rpc/server.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <walletinitinterface.h>
@ -19,6 +21,7 @@
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
@ -291,8 +294,20 @@ static bool InitRPCAuthentication()
{
if (gArgs.GetArg("-rpcpassword", "") == "")
{
LogPrintf("Using random cookie authentication.\n");
if (!GenerateAuthCookie(&strRPCUserColonPass)) {
LogInfo("Using random cookie authentication.\n");
std::optional<fs::perms> cookie_perms{std::nullopt};
auto cookie_perms_arg{gArgs.GetArg("-rpccookieperms")};
if (cookie_perms_arg) {
auto perm_opt = InterpretPermString(*cookie_perms_arg);
if (!perm_opt) {
LogInfo("Invalid -rpccookieperms=%s; must be one of 'owner', 'group', or 'all'.\n", *cookie_perms_arg);
return false;
}
cookie_perms = *perm_opt;
}
if (!GenerateAuthCookie(&strRPCUserColonPass, cookie_perms)) {
return false;
}
} else {

View file

@ -673,6 +673,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-rpcbind=<addr>[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC);
argsman.AddArg("-rpcdoccheck", strprintf("Throw a non-fatal error at runtime if the documentation for an RPC is incorrect (default: %u)", DEFAULT_RPC_DOC_CHECK), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
argsman.AddArg("-rpccookiefile=<loc>", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rpccookieperms=<readable-by>", strprintf("Set permissions on the RPC auth cookie file so that it is readable by [owner|group|all] (default: owner [via umask 0077])"), ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
argsman.AddArg("-rpcport=<port>", strprintf("Listen for JSON-RPC connections on <port> (default: %u, testnet: %u, signet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), signetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC);
argsman.AddArg("-rpcservertimeout=<n>", strprintf("Timeout during HTTP requests (default: %d)", DEFAULT_HTTP_SERVER_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);

View file

@ -9,12 +9,11 @@
#include <rpc/request.h>
#include <util/fs.h>
#include <common/args.h>
#include <logging.h>
#include <random.h>
#include <rpc/protocol.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/strencodings.h>
@ -99,7 +98,7 @@ static fs::path GetAuthCookieFile(bool temp=false)
static bool g_generated_cookie = false;
bool GenerateAuthCookie(std::string *cookie_out)
bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms)
{
const size_t COOKIE_SIZE = 32;
unsigned char rand_pwd[COOKIE_SIZE];
@ -113,7 +112,7 @@ bool GenerateAuthCookie(std::string *cookie_out)
fs::path filepath_tmp = GetAuthCookieFile(true);
file.open(filepath_tmp);
if (!file.is_open()) {
LogPrintf("Unable to open cookie authentication file %s for writing\n", fs::PathToString(filepath_tmp));
LogInfo("Unable to open cookie authentication file %s for writing\n", fs::PathToString(filepath_tmp));
return false;
}
file << cookie;
@ -121,11 +120,21 @@ bool GenerateAuthCookie(std::string *cookie_out)
fs::path filepath = GetAuthCookieFile(false);
if (!RenameOver(filepath_tmp, filepath)) {
LogPrintf("Unable to rename cookie authentication file %s to %s\n", fs::PathToString(filepath_tmp), fs::PathToString(filepath));
LogInfo("Unable to rename cookie authentication file %s to %s\n", fs::PathToString(filepath_tmp), fs::PathToString(filepath));
return false;
}
if (cookie_perms) {
std::error_code code;
fs::permissions(filepath, cookie_perms.value(), fs::perm_options::replace, code);
if (code) {
LogInfo("Unable to set permissions on cookie authentication file %s\n", fs::PathToString(filepath_tmp));
return false;
}
}
g_generated_cookie = true;
LogPrintf("Generated RPC authentication cookie %s\n", fs::PathToString(filepath));
LogInfo("Generated RPC authentication cookie %s\n", fs::PathToString(filepath));
LogInfo("Permissions used for cookie: %s\n", PermsToSymbolicString(fs::status(filepath).permissions()));
if (cookie_out)
*cookie_out = cookie;

View file

@ -11,6 +11,7 @@
#include <string>
#include <univalue.h>
#include <util/fs.h>
enum class JSONRPCVersion {
V1_LEGACY,
@ -23,7 +24,7 @@ UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue
UniValue JSONRPCError(int code, const std::string& message);
/** Generate a new RPC authentication cookie and write it to disk */
bool GenerateAuthCookie(std::string *cookie_out);
bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms=std::nullopt);
/** Read the RPC authentication cookie from disk */
bool GetAuthCookie(std::string *cookie_out);
/** Delete RPC authentication cookie from disk */

View file

@ -16,6 +16,7 @@
#include <fstream>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <system_error>
#include <utility>
@ -269,3 +270,42 @@ bool TryCreateDirectories(const fs::path& p)
// create_directories didn't create the directory, it had to have existed already
return false;
}
std::string PermsToSymbolicString(fs::perms p)
{
std::string perm_str(9, '-');
auto set_perm = [&](size_t pos, fs::perms required_perm, char letter) {
if ((p & required_perm) != fs::perms::none) {
perm_str[pos] = letter;
}
};
set_perm(0, fs::perms::owner_read, 'r');
set_perm(1, fs::perms::owner_write, 'w');
set_perm(2, fs::perms::owner_exec, 'x');
set_perm(3, fs::perms::group_read, 'r');
set_perm(4, fs::perms::group_write, 'w');
set_perm(5, fs::perms::group_exec, 'x');
set_perm(6, fs::perms::others_read, 'r');
set_perm(7, fs::perms::others_write, 'w');
set_perm(8, fs::perms::others_exec, 'x');
return perm_str;
}
std::optional<fs::perms> InterpretPermString(const std::string& s)
{
if (s == "owner") {
return fs::perms::owner_read | fs::perms::owner_write;
} else if (s == "group") {
return fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read;
} else if (s == "all") {
return fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read |
fs::perms::others_read;
} else {
return std::nullopt;
}
}

View file

@ -12,6 +12,7 @@
#include <cstdio>
#include <iosfwd>
#include <limits>
#include <optional>
/**
* Ensure file contents are fully committed to disk, using a platform-specific
@ -62,6 +63,19 @@ void ReleaseDirectoryLocks();
bool TryCreateDirectories(const fs::path& p);
fs::path GetDefaultDataDir();
/** Convert fs::perms to symbolic string of the form 'rwxrwxrwx'
*
* @param[in] p the perms to be converted
* @return Symbolic permissions string
*/
std::string PermsToSymbolicString(fs::perms p);
/** Interpret a custom permissions level string as fs::perms
*
* @param[in] s Permission level string
* @return Permissions as fs::perms
*/
std::optional<fs::perms> InterpretPermString(const std::string& s);
#ifdef WIN32
fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true);
#endif

View file

@ -11,12 +11,15 @@ from test_framework.util import (
)
import http.client
import os
import platform
import urllib.parse
import subprocess
from random import SystemRandom
import string
import configparser
import sys
from typing import Optional
def call_with_auth(node, user, password):
@ -85,6 +88,40 @@ class HTTPBasicsTest(BitcoinTestFramework):
self.log.info('Wrong...')
assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
def test_rpccookieperms(self):
p = {"owner": 0o600, "group": 0o640, "all": 0o644}
if platform.system() == 'Windows':
self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
return
self.log.info('Check cookie file permissions can be set using -rpccookieperms')
cookie_file_path = self.nodes[1].chain_path / '.cookie'
PERM_BITS_UMASK = 0o777
def test_perm(perm: Optional[str]):
if not perm:
perm = 'owner'
self.restart_node(1)
else:
self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
file_stat = os.stat(cookie_file_path)
actual_perms = file_stat.st_mode & PERM_BITS_UMASK
expected_perms = p[perm]
assert_equal(expected_perms, actual_perms)
# Remove any leftover rpc{user|password} config options from previous tests
self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
self.log.info('Check default cookie permission')
test_perm(None)
self.log.info('Check custom cookie permissions')
for perm in ["owner", "group", "all"]:
test_perm(perm)
def run_test(self):
self.conf_setup()
self.log.info('Check correctness of the rpcauth config option')
@ -116,6 +153,8 @@ class HTTPBasicsTest(BitcoinTestFramework):
(self.nodes[0].chain_path / ".cookie.tmp").mkdir()
self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
self.test_rpccookieperms()
if __name__ == '__main__':
HTTPBasicsTest().main()