How to Verify Software Signatures
So, you've decided to download and install Bitcoin Core to set up your own node. Or you want to use a wallet like Samourai or Electrum. How can we make sure we're downloading the legit software and not a malicious copy, uploaded by a hacker that has obtained access to the server?
This kind of question is very frequent in groups by people setting up their nodes and usually there are doubts about some specific step. I wrote this text in an attempt to give a thorough explanation and it became quite long, but topics already known by the reader can be skipped. If you only need the commands, there's the TL;DR at the end.
1. Why Verify Signatures?
As said before, we want to make sure we are downloading the correct software, not a modified version that could steal our bitcoins or cause us all sorts of trouble.
This kind of attack might not be that probable, but this definitely isn't a theoretical threat! For example, in 2016 Mint Linux had its website hacked and the download link was redirected to a hacked version. In the Bitcoin world, many Electrum users fell victim to a similar attack in 2018 and ended up downloading a fake wallet.
Isn't Checking the Hash Good Enough?
Checking that the hash displayed in the site matches the one of the downloaded file proves the file wasn't corrupted during transfer. This is useful to avoid unexpected behaviors or problems (like burning a corrupt Linux ISO onto a DVD), but won't help us that much here. If the hacker obtained access to the site, he might as well have written the hash of the malicious file there.
Let's begin with Bitcoin Core:
After downloading the highlighted files, we get:
- bitcoin-23.0-x86_64-linux-gnu.tar.gz: Bitcoin Core itself;
- SHA256SUMS: hashes of all versions (x86_64, ARM etc);
- SHA256SUMS.asc: developers signatures;
We also need the public keys of whoever signed the software. In Bitcoin Core, many people sign it and their keys are kept in the project's GitHub. Here are some lines of the file:
9D3CC86A72F8494342EA5FD10A41BDC3F4FAFF1C Aaron Clauson (sipsorcery) 617C90010B3BD370B0AC7D424BB42E31C79111B8 Akira Takizawa (akx20000) E944AE667CF960B1004BC32FCA662BE18B877A60 Andreas Schildbach (aschildbach) 152812300785C96444D3334D17565732E08E5E41 Andrew Chow (achow101) 590B7292695AFFA5B672CBB2E13FC145CD3F4304 Antoine Poinsot (darosior)
We don't need to verify everybody's signatures. As the download page states, it's enough to choose one or more persons you trust and then verify their signatures.This file does not hold the public keys, but the hashes of those keys. A hash is a shorter version of the key, also known as fingerprint.
Let's import, for example, the public key of Andrew Chow. Since we only have the fingerprint, we have to get the public key first:
$ gpg --recv-keys 152812300785C96444D3334D17565732E08E5E41 gpg: key 17565732E08E5E41: 6 duplicate signatures removed gpg: key 17565732E08E5E41: public key "Andrew Chow (Official New Key) <email@example.com>" imported gpg: Total number processed: 1 gpg: imported: 1
The command can also take the optional
--keyserver parameter to specify the server from where the key will be downloaded.This procedure can vary a little bit. In Samourai's case, for example:
The files are:
- sw-signed-0.99.98e.apk: the Samourai Wallet;
- sig: the signature;
This last file doesn't really exist. The contents of the signature are in the link above; just copy and paste them in a file called sig. The public keys are available in the footer of the same page. In this case, since we have the public keys instead of the fingerprints, we can save the pgp.txt file and import the keys with
gpg --import pgp.txt.
However, since there are many keys in that file and the person who signs the apk is always TDev (@Samouraidev), I prefer to copy/paste in the pgp.txt file only his public key (copy the lines from
BEGIN PGP PUBLIC KEY BLOCK to
END PGP PUBLIC KEY).
$ cat pgp.txt -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: Samourai Wallet Dev (@Samouraidev) mQINBFVhmH8BEADKsmq7A+VJemKUp6BkFhrYd/jTPypB6kBfpF8ZPw1XQvohbjYI beaPp4cjbISLyf5denvZd87GzHJtVFI15eV0SHpbQrBPgX0PQ3X7vPceEWJk4BNA X7nsgIAsXnXfaT7AAtYOX1117705wgp9T/OFT+Qqfh/cT0f/A9CzTNH8DuB16ZAL ... +4dyO9c4XCms6bo3i1nUbyRQhA2y0OBV/YcuHs7td7mT4pBAseUFKtu/tHeNlj8x 8rK20Y7H/lEmyphP+L2y5p9p9munyzh7+nhtWMfFrSrtHN1VeVtMkkeUbHtvcUEV KacID3YioW9iTP4/YO7JZ3raYzg= =q8Nb -----END PGP PUBLIC KEY BLOCK----- $ gpg --import pgp.txt gpg: key 72B5BACDFEDF39D7: public key "T Dev D (Samourai) <firstname.lastname@example.org>" imported gpg: Total number processed: 1 gpg: imported: 1
Hey, Wait a Second!
If we can't trust the hash in order to verify the file, since the hacker might have edited the web page, how would we know the hacker didn't put HIS public key in the page?
Yes, that's a very good question. This issue doesn't have a perfect solution, but I will dive into this at the end. For now, let's just assume we can trust the public keys, alright?
Let's check the keys we have imported so far:
$ gpg --list-keys /home/user/.gnupg/pubring.kbx ----------------------------- pub rsa4096 2015-03-05 [SC] [expires: 2023-03-01] 152812300785C96444D3334D17565732E08E5E41 uid [ unknown] Andrew Chow (Official New Key) <email@example.com> uid [ unknown] Andrew Chow <firstname.lastname@example.org> uid [ unknown] Andrew Chow <email@example.com> uid [ unknown] Andrew Chow <firstname.lastname@example.org> uid [ unknown] Andrew Chow <email@example.com> uid [ unknown] Andrew Chow <firstname.lastname@example.org> uid [ unknown] Andrew Chow <email@example.com> sub rsa4096 2015-03-05 [E] [expires: 2023-03-01] pub rsa4096 2015-05-24 [SC] [expires: 2030-05-24] ED1A1280DEFCA60314CD15BF72B5BACDFEDF39D7 uid [ unknown] T Dev D (Samourai) <firstname.lastname@example.org> sub rsa4096 2015-05-24 [E] [expires: 2030-05-24]
Unknown means the trust level of those keys is unknown, because we have just imported them. If we go on with the verification, we'll get a warning telling us there's no indication the key really belongs to its owner. We could just ignore the warning in this case, but note no authentication was needed to add those keys, so anybody accessing the computer could've added or deleted keys.
A good practice is to sign new keys with our own private key. This will remind us in the future that we do trust those new keys.
Let's create a private key. If you already have one or you don't think this is necessary, you can just skip this step.
2.1. Creating a Private Key
Let's start with
gpg --full-gen-key. The gpg software will ask several questions:
- Kind of key: this can be the default (RSA and RSA);
- Number of bits: there's no reason to not use the safest, so 4096;
- Expiration date: this can be useful if an attacker gets access to the key. In this case, he would only be able to impersonate the legit key owner until the expiration date, minimizing damages.
You certainly won't be using this key to sign important software, otherwise you wouldn't be reading a newbie guide like this :)
Let's keep it without an expiration date.
Then, there are some optional personal data to identify the key, like name and e-mail. Write whatever you feel suitable, confirm everything with "O" and choose a password to encrypt your private key.
Finally, the following message will be displayed:
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy.
Type some characters or move your mouse to provide extra entropy and the key will finally be created. Here's the private key:
$ gpg --list-secret-keys /home/user/.gnupg/pubring.kbx -------------------------------- sec rsa4096 2022-09-17 [SC] CBD31315C62216ABFDB3E3DAD58CCEB2EC00FC49 uid [ultimate] User <email@example.com> ssb rsa4096 2022-09-17 [E]
2.2. Signing the Public Keys
We can now sign the keys using the password of our own private key:
$ gpg --sign-key 152812300785C96444D3334D17565732E08E5E41 $ gpg --sign-key ED1A1280DEFCA60314CD15BF72B5BACDFEDF39D7
If we list all keys, we'll see the keys are now marked as full instead of unknown (and ours, as ultimate):
$ gpg --list-keys /home/user/.gnupg/pubring.kbx -------------------------------- pub rsa4096 2015-05-24 [SC] [expires: 2030-05-24] ED1A1280DEFCA60314CD15BF72B5BACDFEDF39D7 uid [ full ] T Dev D (Samourai) <firstname.lastname@example.org> sub rsa4096 2015-05-24 [E] [expires: 2030-05-24] pub rsa4096 2015-03-05 [SC] [expires: 2023-03-01] 152812300785C96444D3334D17565732E08E5E41 uid [ full ] Andrew Chow (Official New Key) <email@example.com> uid [ full ] Andrew Chow <firstname.lastname@example.org> uid [ full ] Andrew Chow <email@example.com> uid [ full ] Andrew Chow <firstname.lastname@example.org> uid [ full ] Andrew Chow <email@example.com> uid [ full ] Andrew Chow <firstname.lastname@example.org> uid [ full ] Andrew Chow <email@example.com> sub rsa4096 2015-03-05 [E] [expires: 2023-03-01] pub rsa4096 2022-09-17 [SC] CBD31315C62216ABFDB3E3DAD58CCEB2EC00FC49 uid [ultimate] User <firstname.lastname@example.org> sub rsa4096 2022-09-17 [E]
3. Verifying the Signatures
We already have everything needed to verify the signatures. Starting with Core, we can use
gpg --verify SHA256SUMS.asc SHA256SUMS. This way we are checking that the signatures of SHA256SUMS.asc did sign SHA256SUMS. Since both files have the same name (except for the .asc), we can omit the latter:
There will probably be many other lines indicating it wasn't possible to verify some signature, because we don't have the public keys from everybody, but the important line here is the one containing "Good signature".
This proves Andrew Chow (besides the others whose keys we didn't import) signed the SHA256SUMS file.
So What? Who Cares About This File?
That's right, we are interested in Bitcoin Core, not in SHA256SUMS. This verification alone is worthless. But if we open that file, we'll see lines like:
$ cat SHA256SUMS a5a86632775fb2c1db4235bd56396ecfeb233bfa24431baf936c41e51cc24fdf bitcoin-23.0-x86_64-linux-gnu-debug.tar.gz 2cca490c1f2842884a3c5b0606f179f9f937177da4eadd628e3f7fd7e25d26d0 bitcoin-23.0-x86_64-linux-gnu.tar.gz 4198eba8ac326d8746ab43364a44a5f20c157b6701f8c35b80d639a676df9011 bitcoin-23.0-win64-setup.exe 02f6c3bde5448527282aafafe7fdb80f35d4f984d9b012a9cb5e5efd28861614 bitcoin-23.0-win64-debug.zip
This file contains hashes of all files of release 23, which means the signers attest the hash of each file. We now just have to check if the file we downloaded has this very same hash.
This could be done manually, but
sha256sum can do it automatically with the
-c option ("check") and
--ignore-missing to ignore the files we did not download:
$ sha256sum --ignore-missing -c SHA256SUMS bitcoin-23.0-x86_64-linux-gnu.tar.gz: OK
This is it, we have the legit file. This method is common when there are many files, like for different architectures and operating systems.
In Samourai's case, since there is a single file to be signed (the apk), there isn't a hashes file. The sig contains the signature and also the message that was signed, which is the apk's hash:
$ cat sig -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 ef54ae12488efc9ed88fc961829c81a7ba0aabe8a938517a3ef81801081070ca -----BEGIN PGP SIGNATURE----- iQIzBAEBCAAdFiEE7RoSgN78pgMUzRW/crW6zf7fOdcFAmLESm8ACgkQcrW6zf7f OdfK/RAAurzLDXDVd1XvOtuPcLBB8AArgbc0AO6TzoOcchIGFEqvDAYbSGLgnNII Foa6Vzhn+XL4VE0FhKI0HqVaMRzjXeYy+hbWQiBd41BN5uMsMhdlBQsgD6MDklzf ON9oOpmZtbtTS1BlwadZTQX4pzlyydspRusPB9fE8cRVRXPIlvvEaOWmwGOHIY6L g3FQmBwvLrChdn4bB+7V+8TQhtbynDMuZIzXAvQ5qSGdc1/b3b77XwQyyIRVxjgu kKVhl7Akwe7oE2ED4t69gMKLztc0EER2cJEAyQxoytIOqKp+cqgV47f1byw9mabw RbzhHnTHWhpCOIeXYS3myrOJ33Nt7NFHV2NnyCisRL3Bxnrv0RzDxhNRcZBK9Y+r tTyxc+tPSHu/yJE+lPoYj3jfcJhVcfkZ61oVZK/4NqdGwZdb5Ouc1+M5bbaK3bHq K7iFxvy4+EYAJrWyY1gRpzuWYagScLqPGdv55gg/1CpMQqXCFiu9gwEdWzZJ6bRi xoDlOLXzyG+qGlndMLoiaOo5LLpUZTAdURPkWVBLW91wawsLORNquG9k74WZdjJ2 e4KM7zaX0vEuaa7uuBE/iqcc7m3NKeERtB5cHjUhDl630VFqoThzZpnXgeokOKxn 8N4Nx6iag7+dmM89QwpR+QZbGbRlKjzuTfV0EI3eGyie2ZQHTCI= =Y1qL -----END PGP SIGNATURE-----
Let's verify the signature and the apk's hash:
$ gpg --verify sig gpg: Signature made tue 05 jul 2022 11:27:59 -03 gpg: using RSA key ED1A1280DEFCA60314CD15BF72B5BACDFEDF39D7 gpg: Good signature from "T Dev D (Samourai) <email@example.com>" [full] $ sha256sum sw-signed-0.99.98e.apk ef54ae12488efc9ed88fc961829c81a7ba0aabe8a938517a3ef81801081070ca sw-signed-0.99.98e.apk
Good signature and the same hash. Done, signatures verified!
4. What about Trusting Public Keys?
Alright. Truth is there isn't a definitive way to make sure we got the correct public key. What we can do, however, is reduce the odds of being fooled. Some possibilities:
- Download the key from multiple servers:
gpg --recv-keyscommand takes an optional parameter which is the server from where to get the key. We could have used
--keyserverwith different servers, like pgp.mit.edu, keyserver.ubuntu.com or keys.openpgp.org;
- Check devs profiles on social networks:
Many of them add their public key fingerprints on social networks. Examples:
- Ask around:
Why not? If unsure about a certain key, ask in groups or friends who've already verified it. Maybe even the developer himself;
- Check in a non-electronic medium:
In the excellent Grokking Bitcoin book, by Kalle Rosenbaum, there is a section called Running a Full Node, from which part of this text was based on. That section has written on it the public key of Wladimir van der Laan, which at the time was the only developer who signed Bitcoin Core releases.
Another example: Thomas Voegtlin, from Electrum, has shown his fingerprint in a presentation:
Notice none of these methods is 100% guaranteed and maybe the only way to assure you got the right key is by meeting the owner in person. What are the odds, however, of somebody hacking all those means?
Every extra verification helps to make this possibility infeasible.
5. TL;DR (too long, didn't read)
- Import public key(s) with
gpg --recv-keys <fingerprint>or
gpg --import <public key>;
- Download the desired software, the file containing the signature and, if existent, the hashes file;
- Verify the signature with
gpg --verify <signature>;
- Verify the hash of the program is the same as in the hashes file with
sha256sum -c <hashes> --ignore-missingor it's equal to the one in the signature file with