From 7df03f1a923e239cea8c9b0d603a9eb00863a40c Mon Sep 17 00:00:00 2001 From: willcl-ark Date: Thu, 27 Jun 2024 10:00:17 +0100 Subject: util: add perm string helper functions PermsToSymbolicString will convert from fs::perms to string type 'rwxrwxrwx'. InterpretPermString will convert from a user-supplied "perm string" such as 'owner', 'group' or 'all, into appropriate fs::perms. --- src/util/fs_helpers.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ src/util/fs_helpers.h | 14 ++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/util/fs_helpers.cpp b/src/util/fs_helpers.cpp index 8952f20f79..41c8fe3b8f 100644 --- a/src/util/fs_helpers.cpp +++ b/src/util/fs_helpers.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -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 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; + } +} diff --git a/src/util/fs_helpers.h b/src/util/fs_helpers.h index ea3778eac3..28dd6d979d 100644 --- a/src/util/fs_helpers.h +++ b/src/util/fs_helpers.h @@ -12,6 +12,7 @@ #include #include #include +#include /** * 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 InterpretPermString(const std::string& s); + #ifdef WIN32 fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true); #endif -- cgit v1.2.3 From f467aede78533dac60a118e1566138d65522c213 Mon Sep 17 00:00:00 2001 From: willcl-ark Date: Mon, 8 Jan 2024 15:02:44 +0000 Subject: init: add option for rpccookie permissions Add a bitcoind launch option `-rpccookieperms` to configure the file permissions of the cookie on Unix systems. --- src/httprpc.cpp | 19 +++++++++++++++++-- src/init.cpp | 1 + src/rpc/request.cpp | 21 +++++++++++++++------ src/rpc/request.h | 4 +++- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/httprpc.cpp b/src/httprpc.cpp index c72dbf10bc..66e0591c44 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -244,8 +247,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 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 { diff --git a/src/init.cpp b/src/init.cpp index fbf25a0341..6bbc0827bd 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -650,6 +650,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-rpcbind=[: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=", "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=", 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=", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpcport=", strprintf("Listen for JSON-RPC connections on (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=", strprintf("Timeout during HTTP requests (default: %d)", DEFAULT_HTTP_SERVER_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); diff --git a/src/rpc/request.cpp b/src/rpc/request.cpp index b7acd62ee3..eae7c4ba18 100644 --- a/src/rpc/request.cpp +++ b/src/rpc/request.cpp @@ -5,12 +5,11 @@ #include -#include - #include #include #include #include +#include #include #include @@ -82,7 +81,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 cookie_perms) { const size_t COOKIE_SIZE = 32; unsigned char rand_pwd[COOKIE_SIZE]; @@ -96,7 +95,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; @@ -104,11 +103,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; diff --git a/src/rpc/request.h b/src/rpc/request.h index a682c58d96..c7e723d962 100644 --- a/src/rpc/request.h +++ b/src/rpc/request.h @@ -7,9 +7,11 @@ #define BITCOIN_RPC_REQUEST_H #include +#include #include #include +#include UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id); UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id); @@ -17,7 +19,7 @@ std::string JSONRPCReply(const UniValue& result, const UniValue& error, const Un 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 cookie_perms=std::nullopt); /** Read the RPC authentication cookie from disk */ bool GetAuthCookie(std::string *cookie_out); /** Delete RPC authentication cookie from disk */ -- cgit v1.2.3 From d2afa2690cceb0012b2aa1960e1cfa497f3103fa Mon Sep 17 00:00:00 2001 From: willcl-ark Date: Tue, 9 Jan 2024 12:36:29 +0000 Subject: test: add rpccookieperms test Tests various perms on non-Windows OSes --- test/functional/rpc_users.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/functional/rpc_users.py b/test/functional/rpc_users.py index 66cdd7cf9a..153493fbab 100755 --- a/test/functional/rpc_users.py +++ b/test/functional/rpc_users.py @@ -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): @@ -84,6 +87,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') @@ -115,6 +152,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() -- cgit v1.2.3 From 73f0a6cbd0b628675028fbd5a37eff8115e7ccfe Mon Sep 17 00:00:00 2001 From: willcl-ark Date: Mon, 22 Apr 2024 12:24:09 +0100 Subject: doc: detail -rpccookieperms option Co-authored-by: tdb3 <106488469+tdb3@users.noreply.github.com> --- doc/init.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/init.md b/doc/init.md index 7f79027718..d0b73593c0 100644 --- a/doc/init.md +++ b/doc/init.md @@ -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. -- cgit v1.2.3