Electrum Verifier

10 minute read

1. Introduction

Whenever it is recommended that the digital signature and hashes of a file be verified to avoid files being modified in transit or at rest by trojans or any other malicious code, the average user encounters some difficulties in understanding the process and executing the necessary commands. Sometimes because the user lacks technical knowledge, or because the process may vary depending on how the signature was generated or with the cryptographic method chosen.

The Electrum Verifier script does just that: it downloads Electrum wallet, its digital signatures from the official website and verifies that the signature is valid. It is a script (bash or python) that does not need root privileges and runs on macOS and Linux (python version is supposed to run on Windows). The difference between this script and Decred Verifier is that the Electrum developer signed the wallet file directly. For Decred files the developers generated a SHA-256 hash of each file, inserted these hashes into a text file named manifest, and the manifest was digitally signed. Therefore, for Decred files it is necessary to verify the digital signature of the manifest and compare the hashes while here all it takes is the digital signature verification.

To download the public key of Thomas Voegtlin, main Electrum developer, the script will initiate a TCP 11371 (hkp) connection to the GNUPG key server, and to download the packages from Electrum website it will be necessary to ‘talk’ TCP 443 (https). There is also another mode of operation where the public key will be downloaded from Github repo using TCP 443.

Learn more about the verification of digital signatures or about the procedures to verify the digital signature of Electrum wallet.

2. Bash Script

To run the script, paste the ~160 lines of code below to a local file and add the executable attribute as shown here:

$ chmod +x electrum_verifier.sh

Electrum Verifier:

#! /bin/bash -

# Electrum Verifier v0.2.1
# This script downloads and verifies the digital signature and hash of Electrum Bitcoin wallet.
# Electrum was created by Thomas Voegtlin and is available at electrum.org
# Reference: https://electrum.org/#download
# Author: Marcelo Martins (stakey.club)

# Syntax: electrum_verifier.sh [version number] [mode]
# Version defaults to the latest auto-detected version if left blank
# Mode of operation must be: (public key download)
# 0 = HKP, via TCP 11371 (default)
# 1 = HTTPS, via TCP 443
# Examples: ./electrum_verifier.sh
#           ./electrum_verifier.sh 3.3.4
#           ./electrum_verifier.sh 3.3.4 1

print_help () {
    echo "Version defaults to the latest auto-detected version if left blank"
    echo "Mode of operation must be: (public key download)"
    echo "0 = HKP, via TCP 11371 (default)"
    echo "1 = HTTPS, via TCP 443"
    echo "Syntax:  $0 [version] [mode]"
    echo "Examples: $0"
    echo "          $0 3.3.4"
    echo "          $0 3.3.4 1"
    exit 1
}

if [[ `uname -a | grep -c "Darwin"` -gt 0 ]]; then
	FLAVOR="Mac"
elif [[ `uname -a | grep -c "Linux"` -gt 0 ]]; then
	FLAVOR="Linux"
else
    FLAVOR="Unknown"
fi

if [[ -z $1 ]]; then
    if [[ $FLAVOR == "Mac" ]]; then
        VERSION=`curl -L -Ssf https://download.electrum.org/ | grep -o '[0-9]\{1,2\}\.[0-9]\{1,2\}\.[0-9]\{0,2\}' | tail -n 1`
    else
        VERSION=`wget -S -nv -q -O - https://download.electrum.org/ | grep -o '[0-9]\{1,2\}\.[0-9]\{1,2\}\.[0-9]\{0,2\}' | tail -n 1`
    fi
    if [[ -z $VERSION ]]; then
        echo -e "ERROR: Package version must be provided.\n"
        print_help
    fi
elif [[ ! $1 =~ ^[0-9]\.[0-9]{1,2}\.?[0-9]{0,2} ]]; then
    echo -e "ERROR: Package version seems to be in the wrong format.\n"
    print_help
fi

if [[ -z $2 ]]; then
    MODE=0
elif [[ $2 =~ ^[0-1]$ ]]; then
    MODE=$2
else
    echo -e "ERROR: Mode of operation is invalid.\n"
    print_help
fi

ELECTRUM_DOWNLOAD="https://download.electrum.org/"
ELECTRUM_DMG="electrum-$VERSION.dmg"
ELECTRUM_DMG_ASC="electrum-$VERSION.dmg.asc"
ELECTRUM_TAR="Electrum-$VERSION.tar.gz"
ELECTRUM_TAR_ASC="Electrum-$VERSION.tar.gz.asc"
THOMASV_PUBKEY_GITHUB="https://raw.githubusercontent.com/spesmilo/electrum/master/pubkeys/ThomasV.asc"
THOMASV_ASC="ThomasV.asc"
THOMASV_PUBKEY_ID="0x2BD5824B7F9470E6" # Same format as fingerprint below
THOMASV_PUBKEY_ID_FINGERPRINT="2BD5824B7F9470E6" #last part, from youtube: https://www.youtube.com/watch?v=hjYCXOyDy7Y
THOMASV_PUBKEY_GNUPG="http://keys.gnupg.net/pks/lookup?op=get&search=$THOMASV_PUBKEY_ID&options=mr" # Not used
GNUPG_KEYHOST="keys.gnupg.net"

[[ ! -x `which gpg` ]] && echo "ERROR: Could not locate gpg." && exit 5
if [[ $FLAVOR="Mac" ]]; then
	[[ ! -x `which curl` ]] && echo "ERROR: Could not locate curl." && exit 6
	ELECTRUM_PKG_ASC=$ELECTRUM_DMG_ASC
	ELECTRUM_PKG=$ELECTRUM_DMG
else  # FLAVOR="Linux"
	[[ ! -x `which wget` ]] && echo "ERROR: Could not locate wget." && exit 7
	ELECTRUM_PKG_ASC=$ELECTRUM_TAR_ASC
	ELECTRUM_PKG=$ELECTRUM_TAR
fi

if [[ `gpg --with-colons -k $THOMASV_PUBKEY_ID | grep -c "electrum.org"` -gt 0 ]]; then
	echo "Thomas Voegtlin's public key $THOMASV_PUBKEY_ID has already been imported."
else
	if [[ $MODE -eq 0 ]]; then
		# Uses keys.gnupg.org to import the key (uses HKP, depends on TCP 11371). This is the default.
		echo "Importing Thomas Voegtlin's public key $THOMASV_PUBKEY_ID from $GNUPG_KEYHOST"
		GPG_OUT=$(gpg --with-colons --status-fd 1 --keyserver $GNUPG_KEYHOST --recv-keys $THOMASV_PUBKEY_ID 2> /dev/null)
		if [[ `echo $GPG_OUT | grep -c FAILURE` -eq 1 ]]; then
			echo "[ FAIL ] GPG failed to import key $THOMASV_PUBKEY_ID from $GNUPG_KEYHOST. It may have timed out."
			echo "Make sure you can send traffic using port TCP 11371,"
			echo "or alternate to 'mode 1' of operation running:"
			echo "$0 $VERSION 1"
			exit 2
		fi
	else
		# Downloads the key from githubusercontent.com (uses https, TCP 443)
		echo "Downloading and importing Thomas Voegtlin's public key $THOMASV_PUBKEY_ID from Github."
		if [[ $FLAVOR == "Mac" ]]; then
			[[ ! -f $THOMASV_ASC ]] && curl -L -# $THOMASV_PUBKEY_GITHUB -o $THOMASV_ASC
		else
			[[ ! -f $THOMASV_ASC ]] && wget $THOMASV_PUBKEY_GITHUB
		fi
		GPG_OUT=$(gpg --with-colons --status-fd 1 --import $THOMASV_ASC 2> /dev/null)
		if [[ `echo $GPG_OUT | grep -c $THOMASV_PUBKEY_ID_FINGERPRINT` -lt 1 ]]; then
			echo "[ FAIL ] GPG failed to import key $THOMASV_PUBKEY_ID from $THOMASV_ASC."
			echo "Try 'mode 0' of operation by running:"
			echo "$0 $VERSION"
			echo "Or try to reach the author at https://electrum.org/#community"
			exit 2
		fi
	fi
fi

if [[ $FLAVOR == "Mac" ]]; then
  if [[ `curl -Is $ELECTRUM_DOWNLOAD/$VERSION/ | head -n 1 | grep -c "404"` -gt 0 ]]; then
    echo "ERROR: could not find folder $ELECTRUM_DOWNLOAD/$VERSION/."
    exit 8
  fi
else
  if [[ `wget -S -nv -q -O - $ELECTRUM_DOWNLOAD/$VERSION/ 2>&1 | head -n 1 | grep -c "404"` -gt 0 ]]; then
    echo "ERROR: could not find folder $ELECTRUM_DOWNLOAD/$VERSION/."
    exit 8
  fi
fi

if [[ ! -f $ELECTRUM_PKG_ASC || ! -f $ELECTRUM_PKG ]]; then
	echo "Downloading files from Github..."
	if [[ $FLAVOR == "Mac" ]]; then
		[[ ! -f $ELECTRUM_PKG_ASC ]] && curl -L -# $ELECTRUM_DOWNLOAD/$VERSION/$ELECTRUM_PKG_ASC -o $ELECTRUM_PKG_ASC
		[[ ! -f $ELECTRUM_PKG ]] && curl -L -# $ELECTRUM_DOWNLOAD/$VERSION/$ELECTRUM_PKG -o $ELECTRUM_PKG
	else
		[[ ! -f $ELECTRUM_PKG_ASC ]] && wget -nv -q --show-progress $ELECTRUM_DOWNLOAD/$VERSION/$ELECTRUM_PKG_ASC
		[[ ! -f $ELECTRUM_PKG ]] && wget -nv -q --show-progress $ELECTRUM_DOWNLOAD/$VERSION/$ELECTRUM_PKG
	fi
else
	echo "All files have already been copied locally."
fi
echo "- $ELECTRUM_PKG_ASC"
echo "- $ELECTRUM_PKG"
[[ -f $THOMASV_ASC ]] && echo "- $THOMASV_ASC"

echo -e "\nVerifying digital signature...\n"
GPG_OUT2=$(gpg --with-colons --status-fd 1 --verify $ELECTRUM_PKG_ASC 2> /dev/null)

if [[ `echo $GPG_OUT2 | grep -c GOOD` -eq 1 ]]; then
	echo -e "[ SUCCESS ] GPG signature for $ELECTRUM_PKG successfully verified.\n"
	# The script won't delete files.
	if [[ -f $THOMASV_ASC ]]; then
		echo -e "It's now safe to delete files $ELECTRUM_PKG_ASC and $THOMASV_ASC\n"
	else
		echo -e "It's now safe to delete file $ELECTRUM_PKG_ASC\n"
	fi
	echo "RESULT: SUCCESS. It means that $ELECTRUM_PKG wasn't modified after its creation"
	echo "because it was signed by Thomas Voegtlin's private key (main developer)."
	echo "Learn more at https://stakey.club/en/verifying-digital-signatures/\n"
	# The script won't install or run the wallet.
	if [[ $FLAVOR == "Mac" ]]; then
		echo "To install, open $ELECTRUM_PKG and move Electrum.app to the Applications folder."
	else
		echo "To install:"
		echo "1. unpack $ELECTRUM_PKG with 'tar -xvf $ELECTRUM_PKG'"
		echo "2. run 'python3 Electrum-$VERSION/run_electrum'"
	fi
else
	echo -e "[ FAIL ] GPG did not find a \"good signature\" for $ELECTRUM_PKG. Verification FAILED!"
	exit 4
fi

3. Python Script

To execute this Python script, the following packages must be installed on the computer. On Windows, Python and gpg4win must be installed:

$ sudo pip install python-gnupg
$ sudo pip install certifi

$ python electrum_verifier.sh

Electrum Verifier:

#!/usr/bin/env python3

import argparse
from pathlib import Path
import re
import platform
import urllib.request
import ssl
import gnupg
import shutil
import certifi

# Electrum Verifier v0.2
# This script downloads and verifies the digital signature and hash of Electrum Bitcoin wallet.
# Electrum was created by Thomas Voegtlin and is available at electrum.org
# Reference: https://electrum.org/#download
# Author: Marcelo Martins (stakey.club)

# Syntax: python electrum_verifier.py -v [version number] -m [mode]
# Version defaults to the latest auto-detected version if left blank
# Mode of operation must be: (public key download)
# 0 = HKP, via TCP 11371 (default)
# 1 = HTTPS, via TCP 443
# Examples: $ python electrum_verifier.py -v 3.3.4
#           $ python electrum_verifier.py -v 3.3.4 -m 1


parser = argparse.ArgumentParser()
parser.add_argument("-v", "--version", help="Version to download", dest='version')
parser.add_argument("-m", "--mode", help="Mode of operation", dest='mode')
args = parser.parse_args()

__version__ = '0.2'


def print_help():
    print("Version defaults to the latest auto-detected version if left blank")
    print("Mode of operation must be: (public key download)")
    print("0 = HKP, via TCP 11371 (default)")
    print("1 = HTTPS, via TCP 443")
    print("Syntax:  $0 [version] [mode]")
    print("Examples: $0")
    print("          $0 -v 3.3.4")
    print("          $0 -v 3.3.4 -m 1")
    exit(1)


client_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
client_context.options |= ssl.OP_NO_TLSv1
client_context.options |= ssl.OP_NO_TLSv1_1
client_context.load_default_certs(purpose=ssl.Purpose.SERVER_AUTH)
client_context.load_verify_locations(capath=certifi.where())

if not args.version:
    VERSION = re.findall('[0-9]{1,2}\.[0-9]{1,2}\.?[0-9]{0,2}',
                         str(urllib.request.urlopen("https://download.electrum.org/",
                                                    context=client_context).read(90000)))[-1]
    if not VERSION:
        print("ERROR: Package version must be provided.\n")
        print_help()

elif re.search('^[0-9]{1,2}.[0-9]{1,2}.?[0-9]{0,2}', args.version):
    VERSION = args.version
else:
    VERSION = ""
    print("ERROR: Package version seems to be in the wrong format.\n")
    print_help()


if not args.mode:
    MODE = 0
elif re.search('^[0-1]$', args.mode):
    MODE = args.mode
else:
    MODE = 0
    print("ERROR: Mode of operation is invalid.\n")
    print_help()

ELECTRUM_DOWNLOAD = "https://download.electrum.org/"
ELECTRUM_DMG = "electrum-" + VERSION + ".dmg"
ELECTRUM_DMG_ASC = "electrum-" + VERSION + ".dmg.asc"
ELECTRUM_TAR = "Electrum-" + VERSION + ".tar.gz"
ELECTRUM_TAR_ASC = "Electrum-" + VERSION + ".tar.gz.asc"
ELECTRUM_EXE = "electrum-" + VERSION + ".exe"
ELECTRUM_EXE_ASC = "electrum-" + VERSION + ".exe.asc"
THOMASV_PUBKEY_GITHUB = "https://raw.githubusercontent.com/spesmilo/electrum/master/pubkeys/ThomasV.asc"
THOMASV_ASC = "ThomasV.asc"
THOMASV_PUBKEY_ID = "0x2BD5824B7F9470E6"  # Same format as fingerprint below
THOMASV_PUBKEY_ID_FINGERPRINT = "2BD5824B7F9470E6"  # last part, youtube: https://www.youtube.com/watch?v=hjYCXOyDy7Y
THOMASV_PUBKEY_GNUPG = "http://keys.gnupg.net/pks/lookup?op=get&search=" + THOMASV_PUBKEY_ID + "&options=mr"  # Not used
GNUPG_KEYHOST = "keys.gnupg.net"
ELECTRUM_PKG_ASC, ELECTRUM_PKG = "", ""

if platform.system() == 'Darwin':
    ELECTRUM_PKG_ASC = ELECTRUM_DMG_ASC
    ELECTRUM_PKG = ELECTRUM_DMG
elif platform.system() == 'Linux':
    ELECTRUM_PKG_ASC = ELECTRUM_TAR_ASC
    ELECTRUM_PKG = ELECTRUM_TAR
elif platform.system() == 'Windows':
    ELECTRUM_PKG_ASC = ELECTRUM_EXE_ASC
    ELECTRUM_PKG = ELECTRUM_EXE
else:
    print("ERROR: Unknown platform.")
    exit(9)

gpg = gnupg.GPG()
thomasv_key = gpg.list_keys(keys=THOMASV_PUBKEY_ID_FINGERPRINT)
if thomasv_key:
    print("Thomas Voegtlin's public key", THOMASV_PUBKEY_ID, "has already been imported.")
else:
    if int(MODE) == 0:
        # Uses keys.gnupg.org to import the key (uses HKP, depends on TCP 11371). This is the default.
        print("Importing Thomas Voegtlin's public key", THOMASV_PUBKEY_ID, "from", GNUPG_KEYHOST)
        gpg_out = gpg.recv_keys(GNUPG_KEYHOST, THOMASV_PUBKEY_ID_FINGERPRINT)
        if not gpg_out.results[0]['ok']:
            print("[ FAIL ] GPG failed to import key", THOMASV_PUBKEY_ID,
                  "from", GNUPG_KEYHOST + ". It may have timed out.")
            print("Make sure you can send traffic using port TCP 11371,")
            print("or alternate to 'mode 1' of operation running:")
            print("python electrum_verifier.py -v", VERSION, "-m 1")
            exit(2)
    else:
        # Downloads the key from githubusercontent.com (uses https, TCP 443)
        print("Downloading and importing Thomas Voegtlin's public key", THOMASV_PUBKEY_ID, "from Github.")
        print("-", THOMASV_ASC)
        with urllib.request.urlopen(THOMASV_PUBKEY_GITHUB, context=client_context) \
                as response, open(THOMASV_PUBKEY_GITHUB, 'wb') as out_file:
            shutil.copyfileobj(response, out_file)
        gpg_out = gpg.import_keys(THOMASV_ASC)
        if not gpg_out.results[0]['ok']:
            print("[ FAIL ] GPG failed to import key", THOMASV_PUBKEY_ID, "from", THOMASV_ASC + ".")
            print("Try 'mode 0' of operation by running:")
            print("$0 $VERSION")
            print("Or try to reach the author at https://electrum.org/#community")
            exit(2)

response = urllib.request.urlopen(ELECTRUM_DOWNLOAD + "/" + VERSION, context=client_context)
if response.code == 404:
    print("ERROR: could not find folder", ELECTRUM_DOWNLOAD + "/" + VERSION, "on Github.")
    exit(8)

if not Path(ELECTRUM_PKG_ASC).is_file() and not Path(ELECTRUM_PKG).is_file():
    print("Downloading files from Github...")
    if not Path(ELECTRUM_PKG_ASC).is_file():
        print("-", ELECTRUM_PKG_ASC)
        with urllib.request.urlopen(ELECTRUM_DOWNLOAD + "/" + VERSION + "/" + ELECTRUM_PKG_ASC,
                                    context=client_context) as response, open(ELECTRUM_PKG_ASC, 'wb') as out_file:
            shutil.copyfileobj(response, out_file)
    if not Path(ELECTRUM_PKG).is_file():
        print("-", ELECTRUM_PKG)
        with urllib.request.urlopen(ELECTRUM_DOWNLOAD + "/" + VERSION + "/" + ELECTRUM_PKG, context=client_context) \
                as response, open(ELECTRUM_PKG, 'wb') as out_file:
            shutil.copyfileobj(response, out_file)
else:
    print("All files have already been copied locally.")
    print("-", ELECTRUM_PKG_ASC)
    print("-", ELECTRUM_PKG)
Path(THOMASV_ASC).is_file() and print("-", THOMASV_ASC)

print("\nVerifying digital signature...\n")
v = open(ELECTRUM_PKG_ASC, "rb")
verify_result = gpg.verify_file(v, ELECTRUM_PKG)
v.close()

if verify_result.status == "signature valid":
    print("[ SUCCESS ] GPG signature for", ELECTRUM_PKG, "successfully verified.\n")
    # The script won't delete files.
    if Path(THOMASV_ASC).is_file():
        print("It's now safe to delete files", ELECTRUM_PKG_ASC, "and", THOMASV_ASC, "\n")
    else:
        print("It's now safe to delete file", ELECTRUM_PKG_ASC, "\n")
    print("RESULT: SUCCESS. It means that", ELECTRUM_PKG, "wasn't modified after its creation")
    print("because it was signed by Thomas Voegtlin's private key (main developer).")
    print("Learn more at https://stakey.club/en/verifying-digital-signatures/\n")
    # The script won't install or run the wallet.
    if platform.system() == 'Darwin' and MODE == 0:
        print("To install, open", ELECTRUM_PKG, "and move Electrum.app to the Applications folder.")
    elif platform.system() == 'Windows' and MODE == 0:
        print("To install, run", ELECTRUM_PKG)
    else:
        print("To install:")
        print("1. unpack", ELECTRUM_PKG, "with 'tar -xvf", ELECTRUM_PKG + "'")
        print("2. run 'python3 Electrum-" + VERSION + "/run_electrum'")
else:
    print("[ FAIL ] GPG did not find a \"good signature\" for", ELECTRUM_PKG, ". Verification FAILED!")
    exit(4)