Hidden Services over Mix
Abstract / Summary
Mixnets already provide sender anonymity, and Sphinx-style mixnets support Single-Use Reply Blocks (SURBs), which can act as anonymous return addresses. If one party can send anonymously and the other can reply through a SURB, then building something similar to Tor onion services on top of Mix might be possible.
This research post will mainly revolve around the question:
Can hidden services be built over a mixnet, and if so, how? And what anonymity guarantees can they realistically provide?
Our motivation comes from provider anonymity in Logos Storage. Anonymous downloads can be supported with the current Mix protocol because the downloader can hide behind the mixnet. Provider anonymity is harder. A storage provider needs to be reachable, needs to advertise that it is serving some content, and needs to respond to clients, of which some might be malicious.
The natural starting point is Tor onion services. Tor already has a somewhat mature protocol for making services reachable without exposing their network location. A Tor onion service publishes a descriptor, establishes introduction points, and communicates with clients through rendezvous points. We therefore studied the details of Tor’s hidden service protocol to understand the reasoning behind each component, and then investigated whether the same approach could be adapted to a decentralized Sphinx-style mixnet using the primitives already available in Mix.
The resulting hidden service over Mix protocol has three sub-protocols:
- an introduction protocol, so clients can bootstrap contact without learning the service’s address.
- a descriptor protocol, so clients can discover how to reach a service.
- a rendezvous protocol, so the client and service can continue communicating anonymously.
This gives us a working Tor-style hidden service design over Mix. See the draft specification here for full details. However, the design is vulnerable to a serious problem. In a decentralized mixnet, paths are typically sampled for each packet from the available mix nodes. If an adversary controls some fraction of those nodes, then every newly sampled path has some probability of being fully malicious. For a long-lived hidden service, this probability accumulates over many client interactions. A malicious client can repeatedly contact the service (or collect SURBs) and wait until the service eventually uses a path (or SURB) controlled entirely by the adversary.
In this setting, this changes the question from:
what anonymity guarantees can a hidden service provide?
to:
How long can a hidden service remain anonymous?
It is therefore possible that Tor-style hidden services may not be directly compatible with decentralized mixnets in this form, and further research is required to address this problem or find an alternative approach if the problem is deemed unsolvable. Tor faces the same problem but the severity is much less for Tor since it uses persistent circuits. Additionally, Tor uses the Guard and Vanguard approach (see Tor spec) for its hidden service. These can help delay the expected time until a fully compromised path is chosen, but doesn’t solve the underlying problem. We are currently investigating the guard and vanguard protocols to understand if they can make such attacks infeasible in practice. Another approach to solving the problem is if some nodes are centralized, trusted, or permissioned, then the fully malicious path problem may become easier to manage, but the system then has a centralization problem instead.
The rest of the document is structured as follows. First, we give a high-level overview of the hidden services over Mix protocol. We then describe the three main protocol components: the introduction protocol, the descriptor protocol, and the rendezvous protocol. Finally, we discuss the malicious path problem in more details, as well as planned future work to potentially limit this problem.
High-level overview
The goal is to adapt the Tor-style hidden services to work in a Sphinx-based mixnet, specifically the Logos Mix protocol. The data flows through the mixnet with help from randomly chosen intermediate nodes called introduction and rendezvous points. With the Sphinx-style mixnet, we have SURBs for replies and we can use them to create a bidirectional connection between a client and a service with rendezvous points in the middle.
Definition
Hidden services extend normal mixnet communication by providing anonymity to both communicating parties, i.e. the sender and receiver. In this sense it provides bi-directional anonymity.
The main additional property of a hidden service is that it allows a service to be reachable without exposing its network location. More specifically, an adversary should not be able to reliably link a service identifier or descriptor to the network identity of the node or nodes operating that service.
An adversary may:
- Observe some of the network traffic,
- Control some fraction of peers/providers,
- Control some fraction of mix nodes,
- Control some fraction of DHT nodes.
Functional Requirements
- Nodes are able to anonymously serve data. This requires:
- An anonymous way for nodes to advertize themselves (e.g. on a DHT) as providers for some service/content/datasets.
- Clients are able to discover these advertized anonymous services and connect to them anonymously
- Making contents/datasets retrievable without revealing which node(s) are providing/serving it and which nodes are receiving it.
- The protocol can support any arbitrary applications (with different payload size requirements) running on top of it.
Workflow
We can assume the following parties:
- service: the hidden service provider. i.e. a node running the hidden services protocol as a service provider.
- client: a user seeking a service from a hidden service provider.
- Discovery/DHT: a discovery network that both services and clients can contact to store and retrieve (key → value) records such as a service descriptor.
- Introduction points: mix nodes that are chosen by the service as contact points and are publicly reachable (we can assume for now that all mix nodes are publicly reachable).
- rendezvous points: mix nodes chosen by a SURB user to relay a payload to the SURB creator on its behalf.
Note: a SURB creator is the party that created the SURB (i.e., the Sphinx header), and a SURB user is the party that uses the SURB to send a payload to the SURB creator. Clients and services play both roles since we use SURBs for bidirectional connection
At a high-level, we can summarize the workflow of Tor-style hidden service over mix as follows:
- Introduction protocol: establishing introduction points
- Service establishes multiple intro points anonymously through Mix and supplies these intro points with
intro-SURBs. Theseintro-SURBsallow intro points to forward client requests to the service.
- Service establishes multiple intro points anonymously through Mix and supplies these intro points with
- Descriptor protocol
- Service publishes anonymously through Mix a
descriptorto the discovery (DHT) which contains information needed for a client to connect to the service as well as addresses of intro points to reach the service, and any other metadata needed to bootstrap the connection. Discovery would store thekey -> valuemapping askey -> descriptor. - Service shares a
.mixaddress with clients out-of-band. - Client derives
keyfrom the.mixaddress and fetches thedescriptorfrom discovery/DHT.
- Service publishes anonymously through Mix a
- Introduction protocol: Bootstrapping a connection
- Client contacts the service via one of the intro points, and includes
client-SURBsso the service can reply anonymously. - Intro point forwards client request to service using
intro-SURBs.
- Client contacts the service via one of the intro points, and includes
- Rendezvous protocol
- After receiving the introduction request, the service replies using a SURB-rendezvous connection. This connection mode allows a SURB user to remain anonymous by routing the packet through a random one-time rendezvous point before it is forwarded using the provided SURB. The service’s reply includes
service-SURBsthat the client can use for subsequent messages. - The client and service communicate over an anonymous bidirectional connection. Each message is sent using a SURB-rendezvous connection, and both sides continue exchanging fresh SURBs as needed.
- After receiving the introduction request, the service replies using a SURB-rendezvous connection. This connection mode allows a SURB user to remain anonymous by routing the packet through a random one-time rendezvous point before it is forwarded using the provided SURB. The service’s reply includes
The following figure summarizes the workflow. All communication shown below is assumed to take place over mix. Solid lines denote forward messages, while dotted lines denote backward messages sent using SURBs.
sequenceDiagram
autonumber
participant S as Service
participant IP as Introduction Point
participant Disc as Discovery/DHT
participant C as Client
participant R as Rendezvous
Note over S,IP: Establish introduction point
S->>S: Generate intro-SURBs for intro forwarding
S->>IP: Register introduction point + supply intro-SURBs
Note over S,Disc: Publish hidden-service descriptor
S->>S: Build descriptor {service info, intro points, bootstrap metadata}
S->>Disc: Publish descriptor
Note over C,Disc: Client lookup descriptor
C->>Disc: Lookup descriptor
Disc-->>C: Return descriptor
Note over C,S: Bootstrap via introduction point
C->>C: Generate client-SURBs
C->>IP: INTRODUCE{request, client-SURBs}
IP-->>S: Forward INTRODUCE using stored intro-SURB
Note over S,C: Service replies using client-SURBs
S->>S: Process request
S->>S: Generate service-SURBs for direct connection
S->>R: SURB-rendezvous{client-SURBs, payload{response, service-SURBs}}
R-->>C: client-SURBs{response, service-SURBs}
Note over C,S: Direct anonymous bidirectional connection
loop Ongoing session
C->>R: SURB-rendezvous{service-SURBs, payload{request, clientSURBs}}
R-->>S: service-SURBs{request, clientSURBs}
S->>R: SURB-rendezvous{client-SURBs, payload{response, service-SURBs}}
R-->>C: client-SURBs{response, service-SURBs}
end
1. Introduction protocol
The introduction protocol consists of two main parts:
A. the service establishes introduction points.
B. the client bootstraps a connection through one of those introduction points.
Note: the naming convention used in this mix hidden service protocol follows Tor’s onion services protocol for consistency. However, the contents and processing of these messages are different since they are adapted for the Mix setting.
A. Establishing introduction points
Before a service can announce how it can be reached, i.e., before publishing a descriptor, it must establish introduction points. The service provider establishes a set of mix nodes as introduction points. This is done as follows:
- Select
krandom mix nodes from discovery to act as intro points. Discovery should support unbiased random sampling from the set of live mix nodes. - build
kmix paths, where each path terminates at the selected intro points, i.e. intro points become the exit node. - Send the
EstablishIntromessage to each selected intro point. - The default value for
kis three, resulting in three intro points.
The EstablishIntro message needs to contain the following:
EstablishIntro {
auth_key: bytes
auth_mac: bytes
sig: bytes
}
where
-
auth_key
The hidden service creates a signing key pair for each intro point and places the public key in theauth_keyfield. A unique key pair must be created for each intro point, i.e. no key pair is ever used with more than one intro point. These keys are used later to sign theEstablishIntrorequest. The intro points will use theauth_keyas a public identifier for the hidden service and map client requests to the hidden service.
Theauth_keyserves as a public identifier for the service at a specific introduction point instead of say a random value/cookie. This is because we want a way for intro points to check ownership of this identifier. i.e. intro points will only associate a public identifier to a mix node after checking ownership of this identifier to prevent unauthorized use of the identifier. The hidden service provider proves ownership of theauth_keyby signing the message content using the secret key corresponding to that publicauth_key. -
auth_mac
Theauth_macfield contains a MAC of all earlier fields in the message, and keyed by the shared secret/keysderived from the service and intro point Mix path. Since the intro point is the exit/last hop as stated above, it must have a per-hop shared key material with the service i.e.sunder the Sphinx packet construction. Refer to the mix specification for more details on howsis derived. This MAC binds theEstablishIntrorequest to the specific “session” in which it was sent/received.
Theauth_macin the message provides a form of replay resistance for theEstablishIntromessage by binding the message to the session on which it was created, i.e. the session between the two parties withsas their shared key material.
Without a MAC derived from a unique session key, an attacker could:- Replay the exact same signed
EstablishIntroon a different intro point leading to unauthorized registration of introduction points. - Cause clients to use attacker-controlled or invalid introduction points.
- Replay the exact same signed
-
sig
Contains the signature of all message contents, includingauth_mac, using the private key corresponding to theauth_keyand the domain separation string"establish intro signature".
On receiving an EstablishIntro message, the mix node will verify the signature sig using auth_key, and verify the MAC auth_mac using the shared key s. The mix node must reject the EstablishIntro message and drop the received packet in the following cases:
- The key
auth_keyor signaturesigare malformed or unsupported. - The signature is invalid.
- The MAC
auth_macis invalid. - The mix node is already serving as an intro point for the same
auth_key. - The mix node has reached its local capacity for hidden services.
If all checks succeed, then the mix node will serve as an intro point for the hidden service and associate the supplied auth_key with a hidden service state i.e., map auth_key -> service_state. Such service_state contains information needed to relay future client introduction messages to the service such as intro-SURBs and other metadata. The intro point can optionally acknowledge success with an IntroEstablished reply message.
B. Bootstrapping through an introduction point
Client behaviour
To bootstrap a connection to a hidden service, the client first fetches and validates the hidden service descriptor as described in the descriptor protocol. After successful validation, the client obtains a list of introduction points, each containing:
- intro point information
info - authentication key
auth_key - service key
service_key
The client then proceeds as follows:
- Select a random intro point from the list. Clients select an intro point at random to distribute load.
- Generate a fresh client encryption key
client_key( unique one for every introduction attempt). This is required for replay prevention since replay tags are derived fromclient_key. - Derive the shared secret
s_introusing the freshclient_keyand theservice_keyfor that intro point.s_intro = DH.SharedKey(client_key, service_key) - Derives symmetric keys using a KDF with domain-separated SHA256:
enc_key = SHA256( "hs intro request encryption key" || s_intro) enc_iv = SHA256( "hs intro request encryption iv" || s_intro) mac_key = SHA256( "hs intro request mac key" || s_intro) - Construct the request
Requestwhich includes the plaintext application-specific request andclient-SURBsfor the service to reply. The request should contain enoughclient-SURBsfor the service to then build aSURB-rendezvousconnection (see rendezvous protocol section). - Encrypt the plaintext request and create a MAC.
enc_payload = Enc(enc_key, enc_iv, request) mac = MAC(mac_key, auth_key || client_key || enc_payload) - Create an
Introduce1message:
Introduce1{
auth_key: bytes
client_key: bytes
enc_payload: bytes
mac: bytes
}
where the encrypted payload enc_payload contains:
Request {
app_data: bytes
client_surbs: [SURB]
}
and send it to the chosen intro point. The sphinx packet containing Introduce1 message must also provide SURBs for the intro point to reply (1-2 SURBs would likely be sufficient).
8. If the introduction point accepts the request for forwarding, it must reply with and ack e.g., IntroduceAck { status: Ok }
9. Missing ack or timeout is treated as failure. On failure, the client may retry with a different intro point, but must generate a fresh client_key and a fresh request (i.e. repeat from step 1).
10. Wait for a service reply coming through the SURBs in the request or timeout and start the process again from step 1.
11. On recieved reply from the service, the client creates a SURB-rendezvous connection for its communication with the service.
12. Pass the reply plaintext to the application for further processing.
13. all subsequent communication is done over the created SURB-rendezvous connection session following the steps in the Rendezvous protocol.
Intro point behaviour
The mix node after accepting the role of being an intro point must maintain a state about the service which includes:
- authentication key
auth_key - validity/expiry time.
- a session/state with the service to forward requests that the intro point receives.
The intro point will then act only as a forwarding node:
- check for replays (see replay resistance subsection)
- uses
auth_keyto forward incoming requests to an active hidden-service state using the session/state for that service. Ifauth_keyis not found or expired, then the request is dropped. - send an ack (
IntroduceAck) to the client using the provided SURB. - send
Introduce2which is simply the forwardedIntroduce1contents.
Service behaviour
A hidden service accepts intro requests only for intro points that it has previously established. For each intro point the service must maintain a state containing the following:
- authentication key
auth_key - service key
service_key - validity/expiry time.
- a session/state to receive requests routed through the intro point.
When the hidden service receives an Introduce2 message from an introduction point, it processes it as follows:
- check for replays (see replay resistance subsection)
- Validate that the
auth_keymatches the intro point’s assignedauth_key. If not, drop the request. - Validate that the
client_keyis well-formed, and derive the shared secrets_intro. - Derive
enc_key,enc_iv, andmac_keyin the same way as the client. - Validate the
macin theIntroduce2message. If verification fails, drop the request. - decrypt the encrypted payload
enc_payloadand process the plaintext request by local service. - create a session over a
SURB-rendezvousconnection for subsequent communication (see rendezvous protocol section).
Replay resistance
Replay resistance is required to prevent an adversary from resending previously valid intro messages in order to:
- DOS/spam the service by repeatedly sending a valid looking intro request message, and possibly consuming all SURBs stored at the intro point (this is dependent on how SURBs are managed).
- DOS/spam the service by triggering repeated processing of the packet at the hidden service.
- Consume client-SURBs stored at the hidden service end.
- Create duplicate application requests.
In this protocol, replay resistance is achieved by the following:
- Binding to
auth_key: the MAC inIntroduce1is computed overauth_key || client_key || enc_payload, therefore, an attacker cannot move a valid encrypted intro payload from one intro point to another or changeauth_keywithout invalidating MAC. - Client tags: The intro point and service must maintain replay cache/table of previously seen client tags for each service-intro session/state. These client tags are computed as
client_tag = H(client_key). When a request is received by either the intro point or service, they will check the existence of such tag in the local cache/table. If not present, record the tag in the cache/table and continue processing. If present, the request must be dropped with no further processing. The local cache for each service-intro session can be flushed when it expires or gets terminated (i.e. whenauth_keyis no longer assigned to a service/intro).
Note: these replay preventions are not sufficient for DOS/spam prevention. An additional (pluggable) component is needed for DOS/spam prevention. This will be specified in the future.
2. Descriptor protocol
A hidden service descriptor is a signed object that the service publishes anonymously to the discovery/DHT in order for clients to learn how to reach the hidden service. The descriptor is generated by the service after it has established its introduction points. Publishing the descriptor effectively makes the service available and ready to accept connections.
Remark: we are assuming here that there exists an Anonymous discovery protocol with the following properties:
- An anonymous way to query the DHT, i.e. the ability to find and retrieve a
valuethat corresponds to the querykeyanonymously. - An anonymous way to publish a
(key, value)pair to the discovery/DHT nodes. - Discovery/DHT nodes must validate the published
(key, value)pair according to the specification in this document. - The above might require discovery/DHT nodes to extend the protocol with a specific
publish()call, e.g.publish_descriptor().
At a high level, the descriptor requires the following:
- A persistent service identifier that the service can distribute to clients or publish publicly. This must be derived from a long-term identity key for the service.
- A deterministic period-specific identifier for the descriptor which can be used as the key in the
key -> valuemapping used in the discovery/DHT. Such identifier/key must be derived from the long-term identity key for the service together with the current epoch (time period), and should also include public epoch-specific entropy so that descriptor placement is not fully predictable in advance. - The descriptor must contain the information clients need in order to reach the hidden service, including intro points and the public keys needed for encryption and authentication.
- The descriptor must be signed using the derived epoch-specific key to make it time/epoch-bound, i.e. its validity is tied to a specific epoch/time period.
The following subsections describe these requirements in more detail and specify how the descriptor is constructed and published to the discovery/DHT. However, as can be observed, we require multiple keys, therefore, we first summarize their definitions and uses in the following:
- Identity key (
hs_id): A “master” signing keypair used as the long-term identity for a hidden service. This key is not used on its own to sign anything, and only used to generate blinded keys. The public part of this key is encoded in the hidden service identifier (the.mixaddress). - Blinded keys (
bk): A signing keypair derived fromhs_idand used as the descriptor signing key. For each epochia different blinded keybk_iis generated. - Discovery key (
disc_key): An index in the DHT or DHT-like discovery which maps (disc_key -> value). It is derived frombk_i, epochi, and public entropye. - Intro authentication key (
auth_key): A short-term signing keypair created by the service for each intro point. - Service encryption key (
service_key): A short-term encryption keypair created by the service for each intro point and included in the descriptor. It is used by the client to encrypt requests toward the service via an intro point. - Client encryption key (
client_key): A short-term encryption keypair created by the client and included in requests toward the service via an intro point. It is used to generate a shared secret between the client and service when encrypting the request payload to the service.
Note: signing keys are keys generated using \mathsf{Sig.KeyGen()} while encryption keys are generated using \mathsf{DH.KeyGen()}. See the spec for more details.
Hidden service identifier (Mix address)
A hidden service requires a stable identifier analogous to Tor’s onion address. This identifier can then be distributed to clients out-of-band.
The mix address must be constructed using the service long-term public identity key hs_id:
pk, sk <- Sig.KeyGen()
hs_id = pk
checksum = H("hidden-service-checksum" | hs_id)[:2]
mix_add = base32(hs_id | checksum)
where H is the hash function used (e.g. SHA265), pk is the public key generated from a signature scheme with key blinding, and checksum is truncated to two bytes before adding it to mix_add. The checksum is included only to detect accidental errors and does not provide authentication.
Optionally, the human-readable string .mix may be appended:
[mix_add].mix
Periodic descriptor publication
Descriptors must be published periodically to the discovery/DHT network. Every time a hidden service publishes its descriptor, it sets a validity time e.g. 180 minutes (3 hours). The purpose of periodic publication is to ensure that the descriptor is “fresh”.
Aside from this validity time window, we need epochs which decide where the descriptor is stored in the DHT. I.e., the descriptor is stored at different locations over time, so that the same small set of discovery nodes does not remain responsible for the service indefinitely. This makes targeted denial-of-service (DOS) attacks against the hidden service more difficult.
Discovery provides a key -> value mapping, where the key determines the region of the DHT in which the mapping is stored. Only discovery nodes whose identifiers are close to that key (in the DHT space) are expected to store the descriptor. Therefore, that key must be different on each epoch.
The key -> value mapping in this protocol:
- the value is the hidden-service descriptor.
- the discovery key (
disc_key) is a deterministic epoch-specific value derived fromhs_id, the current epoch (epoch), and a public epoch-specific randomness value (e). We’ll discuss later how this key is derived.
Epochs and Descriptor Lifetime
Hidden-service descriptors are bound to:
- Descriptor epoch: determines which blinded service keys
bkanddisc_keylocations are used. - Descriptor lifetime: determines how long a published descriptor remains usable in the DHT before it must be refreshed.
These are simplified versions of what Tor uses and removes all the unnecessary complications, which are not relevant to the Mix setting.
Descriptor Epoch
Each epoch is identified by interval_num and length.
The default length is:
length = 86400 seconds // 1 day
The interval_num is derived from Unix time. Let L be the period length in seconds, and let t be the current Unix timestamp in seconds. The current interval_num is:
interval_num = floor(t / L)
Therefore interval_num covers the time interval:
[interval_num * L, (interval_num + 1) * L)
Descriptor Lifetime
The descriptor lifetime controls how long a published descriptor remains usable in the DHT cache. After this lifetime expires, DHT nodes should treat the descriptor as expired and no longer return it to clients.
By default, the descriptor lifetime is:
10800 seconds = 180 minutes
Services must periodically refresh/republish descriptors before they expire. Republishing a descriptor does not necessarily change its epoch, it only refreshes the descriptor’s availability in the DHT. To do this, the service samples a random time value between 60 to 120 minutes, and republishes the descriptor to the DHT.
Descriptor Overlap
To tolerate network delays, and clients using slightly different views of time, services should publish descriptors for overlapping epochs. This overlap ensures that a service remains reachable.
Epoch-specific blinded keys
The descriptor requires an epoch-specific discovery key (disc_key), but it also requires the descriptor to be authenticated in a way that is bound to both the service identity (hs_id) and to the current epoch. The protocol uses epoch-specific blinded signing keys (bk) derived from the long-term service identity key (hs_id) using key blinding (see the spec for more details).
Blinded keys
Let epoch_i denote the current epoch. The service performs key blinding as follows:
(sk, pk) = Sig.KeyGen()
hs_id = pk
r_i = H("key blinding" || hs_id || epoch_i)
(sk_i, pk_i) = Sig.BlindKey(pk, sk, r_i)
bk_i = pk_i
The blinded key (bk_i) derivation for clients is defined as follows:
hs_id = decode_base32(mix_add)
r_i = H("key blinding" || hs_id || epoch_i)
bk_i = Sig.BlindPk(hs_id, r_i)
Since epoch_i is predictable, blinded signing keys bk_i can be generated in advance.
Discovery keys
The epoch-specific discovery key disc_key is derived from the blinded public key bk_i, the current epoch, and a public epoch-specific randomness value:
e = get_public_entropy(epoch_i)
disc_key = H("discovery key" || bk_i || e_i)
where e is public epoch-specific entropy, for example a randomness beacon, block hash, or other agreed public entropy source. Including the public entropy e ensures that descriptor placement is not determined solely by public long-term service identity and epoch. This makes the storage location less predictable in advance and reduces the risk that an attacker can precompute and target the responsible discovery/DHT nodes for a future epoch.
The disc_key will then be used to publish or retrieve the descriptor from discovery/DHT. More details on discovery and client behaviour is discussed in the next sections.
Summary
This design provides the following properties:
- Periodic re-publication of the descriptor with a unique epoch-specific
disc_keyto avoid DoS attacks (different descriptor location in each epoch). disc_keyis bound to a signing key (bk_i) used to sign the descriptor.- The descriptor is signed using
bk_iand valid only for epochi. bk_iis be bound tohs_idthrough key blinding, i.e. ownership ofhs_idimplies ownership ofbk_i.- Knowledge of
hs_idis sufficient for clients to derive the epoch-specificdisc_keyfor the descriptor and query the discovery/DHT.
Note: if we care about keeping keys offline we can add an additional descriptor signing keys in the same way as Tor. For now we can simply sign the descriptor with the blinded key.
Descriptor components
The descriptor is split into two main parts:
(1) Outer Layer
Contains metadata needed for discovery/DHT nodes to validate it before storing it. This outer layer of the descriptor contains the following fields:
validity: an unsigned integer specifying how long (in seconds) the descriptor should remain valid.revision_counter: version counter for the descriptor. Allows the service to revise the descriptor. If discovery/DHT receives multiple descriptors for the same key, it keeps the one with the higher revision counter.inner_layer: An encrypted blob data type containing salt, encrypted data, and MAC.signature: A signature over all previous fields, made using the blinded signing keybk_ifor the current epochi.signatureis a tuple containing the pk used to sign and the signature:sig = Sig.Sign(bk_i.sk, "mix-hs-desc-outer" || validity || revision_counter || inner_layer) signature = Signature.new(bk_i.pk, sig) Signature { signing_key: bytes sig: bytes }
We can summarize the content of the outerlayer as follows:
OuterLayer {
validity: uint
revision_counter: uint
inner_layer: EncryptedBlob
signature: Signature
}
EncryptedBlob {
salt: bytes
blob: bytes
mac: bytes
}
See below for how EncryptedBlob is created and what its contents are.
(2) Inner Layer
An encrypted data blob EncryptedBlob containing the actual connection details needed to reach the service. The encryption provides confidentiality against entities that don’t know the identity key of the hidden service (hs_id) such as the discovery/DHT nodes.
To encrypt the inner layer, we first need to derive a symmetric encryption (stream cipher) key, IV, and MAC key from information known to both the service and client.
First, we generate a random salt and then use it with hs_id, bk_i, and revision_counter to create a shared secret input:
secret_input = "inner layer" || salt || hs_id || bk_i || revision_counter
Then we use a key derivation function (KDF) to generate the needed key materials. For KDF, we use domain-separated SHA256 with the output truncated to 128-bits. Once we have the key material, we can encrypt with a stream cipher e.g. AES-128-CTR, and create a MAC e.g. with HMAC-SHA-256. These schemes are already used by the Mix protocol and can work here as well.
We can summarize the steps to encrypt the inner layer (i.e. the EncryptedBlob) as follows:
salt = random()
secret_input = "inner layer" || salt || hs_id || bk_i || revision_counter
enc_key = SHA256("blob encryption key" || secret_input)
enc_iv = SHA256("blob iv" || secret_input)
mac_key = SHA256("blob mac key" || secret_input)
blob = streamcipher.enc(inner_layer, enc_key, enc_iv)
mac = mac(salt || blob, mac_key)
encrypted_blob = EncryptedBlob.new(salt, blob, mac)
The plaintext of this inner layer contains the following fields:
intro_points: A list of intro point objectsIntroPoint, each containing:info: AMixPubInfoobject containing mix routing information and public key for the intro point. The contents of this object depend on how mix routing is implemented, but in general it should contain sufficient information to create a sphinx packet with the intro point as the exit.auth_key: the authentication key for clients to use at this intro point.service_key: an encryption keyservice_keyfor clients to use to encrypt requests for the service, routed through the intro point.
InnerLayer {
intro_points: seq[IntroPoint]
}
IntroPoint {
info: MixPubInfo
auth_key: bytes
service_key: bytes
}
MixPubInfo {
peer_id: PeerId
multi_addr: MultiAddress
mix_pk: bytes
}
Creating descriptors
To publish a hidden service descriptor, the service will follow these steps:
- Determine the current
epoch, and thevaliditytime period for the descriptors. - Create a hidden service public identity
hs_idand derive the mix address. - Use
hs_idto create the blinded keybk_ifor the currentepoch. - Create a descriptor object with inner and outer layers using intro point information.
- derive the discovery key
disc_key. - publish the descriptor anonymously to discovery/DHT.
- Before the descriptor validity period expires, republish the descriptor. When the epoch changes, derive the new
bk_ianddisc_key, starting from step 3.
Discovery Node Processing
- Deserialize, extract the outer layer, and check if the outer layer is well-formed.
- If the node has a descriptor for this hidden service (i.e. same
disc_key), therevision_counterof the uploaded descriptor must be greater than therevision_counterof the stored one. - The descriptor (
validity) is within the acceptable range of 30 - 720 minutes. - The discovery key
disc_keyis derived correctly from the blinded key insignature. - The descriptor outer layer
signatureis valid.
If any of these outer layer validity checks fail, the discovery/DHT node must reject the descriptor. The descriptor is only stored for the validity time period specified in the descriptor.
Note: Descriptors could pass these checks but still have an invalid inner layer. However, the discovery is not able (not expected) to validate the encrypted inner layer of the descriptor.
Client Processing
The mix address mix_add should be sufficient for clients to query the discovery/DHT and fetch the descriptor. Therefore, clients will extract the public identity key hs_id from the mix address and use it to derive the epoch-specific blinded key.
The process can be summarized as follows:
- Client receives the
.mixaddress out-of-band - Validate the
.mixaddress using the checksum. - Extract the public identity key
hs_idfrom the.mixaddress. - Determine the current
epoch. - Derive the blinded key
bk_ifor the current epoch - Derive the discovery key
disc_key. - Query the discovery/DHT using
disc_keyto retrieve the descriptor. - Validate the returned descriptor by:
- Validate the outer layer in the same way as a discovery/DHT node (see “Discovery Node Processing” section above).
- Check that
signature.signing_key == bk_i - Derive the secret_input for the inner layer
- Use KDF to generate inner layer decryption keys (stream cipher key, IV, MAC key).
- Decrypt the inner layer data blob, and check its MAC.
- Proceed with the intro protocol to establish a SURB-rendezvous connection to the hidden service.
3. Rendezvous protocol
After the bootstrapping a connection through an intro point, the client and hidden service can communicate directly using SURBs since each party has SURBs from the other. At this point, introduction points are no longer involved in the communication path unless the session fails and the client needs to re-bootstrap through an introduction point. However, since each party cannot be sure that the SURBs are safe to use and could contain a malicious mix path, a different type/mode of connection is needed which we refer to here as a "SURB-rendezvous" connection.
To describe this, we can abstract the mix connections as three types of connections, each providing different anonymity properties:
- Forward connection: provides sender anonymity but doesn’t hide the destination from the sender.
- SURB-reply connection: provides anonymity to the SURB creator not the SURB user.
- SURB-rendezvous connection: provides anonymity to both the SURB creator and the user.
The Mix protocol already supports both 1 and 2 above. The goal here is to design a solution for 3.
SURB-rendezvous Connection
A SURB-rendezvous connection is a connection mode in which packets are sent using another party’s SURB, but only after the sender first routes the SURB packet through a sender-chosen mix prefix path.
To illustrate this better, let’s denote the sender (SURB user) S and receiver/exit (SURB creator) E.
Schooses a random mix node as a rendezvous pointRand builds a mix path to it and prepares a Sphinx packetP1.Splaces both the SURB and the message (encrypted with a shared secretz) intended forEin the payload ofP1, adds a flag to indicate that the packet isSURB-rendezvous(we’ll discuss this later), and sends the Sphinx packet.Rreceives the payload and sees theSURB-rendezvousflag, so it parses the payload to get the SURB and message.Ruses the SURB to create a Sphinx packet with the given message as its payload, pad it (since it would be smaller than a Sphinx payload), and sends the packet.Ereceives the packet and recognizes the packet since it created the SURB and processes it as a normal SURB-reply. The result would be an encrypted message underz+ padding.Eremoves the padding and decrypts usingz.
Note: in here, rendezvous points can used once, or multiple times which would then be similar to Tor which maintain a circuit to the rendezvous points. Although in here, the client and service would have different rendezvous points.
If we assume that possession of the SURB by itself binds the reply/message to the sender/session, then we can modify the above approach slightly. We can drop the need for the shared secret and use the reply key in the SURB. Then, R only need to use the header and the already encrypted message as payload.
This type of connection is not yet supported in Mix and so we don’t have a flag for it to indicate how an exit (rendezvous points) should process it. In the future we will likely have a better way to encode packet processing behaviour in the Sphinx header. However, for now we can rely on the reserved internal hidden service protocol codec (e.g. "/mix/hiddenservices/1.0.0").
Creating a SURB-rendezvous connection
For parties A and B to connect over SURB-rendezvous connection, they must obtain:
- a
SURBfrom the other party - a shared secret
s
These should be exchanged out of band e.g. through the intro protocol.
Then each party must maintain a state:
RendezvousState {
incoming_surbs: [SURB]
outgoing_surbs: [SURB]
shared_secret: bytes
}
where:
incoming_surbsare the set of SURBs we expect the other party messages to come from.outgoing_surbsare the set of SURBs we can use to send a message to the other party.shared_secretis the shared secret we have with the other party.
This state must be maintained in order to create packets and process incoming packets as we will see next.
Creating a SURB-rendezvous packet
To send a message m (a Sphinx payload) to E using a SURB created by E, the mix node must:
- Choose a random mix node (rendezvous point)
R(must not beE), create a mix path and prepare a Sphinx packetP. - Use the internal hidden service protocol codec as the codec for the payload.
- Pad
mto a fixed sizejso thatinner_payloadhas fixed pre-defined size. - Apply one layer of encryption to
musing a shared secretsbetween sender andE. This layer of encryption can be thought as a “virtual hop” where you takemas the sphinx payload ( i.e., \delta) and apply one LIONESS encryption using the shared secrets. - Place the
SURBand the encryptedmin the message type:Rendezvous { surb: bytes, inner_payload: bytes } - Place the codec and
Rendezvousmessage in the payload ofP.
Exit/Rendezvous processing behaviour
For an exit which in this case is a rendezvous point to process a SURB-rendezvous packet, it must identify it first by:
- parse the payload as
(codec, msg) - sees the codec as the internal hidden service protocol codec.
- identify the
msgasRendezvous
Then the exit node must:
- use the
SURBto create a Sphinx header. - use the
inner_payloadin theRendezvousmessage + padding (to Sphinx payload size) as the Sphinx payload. - send the Sphinx packet.
Receiver processing behaviour
A mix node that receives a SURB reply must map that SURB to an existing SURB-rendezvous connection. If none exist, drop the packet.
If a SURB-rendezvous connection exists, then:
- start with normal SURB processing.
- take the first j bytes as the
inner_payloadand drop the rest (the padding). - use the stored shared secret to apply one layer of decryption to
inner_payload. - pass the message to the application layer to process it.
Note: again here we use a shared secret but we can easily adapt this to use reply_key instead.
Limitations and open problems
The design above gives a Tor-style hidden service construction. It is however, missing some additional components such as:
- Denial of service.
- Restricted discovery mode.
- SURB management.
These can be solved with proper designs possibly adapting what Mix has, e.g., RLN. The most serious open problem that we identified is the malicious path problem which is discussed in the following section.
The malicious path problem
In a mixnet, anonymity relies on the assumption: a path is safe if at least one node on the path is honest. If all nodes on the path are malicious, the adversary is able to link the sender to the receiver, or the hidden service identifier to its network identity.
If an adversary controls a fraction f of the mix nodes and each path has length l, then the probability that one randomly selected path is fully malicious is approximately:
p = f^l
For example, if the adversary controls 10% of the mix nodes and paths have length 3:
p = 0.1^3 = 0.001
That is one fully malicious path in one thousand independently sampled paths (1/1000). For a single message, this is small. For a hidden service, it is much more serious.
A hidden service is a long-lived (possibly public) reachable target. A malicious client can repeatedly contact the service. Each contact can cause the service to create or use fresh paths, fresh SURBs, or fresh rendezvous points. Over time, the adversary gets many chances to control a fully malicious path.
If each attempt has a compromise probability p, then after n independent attempts the probability of at least one compromised attempt is approx:
P = 1 - (1 - p)^n
With p = 0.001:
One can see how this is a serious problem because of the accumulating risk over repeated malicious interactions. The problem can appear in several places in the protocol:
- when the service establishes introduction points and supplies intro-SURBs.
- when the service replies to clients.
- when the service generates service-SURBs for client-service communication using the
SURB-rendezvousconnection.
Note that here the adversary does not necessarily need to attack the service by repeated interaction. If a malicious intro point, client, or other party can repeatedly obtain SURBs from the service, then it has many opportunities to observe a SURB whose path is fully malicious. Over time, this turns SURB into another attack vector.
The small packet size also helps the attacker in this setting because sending and receiving large data would require chunking it and sending each chunk in a separate packet with a newly sampled path. Current packet size is \approx 4KB, meaning that a fully malicious path would be very likely with >10MB assuming p = 0.001.
Tor Guards and vanguards Protocols
Tor does not sample completely fresh independent paths for every circuit. Entry guards exist because repeatedly choosing fresh entry (first hop) relays would eventually give an adversary a chance to become the first hop of a user’s circuit. Onion services also use additional guard and vanguard nodes to reduce the risk of guard discovery and related attacks (see Tor spec).
A similar approach may work for hidden services over Mix. Instead of choosing all mix paths independently, a hidden service could use a stable set of guard nodes. It could also use a second layer of vanguard nodes, so the paths are constrained through a smaller, longer-lived set of selected nodes.
This changes the risk. If the service initially chooses honest guards or vanguards, the adversary gets fewer chances. If the service chooses both malicious guards and vanguards, the service becomes exposed to the malicious path attack. This is not a complete solution. It delays the risk, it does not eliminate it. Whether this makes the attack impractical in the hidden service over Mix setting is still an open question and is something we are currently investigating. Recent work seems to suggest that guard-style approach can significantly improve time to “first compromise” in stratified mixnets (see the paper: Stopping Silent Sneaks: Defending against Malicious Mixes with Topological Engineering). This still needs to be simulated/evaluated for the hidden service setting and for the free-route Mix.
References
Sphinx Packet Format and Single-Use Reply Blocks (SURBs)
- The original Sphinx design, see the paper: Sphinx: A Compact and Provably Secure Mix Format.
- A write-up on Sphinx
- Sphinx section of the mix specification