Skip to content

Complete Tor Browser Guide

💡
Before you start

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.

ℹ️
Why Verify?

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:

Bash
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

  1. Go to: https://www.torproject.org/download/
  2. Download the Linux (64-bit) version
  3. Also download the .asc signature file (click "Signature" link next to the download)
  4. Move both files to your ~/Downloads/Tor_Browser directory

Option B: Download via Terminal (wget)

Bash
# 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)

Bash
# 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

Bash
ls -la

Expected output:

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)

Bash
gpg --auto-key-locate nodefault,wkd --locate-keys torbrowser@torproject.org

Method 2: Import from Keyserver

Bash
gpg --keyserver keyserver.ubuntu.com --recv-keys EF6E286DDA85EA2A4BA7DE684E2C6E8793298290

Expected output (either method):

Output
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]
⚠️
Important: Verify the Fingerprint

The fingerprint MUST match exactly:

EF6E 286D DA85 EA2A 4BA7 DE68 4E2C 6E87 9329 8290

Step 5: Verify the Signature

Bash
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:

Output
gpg: Good signature from "Tor Browser Developers (signing key) <torbrowser@torproject.org>"

And verify the primary key fingerprint:

Output
Primary key fingerprint: EF6E 286D DA85 EA2A 4BA7  DE68 4E2C 6E87 9329 8290
Success!

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:

Output
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

Critical: Bad Signature

If you see this, DELETE the files immediately and re-download:

gpg: BAD signature from "Tor Browser Developers..."

Step 7: Extract Tor Browser

Bash
tar -xvf tor-browser-linux-x86_64-15.0.2.tar.xz

This creates a tor-browser directory.

Step 8: Run Tor Browser

Bash
cd tor-browser
./start-tor-browser.desktop

Or register it with your desktop environment (adds to application menu):

Bash
./start-tor-browser.desktop --register-app

Step 9: (Optional) Move to Permanent Location

If you want Tor Browser in a standard location:

Bash
# 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.

1
Build a checker that reads an onion address without connecting to it

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.

2
Mistype it and watch the checksum catch you

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.

3
Make a valid address that belongs to nobody

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.

4
Defeat the check people actually perform

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.

5
Use the only check that actually works

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.

🎉
Check yourself before moving on

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

📋
Copy-Paste Ready

All commands in one place for quick reference.

Bash
# 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:

Bash
sudo apt update
sudo apt install gnupg

"wget: command not found"

Install wget:

Bash
sudo apt install wget

Key import fails

Try alternative keyservers:

Bash
gpg --keyserver keys.openpgp.org --recv-keys EF6E286DDA85EA2A4BA7DE684E2C6E8793298290

Or:

Bash
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

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