Skip to content

Commit

Permalink
Merge bitcoin#28167: init: Add option for rpccookie permissions (repl…
Browse files Browse the repository at this point in the history
…ace 26088)

73f0a6c doc: detail -rpccookieperms option (willcl-ark)
d2afa26 test: add rpccookieperms test (willcl-ark)
f467aed init: add option for rpccookie permissions (willcl-ark)
7df03f1 util: add perm string helper functions (willcl-ark)

Pull request description:

  This PR picks up bitcoin#26088 by aureleoules which adds a bitcoind launch option `-rpccookieperms` to set the file permissions of the cookie generated by bitcoin core.

  Example usage to make the generated cookie group-readable: `./src/bitcoind -rpccookieperms=group`.

  Accepted values for `-rpccookieperms` are `[owner|group|all]`. We let `fs::perms` handle platform-specific permissions changes.

ACKs for top commit:
  achow101:
    ACK 73f0a6c
  ryanofsky:
    Code review ACK 73f0a6c. Main change since last review is no longer throwing a skip exception in the rpc test on windows, so other checks can run after it, and overall test result is passing, not skipped. Also were clarifying renames and documentation improvements.
  tdb3:
    cr ACK 73f0a6c

Tree-SHA512: e800d59a44aca10e1c58ca69bf3fdde9f6ccf5eab4b7b962645af6d6bc0cfa3a357701e409c8c60d8d7744fcd33a91e77ada11790aa88cd7811ef60fab86ab11
  • Loading branch information
ryanofsky committed Jun 27, 2024
2 parents f0745d0 + 73f0a6c commit d38dbaa
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 11 deletions.
6 changes: 4 additions & 2 deletions doc/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
19 changes: 17 additions & 2 deletions src/httprpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -19,6 +21,7 @@
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,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);
Expand Down
21 changes: 15 additions & 6 deletions src/rpc/request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,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>

Expand Down Expand Up @@ -95,7 +94,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];
Expand All @@ -109,19 +108,29 @@ 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;
file.close();

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;
Expand Down
3 changes: 2 additions & 1 deletion src/rpc/request.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <string>

#include <univalue.h>
#include <util/fs.h>

enum class JSONRPCVersion {
V1_LEGACY,
Expand All @@ -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 */
Expand Down
40 changes: 40 additions & 0 deletions src/util/fs_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <fstream>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <system_error>
#include <utility>
Expand Down Expand Up @@ -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;
}
}
14 changes: 14 additions & 0 deletions src/util/fs_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <cstdio>
#include <iosfwd>
#include <limits>
#include <optional>

/**
* Ensure file contents are fully committed to disk, using a platform-specific
Expand Down Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions test/functional/rpc_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
)

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):
Expand Down Expand Up @@ -84,6 +87,40 @@ def test_auth(self, node, user, password):
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')
Expand Down Expand Up @@ -115,6 +152,8 @@ def run_test(self):
(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()

0 comments on commit d38dbaa

Please sign in to comment.