Up to at least October 2013 the hidden services work like this:
A hidden service calculates its key pair (private and public key, asymmetric encryption).
Then the hidden service picks some relays as its introduction points.
It tells its public key to those introduction points over Tor circuits.
After that the hidden-service creates a hidden service descriptor, containing its public key and what its introduction points are.
The hidden service signs the hidden service descriptor with its private key.
It then uploads the hidden service descriptor to a distributed hash table (DHT).
Clients learn the .onion address from a hidden service out-of-band. (e.g. public website) (A $hash.onion is a 16 character name derived from the service's public key.)
After retrieving the .onion address the client connects to the DHT and asks for that $hash.
If it exists the client learns about the hidden service's public key and its introduction points.
The client picks a relay at random to build a circuit to it, to tell it a one-time secret. The picked relay acts as rendezvous point.
The client creates a introduce message, containing the address of the rendezvous point and the one-time secret, before encrypting the message with the hidden service's public key.
The client sends its message over a Tor circuit to one of the introduction points, demanding it to be forwarded to the hidden service.
The hidden service decrypts the introduce message with its private key to learn about the rendezvous point and the one-time secret.
The hidden service creates a rendezvous message, containing the one-time secret and sends it over a circuit to the rendezvous point.
The rendezvous point tells the client that a connection was established.
Client and hidden service talk to each other over this rendezvous point. All traffic is end-to-end encrypted and the rendezvous point just relays it back and forth. Note that each of them, client and hidden service, build a circuit to the rendezvous point; at three hops per circuit this makes six hops in total.