Understanding Tor circuit construction: the part that actually matters
I got confused about Tor's circuit construction for a specific reason, and solving that confusion actually reveals what makes Tor work.
The Confusion
The Wikipedia article says: "Using asymmetric key cryptography, the originator obtains a public key from the directory node to send an encrypted message to the first ('entry') node, establishing a connection and a shared secret ('session key'). Using the established encrypted link to the entry node, the originator can then relay a message through the first node to a second node in the chain using encryption that only the second node, and not the first, can decrypt."
I couldn't figure out: if the first node receives a message encrypted for the second node (that the first node can't decrypt), how does it know WHERE to send it? The address would have to be either unencrypted (breaking anonymity) or encrypted to the first node (but then the first node learns the destination).
The Answer
Tor doesn't work with simple nested encryption. It works with circuit extension through incremental handshakes.
When Alice wants to build a circuit A → R1 → R2 → R3 → S:
1. Alice to R1: Send CREATE_FAST(challenge). R1 responds with CREATED_FAST(response). They establish shared secret K1.
2. Alice to R1 (then R1 to R2): Alice encrypts with K1: EXTEND(R2_address, DH_pubkey). R1 decrypts, sees "extend to R2", initiates handshake with R2. R1 and R2 establish K2. R1 sends back to Alice the CREATED response (still encrypted with K1). Alice can decrypt this and extract the info needed to compute K2.
3. Circuit now exists: A ←K1→ R1 ←K2→ R2. Alice now has K1 and K2.
4. To extend further: Alice wants to tell R2 to extend to R3. She encrypts: Enc_K1(Enc_K2(EXTEND, [R3_address])). This packet goes to R1. R1 decrypts with K1, gets Enc_K2(EXTEND, ...). R1 doesn't understand this (it's encrypted for R2), so it just forwards to R2. R2 decrypts with K2, sees EXTEND, does the handshake with R3.
Why This Solves The Problem
- R1 never learns R2 or R3's address. When R1 receives "extend to R2," it just sees that instruction and acts. It doesn't know if this is a single-hop circuit or if there are more hops beyond R2.
- R2 never learns Alice's identity or R1's role. R2 receives a CREATE request, but it comes through R1. R2 doesn't know if R1 is the originating client or just another relay.
- R3 never learns the origin. Same logic.
- Only Alice knows the full path. Because Alice constructed each hop and has all the shared secrets.
The Actual Hard Part
The hard part isn't the encryption logic (that's actually straightforward once you understand it). The hard part is implementing the EXTEND command correctly so that intermediate relays can pass along encrypted instructions they themselves cannot decrypt.
In practice, Tor uses a structure like: `` [CircuitID][CellType][Encrypted_Payload][Padding] ``
When you EXTEND, the payload includes:
- Address of next node
- DH public key for next node's handshake
- All wrapped in encryption that only the next relay can decrypt
When R1 receives this on an existing circuit, it: 1. Recognizes the circuit ID 2. Decrypts its layer (K1) to understand the command (EXTEND) 3. Takes the encrypted next-layer (which it cannot decrypt) and forwards it to R2 in a new CREATE message 4. Receives the CREATED response 5. Re-encrypts the response and sends back to Alice through the existing circuit
Why This Matters
This is a solution to a subtle problem: how do you build multi-hop circuits through untrusted relays, where each relay learns only what it needs to, without ever revealing the path to any single node?
The answer: not through a single nested encryption (which would require knowing all relay addresses up front when you encrypt for the final relay), but through incremental handshakes where each relay only extends one hop and passes along encrypted instructions it doesn't understand.
This means Alice can be building the circuit in real time, adding relays one at a time, and no relay ever learns more than its immediate neighbors. Plus, because each relay operates a circuit identified by a circuitID (which is different on each side of the relay — R1 knows Alice's circuit as ID=123 but calls it ID=456 when talking to R2), no relay can trace how traffic flows through the network overall.