July 4, 2026
How Tor Actually Works, The Internals Nobody Explains Properly
Table of Contents

By SHADOW
37 min read
Table of Contents
- Before we start, what you should already know
- What is Tor, and what is the dark web, clearing the confusion first
- What is Tor and why does it exist
- The core idea, onion routing
- The 3-hop circuit, Entry, Middle, Exit
- How your Tor Browser connects to the Tor network, Directory Authorities and the Consensus
- How a .onion address is actually built, V2 and V3
- How a hidden service sets itself up, the full process
- How a client actually connects to a .onion site, the full flow
- How the INTRODUCE1 message is actually constructed, the first secure communication
- How the circuit is actually built, ntor handshake and forward secrecy
- How the Rendezvous Point actually works, the two-socket model
- Known attack surfaces
- Closing
So once I thought of using Tor just to visit the dark web, out of plain curiosity. But after using it a couple times, way more questions started popping up in my head than answers. How is this even different from the normal web? Are we actually tracked here or not? Why can't I just open a .onion link in Chrome, why do I specifically need the Tor Browser for this? What does the Tor Browser even do differently from a normal browser anyway? How does my traffic even reach these sites without anyone knowing it's me?
That curiosity is what pulled me in. I started going through blogs, writeups, YouTube videos, chatting with AI to fill in the gaps, and piecing things together from wherever I could. And the more I read, the more I noticed something, most explanations stop at "your traffic goes through 3 nodes and gets encrypted in layers." Cool, but that's like explaining a car by saying "it has wheels and it moves."
So I decided to go a little deeper and actually understand how the whole thing works, get a proper overview of what's really happening under the hood. This blog is the result of that.
Fair warning, this is long. It's meant to be read slowly, maybe over a coffee, not skimmed in two minutes. But by the end, you'll have a solid idea of how Tor actually works, not everything there is to know about it, but enough that you won't need to go looking for another explanation after this.
Before we start, what you should already know
You don't need a security background to follow this, but a little bit of two things will make everything click faster: basic networking (what an IP address is, what a client and a server are, roughly what a "packet" means) and a little bit of cryptography (the idea that a public key and private key are a pair, and that a hash function takes input and spits out a fixed size fingerprint of it). If none of that rings a bell, don't worry, I'll explain things as we go. But if you do know this stuff, you'll fly through this faster.
What is Tor, and what is the dark web, clearing the confusion first
Before going any deeper, let's clear up something a lot of people mix up. Tor and the dark web are not the same thing.
Tor is the network and the technology. It's the system of relays and encryption that lets your traffic travel anonymously. Tor Browser is just the app you use to access that network. It's basically Firefox with Tor wired into it and a bunch of privacy settings locked down by default. That's also why you can't just open a .onion link in Chrome or any normal browser. A normal browser has no idea what the Tor network even is, it only knows how to talk to the regular internet using normal DNS and routing. Tor Browser knows how to route your traffic through the Tor network specifically, which is an entirely different routing system.
The dark web is just a destination you can reach using Tor. It refers to websites ending in .onion that only exist inside the Tor network and cannot be reached through a normal browser at all. So Tor is the road, the dark web is one possible place that road leads to. Most people who use Tor every day are not even visiting the dark web, they're using Tor just to browse the regular internet anonymously.
Now a quick word on the bigger confusion too. The surface web is anything Google can index and show you in search results, news sites, YouTube, blogs. The deep web is everything not indexed but still on the regular internet, your email inbox, your bank dashboard, anything behind a login wall. It's "deep" because search engines can't crawl behind a login, not because it's secretive. The deep web is actually massively bigger than the surface web. The dark web is the small slice that needs special software like Tor to access at all, because it's not even running on the regular internet addressing system.
What is Tor and why does it exist
Tor stands for The Onion Router. It was originally built by the U.S. Naval Research Laboratory in the mid-1990s to protect government communications. It was later open-sourced and handed over to the nonprofit Tor Project, and today it's used by a much wider range of people than its origins suggest.
Journalists use it to talk to sources without exposing them. Whistleblowers use it to leak information without revealing who they are. People living under heavy censorship use it to access blocked websites and speak freely. Regular privacy-conscious users use it just to stop their ISP and advertisers from building a profile on them. And yes, some people use it for illegal stuff too, but that's true of literally every privacy tool ever made, including encrypted messaging apps and VPNs.
To give you a sense of the scale of what we're talking about here, the Tor network currently runs on roughly 8,000 to 9,000 active relays as of 2026, with around 1,500 to 2,000 of those being exit relays, and total aggregate bandwidth around 800 Gbit/s. On a typical day, somewhere between 2.0 to 2.5 million people connect directly to the network, and around 800,000 unique .onion addresses are visible on the network at any given time. Interestingly, most of that traffic isn't even going to .onion sites. Research shows only about 3 to 7% of Tor traffic actually stays inside the network on hidden services. The rest exits out to the regular internet. So most people are using Tor just to browse normal websites anonymously, not specifically for the dark web.
This is a network built and kept alive almost entirely by volunteers running relays out of their own bandwidth, with no single company owning or controlling it.
The core idea, onion routing
Here's the foundational concept everything else builds on. Tor doesn't just encrypt your traffic once and send it off. It wraps your data in multiple layers of encryption, like an onion, where each layer can only be peeled off by one specific node along the path.
Picture sending a letter, but you put it inside three separate locked boxes, nested inside each other. Each box has a different lock, and a different person along the delivery route holds the key to exactly one of those locks. The first person can only open the outermost box. Inside they find another locked box addressed to the next person, with no idea what's inside it. They pass it on. The next person opens their box, finds another locked box, passes that on too. Only the last person, the one closest to the actual destination, opens the final box and sees what's really inside.
That's onion routing. No single person in that chain ever sees both who sent the letter and what's actually inside it.
In Tor's implementation, when you send a request, your Tor Browser (client) encrypts your data three times, once for each node in your path, using a key it has already negotiated with each of those nodes ahead of time. The outermost layer can only be decrypted by the first node, the next layer only by the second node, and the innermost layer only by the third.
Here's that structure visually, the actual packet, the layers, and what each node sees when it peels its own layer:
What this buys you is simple but powerful. No single node along the path ever has the full picture. The first node knows who you are but doesn't know your final destination. The last node knows the destination but has no idea who you are. Nobody in between has either piece.
The 3-hop circuit, Entry, Middle, Exit
Now let's connect this to something you'd actually do. Say you open Tor Browser and type in a website. What actually happens between you hitting enter and the page loading?
Your traffic doesn't go straight to the website. It travels through exactly three relays first. This path of three relays is called a circuit.
The Entry Guard is the first relay your traffic touches. It knows your real IP address because you're connecting to it directly from your device. But it has no idea where your traffic is ultimately headed, that information is still wrapped inside the inner encryption layers it cannot read.
The Middle Relay sits in between. It knows the IP of the Entry Guard before it and the Exit Node after it, but it cannot see your real IP, and it cannot see the final destination either. It's essentially blind to both ends of the connection.
The Exit Node is the last relay. It decrypts the final layer and sends your actual request out to the destination website on the regular internet. It knows where the traffic is going and can read the content if the site is not using HTTPS, but it has no idea who originally sent it because all it sees is traffic coming from the Middle Relay.
Here's the full picture of what each node knows and cannot know:
You might already have a doubt forming here, okay the layers exist, but how exactly does the encryption between each node actually happen? How does your Tor Browser (client) even establish those keys with the Entry, Middle, and Exit nodes without anyone in between being able to intercept them? That's a really good question and we will look at it in detail a bit later, because understanding that properly needs a few more things to be in place first. Keep that question in mind and we'll come back to it.
This 3-hop design is not random. Fewer hops would mean weaker anonymity, easier for someone watching both ends to correlate sender and receiver. More hops would add unnecessary delay without meaningfully better protection. Three is Tor's deliberately chosen sweet spot.
Now think about what just happened here. You opened Tor Browser, typed a site, and in the background it built this exact 3-hop path within seconds. But how does it even know which relays exist out there? How does it decide which three to pick? That's what we're getting into next.
How your Tor Browser connects to the Tor network, Directory Authorities and the Consensus
So we just said Tor builds a 3-hop circuit before reaching any site. But how does this circuit even get built? Where does your Tor Browser (client) get the list of relays to choose from?
Think about it this way. You've just installed Tor Browser and opened it for the first time. Right now your Tor Browser (client) doesn't know a single relay exists. It has no idea who's out there. Before it can build any circuit at all, it first needs to find out what the Tor network even looks like right now.
This is where Directory Authorities come in.
Directory Authorities are a small set of trusted servers, around 9 to 10 of them, run by trusted people and organizations within the Tor Project. Their IP addresses are hardcoded directly into the Tor Browser software itself. The moment you install Tor Browser, it already knows exactly where to find these Directory Authorities. No discovery process, no searching, they're already baked in from day one.
Now what do these Directory Authorities actually contain? Every relay in the Tor network regularly checks in with the Directory Authorities and reports information about itself: its IP address, its public key, how much bandwidth it can handle, how long it has been running stably online, and other properties. The Directory Authorities collect all of this from every relay and then, roughly once every hour, they all vote together and agree on a single document called the Consensus.
Think of the Consensus document as the master map of the entire Tor network at that exact moment. It lists every known relay, its IP address, its capabilities, and special flags assigned to it based on its behavior. For example, a relay that has been consistently online and stable gets flagged as a Guard relay, meaning it's considered reliable enough to be someone's Entry node. A relay with enough bandwidth and stability gets flagged as Exit, meaning it's allowed to send traffic out to the regular internet. These flags are important because your Tor Browser (client) uses them when picking which relay to assign to which role in your circuit.
So what does your Tor Browser (client) actually do with all of this?
The moment Tor Browser starts up, right after you open it, it contacts one of those hardcoded Directory Authority IPs and downloads the latest Consensus document. Once it has that document, it now has the complete picture of every available relay on the network. It doesn't need to ask anyone in real time anymore. From that single downloaded document, it can now pick an Entry Guard, a Middle Relay, and an Exit Node, and build your 3-hop circuit entirely on its own within a few seconds.
That's the bootstrapping process, and it happens every time you open Tor Browser before you've even typed a single URL.
This is also why Directory Authorities are such a critical and sensitive piece of the whole system. If a majority of them were ever compromised or forced to publish a fake Consensus with malicious relays, it could seriously undermine the network's anonymity guarantees. That's part of why their operators are deliberately spread across different individuals and different countries rather than being centralized in one place or under one organization's control.
How a .onion address is actually built, V2 and V3
So far we've talked about Tor as a network, how circuits work, and how your Tor Browser (client) finds out which relays even exist. Now let's get into what makes .onion addresses different from normal website addresses and what's actually hidden inside them.
First, a quick word on two versions, V2 and V3. V2 is the old system, now completely deprecated and disabled. V3 is the current system and the one that matters. I'll mention V2 briefly just so you understand why V3 was needed, then we'll go deep on V3.
V2, the old way (deprecated)
In V2, a hidden service generated an RSA 1024-bit key pair. The onion address was then computed like this:
onion_address = Base32( SHA1(PublicKey) ) + ".onion"onion_address = Base32( SHA1(PublicKey) ) + ".onion"This produced a 16-character address like duskgytldkxiuqc6.onion. The problem is SHA1 is cryptographically weak, RSA 1024-bit is considered breakable with modern hardware, and 16 characters gives very little collision resistance. V2 was fully deprecated and disabled across the network by 2021.
V3, the current system
V3 fixed everything. The hidden service generates an ED25519 key pair instead. ED25519 is a modern elliptic curve algorithm that produces 32-byte (256-bit) keys and is considered significantly stronger.
The keys are stored in two files on the server running the hidden service:
/var/lib/tor/hidden_service/
├── hostname ← contains the final .onion address
├── hs_ed25519_secret_key ← private key, 32 bytes
└── hs_ed25519_public_key ← public key, 32 bytes/var/lib/tor/hidden_service/
├── hostname ← contains the final .onion address
├── hs_ed25519_secret_key ← private key, 32 bytes
└── hs_ed25519_public_key ← public key, 32 bytesNow here is the actual process of deriving the .onion address from that public key.
Step 1: compute the checksum
checksum = SHA3-256( ".onion checksum" + public_key + version_byte )checksum = SHA3-256( ".onion checksum" + public_key + version_byte )Take the first 2 bytes of this SHA3–256 output. Those 2 bytes are your checksum. The string ".onion checksum" is a fixed prefix baked into the spec, it's there to make this hash operation domain-specific so it can't be confused with any other hash in the system.
Step 2: set the version byte
For V3, the version byte is always 0x03. That single byte tells any Tor client reading this address "this is a V3 address, treat it accordingly."
Step 3: concatenate all three parts
raw = public_key (32 bytes) + checksum (2 bytes) + version_byte (1 byte)
= 35 bytes totalraw = public_key (32 bytes) + checksum (2 bytes) + version_byte (1 byte)
= 35 bytes totalStep 4: Base32 encode
35 bytes × 8 bits = 280 bits
280 bits ÷ 5 bits per Base32 character = 56 characters35 bytes × 8 bits = 280 bits
280 bits ÷ 5 bits per Base32 character = 56 charactersThat's where the 56-character V3 address comes from. Not an arbitrary length, it follows directly from the math.
onion_address = Base32( raw_35_bytes ) + ".onion"onion_address = Base32( raw_35_bytes ) + ".onion"Example of a real V3 address:
facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onionfacebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion
Here's the important thing to understand about this: the address itself is the public key. There's no central registry, no DNS lookup, no certificate authority vouching for anything. When your Tor Browser (client) reads a .onion address, it literally decodes those 56 characters back into 35 bytes, pulls out the 32-byte public key, recomputes the checksum from that key, and checks it matches. If it does, the address is valid. If it doesn't, something's wrong. The cryptography is self-contained in the address itself, that's the elegance of V3.
How a hidden service sets itself up, the full process
This is where things get genuinely interesting. Before a client can ever visit a .onion site, the hidden service itself has to do quite a bit of work to announce its existence to the network, without ever revealing its actual IP address. Let's walk through this step by step.
Think of it like this. On the normal internet, if you run a website, you register a domain with DNS and DNS tells the world your server's IP address. Anyone who types your domain gets your IP and connects to you directly. But on Tor, revealing the server's IP defeats the entire purpose. So Tor had to solve a different problem, how do you let people find you and contact you, without ever telling anyone where you actually are?
The answer is a clever combination of intro points, a descriptor, and a hash ring of HSDirs. Let's go through each one.
Step 1: Generate ED25519 keys and derive the .onion address
The hidden service generates its ED25519 key pair (as explained in the previous section) and computes its .onion address. This address is now its permanent identity on the network.
Step 2: Choose 3 intro points and build a circuit to each
The hidden service now randomly selects 3 relays from the Tor network to act as its Introduction Points. These are just regular Tor relays that have agreed to play this role.
But here's the critical detail, the hidden service doesn't connect to these intro points directly. If it did, the intro point would immediately know the hidden service's real IP address. That would break everything.
Instead, the hidden service builds a full 3-hop Tor circuit to each intro point:
Hidden Service → Entry Relay → Middle Relay → Intro PointHidden Service → Entry Relay → Middle Relay → Intro PointNow the intro point only ever sees "traffic arriving from the Middle Relay." It has no idea where the hidden service actually is. These three circuits are kept open and alive persistently, constantly waiting.
What does an intro point actually know? Just this: "Someone built a Tor circuit to me. I'll wait here. If a client ever asks to reach this service through me, I'll forward that request down the circuit." That's literally it. The intro point doesn't know the hidden service's IP, doesn't know the client's IP, and cannot see any of the actual content being communicated.
Step 3: Build the descriptor
Now the hidden service needs to let potential clients know that these intro points exist and how to use them. It does this by creating something called a descriptor.
Think of the descriptor like a contact card. It doesn't contain the server's IP, it contains the information needed to make anonymous contact. A simplified view of what's inside:
Descriptor
----------------------------
Service Public Key
Intro Point 1 (IP + keys)
Intro Point 2 (IP + keys)
Intro Point 3 (IP + keys)
Encryption Keys
Signature (signed with hidden service private key)Descriptor
----------------------------
Service Public Key
Intro Point 1 (IP + keys)
Intro Point 2 (IP + keys)
Intro Point 3 (IP + keys)
Encryption Keys
Signature (signed with hidden service private key)The signature is important. Without it, an attacker could create a fake descriptor pointing to malicious intro points, and clients would connect to the attacker instead of the real service. Since the descriptor is signed with the hidden service's private key, any client can verify it using the public key already embedded in the .onion address. Fake descriptors fail this verification immediately.
Step 4: Compute the blinded key pair (hidden service side)
This part is a little bit of math, but bear with me because it's actually very clever once you see it. Before going into this, one important note: what follows is a simplified toy example to help you understand the concept. The actual Tor v3 ED25519 blinding algorithm is more complex under the hood, the real keys and factors are huge numbers (256-bit scalars), not simple integers like 5 or 3. But the core idea is preserved exactly. If you already know about elliptic curve cryptography, you'll understand how this maps to the real implementation.
Let's say the hidden service's key pair is:
Private Key (SK) = 5
Public Key (PK) = 7Private Key (SK) = 5
Public Key (PK) = 7The server stores both. The client only ever knows the Public Key = 7 (extracted from the .onion address).
Now both sides know the current time period. Say today is:
Current Time Period = 125Current Time Period = 125This time period changes every 1440 minutes, exactly 24 hours.
Computing the blinding factor
Both the hidden service and the client independently run the Tor v3 ED25519 Blinding Algorithm using the same inputs:
Public Key = 7
+
Current Time Period = 125
+
Protocol Constants
(fixed values defined by the Tor spec, never change)
+
(Optional Client Authorization Secret, empty for normal services)
▼
Tor v3 ED25519 Blinding Algorithm
▼
Blinding Factor = 3Public Key = 7
+
Current Time Period = 125
+
Protocol Constants
(fixed values defined by the Tor spec, never change)
+
(Optional Client Authorization Secret, empty for normal services)
▼
Tor v3 ED25519 Blinding Algorithm
▼
Blinding Factor = 3Both sides get the same Blinding Factor = 3 because they used the exact same inputs. The private key is not involved here, that's why the client can compute the same blinding factor independently.
Hidden service computes its blinded key pair
Blinded Private Key = Private Key × Blinding Factor = 5 × 3 = 15
Blinded Public Key = Public Key × Blinding Factor = 7 × 3 = 21Blinded Private Key = Private Key × Blinding Factor = 5 × 3 = 15
Blinded Public Key = Public Key × Blinding Factor = 7 × 3 = 21These still form a valid matching key pair. The hidden service now uses the Blinded Private Key (15) to sign the descriptor it is about to upload.
Step 5: Client computes the same blinded public key
The client has no private key. It only has:
Public Key = 7
Current Time Period = 125Public Key = 7
Current Time Period = 125It runs the exact same blinding algorithm with the exact same inputs:
Public Key = 7
+
Current Time Period = 125
+
Protocol Constants
▼
Tor v3 ED25519 Blinding Algorithm
▼
Blinding Factor = 3
▼
Blinded Public Key = 7 × 3 = 21Public Key = 7
+
Current Time Period = 125
+
Protocol Constants
▼
Tor v3 ED25519 Blinding Algorithm
▼
Blinding Factor = 3
▼
Blinded Public Key = 7 × 3 = 21The client now has Blinded Public Key = 21. Exactly the same as what the hidden service derived. Without ever exchanging anything. Without ever knowing the private key.
When the client later downloads the descriptor from an HSDir, it verifies the signature using this Blinded Public Key (21). If the signature checks out, the client knows the descriptor was genuinely created by the real hidden service and has not been tampered with.
What each term means
Term | Example | What it actually is
------------------------------|----------------|-------------------------------------------------------------------------------------------------
Private Key (SK) | 5 | Secret identity of the hidden service. Never leaves the server. Real length: 32 bytes (256 bits)
| |
Public Key (PK) | 7 | Public identity. Encoded inside the .onion address. Real length: 32 bytes (256 bits)
| |
Current Time Period | 125 | A protocol-defined time window. Changes every 1440 minutes (24 hours exactly)
| |
Protocol Constants | Fixed values | Constants baked into the Tor spec. Same for every implementation. Ensure the algorithm behaves consistently
| |
Optional Client Auth Secret | Empty | Only used if the service enables client authorization. Empty for normal .onion services
| |
Blinding Factor | 3 | Temporary scalar derived by the algorithm. Real length: approximately 256 bits, a huge number
| |
Blinded Private Key | 15 (= 5 x 3) | Used by the hidden service to sign the descriptor. Real operation: elliptic curve scalar multiplication
| |
Blinded Public Key | 21 (= 7 x 3) | Computed independently by both sides. Used to verify descriptor signature and for placement
| |Term | Example | What it actually is
------------------------------|----------------|-------------------------------------------------------------------------------------------------
Private Key (SK) | 5 | Secret identity of the hidden service. Never leaves the server. Real length: 32 bytes (256 bits)
| |
Public Key (PK) | 7 | Public identity. Encoded inside the .onion address. Real length: 32 bytes (256 bits)
| |
Current Time Period | 125 | A protocol-defined time window. Changes every 1440 minutes (24 hours exactly)
| |
Protocol Constants | Fixed values | Constants baked into the Tor spec. Same for every implementation. Ensure the algorithm behaves consistently
| |
Optional Client Auth Secret | Empty | Only used if the service enables client authorization. Empty for normal .onion services
| |
Blinding Factor | 3 | Temporary scalar derived by the algorithm. Real length: approximately 256 bits, a huge number
| |
Blinded Private Key | 15 (= 5 x 3) | Used by the hidden service to sign the descriptor. Real operation: elliptic curve scalar multiplication
| |
Blinded Public Key | 21 (= 7 x 3) | Computed independently by both sides. Used to verify descriptor signature and for placement
| |The real Blinding Factor is not 3, it's a 256-bit scalar, a number so large it would take many lines just to write out. The multiplication is not normal arithmetic either, it's elliptic curve scalar multiplication. But the core idea is exactly as shown: both sides independently run the same deterministic algorithm on the same inputs and arrive at the same blinded keys, without ever talking to each other about it. That's the elegance of this design.
Step 6: Compute the descriptor ID (hs_service_index)
Now that both sides have the blinded public key, they need to figure out the exact position on the hash ring where the descriptor should be stored. This position is called the hs_service_index, which is what most tutorials loosely call the "descriptor ID."
Here is the actual formula from the Tor v3 specification:
hs_service_index = SHA3-256(
"store-at-idx" ||
blinded_public_key ||
replica_number ||
period_length ||
period_number
)hs_service_index = SHA3-256(
"store-at-idx" ||
blinded_public_key ||
replica_number ||
period_length ||
period_number
)What each term means:
Term | What it is | Purpose
---------------------|-------------------------------------------------|------------------------------------------------------------
SHA3-256 | Cryptographic hash function | Produces a fixed 32-byte (256-bit) hash output used as the
| | position on the HSDir hash ring.
"store-at-idx" | Fixed protocol string (Domain Separator) | Prevents this hash from being confused with hashes used
| | elsewhere in the Tor protocol.
blinded_public_key | Temporary public key for the current period | Represents the hidden service's identity for the current
| | time period.
replica_number | Either 0 or 1 | Tor creates two descriptor replicas. Each replica gets a
| | different hs_service_index and is stored on different HSDirs.
period_length | Protocol-defined descriptor period length | Specifies how long a descriptor remains valid before a
| (currently 1 day) | new one is generated.
period_number | Current descriptor time period | Ensures the hs_service_index changes every period so the
| | descriptor moves to different HSDirs over time.
|| | Concatenation operator | Joins all input bytes together before they are hashed.
hs_service_index | 32-byte SHA3-256 hash output | Deterministic value used to locate the responsible HSDirs
| | on the hash ring.Term | What it is | Purpose
---------------------|-------------------------------------------------|------------------------------------------------------------
SHA3-256 | Cryptographic hash function | Produces a fixed 32-byte (256-bit) hash output used as the
| | position on the HSDir hash ring.
"store-at-idx" | Fixed protocol string (Domain Separator) | Prevents this hash from being confused with hashes used
| | elsewhere in the Tor protocol.
blinded_public_key | Temporary public key for the current period | Represents the hidden service's identity for the current
| | time period.
replica_number | Either 0 or 1 | Tor creates two descriptor replicas. Each replica gets a
| | different hs_service_index and is stored on different HSDirs.
period_length | Protocol-defined descriptor period length | Specifies how long a descriptor remains valid before a
| (currently 1 day) | new one is generated.
period_number | Current descriptor time period | Ensures the hs_service_index changes every period so the
| | descriptor moves to different HSDirs over time.
|| | Concatenation operator | Joins all input bytes together before they are hashed.
hs_service_index | 32-byte SHA3-256 hash output | Deterministic value used to locate the responsible HSDirs
| | on the hash ring.Notice the replica_number in the formula. Tor creates two replicas of the descriptor, one with replica_number = 0 and one with replica_number = 1. Each replica gets a completely different hs_service_index, which means each one lands on a completely different set of HSDirs on the hash ring. This is for redundancy, if one set of HSDirs goes offline, the other replica is still findable.
Also worth knowing: the numeric values (replica_number, period_length, period_number) are encoded as 8-byte integers (INT_8) before being concatenated and hashed.
Both the hidden service and the client run this exact same formula with the exact same inputs and arrive at the same hs_service_index independently. That's how the client knows exactly which HSDirs to contact to fetch the descriptor, without the hidden service ever telling it.
Step 7: Find the right HSDirs using the hash ring
Now the hidden service needs to figure out which relays to actually upload its descriptor to. But first, what even is an HSDir?
HSDir stands for Hidden Service Directory. Think of HSDirs the way you'd think of DNS on the regular internet. On the normal web, DNS stores the IP address of every domain. On Tor, HSDirs store the descriptors of hidden services. Instead of "give me the IP for google.com," it's "give me the descriptor for this .onion address." They're essentially the phonebook of the Tor network's hidden service layer.
Not every relay on the Tor network is an HSDir. Only a small subset qualifies. To get the HSDir flag in the Consensus document, a relay must have been running stably and continuously online for at least 25 hours. The Directory Authorities assign this flag during the consensus voting process. Out of 8,000 to 9,000 total relays on the network, only a fraction hold this flag at any given time.
Why not store descriptors on every relay? Because HSDirs need to be trustworthy and stable. If a relay goes offline constantly, it's useless as a directory. The 25-hour uptime requirement filters out unreliable relays.
Now, with that clear, how does the hidden service know which specific HSDirs to upload to?
All HSDirs are sorted by their identity key fingerprint and arranged in a conceptual circle called a hash ring. The hs_service_index we computed in Step 6 is placed on this same ring as a position. The hidden service then walks clockwise from that position and picks the first 4 HSDirs it encounters. Those are the ones that will store the descriptor for that replica.
Remember from Step 6, Tor computes two different hs_service_index values, one for replica 0 and one for replica 1. Each one lands at a completely different position on the ring and picks a completely different set of 4 HSDirs. Let's make this concrete:
Replica 0: hs_service_index = 10
Replica 1: hs_service_index = 20
HSDirs on the ring (sorted by fingerprint):
2, 5, 8, 11, 12, 13, 14, 17, 19, 21, 22, 23, 24, 27, 30Replica 0: hs_service_index = 10
Replica 1: hs_service_index = 20
HSDirs on the ring (sorted by fingerprint):
2, 5, 8, 11, 12, 13, 14, 17, 19, 21, 22, 23, 24, 27, 30Replica 0, position 10, walk clockwise, pick first 4 HSDirs:
hs_service_index = 10
▼
HSDir 11 ✓
HSDir 12 ✓
HSDir 13 ✓
HSDir 14 ✓hs_service_index = 10
▼
HSDir 11 ✓
HSDir 12 ✓
HSDir 13 ✓
HSDir 14 ✓Replica 1, position 20, walk clockwise, pick first 4 HSDirs:
hs_service_index = 20
▼
HSDir 21 ✓
HSDir 22 ✓
HSDir 23 ✓
HSDir 24 ✓hs_service_index = 20
▼
HSDir 21 ✓
HSDir 22 ✓
HSDir 23 ✓
HSDir 24 ✓Important: 10 and 20 are just the computed index positions on the ring. They are not HSDirs themselves. The HSDirs are the actual relays whose fingerprints fall clockwise after those positions. The descriptor is uploaded to HSDir 11, 12, 13, 14 and HSDir 21, 22, 23, 24, 8 HSDirs total across both replicas (4 per replica, called hsdir_spread_store, default value 4).
Also worth being clear about, the numbers in this example (10, 20, 11, 12, 13…) are just a teaching simplification. In the real Tor network, none of these are small integers. The hs_service_index values are 32-byte SHA3–256 hashes, huge 256-bit numbers that look something like a3f8c2d1... in hex. The HSDir positions on the ring are also derived from their ED25519 identity key fingerprints, which are similarly large hash values. The ring contains hundreds of these large hash values sorted in order, and the clockwise walk happens across those real cryptographic values. The toy example with 10, 20, 11, 12 just makes the concept easy to follow, the actual mechanics are identical, just with much larger numbers.
When a client later wants to fetch the descriptor, it runs the exact same formula, arrives at the same hs_service_index values, walks the same hash ring, and picks randomly from among the first 3 HSDirs after the index (called hsdir_spread_fetch, default value 3). It only needs one honest HSDir to successfully get the descriptor. Both sides always arrive at the same HSDirs independently, no communication needed.
Why does the descriptor rotate every 24 hours? Because if your descriptor always landed on the same HSDirs, an attacker could just sit and watch those specific HSDirs forever, learning a lot about who is visiting your service and when. By rotating the blinded key every time period, the hs_service_index changes and the descriptor lands on a completely different set of HSDirs each day. Attackers cannot camp on one spot.
The hidden service is now live
At this point the hidden service has:
- Built 3 persistent circuits to 3 intro points
- Created and signed a descriptor listing those intro points
- Uploaded that descriptor to the correct HSDirs
It sits and waits. Now any client that has the .onion address can find it.
How a client actually connects to a .onion site: the full flow
Alright, this is the section everything has been building toward. We've covered what Tor is, how circuits work, how Tor Browser connects to the network, how .onion addresses are derived, and how a hidden service sets itself up and announces itself to the network. Now let's put it all together and walk through exactly what happens in the background when you type a .onion address into your Tor Browser and hit enter.
Every step below is something real happening while you're just sitting there waiting for that page to load.
One more thing before we start throughout this section you'll see mentions of different key types. This trips a lot of people up because the hidden service actually uses multiple different key pairs, each doing a completely different job. Here's a quick reference:
Key | Type | Purpose
--------------------------------|--------------------------|------------------------------------------------------------
ED25519 Identity Key Pair | Signing key | Permanent identity of the hidden service. Used to sign the
| | descriptor and verify its authenticity. Never used for
| | encryption or key exchange.
Blinded ED25519 Key Pair | Temporary signing key | Derived from the ED25519 identity key and the current
| | time period. Used only for descriptor signing and
| | descriptor placement during that time period.
Curve25519 ntor Onion Key Pair | Key agreement key | Long-term key pair used during the ntor handshake to
| | derive a shared secret. It does not encrypt data
| | directly. Encryption uses symmetric session keys
| | derived from this shared secret.
Ephemeral Curve25519 Keys | Temporary key agreement | Fresh key pair generated for each ntor handshake.
| | Provides Forward Secrecy by ensuring every connection
| | derives a unique shared secret.
Symmetric Session Keys | Encryption keys | Derived from the shared secret using HKDF. Used to
| | encrypt, decrypt, and authenticate the actual
| | application data exchanged between the client and
| | the hidden service.Key | Type | Purpose
--------------------------------|--------------------------|------------------------------------------------------------
ED25519 Identity Key Pair | Signing key | Permanent identity of the hidden service. Used to sign the
| | descriptor and verify its authenticity. Never used for
| | encryption or key exchange.
Blinded ED25519 Key Pair | Temporary signing key | Derived from the ED25519 identity key and the current
| | time period. Used only for descriptor signing and
| | descriptor placement during that time period.
Curve25519 ntor Onion Key Pair | Key agreement key | Long-term key pair used during the ntor handshake to
| | derive a shared secret. It does not encrypt data
| | directly. Encryption uses symmetric session keys
| | derived from this shared secret.
Ephemeral Curve25519 Keys | Temporary key agreement | Fresh key pair generated for each ntor handshake.
| | Provides Forward Secrecy by ensuring every connection
| | derives a unique shared secret.
Symmetric Session Keys | Encryption keys | Derived from the shared secret using HKDF. Used to
| | encrypt, decrypt, and authenticate the actual
| | application data exchanged between the client and
| | the hidden service.Keep this in mind as we go. Now let's walk through the steps.
Step 1: Tor Browser decodes the .onion address and extracts the public key
You type a .onion address. The first thing your Tor Browser (client) does is decode those 56 characters. Remember from the previous part a V3 .onion address is not just a random string. It is Base32 encoded data that contains the hidden service's ED25519 identity public key (32 bytes), a 2-byte checksum, and a 1-byte version number.
Your Tor Browser (client) decodes the address, pulls out the 32-byte ED25519 identity public key, and verifies the checksum. If it doesn't match, the address is either typed wrong or corrupted and the whole thing stops right here.
If the checksum passes, the client now has the hidden service's identity public key. This is the starting point for everything that follows.
Step 2: Client calculates where the descriptor is stored completely on its own
Now the client needs to find the descriptor of this hidden service. The descriptor, if you remember from the previous part, is the "contact card" the hidden service uploaded to the network it contains the intro point information the client needs to actually reach the service.
But here's the interesting thing the client doesn't ask anyone "hey where is the descriptor for this .onion?" It figures it out completely independently using the same math the hidden service used when it uploaded the descriptor.
The client takes the ED25519 identity public key it just extracted and runs the same Tor v3 ED25519 blinding algorithm the hidden service ran. It uses the same inputs the public key and the current time period (which both sides know from their own clocks). The output is the blinded public key, a temporary 32-byte key that changes every 24 hours.
From the blinded public key, the client then computes the hs_service_index the actual position on the HSDir hash ring where the descriptor was stored. The formula:
hs_service_index = SHA3-256(
"store-at-idx" ||
blinded_public_key ||
replica_number ||
period_length ||
period_number
)hs_service_index = SHA3-256(
"store-at-idx" ||
blinded_public_key ||
replica_number ||
period_length ||
period_number
)It runs this twice once with replica_number = 0 and once with replica_number = 1 giving two different positions on the ring. This is the same formula the hidden service ran when it uploaded the descriptor. So both sides always land on the same HSDirs without ever communicating about it. That's the elegance of this design.
Now the client knows exactly which HSDirs are holding the descriptor right now.
Step 3: Client builds a circuit to an HSDir and downloads the descriptor
The client doesn't contact an HSDir directly. If it did, the HSDir would know its real IP address. So instead it builds a 3-hop circuit first:
Client → Entry Guard → Middle Relay → HSDirClient → Entry Guard → Middle Relay → HSDirThrough this circuit, the client asks the HSDir for the descriptor matching the hs_service_index it computed. The HSDir finds it and sends it back through the same circuit.
Once the descriptor arrives, the client verifies its signature using the blinded ED25519 public key. Remember the hidden service signed the descriptor with its blinded private key, and the client verifies it with the matching blinded public key. If the signature is valid, the client knows this descriptor genuinely came from the real hidden service and nobody tampered with it. If it fails, the client discards it and tries the other replica which is exactly why Tor creates two replicas. One can fail or be malicious and the other still works.
Inside the verified descriptor the client now finds:
- The list of intro points these are relay identities pointing to the intro points the hidden service is currently listening on. The actual IP addresses of these relays come from the Consensus document the client already has
- The hidden service's Curve25519 ntor onion key this is a completely different key from the ED25519 identity key. This one is used for the actual handshake coming up. ED25519 cannot encrypt anything, remember
Step 4: Client picks a Rendezvous Point and builds a circuit to it
Before reaching out to any intro point, the client first needs to set up a meeting place. This is the Rendezvous Point (RP).
Think of it like this when two people want to meet without revealing where they each live, they pick a neutral public place and both travel there independently. Nobody knows where the other person started from. The Rendezvous Point is exactly that neutral meeting place in Tor's architecture.
The client picks a random relay from the Consensus to be the RP. It then builds a full 3-hop circuit to it:
Client → Entry Guard → Middle Relay → Rendezvous PointClient → Entry Guard → Middle Relay → Rendezvous PointThe client also creates a random one-time cookie just a short random value. It sends this cookie to the RP along with a message saying "wait here, someone will come from the other side soon and will prove they're the right party by presenting this same cookie."
The RP doesn't know who the client is. It doesn't know who will be connecting from the other side. It just waits.
Step 5: Client builds a circuit to an intro point and sends the INTRODUCE1 cell
Now the client picks one of the intro points listed in the descriptor and builds another circuit to it:
Client → Entry Guard → Middle Relay → Intro PointClient → Entry Guard → Middle Relay → Intro PointThrough this circuit, the client sends a special message called an INTRODUCE1 cell. To construct this cell, the client uses the hidden service's Curve25519 ntor onion key from the descriptor to perform an ntor key agreement not the ED25519 key, which only does signing. The client generates a fresh ephemeral Curve25519 key pair, performs a Diffie-Hellman operation with the hidden service's ntor onion public key, and runs the result through HKDF to derive symmetric session keys. Those session keys then encrypt the sensitive rendezvous information inside the cell. Only the hidden service can complete the same key agreement using its ntor onion private key and arrive at the same symmetric keys so only the hidden service can decrypt what's inside.
What's packed inside the INTRODUCE1 cell:
INTRODUCE1 cell structure:
┌─────────────────────────────────────────────┐
│ Client Ephemeral Curve25519 Public Key │ ← sent in plaintext so HS can do key agreement
├─────────────────────────────────────────────┤
│ Encrypted with symmetric keys from HKDF: │
│ - Identity of the chosen Rendezvous Point │
│ - The one-time cookie │
│ - Protocol metadata │
└─────────────────────────────────────────────┘INTRODUCE1 cell structure:
┌─────────────────────────────────────────────┐
│ Client Ephemeral Curve25519 Public Key │ ← sent in plaintext so HS can do key agreement
├─────────────────────────────────────────────┤
│ Encrypted with symmetric keys from HKDF: │
│ - Identity of the chosen Rendezvous Point │
│ - The one-time cookie │
│ - Protocol metadata │
└─────────────────────────────────────────────┘The client's ephemeral public key must stay in plaintext the hidden service needs it to independently compute the same shared secret and derive the same symmetric keys to decrypt the rest. The intro point receives this cell but cannot derive the shared secret (it doesn't hold the ntor onion private key) so it cannot read any of the encrypted portion. It just blindly forwards the entire cell down the pre-built persistent circuit the hidden service established when it published its descriptor. The intro point is just a courier delivering a locked box it has no key to open.
We will look at exactly how the INTRODUCE1 message is constructed and how the secure key exchange works step by step in the next section.
Step 6: Hidden service receives and decrypts the INTRODUCE1 cell
The INTRODUCE1 cell travels through the intro point and arrives at the hidden service via its pre-built circuit. The hidden service reads the client's ephemeral Curve25519 public key (which was sent in plaintext) and uses its own ntor onion private key to complete the same Diffie-Hellman key agreement the client performed. It runs the result through HKDF and arrives at the exact same symmetric session keys. Using those keys, it decrypts the protected portion and recovers:
- The identity of the Rendezvous Point the client chose
- The one-time cookie
- Protocol metadata
Now the hidden service knows exactly where to go to meet the client.
Step 7: Hidden service builds its own circuit to the Rendezvous Point
The hidden service builds its own fresh 3-hop circuit to the same RP:
Hidden Service → Entry Guard → Middle Relay → Rendezvous PointHidden Service → Entry Guard → Middle Relay → Rendezvous PointIt sends a REND1 cell to the RP containing the one-time cookie. The RP compares this cookie with the one the client sent earlier. They match so the RP now knows both circuits belong to the same session and joins them together.
Step 8: Rendezvous Point joins both circuits, connection goes live
The RP now has two separate circuits connected to it:
Client → Entry → Middle → RP
RP → Middle → Entry → Hidden ServiceClient → Entry → Middle → RP
RP → Middle → Entry → Hidden ServiceIt maps these two circuits together using internal circuit IDs and starts relaying encrypted cells from one to the other. The total path is now:
Client → Entry → Middle → RP → Middle → Entry → Hidden ServiceClient → Entry → Middle → RP → Middle → Entry → Hidden Service6 hops total. 3 hops on the client side, 3 hops on the hidden service side, the RP sitting in the middle. Neither side ever learns the other's real IP address.
Step 9: End-to-end encryption between client and hidden service
Now, the last piece. There are actually three separate layers of cryptography all working at the same time during this connection:
Layer 1 — TLS (between every neighboring pair of nodes)
Client ←TLS→ Entry Guard ←TLS→ Middle Relay ←TLS→ Rendezvous Point
↑ ↓
│ │
Hidden Service ←TLS→ Entry Guard ←TLS→ Middle Relay ←TLS──────┘
Every direct TCP connection between two neighboring Tor nodes is
protected by TLS, just like the TLS layer used by HTTPS.
Purpose:
• Protects traffic while it is travelling between two adjacent nodes.
• Prevents eavesdropping or modification of packets on the network.
• Every pair of neighboring nodes has its own independent TLS connection.
Layer 2 — ntor Circuit Encryption (between a Tor endpoint and each relay)
Client-side circuit:
Client ↔ Entry Guard → Shared Session Key A
Client ↔ Middle Relay → Shared Session Key B
Client ↔ Rendezvous Point → Shared Session Key C
Hidden service-side circuit:
Hidden Service ↔ Entry Guard → Shared Session Key D
Hidden Service ↔ Middle Relay → Shared Session Key E
Hidden Service ↔ Rendezvous Point → Shared Session Key F
Each shared session key is established using a separate ntor
(Curve25519 Diffie-Hellman) handshake.
Purpose:
• Creates onion-encryption layers for each hop.
• Every relay only knows the session key shared with itself.
• No relay knows the keys of any other relay.
• These keys are completely independent of one another.
Layer 3 — Onion Service ntor (End-to-End Encryption)
The client already knows the hidden service's long-term
Curve25519 ntor Onion Public Key from the descriptor.
The client generates a fresh ephemeral Curve25519 key pair and
includes its ephemeral public key inside the INTRODUCE1 message.
Client has:
Client Ephemeral Private Key
Client Ephemeral Public Key
Hidden Service has:
Long-term Curve25519 ntor Onion Private Key
Long-term Curve25519 ntor Onion Public Key
Both independently perform the ntor (ECDH) key agreement.
Client computes:
Shared Secret =
Client Ephemeral Private Key
×
Hidden Service ntor Onion Public Key
Hidden Service computes:
Shared Secret =
Hidden Service ntor Onion Private Key
×
Client Ephemeral Public Key
Because of the Diffie-Hellman property, both sides arrive at
exactly the same shared secret without ever transmitting it.
The shared secret is then passed through HKDF:
Shared Secret
│
▼
HKDF
│
▼
Symmetric Session Keys
+
Integrity Keys
Purpose:
• Establishes a secure end-to-end encrypted channel between the
client and the hidden service.
• These keys are independent of all relay keys created in Layer 2.
• No relay—including the Rendezvous Point—can derive or access
these session keys.
• Application data is first protected using these end-to-end
session keys, and the resulting encrypted data is then carried
inside Tor relay cells, which are themselves protected hop-by-hop
using the Layer 2 ntor circuit encryption.Layer 1 — TLS (between every neighboring pair of nodes)
Client ←TLS→ Entry Guard ←TLS→ Middle Relay ←TLS→ Rendezvous Point
↑ ↓
│ │
Hidden Service ←TLS→ Entry Guard ←TLS→ Middle Relay ←TLS──────┘
Every direct TCP connection between two neighboring Tor nodes is
protected by TLS, just like the TLS layer used by HTTPS.
Purpose:
• Protects traffic while it is travelling between two adjacent nodes.
• Prevents eavesdropping or modification of packets on the network.
• Every pair of neighboring nodes has its own independent TLS connection.
Layer 2 — ntor Circuit Encryption (between a Tor endpoint and each relay)
Client-side circuit:
Client ↔ Entry Guard → Shared Session Key A
Client ↔ Middle Relay → Shared Session Key B
Client ↔ Rendezvous Point → Shared Session Key C
Hidden service-side circuit:
Hidden Service ↔ Entry Guard → Shared Session Key D
Hidden Service ↔ Middle Relay → Shared Session Key E
Hidden Service ↔ Rendezvous Point → Shared Session Key F
Each shared session key is established using a separate ntor
(Curve25519 Diffie-Hellman) handshake.
Purpose:
• Creates onion-encryption layers for each hop.
• Every relay only knows the session key shared with itself.
• No relay knows the keys of any other relay.
• These keys are completely independent of one another.
Layer 3 — Onion Service ntor (End-to-End Encryption)
The client already knows the hidden service's long-term
Curve25519 ntor Onion Public Key from the descriptor.
The client generates a fresh ephemeral Curve25519 key pair and
includes its ephemeral public key inside the INTRODUCE1 message.
Client has:
Client Ephemeral Private Key
Client Ephemeral Public Key
Hidden Service has:
Long-term Curve25519 ntor Onion Private Key
Long-term Curve25519 ntor Onion Public Key
Both independently perform the ntor (ECDH) key agreement.
Client computes:
Shared Secret =
Client Ephemeral Private Key
×
Hidden Service ntor Onion Public Key
Hidden Service computes:
Shared Secret =
Hidden Service ntor Onion Private Key
×
Client Ephemeral Public Key
Because of the Diffie-Hellman property, both sides arrive at
exactly the same shared secret without ever transmitting it.
The shared secret is then passed through HKDF:
Shared Secret
│
▼
HKDF
│
▼
Symmetric Session Keys
+
Integrity Keys
Purpose:
• Establishes a secure end-to-end encrypted channel between the
client and the hidden service.
• These keys are independent of all relay keys created in Layer 2.
• No relay—including the Rendezvous Point—can derive or access
these session keys.
• Application data is first protected using these end-to-end
session keys, and the resulting encrypted data is then carried
inside Tor relay cells, which are themselves protected hop-by-hop
using the Layer 2 ntor circuit encryption.
How the INTRODUCE1 message is actually constructed the first secure communication
In Step 5 above we said the client sends an INTRODUCE1 cell and that only the hidden service can read it. But we didn't go into how that actually works. Let's fix that now because this is genuinely interesting and most explanations skip over it completely.
The challenge here is that the client and the hidden service have never communicated before. They share no secret, no session key, nothing. So how does the client send a message that only the hidden service can read?
The answer is the ntor key agreement. Here's the full process step by step:
Step 1: Client already has the ntor onion public key from the descriptor
From the descriptor it downloaded, the client has the hidden service's Curve25519 ntor onion public key. The hidden service keeps the matching private key secret. These are completely separate from the ED25519 identity keys their only job is key agreement.
Step 2: Client generates a fresh ephemeral Curve25519 key pair
For this introduction only, the client generates a brand new temporary Curve25519 key pair:
Client Ephemeral Private Key (discarded after session)
Client Ephemeral Public Key (sent inside the INTRODUCE1 cell)Client Ephemeral Private Key (discarded after session)
Client Ephemeral Public Key (sent inside the INTRODUCE1 cell)Fresh keys every time. This is what gives forward secrecy.
Step 3: Client computes the shared secret
The client performs a Diffie-Hellman operation:
Shared Secret = Client Ephemeral Private Key × HS ntor Onion Public KeyShared Secret = Client Ephemeral Private Key × HS ntor Onion Public KeyThis shared secret exists only on the client's side right now.
Step 4: HKDF derives symmetric session keys from the shared secret
The shared secret is never used directly. It goes through HKDF:
Shared Secret
▼
HKDF
▼
Symmetric Encryption Key
Integrity Key
Other session keysShared Secret
▼
HKDF
▼
Symmetric Encryption Key
Integrity Key
Other session keysNow the client has symmetric keys to protect the introduction data.
Step 5: Client encrypts the rendezvous information with those symmetric keys
Using the derived symmetric encryption key, the client encrypts:
- Identity of the chosen Rendezvous Point
- The one-time cookie
- Protocol metadata- Identity of the chosen Rendezvous Point
- The one-time cookie
- Protocol metadataNotice: the Client Ephemeral Public Key itself is NOT encrypted. It stays in plaintext because the hidden service needs to see it to compute the same shared secret.
Step 6: The INTRODUCE1 message is assembled
INTRODUCE1 message:
┌──────────────────────────────────────────────────-┐
│ Client Ephemeral Curve25519 Public Key (plaintext)│
├──────────────────────────────────────────────────-┤
│ Encrypted with symmetric keys from HKDF: │
│ - Rendezvous Point Identity │
│ - Rendezvous Cookie │
│ - Protocol Metadata │
└──────────────────────────────────────────────────-┘INTRODUCE1 message:
┌──────────────────────────────────────────────────-┐
│ Client Ephemeral Curve25519 Public Key (plaintext)│
├──────────────────────────────────────────────────-┤
│ Encrypted with symmetric keys from HKDF: │
│ - Rendezvous Point Identity │
│ - Rendezvous Cookie │
│ - Protocol Metadata │
└──────────────────────────────────────────────────-┘The intro point forwards this sealed message to the hidden service and cannot read any of the encrypted portion.
Step 7: Hidden service independently derives the same shared secret
The hidden service receives the INTRODUCE1 message and reads the Client Ephemeral Public Key from the plaintext portion. It then does:
Shared Secret = HS ntor Onion Private Key × Client Ephemeral Public KeyShared Secret = HS ntor Onion Private Key × Client Ephemeral Public KeyBecause of how Diffie-Hellman works:
Client Private × HS Public = HS Private × Client PublicClient Private × HS Public = HS Private × Client PublicBoth sides independently arrive at the exact same shared secret without ever transmitting it. The hidden service then runs the same HKDF computation and gets the same symmetric session keys. It uses those keys to decrypt the rendezvous information and learns exactly where the client is waiting.
This is why the intro point cannot read the message even though it physically handles it. it doesn't hold the ntor onion private key, so it can never derive the shared secret or the symmetric keys.
How the circuit is actually built ntor handshake and forward secrecy
Earlier in this blog, right after the 3-hop circuit section, we said there was a question to come back to how does the Tor Browser (client) actually establish those encryption keys with each node? Now that you've seen the full connection flow, this will make complete sense.
Why Tor doesn't use static public keys for encryption
Suppose Tor encrypted all circuit traffic directly with a Guard node's long-term public key. Simple enough. But here's the problem — what if someone records all that encrypted traffic today and then years later steals the Guard node's private key? They could go back and decrypt every conversation you ever had through that Guard. This is called no forward secrecy and it's a serious problem.
Tor solves this with ephemeral key exchange. Instead of using any node's long-term key for encryption, your Tor Browser (client) and each node perform a Diffie-Hellman exchange using fresh temporary keys to arrive at a shared secret, and then use that shared secret to derive symmetric session keys (via HKDF). When the circuit is torn down, those temporary keys are deleted forever. Even if the node's long-term private key is stolen later, there's nothing to decrypt.
Tor calls this the ntor handshake.
How the circuit is built hop by hop
Hop 1: Client to Guard:
The client generates a fresh ephemeral Curve25519 key pair just for this session. It sends its ephemeral public key to the Guard. The Guard uses its ntor key material to complete the handshake. Both arrive at the same shared secret:
Shared Secret A from ntor handshake between client and Guard
Shared Secret A → HKDF → Key A (symmetric key for this hop)Shared Secret A from ntor handshake between client and Guard
Shared Secret A → HKDF → Key A (symmetric key for this hop)If the Guard's long-term private key is stolen later, Key A cannot be derived from it. Forward secrecy achieved.
Hop 2: Extending to Middle:
The client cannot contact Middle directly that would reveal its real IP to Middle. So it sends an EXTEND cell to the Guard, encrypted with Key A, saying "connect to this Middle relay and forward this handshake data." Guard does so and the client performs an ntor handshake with Middle through the Guard:
Shared Secret B from ntor handshake between client and Middle
Shared Secret B → HKDF → Key BShared Secret B from ntor handshake between client and Middle
Shared Secret B → HKDF → Key BGuard does not know Key B.
Hop 3: Extending to Exit:
Same process, through both Guard and Middle:
Shared Secret C from ntor handshake between client and Exit
Shared Secret C → HKDF → Key CShared Secret C from ntor handshake between client and Exit
Shared Secret C → HKDF → Key CNeither Guard nor Middle knows Key C.
Now the client has three keys Key A for Guard, Key B for Middle, Key C for Exit. When sending data, it encrypts with Key C first (innermost), then Key B, then Key A (outermost). Each relay peels exactly one layer. Nobody except the client holds all three keys simultaneously. When the circuit closes, all ephemeral keys are gone permanently.
How the Rendezvous Point actually works. the two-socket model
A lot of people assume the 6-hop path is one big continuous circuit passing through 6 relays in a line. That's a common misconception. It's actually two completely separate circuits that happen to meet at the same relay.
Think of the Rendezvous Point like a relay station with two separate sockets:
Socket A - connected to the client's 3-hop circuit
Socket B - connected to the hidden service's 3-hop circuitSocket A - connected to the client's 3-hop circuit
Socket B - connected to the hidden service's 3-hop circuitThese two circuits end at the RP. They don't merge. What the RP does is very simple it copies encrypted cells from one socket to the other.
The RP tracks the mapping using circuit IDs. When the client built its circuit to the RP, the RP assigned it circuit ID 0x111. When the hidden service connected and sent the REND1 cell with the matching cookie, the RP assigned it 0x222. The RP was then instructed:
Circuit 0x111 ↔ Circuit 0x222Circuit 0x111 ↔ Circuit 0x222Every encrypted cell arriving on 0x111 gets copied onto 0x222 and forwarded to the hidden service side, and vice versa. No IP lookup. No decryption. Just:
Encrypted cell arrives on 0x111
▼
RP looks up: 0x111 maps to 0x222
▼
Copies cell onto 0x222
▼
Cell travels onward to hidden serviceEncrypted cell arrives on 0x111
▼
RP looks up: 0x111 maps to 0x222
▼
Copies cell onto 0x222
▼
Cell travels onward to hidden serviceThe RP can't see the conversation content because the onion service session keys from Layer 3 are keys the RP has never seen and will never know. It's just forwarding sealed envelopes.
Known attack surfaces
Tor is not perfect. It provides very strong anonymity in most threat models, but there are well-documented attack scenarios that researchers have identified. If you're reading this from a security perspective, these are important to know.
Traffic correlation attacks
This is the most fundamental and hardest to defend against. If an adversary can observe both the Entry Guard on your side and the Exit Node (or hidden service) on the other end, they can watch the timing and volume of packets going in and compare them to packets coming out. Even without breaking any encryption, this correlation can link you to your destination.
Tor's design assumes no single entity can see both ends simultaneously. Against a global passive adversary who can monitor large portions of internet traffic, this assumption breaks down. This is called the global adversary problem and it's considered Tor's most fundamental limitation.
Guard discovery attacks
Your Entry Guard knows your real IP. If an attacker figures out which relay is your Guard, they've learned something significant. Researchers have shown that by running enough malicious relays and watching circuit timing, attackers can probabilistically identify a user's Guard. The Vanguards addon was created specifically to defend against this.
Malicious HSDir enumeration
Any relay with the HSDir flag can store descriptors. Descriptors are signed but not encrypted an HSDir operator can read them. However, descriptors don't reveal the hidden service's real IP. What a malicious HSDir operator can learn is metadata which services upload descriptors to them, when they come online and go offline, how often they rotate. Over time this is useful for mapping the hidden service ecosystem. This is called HSDir enumeration.
Exit node MITM on clearnet traffic
When you use Tor to browse a regular website, your traffic exits through an Exit node in plaintext if the site doesn't use HTTPS. The Exit node operator can read and modify that traffic. This is why HTTPS matters even on Tor, and why .onion sites are actually safer in this specific way there is no Exit node for .onion traffic, so this attack doesn't apply.
Closing
If you've made it this far, honestly, respect 🫡. This is not light reading.
You now know more about how Tor works internally than the vast majority of people who use it every day. You understand why .onion addresses are exactly 56 characters, what the blinded public key is and why it rotates every 24 hours, how the client and hidden service independently land on the same HSDirs without ever talking to each other, why the Rendezvous Point can't see your traffic even though it sits in the middle of the path, and what three simultaneous layers of cryptography are doing while you wait for a .onion page to load.
I'll be honest this topic is genuinely difficult to explain in a single blog post. There's so much happening at every layer that covering all of it simply in one place is hard. I tried my best to keep it accurate without making it impossible to read. If you found some parts difficult, that's normal. Try reading it again the second read almost always hits differently once the overall picture is in your head.
And if anything here is technically wrong, off, or imprecise drop it in the comments. I genuinely mean that. I'd rather fix it than leave something incorrect here. This is a complex topic and there's always something more to learn.
One more thing the next blog I'm writing is about the safest way to actually access Tor, covering how to bypass ISP blocking and set up the most secure configuration possible. If that sounds interesting, stay tuned.
Happy reading.