Python 3 and a terminal. Tor Browser is not required and the Tor network is never contacted — every check in this lab is arithmetic on the address itself.
You need no cryptography background. The single idea is that an onion address is not a name someone registered; it is a public key written in base32, with a short checksum attached.
Nothing is installed and nothing on your machine changes. The lab creates three small Python files in a scratch folder you can delete afterwards.
Platform: Ubuntu / Linux Mint · ~15 min read
Overview
This guide walks you through securely downloading, verifying, and installing Tor Browser on a fresh Linux system. GPG verification ensures the file is authentic and hasn't been tampered with.
Verifying the GPG signature proves the download came from the Tor Project and wasn't modified by attackers. This is critical for privacy software.
Tor Browser Version: 15.0.2 (update version number in commands as needed)
Step 1: Create a Directory for Tor Browser
First, create a dedicated folder to keep things organized:
mkdir -p ~/Downloads/Tor_Browser
cd ~/Downloads/Tor_Browser
Step 2: Download Tor Browser and Signature File
You need two files: the Tor Browser archive and its GPG signature file.
Option A: Download via Browser
- Go to: https://www.torproject.org/download/
- Download the Linux (64-bit) version
- Also download the .asc signature file (click "Signature" link next to the download)
- Move both files to your
~/Downloads/Tor_Browserdirectory
Option B: Download via Terminal (wget)
# Download Tor Browser (update version number as needed)
wget https://www.torproject.org/dist/torbrowser/15.0.2/tor-browser-linux-x86_64-15.0.2.tar.xz
# Download the signature file
wget https://www.torproject.org/dist/torbrowser/15.0.2/tor-browser-linux-x86_64-15.0.2.tar.xz.asc
Option C: Download via Terminal (curl)
# Download Tor Browser
curl -O https://www.torproject.org/dist/torbrowser/15.0.2/tor-browser-linux-x86_64-15.0.2.tar.xz
# Download the signature file
curl -O https://www.torproject.org/dist/torbrowser/15.0.2/tor-browser-linux-x86_64-15.0.2.tar.xz.asc
Step 3: Verify You Have Both Files
ls -la
Expected output:
tor-browser-linux-x86_64-15.0.2.tar.xz
tor-browser-linux-x86_64-15.0.2.tar.xz.asc
You should see:
.tar.xz— The Tor Browser archive.tar.xz.asc— The GPG signature file
Step 4: Import the Tor Browser Developers Signing Key
Method 1: Auto-locate via WKD (Preferred)
gpg --auto-key-locate nodefault,wkd --locate-keys torbrowser@torproject.org
Method 2: Import from Keyserver
gpg --keyserver keyserver.ubuntu.com --recv-keys EF6E286DDA85EA2A4BA7DE684E2C6E8793298290
Expected output (either method):
pub rsa4096 2014-12-15 [C] [expires: 2027-07-15]
EF6E286DDA85EA2A4BA7DE684E2C6E8793298290
uid [ unknown] Tor Browser Developers (signing key) <torbrowser@torproject.org>
sub rsa4096 2024-07-15 [S] [expires: 2026-10-26]
The fingerprint MUST match exactly:
EF6E 286D DA85 EA2A 4BA7 DE68 4E2C 6E87 9329 8290
Step 5: Verify the Signature
gpg --verify tor-browser-linux-x86_64-15.0.2.tar.xz.asc tor-browser-linux-x86_64-15.0.2.tar.xz
Step 6: Interpret the Results
✅ GOOD — Safe to Use
Look for this in the output:
gpg: Good signature from "Tor Browser Developers (signing key) <torbrowser@torproject.org>"
And verify the primary key fingerprint:
Primary key fingerprint: EF6E 286D DA85 EA2A 4BA7 DE68 4E2C 6E87 9329 8290
If you see "Good signature" and the fingerprint matches, your download is authentic. Proceed to extraction.
⚠️ WARNING — Can Be Ignored
This warning is normal and safe to ignore:
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
This appears because you haven't personally signed the Tor Project's key in your GPG keyring. It does NOT mean the file is compromised.
❌ BAD — Do Not Use
If you see this, DELETE the files immediately and re-download:
gpg: BAD signature from "Tor Browser Developers..."
Step 7: Extract Tor Browser
tar -xvf tor-browser-linux-x86_64-15.0.2.tar.xz
This creates a tor-browser directory.
Step 8: Run Tor Browser
cd tor-browser
./start-tor-browser.desktop
Or register it with your desktop environment (adds to application menu):
./start-tor-browser.desktop --register-app
Step 9: (Optional) Move to Permanent Location
If you want Tor Browser in a standard location:
# Move to /opt (system-wide)
sudo mv ~/Downloads/Tor_Browser/tor-browser /opt/tor-browser
# Or keep in home directory
mv ~/Downloads/Tor_Browser/tor-browser ~/tor-browser
Update your launch command accordingly.
Now Do It Yourself: Verify an Onion Address, Then Break the Check
Onion addresses look like they defend themselves — 56 random characters ending in .onion, with a built-in checksum. In fifteen minutes you can verify one, forge a valid one, and defeat the check most people actually perform. What survives that is the only habit worth keeping.
This page has already shown you how to verify the Tor Browser download with a GPG signature. That protects the software. It does nothing for the addresses you visit afterwards — and that is where the more common attack lives.
You need Python 3. Nothing here connects to the Tor network, or to anything else — an onion address can be checked entirely offline, because the address IS the key. Every output below came from running these exact commands.
Go: open a terminal, then mkdir onionlab and cd onionlab. Nothing in this lab touches the Tor network — every check is arithmetic on the address itself.
Do: save this as onioncheck.py. A v3 onion address is base32 of a 32-byte public key, a 2-byte checksum, and a version byte:
import base64, hashlib, sys
def parts(addr):
a = addr.lower().replace(".onion", "")
if len(a) != 56:
sys.exit(f"not a v3 onion address: {len(a)} characters, expected 56")
raw = base64.b32decode(a.upper() + "=" * ((8 - len(a) % 8) % 8))
return raw[:32], raw[32:34], raw[34:35]
def checksum(pub, ver):
return hashlib.sha3_256(b".onion checksum" + pub + ver).digest()[:2]
addr = sys.argv[1]
pub, chk, ver = parts(addr)
want = checksum(pub, ver)
print(f"address : {addr}")
print(f"version : {ver.hex()}")
print(f"checksum : in address {chk.hex()} computed {want.hex()}")
print("VALID — well-formed" if chk == want else "INVALID — this address is corrupt or mistyped")
Run it on the Tor Project's own address:
python3 onioncheck.py 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
You should see: the checksum in the address and the one computed from its key agree:
address : 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
version : 03
checksum : in address ddd9 computed ddd9
VALID — well-formed
Nothing was contacted. An onion address is not a name that gets looked up — it is the key, which is why there is no registrar, no certificate authority, and nobody to ask whether it is genuine.
If not: Incorrect padding means the address lost characters when copying — it must be exactly 56 characters before .onion. not a v3 onion address with a smaller number means you copied a short v2 address, which Tor removed support for in 2021.
Go: the same folder.
Do: run the checker again on the same address with a single character changed — the w near the end becomes an x:
python3 onioncheck.py 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53xid.onion
You should see: the two checksums no longer agree:
address : 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53xid.onion
version : 03
checksum : in address dddd computed ddd9
INVALID — this address is corrupt or mistyped
This is the checksum doing its job: it catches accidents — a dropped character, a bad copy-paste, a misread from a screenshot. Tor Browser performs this same check and refuses to load an address that fails it, which is genuinely useful and is also the limit of what it can do.
If not: if it reports VALID, you did not actually change a character — compare the two addresses carefully, the difference is one letter four from the end.
Go: the same folder.
Do: save this as makeonion.py, then run it and feed the result straight back into your checker:
import base64, hashlib, os
# An onion address is a public key with a checksum stapled on. Anyone can make one.
pub = os.urandom(32) # pretend this is a real ed25519 public key
ver = b"\x03"
chk = hashlib.sha3_256(b".onion checksum" + pub + ver).digest()[:2]
addr = base64.b32encode(pub + chk + ver).decode().lower().rstrip("=")
print(addr + ".onion")
Then:
python3 makeonion.py > fake.txt
cat fake.txt
python3 onioncheck.py "$(cat fake.txt)"
You should see: an address that has never existed, and it passes:
oeoyzmrrzfrighwatjnej4f5i7s3eurxlk2bitdnf6vei6rrpemt3pid.onion
address : oeoyzmrrzfrighwatjnej4f5i7s3eurxlk2bitdnf6vei6rrpemt3pid.onion
version : 03
checksum : in address 3dbd computed 3dbd
VALID — well-formed
Yours will be a different address every run — it is generated from random bytes.
🔴 VALID does not mean trustworthy. It means the address is well-formed. Anyone can produce as many valid addresses as they like, in a fraction of a second, and every one of them will load in Tor Browser without a single warning.
If not: if the checker reports INVALID on your generated address, the address was truncated when writing to the file — run wc -c fake.txt, it should be 63 characters plus a newline.
Go: the same folder.
Do: almost nobody compares all 56 characters. They glance at the first few. Save this as vanity.py and ask it for an address starting with the same four characters as the real Tor Project one:
import base64, hashlib, os, sys, time
target = sys.argv[1].lower()
start = time.time()
tries = 0
while True:
tries += 1
pub = os.urandom(32)
ver = b"\x03"
chk = hashlib.sha3_256(b".onion checksum" + pub + ver).digest()[:2]
addr = base64.b32encode(pub + chk + ver).decode().lower().rstrip("=")
if addr.startswith(target):
print(f"matched \'{target}\' after {tries:,} tries in {time.time()-start:.1f}s")
print(addr + ".onion")
break
Then run it:
python3 vanity.py 2gzy
You should see: a matching prefix in seconds:
matched '2gzy' after 695,714 tries in 3.8s
2gzy5nwbtyscfoqk4t6czy2va3lz5myndewjmfl7sxj6dhzc5zu2rlad.onion
Your address and timing will differ; a few seconds is typical for four characters.
🔴 Put them side by side. The real one is 2gzyxa5ihm7... and yours is 2gzy followed by something else entirely. Both are valid. Both load. Four characters cost a few seconds; six or seven are still cheap. The visual check almost everyone performs — recognising the start of a familiar address — is defeated for the price of a coffee break.
If not: if it runs for more than a minute, you asked for too many characters — each extra character multiplies the work by 32. Use four. Press Ctrl+C to stop it.
Go: a site you already trust over ordinary HTTPS — for example torproject.org, or the official site of whatever service you want the onion address for.
Do: get the onion address from that authenticated source and paste it into your checker rather than typing it. Many sites also publish an Onion-Location header, which Tor Browser turns into a “.onion available” button — that button is trustworthy for the same reason: the address arrived over a connection with a verified certificate.
python3 onioncheck.py <paste the address you copied>
You should see: VALID — well-formed, which now means something, because you already know where the address came from.
This step depends on your own browsing and was not re-run for this page; steps 1–4 above were, and they are what the reasoning rests on.
The checksum proves the address survived the journey intact. Only the source proves it is the right address. That ordering — authenticate first, verify integrity second — is the whole of it, and it is why bookmarking a verified onion address is worth more than any amount of squinting at the characters.
If not: if the address fails the check after you pasted it, you copied a line break or a trailing space — quote it: python3 onioncheck.py "…onion". If a site offers no onion address at all, no amount of searching for one is safe: unofficial mirrors are exactly the attack this lab describes.
A forum post gives an onion address for a service you use. You paste it into your checker and it reports VALID — well-formed. What have you actually learned? Answer: Only that the address is not corrupted or mistyped. Nothing about who controls it. Step 3 produced a valid address belonging to nobody in milliseconds, and step 4 matched a familiar prefix in seconds — so neither validity nor a familiar-looking start is evidence. What would settle it is getting the address from the service's own HTTPS site.
Now do it without the page: explain why onion addresses need no certificate authority, and why that same property means nobody can vouch for one either. If you can hold both halves at once, you understand the trade Tor made.
Quick Reference: All Commands
All commands in one place for quick reference.
# 1. Create directory and navigate to it
mkdir -p ~/Downloads/Tor_Browser
cd ~/Downloads/Tor_Browser
# 2. Download files (update version as needed)
wget https://www.torproject.org/dist/torbrowser/15.0.2/tor-browser-linux-x86_64-15.0.2.tar.xz
wget https://www.torproject.org/dist/torbrowser/15.0.2/tor-browser-linux-x86_64-15.0.2.tar.xz.asc
# 3. Import GPG key
gpg --auto-key-locate nodefault,wkd --locate-keys torbrowser@torproject.org
# 4. Verify signature
gpg --verify tor-browser-linux-x86_64-15.0.2.tar.xz.asc tor-browser-linux-x86_64-15.0.2.tar.xz
# 5. Extract (only if verification passed)
tar -xvf tor-browser-linux-x86_64-15.0.2.tar.xz
# 6. Run
cd tor-browser
./start-tor-browser.desktop
Troubleshooting
"gpg: command not found"
Install GPG:
sudo apt update
sudo apt install gnupg
"wget: command not found"
Install wget:
sudo apt install wget
Key import fails
Try alternative keyservers:
gpg --keyserver keys.openpgp.org --recv-keys EF6E286DDA85EA2A4BA7DE684E2C6E8793298290
Or:
gpg --keyserver pgp.mit.edu --recv-keys EF6E286DDA85EA2A4BA7DE684E2C6E8793298290
Signature verification fails with "No public key"
The key wasn't imported. Go back to Step 4 and import it.
Official Resources
- Tor Project Website: https://www.torproject.org
- Tor Browser Download: https://www.torproject.org/download/
- Signature Verification Guide: Official Tor Verification Docs
- Official Signing Key Fingerprint:
EF6E 286D DA85 EA2A 4BA7 DE68 4E2C 6E87 9329 8290
Notes
- Always verify downloads before running them
- The signing key fingerprint may change over time — verify it on the official Tor Project website
- Update the version number (15.0.2) in commands when downloading newer versions
- Tor Browser will auto-update itself after installation