Skip to content

Commit 438ee59

Browse files
committed
Merge pull request #7044
d52fbf0 Added additional config option for multiple RPC users. (Gregory Sanders)
2 parents 34e02e0 + d52fbf0 commit 438ee59

File tree

6 files changed

+231
-1
lines changed

6 files changed

+231
-1
lines changed

qa/pull-tester/rpc-tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
'mempool_spendcoinbase.py',
8080
'mempool_coinbase_spends.py',
8181
'httpbasics.py',
82+
'multi_rpc.py',
8283
'zapwallettxes.py',
8384
'proxy_test.py',
8485
'merkle_blocks.py',

qa/rpc-tests/multi_rpc.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env python2
2+
# Copyright (c) 2015 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
#
7+
# Test mulitple rpc user config option rpcauth
8+
#
9+
10+
from test_framework.test_framework import BitcoinTestFramework
11+
from test_framework.util import *
12+
import base64
13+
14+
try:
15+
import http.client as httplib
16+
except ImportError:
17+
import httplib
18+
try:
19+
import urllib.parse as urlparse
20+
except ImportError:
21+
import urlparse
22+
23+
class HTTPBasicsTest (BitcoinTestFramework):
24+
def setup_nodes(self):
25+
return start_nodes(4, self.options.tmpdir)
26+
27+
def setup_chain(self):
28+
print("Initializing test directory "+self.options.tmpdir)
29+
initialize_chain(self.options.tmpdir)
30+
#Append rpcauth to bitcoin.conf before initialization
31+
rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144"
32+
rpcauth2 = "rpcauth=rt2:f8607b1a88861fac29dfccf9b52ff9f$ff36a0c23c8c62b4846112e50fa888416e94c17bfd4c42f88fd8f55ec6a3137e"
33+
with open(os.path.join(self.options.tmpdir+"/node0", "bitcoin.conf"), 'a') as f:
34+
f.write(rpcauth+"\n")
35+
f.write(rpcauth2+"\n")
36+
37+
def run_test(self):
38+
39+
##################################################
40+
# Check correctness of the rpcauth config option #
41+
##################################################
42+
url = urlparse.urlparse(self.nodes[0].url)
43+
44+
#Old authpair
45+
authpair = url.username + ':' + url.password
46+
47+
#New authpair generated via contrib/rpcuser tool
48+
rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144"
49+
password = "cA773lm788buwYe4g4WT+05pKyNruVKjQ25x3n0DQcM="
50+
51+
#Second authpair with different username
52+
rpcauth2 = "rpcauth=rt2:f8607b1a88861fac29dfccf9b52ff9f$ff36a0c23c8c62b4846112e50fa888416e94c17bfd4c42f88fd8f55ec6a3137e"
53+
password2 = "8/F3uMDw4KSEbw96U3CA1C4X05dkHDN2BPFjTgZW4KI="
54+
authpairnew = "rt:"+password
55+
56+
headers = {"Authorization": "Basic " + base64.b64encode(authpair)}
57+
58+
conn = httplib.HTTPConnection(url.hostname, url.port)
59+
conn.connect()
60+
conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
61+
resp = conn.getresponse()
62+
assert_equal(resp.status==401, False)
63+
conn.close()
64+
65+
#Use new authpair to confirm both work
66+
headers = {"Authorization": "Basic " + base64.b64encode(authpairnew)}
67+
68+
conn = httplib.HTTPConnection(url.hostname, url.port)
69+
conn.connect()
70+
conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
71+
resp = conn.getresponse()
72+
assert_equal(resp.status==401, False)
73+
conn.close()
74+
75+
#Wrong login name with rt's password
76+
authpairnew = "rtwrong:"+password
77+
headers = {"Authorization": "Basic " + base64.b64encode(authpairnew)}
78+
79+
conn = httplib.HTTPConnection(url.hostname, url.port)
80+
conn.connect()
81+
conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
82+
resp = conn.getresponse()
83+
assert_equal(resp.status==401, True)
84+
conn.close()
85+
86+
#Wrong password for rt
87+
authpairnew = "rt:"+password+"wrong"
88+
headers = {"Authorization": "Basic " + base64.b64encode(authpairnew)}
89+
90+
conn = httplib.HTTPConnection(url.hostname, url.port)
91+
conn.connect()
92+
conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
93+
resp = conn.getresponse()
94+
assert_equal(resp.status==401, True)
95+
conn.close()
96+
97+
#Correct for rt2
98+
authpairnew = "rt2:"+password2
99+
headers = {"Authorization": "Basic " + base64.b64encode(authpairnew)}
100+
101+
conn = httplib.HTTPConnection(url.hostname, url.port)
102+
conn.connect()
103+
conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
104+
resp = conn.getresponse()
105+
assert_equal(resp.status==401, False)
106+
conn.close()
107+
108+
#Wrong password for rt2
109+
authpairnew = "rt2:"+password2+"wrong"
110+
headers = {"Authorization": "Basic " + base64.b64encode(authpairnew)}
111+
112+
conn = httplib.HTTPConnection(url.hostname, url.port)
113+
conn.connect()
114+
conn.request('POST', '/', '{"method": "getbestblockhash"}', headers)
115+
resp = conn.getresponse()
116+
assert_equal(resp.status==401, True)
117+
conn.close()
118+
119+
120+
121+
if __name__ == '__main__':
122+
HTTPBasicsTest ().main ()

share/rpcuser/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
RPC Tools
2+
---------------------
3+
4+
### [RPCUser](/share/rpcuser) ###
5+
6+
Create an RPC user login credential.
7+
8+
Usage:
9+
10+
./rpcuser.py <username>
11+

share/rpcuser/rpcuser.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python2
2+
# Copyright (c) 2015 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
import hashlib
7+
import sys
8+
import os
9+
from random import SystemRandom
10+
import base64
11+
import hmac
12+
13+
if len(sys.argv) < 2:
14+
sys.stderr.write('Please include username as an argument.\n')
15+
sys.exit(0)
16+
17+
username = sys.argv[1]
18+
19+
#This uses os.urandom() underneath
20+
cryptogen = SystemRandom()
21+
22+
#Create 16 byte hex salt
23+
salt_sequence = [cryptogen.randrange(256) for i in range(16)]
24+
hexseq = list(map(hex, salt_sequence))
25+
salt = "".join([x[2:] for x in hexseq])
26+
27+
#Create 32 byte b64 password
28+
password = base64.urlsafe_b64encode(os.urandom(32))
29+
30+
digestmod = hashlib.sha256
31+
32+
if sys.version_info.major >= 3:
33+
password = password.decode('utf-8')
34+
digestmod = 'SHA256'
35+
36+
m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), digestmod)
37+
result = m.hexdigest()
38+
39+
print("String to be appended to bitcoin.conf:")
40+
print("rpcauth="+username+":"+salt+"$"+result)
41+
print("Your password:\n"+password)

src/httprpc.cpp

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
#include "util.h"
1111
#include "utilstrencodings.h"
1212
#include "ui_interface.h"
13+
#include "crypto/hmac_sha256.h"
14+
#include <stdio.h>
15+
#include "utilstrencodings.h"
1316

1417
#include <boost/algorithm/string.hpp> // boost::trim
18+
#include <boost/foreach.hpp> //BOOST_FOREACH
1519

1620
/** Simple one-shot callback timer to be used by the RPC mechanism to e.g.
1721
* re-lock the wellet.
@@ -72,6 +76,50 @@ static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const Uni
7276
req->WriteReply(nStatus, strReply);
7377
}
7478

79+
//This function checks username and password against -rpcauth
80+
//entries from config file.
81+
static bool multiUserAuthorized(std::string strUserPass)
82+
{
83+
if (strUserPass.find(":") == std::string::npos) {
84+
return false;
85+
}
86+
std::string strUser = strUserPass.substr(0, strUserPass.find(":"));
87+
std::string strPass = strUserPass.substr(strUserPass.find(":") + 1);
88+
89+
if (mapMultiArgs.count("-rpcauth") > 0) {
90+
//Search for multi-user login/pass "rpcauth" from config
91+
BOOST_FOREACH(std::string strRPCAuth, mapMultiArgs["-rpcauth"])
92+
{
93+
std::vector<std::string> vFields;
94+
boost::split(vFields, strRPCAuth, boost::is_any_of(":$"));
95+
if (vFields.size() != 3) {
96+
//Incorrect formatting in config file
97+
continue;
98+
}
99+
100+
std::string strName = vFields[0];
101+
if (!TimingResistantEqual(strName, strUser)) {
102+
continue;
103+
}
104+
105+
std::string strSalt = vFields[1];
106+
std::string strHash = vFields[2];
107+
108+
unsigned int KEY_SIZE = 32;
109+
unsigned char *out = new unsigned char[KEY_SIZE];
110+
111+
CHMAC_SHA256(reinterpret_cast<const unsigned char*>(strSalt.c_str()), strSalt.size()).Write(reinterpret_cast<const unsigned char*>(strPass.c_str()), strPass.size()).Finalize(out);
112+
std::vector<unsigned char> hexvec(out, out+KEY_SIZE);
113+
std::string strHashFromPass = HexStr(hexvec);
114+
115+
if (TimingResistantEqual(strHashFromPass, strHash)) {
116+
return true;
117+
}
118+
}
119+
}
120+
return false;
121+
}
122+
75123
static bool RPCAuthorized(const std::string& strAuth)
76124
{
77125
if (strRPCUserColonPass.empty()) // Belt-and-suspenders measure if InitRPCAuthentication was not called
@@ -81,7 +129,12 @@ static bool RPCAuthorized(const std::string& strAuth)
81129
std::string strUserPass64 = strAuth.substr(6);
82130
boost::trim(strUserPass64);
83131
std::string strUserPass = DecodeBase64(strUserPass64);
84-
return TimingResistantEqual(strUserPass, strRPCUserColonPass);
132+
133+
//Check if authorized under single-user field
134+
if (TimingResistantEqual(strUserPass, strRPCUserColonPass)) {
135+
return true;
136+
}
137+
return multiUserAuthorized(strUserPass);
85138
}
86139

87140
static bool HTTPReq_JSONRPC(HTTPRequest* req, const std::string &)
@@ -157,6 +210,7 @@ static bool InitRPCAuthentication()
157210
return false;
158211
}
159212
} else {
213+
LogPrintf("Config options rpcuser and rpcpassword will soon be deprecated. Locally-run instances may remove rpcuser to use cookie-based auth, or may be replaced with rpcauth. Please see share/rpcuser for rpcauth auth generation.");
160214
strRPCUserColonPass = mapArgs["-rpcuser"] + ":" + mapArgs["-rpcpassword"];
161215
}
162216
return true;

src/init.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ std::string HelpMessage(HelpMessageMode mode)
491491
strUsage += HelpMessageOpt("-rpcbind=<addr>", _("Bind to given address to listen for JSON-RPC connections. Use [host]:port notation for IPv6. This option can be specified multiple times (default: bind to all interfaces)"));
492492
strUsage += HelpMessageOpt("-rpcuser=<user>", _("Username for JSON-RPC connections"));
493493
strUsage += HelpMessageOpt("-rpcpassword=<pw>", _("Password for JSON-RPC connections"));
494+
strUsage += HelpMessageOpt("-rpcauth=<userpw>", _("Username and hashed password for JSON-RPC connections. The field <userpw> comes in the format: <USERNAME>:<SALT>$<HASH>. A canonical python script is included in share/rpcuser. This option can be specified multiple times"));
494495
strUsage += HelpMessageOpt("-rpcport=<port>", strprintf(_("Listen for JSON-RPC connections on <port> (default: %u or testnet: %u)"), 8332, 18332));
495496
strUsage += HelpMessageOpt("-rpcallowip=<ip>", _("Allow JSON-RPC connections from specified source. Valid for <ip> are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This option can be specified multiple times"));
496497
strUsage += HelpMessageOpt("-rpcthreads=<n>", strprintf(_("Set the number of threads to service RPC calls (default: %d)"), DEFAULT_HTTP_THREADS));

0 commit comments

Comments
 (0)