From 5a978a2836efb4f1c593852c9d0f9001cdd600ce Mon Sep 17 00:00:00 2001 From: nixbitcoin Date: Mon, 22 Jun 2020 12:10:54 +0000 Subject: [PATCH] bitcoind: switch from rpcpassword to rpcauth Includes bitcoind's `share/rpcauth` to convert apg generated passwords into salted HMAC-SHA-256 hashed passwords. --- modules/bitcoind.nix | 28 +++++++------- modules/clightning.nix | 4 +- modules/electrs.nix | 2 +- modules/liquid.nix | 2 +- modules/lnd.nix | 4 +- modules/presets/secure-node.nix | 12 +++++- pkgs/generate-secrets/default.nix | 5 ++- pkgs/generate-secrets/generate-secrets.sh | 5 ++- pkgs/generate-secrets/rpcauth/rpcauth.py | 46 +++++++++++++++++++++++ 9 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 pkgs/generate-secrets/rpcauth/rpcauth.py diff --git a/modules/bitcoind.nix b/modules/bitcoind.nix index 433fc70..e998d07 100644 --- a/modules/bitcoind.nix +++ b/modules/bitcoind.nix @@ -5,6 +5,7 @@ with lib; let cfg = config.services.bitcoind; inherit (config) nix-bitcoin-services; + secretsDir = config.nix-bitcoin.secretsDir; configFile = pkgs.writeText "bitcoin.conf" '' # We're already logging via journald @@ -33,8 +34,8 @@ let } ${lib.concatMapStrings (rpcbind: "rpcbind=${rpcbind}\n") cfg.rpcbind} ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip} - ${optionalString (cfg.rpcuser != null) "rpcuser=${cfg.rpcuser}"} - ${optionalString (cfg.rpcpassword != null) "rpcpassword=${cfg.rpcpassword}"} + # Credentials for bitcoin-cli + rpcuser=${cfg.rpc.users.privileged.name} # Wallet options ${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"} @@ -110,7 +111,7 @@ in { ''; }; passwordHMAC = mkOption { - type = with types; uniq (strMatching "[0-9a-f]+\\$[0-9a-f]{64}"); + type = types.str; example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae"; description = '' Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the @@ -141,16 +142,6 @@ in { Allow JSON-RPC connections from specified source. ''; }; - rpcuser = mkOption { - type = types.nullOr types.str; - default = "bitcoinrpc"; - description = "Username for JSON-RPC connections"; - }; - rpcpassword = mkOption { - type = types.nullOr types.str; - default = null; - description = "Password for JSON-RPC connections"; - }; testnet = mkOption { type = types.bool; default = false; @@ -297,7 +288,10 @@ in { preStart = '' ${optionalString cfg.dataDirReadableByGroup "chmod -R g+rX '${cfg.dataDir}/blocks'"} - cfg=$(cat ${configFile}; printf "rpcpassword="; cat "${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword") + cfgpre=$(cat ${configFile}; printf "rpcpassword="; cat "${secretsDir}/bitcoin-rpcpassword-privileged") + cfg=$(echo "$cfgpre" | \ + sed "s/bitcoin-HMAC-privileged/$(cat ${secretsDir}/bitcoin-HMAC-privileged)/g" | \ + sed "s/bitcoin-HMAC-public/$(cat ${secretsDir}/bitcoin-HMAC-public)/g") confFile='${cfg.dataDir}/bitcoin.conf' if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile @@ -355,9 +349,13 @@ in { users.groups.${cfg.group} = {}; users.groups.bitcoinrpc = {}; - nix-bitcoin.secrets.bitcoin-rpcpassword = { + nix-bitcoin.secrets.bitcoin-rpcpassword-privileged.user = "bitcoin"; + nix-bitcoin.secrets.bitcoin-rpcpassword-public = { user = "bitcoin"; group = "bitcoinrpc"; }; + + nix-bitcoin.secrets.bitcoin-HMAC-privileged.user = "bitcoin"; + nix-bitcoin.secrets.bitcoin-HMAC-public.user = "bitcoin"; }; } diff --git a/modules/clightning.nix b/modules/clightning.nix index 16b94b3..c7438b4 100644 --- a/modules/clightning.nix +++ b/modules/clightning.nix @@ -13,7 +13,7 @@ let always-use-proxy=${if cfg.always-use-proxy then "true" else "false"} ${optionalString (cfg.bind-addr != null) "bind-addr=${cfg.bind-addr}"} ${optionalString (cfg.bitcoin-rpcconnect != null) "bitcoin-rpcconnect=${cfg.bitcoin-rpcconnect}"} - bitcoin-rpcuser=${config.services.bitcoind.rpcuser} + bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name} rpc-file-mode=0660 ''; in { @@ -112,7 +112,7 @@ in { # The RPC socket has to be removed otherwise we might have stale sockets rm -f ${cfg.dataDir}/bitcoin/lightning-rpc chmod 600 ${cfg.dataDir}/config - echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/config' + echo "bitcoin-rpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/config' ${optionalString cfg.announce-tor "echo announce-addr=$(cat /var/lib/onion-chef/clightning/clightning) >> '${cfg.dataDir}/config'"} ''; serviceConfig = nix-bitcoin-services.defaultHardening // { diff --git a/modules/electrs.nix b/modules/electrs.nix index da2de1b..0d67230 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -74,7 +74,7 @@ in { requires = [ "bitcoind.service" ]; after = [ "bitcoind.service" ]; preStart = '' - echo "cookie = \"${config.services.bitcoind.rpcuser}:$(cat ${secretsDir}/bitcoin-rpcpassword)\"" \ + echo "cookie = \"${config.services.bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \ > electrs.toml ''; serviceConfig = nix-bitcoin-services.defaultHardening // { diff --git a/modules/liquid.nix b/modules/liquid.nix index ea15e31..a655b0a 100644 --- a/modules/liquid.nix +++ b/modules/liquid.nix @@ -246,7 +246,7 @@ in { chmod 640 '${cfg.dataDir}/elements.conf' chown -R '${cfg.user}:${cfg.group}' '${cfg.dataDir}' echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)" >> '${cfg.dataDir}/elements.conf' - echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/elements.conf' + echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/elements.conf' ''; serviceConfig = nix-bitcoin-services.defaultHardening // { Type = "simple"; diff --git a/modules/lnd.nix b/modules/lnd.nix index 8e5ccc3..6a53dec 100644 --- a/modules/lnd.nix +++ b/modules/lnd.nix @@ -25,7 +25,7 @@ let ${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"} bitcoind.rpchost=${cfg.bitcoind-host} - bitcoind.rpcuser=${config.services.bitcoind.rpcuser} + bitcoind.rpcuser=${config.services.bitcoind.rpc.users.public.name} bitcoind.zmqpubrawblock=${config.services.bitcoind.zmqpubrawblock} bitcoind.zmqpubrawtx=${config.services.bitcoind.zmqpubrawtx} @@ -145,7 +145,7 @@ in { after = [ "bitcoind.service" ] ++ onion-chef-service; preStart = '' install -m600 ${configFile} '${cfg.dataDir}/lnd.conf' - echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword)" >> '${cfg.dataDir}/lnd.conf' + echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)" >> '${cfg.dataDir}/lnd.conf' ${optionalString cfg.announce-tor "echo externalip=$(cat /var/lib/onion-chef/lnd/lnd) >> '${cfg.dataDir}/lnd.conf'"} ''; serviceConfig = nix-bitcoin-services.defaultHardening // { diff --git a/modules/presets/secure-node.nix b/modules/presets/secure-node.nix index 2789f69..faa91f3 100644 --- a/modules/presets/secure-node.nix +++ b/modules/presets/secure-node.nix @@ -73,6 +73,16 @@ in { discover = false; addresstype = "bech32"; dbCache = 1000; + rpc.users.privileged = { + name = "bitcoinrpc"; + # Placeholder to be sed'd out by bitcoind preStart + passwordHMAC = "bitcoin-HMAC-privileged"; + }; + rpc.users.public = { + name = "publicrpc"; + # Placeholder to be sed'd out by bitcoind preStart + passwordHMAC = "bitcoin-HMAC-public"; + }; }; services.tor.hiddenServices.bitcoind = mkHiddenService { port = cfg.bitcoind.port; toHost = cfg.bitcoind.bind; }; @@ -96,7 +106,7 @@ in { rpcuser = "liquidrpc"; prune = 1000; extraConfig = '' - mainchainrpcuser=${cfg.bitcoind.rpcuser} + mainchainrpcuser=${config.services.bitcoind.rpc.users.public.name} mainchainrpcport=8332 ''; validatepegin = true; diff --git a/pkgs/generate-secrets/default.nix b/pkgs/generate-secrets/default.nix index 8222057..10c20f2 100644 --- a/pkgs/generate-secrets/default.nix +++ b/pkgs/generate-secrets/default.nix @@ -1,6 +1,9 @@ { pkgs }: with pkgs; +let + rpcauth = pkgs.writeScriptBin "rpcauth" (builtins.readFile ./rpcauth/rpcauth.py); +in writeScript "generate-secrets" '' - export PATH=${lib.makeBinPath [ coreutils apg openssl ]} + export PATH=${lib.makeBinPath [ coreutils apg openssl gnugrep rpcauth python35 ]} . ${./generate-secrets.sh} ${./openssl.cnf} '' diff --git a/pkgs/generate-secrets/generate-secrets.sh b/pkgs/generate-secrets/generate-secrets.sh index 6bb6e9d..2d70741 100755 --- a/pkgs/generate-secrets/generate-secrets.sh +++ b/pkgs/generate-secrets/generate-secrets.sh @@ -6,12 +6,15 @@ makePasswordSecret() { [[ -e $1 ]] || apg -m 20 -x 20 -M Ncl -n 1 > "$1" } -makePasswordSecret bitcoin-rpcpassword +makePasswordSecret bitcoin-rpcpassword-privileged +makePasswordSecret bitcoin-rpcpassword-public makePasswordSecret lnd-wallet-password makePasswordSecret liquid-rpcpassword makePasswordSecret lightning-charge-token makePasswordSecret spark-wallet-password +[[ -e bitcoin-HMAC-privileged ]] || rpcauth privileged $(cat bitcoin-rpcpassword-privileged) | grep rpcauth | cut -d ':' -f 2 > bitcoin-HMAC-privileged +[[ -e bitcoin-HMAC-public ]] || rpcauth public $(cat bitcoin-rpcpassword-public) | grep rpcauth | cut -d ':' -f 2 > bitcoin-HMAC-public [[ -e lightning-charge-env ]] || echo "API_TOKEN=$(cat lightning-charge-token)" > lightning-charge-env [[ -e nanopos-env ]] || echo "CHARGE_TOKEN=$(cat lightning-charge-token)" > nanopos-env [[ -e spark-wallet-login ]] || echo "login=spark-wallet:$(cat spark-wallet-password)" > spark-wallet-login diff --git a/pkgs/generate-secrets/rpcauth/rpcauth.py b/pkgs/generate-secrets/rpcauth/rpcauth.py new file mode 100644 index 0000000..b14c801 --- /dev/null +++ b/pkgs/generate-secrets/rpcauth/rpcauth.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2018 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from argparse import ArgumentParser +from base64 import urlsafe_b64encode +from binascii import hexlify +from getpass import getpass +from os import urandom + +import hmac + +def generate_salt(size): + """Create size byte hex salt""" + return hexlify(urandom(size)).decode() + +def generate_password(): + """Create 32 byte b64 password""" + return urlsafe_b64encode(urandom(32)).decode('utf-8') + +def password_to_hmac(salt, password): + m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), 'SHA256') + return m.hexdigest() + +def main(): + parser = ArgumentParser(description='Create login credentials for a JSON-RPC user') + parser.add_argument('username', help='the username for authentication') + parser.add_argument('password', help='leave empty to generate a random password or specify "-" to prompt for password', nargs='?') + args = parser.parse_args() + + if not args.password: + args.password = generate_password() + elif args.password == '-': + args.password = getpass() + + # Create 16 byte hex salt + salt = generate_salt(16) + password_hmac = password_to_hmac(salt, args.password) + + print('String to be appended to bitcoin.conf:') + print('rpcauth={0}:{1}${2}'.format(args.username, salt, password_hmac)) + print('Your password:\n{0}'.format(args.password)) + +if __name__ == '__main__': + main()