From 9d11a22901ffe9c3ab0007f58baa55b0e08abfc9 Mon Sep 17 00:00:00 2001 From: AkshayaMani Date: Wed, 10 Dec 2025 07:24:23 -0500 Subject: [PATCH] docs: finalize Section 8 Sphinx Packet Construction and Handling (#202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR builds on PR #173 and completes the remaining construction and runtime processing logic in `Section 8` of the Mix Protocol RFC. It finalizes the last steps of packet construction (`Section 8.5.2 step 3. e–f`) and introduces the complete mix node handler logic in `Section 8.6`, including intermediary and exit processing. It clearly separates construction, role determination, and processing logic. ### Changes Introduced in This PR - **8.5.2 Construction Steps (Final Steps Added)** - Sphinx packet construction - [x] Assemble Final Packet - [x] Transmit Packet - **8.6 Sphinx Packet Handling** - [x] **8.6.1 Shared Preprocessing** - Derives session key, validates replay tag and MAC, decrypts header/payload - [x] **8.6.2 Node Role Determination** - Inspects decrypted header prefix and padding to classify node as intermediary or exit - [x] **8.6.3 Intermediary Processing** - Parses next hop address and mean delay - Updates ephemeral key and routing fields - Samples actual forwarding delay and transmits packet - Erases all temporary state. - [x] **8.6.4 Exit Processing** - Verifies payload padding and extracts destination address - Parses and validates application-layer message - Hands off to Exit Layer along with origin protocol codec and destination address ### Highlights - Explicit role determination via zero-delay and padding inspection - Fully decoupled construction and handling logic - Forwarding delay behavior updated: - Sender selects per-hop mean delay - Mix node samples actual delay using pluggable distribution --------- Co-authored-by: kaiserd <1684595+kaiserd@users.noreply.github.com> --- vac/raw/mix.md | 449 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 415 insertions(+), 34 deletions(-) diff --git a/vac/raw/mix.md b/vac/raw/mix.md index a60c2ce..4b6198d 100644 --- a/vac/raw/mix.md +++ b/vac/raw/mix.md @@ -87,8 +87,7 @@ A per-message flag set by the origin protocol to indicate that a message should be routed using the Mix Protocol or not. Only messages with mixify set are forwarded to the Mix Entry Layer. -Other messages SHOULD be routed using the origin protocol’s default behavior. - +Other messages SHOULD be routed using the origin protocol’s default behavior. The phrases 'messages to be mixified', 'to mixify a message' and related variants are used informally throughout this document to refer to messages that either have the @@ -1100,8 +1099,10 @@ For interoperability, a recommended default encoding format involves: - Port number (2 bytes) - Peer IDs (39 bytes, post-Base58 decoding) -- Encoding the forwarding delay as an unsigned 16-bit integer (2 bytes) in - milliseconds, using big endian network byte order. +- Encoding the forwarding delay as an unsigned 16-bit integer (2 bytes), + representing the mean delay in milliseconds for the configured delay + distribution, using big endian network byte order. + The delay distribution is pluggable, as defined in [Section 6.2](#62-delay-strategy). If the encoded address or delay is shorter than its respective allocated field, it MUST be padded with zeros. If it exceeds the allocated size, it @@ -1165,20 +1166,24 @@ The construction MUST proceed as follows: 1. **Prepare Application Message** - Apply any configured spam protection mechanism (_e.g.,_ PoW, VDF, RLN) - to the serialized message. Spam protection mechanisms are pluggable as defined - in [Section 6.3](#63-spam-protection). + to the serialized message. Spam protection mechanisms are pluggable as defined + in [Section 6.3](#63-spam-protection). - Attach one or more SURBs, if required. Their format and processing are - specified in [Section X.X]. - - Append the origin protocol codec. + specified in [Section X.X]. + - Append the origin protocol codec in a format that enables the exit node to + reliably extract it during parsing. A recommended encoding approach is to + prefix the codec string with its length, encoded as a compact varint field + limited to two bytes. Regardless of the scheme used, implementations MUST + agree on the format within a deployment to ensure deterministic decoding. - Pad the result to the maximum application message length of $3968$ bytes - using a deterministic padding scheme. This value is derived from the fixed - payload size in [Section 8.3.2](#832-payload-size) ($3984$ bytes) minus the - security parameter $κ = 16$ bytes defined in - [Section 8.2](#82-cryptographic-primitives). The chosen scheme MUST yield a - fixed-size padded output and MUST be consistent across all mix nodes to - ensure correct interpretation during unpadding. For example, schemes that - explicitly encode the padding length and prepend zero-valued padding bytes - MAY be used. + using a deterministic padding scheme. This value is derived from the fixed + payload size in [Section 8.3.2](#832-payload-size) ($3984$ bytes) minus the + security parameter $κ = 16$ bytes defined in + [Section 8.2](#82-cryptographic-primitives). The chosen scheme MUST yield a + fixed-size padded output and MUST be consistent across all mix nodes to + ensure correct interpretation during unpadding. For example, schemes that + explicitly encode the padding length and prepend zero-valued padding bytes + MAY be used. - Let the resulting message be $m$. 2. **Select A Mix Path** @@ -1220,8 +1225,8 @@ The construction MUST proceed as follows: \end{aligned} `$ - Note that the length of $α_i$ is $32$ bytes as defined in - [Section 8.3](#83-packet-component-sizes). + Note that the length of $α_i$ is $32$ bytes, $0 \leq i \leq L-1$ as defined in + [Section 8.3.1](#831-header-field-sizes). b. **Compute Per-Hop Filler Strings** Filler strings are encrypted strings that are appended to the header during @@ -1246,17 +1251,17 @@ The construction MUST proceed as follows: - Compute the filler string $Φ_i$ using $\text{AES-CTR}^\prime_i$, which is AES-CTR encryption with the keystream starting from - index $((t+1)(r-i)+t+2)\kappa$ : + index $((t+1)(r-i)+t+2)κ$ : $` \begin{array}{l} Φ_i = \mathrm{AES\text{-}CTR}'_i\bigl(Φ_{\mathrm{aes\_key}_{i-1}}, - Φ_{\mathrm{iv}_{i-1}}, Φ_{i-1} \mid 0_{(t+1)\kappa} \bigr), + Φ_{\mathrm{iv}_{i-1}}, Φ_{i-1} \mid 0_{(t+1)κ} \bigr),\; \; \; \text{where notation $0_x$ defines the string of $0$ bits of length $x$.} \end{array} `$ - Note that the length of $Φ_i$ is $(t+1)i\kappa$. + Note that the length of $Φ_i$ is $(t+1)iκ$, $0 \leq i \leq L-1$. c. **Construct Routing Header** The routing header as defined in @@ -1286,21 +1291,22 @@ The construction MUST proceed as follows: `$ - Set the per hop two-byte encoded delay $\mathrm{delay}_i$ as defined in - [Section 8.4](#84-address-and-delay-encoding): + [Section 8.4](#84-address-and-delay-encoding): - If final hop (_i.e.,_ $i = L - 1$), encode two byte zero padding. - - For all other hop $i,\ i < L - 1$, sample a forwarding delay - using the delay strategy configured by the application and encode it in two bytes. - The delay strategy is pluggable as defined in [Section 6.2](#62-delay-strategy). + - For all other hop $i,\ i < L - 1$, select the mean forwarding delay + for the delay strategy configured by the application, and encode it as a + two-byte value. The delay strategy is pluggable, as defined in + [Section 6.2](#62-delay-strategy). - Using the derived keys and encoded forwarding delay, compute the nested - encrypted routing information $β_i$: + encrypted routing information $β_i$: - If $i = L-1$ (_i.e.,_ exit node): $` \begin{array}{l} β_i = \mathrm{AES\text{-}CTR}\bigl(β_{\mathrm{aes\_key}_i}, - β_{\mathrm{iv}_i}, Δ \mid \mathrm{delay}_i \mid 0_{((t+1)(r-L)+2)\kappa} + β_{\mathrm{iv}_i}, Δ \mid \mathrm{delay}_i \mid 0_{((t+1)(r-L)+2)κ} \bigr) \bigm| Φ_{L-1} \end{array} `$ @@ -1310,13 +1316,15 @@ The construction MUST proceed as follows: $` \begin{array}{l} β_i = \mathrm{AES\text{-}CTR}\bigl(β_{\mathrm{aes\_key}_i}, - β_{\mathrm{iv}_i}, \mathrm{addr}_{i+1} \mid $\mathrm{delay}_i$ - \mid γ_{i+1} \mid β_{i+1 \, [0 \ldots (r(t+1) - t)\kappa - 1]} \bigr) + β_{\mathrm{iv}_i}, \mathrm{addr}_{i+1} \mid \mathrm{delay}_i + \mid γ_{i+1} \mid β_{i+1 \, [0 \ldots (r(t+1) - t)κ - 1]} \bigr),\; \; \; + \text{where notation $X_{[a \ldots b]}$ denotes the substring of $X$ + from byte offset $a$ to $b$, inclusive, using zero-based indexing.} \end{array} `$ - Note that the length of $\beta_i$ is $(r(t+1)+1)\kappa$, $0 \leq i \leq L-1$ - as defined in [Section 8.3](#83-packet-component-sizes). + Note that the length of $\beta_i$ is $(r(t+1)+1)κ$, $0 \leq i \leq L-1$ + as defined in [Section 8.3.1](#831-header-field-sizes). - Compute the message authentication code $γ_i$: @@ -1327,8 +1335,8 @@ The construction MUST proceed as follows: \end{array} `$ - Note that the length of $\gamma_i$ is $\kappa$ as defined in - [Section 8.3](#83-packet-component-sizes). + Note that the length of $\gamma_i$ is $κ$, $0 \leq i \leq L-1$ as defined in + [Section 8.3.1](#831-header-field-sizes). d. **Encrypt Payload** The encrypted payload $δ$ contains the message $m$ defined in Step 1, @@ -1357,7 +1365,7 @@ The construction MUST proceed as follows: $` \begin{array}{l} δ_i = \mathrm{AES\text{-}CTR}\bigl(δ_{\mathrm{aes\_key}_i}, - δ_{\mathrm{iv}_i}, 0_{\kappa} \mid m + δ_{\mathrm{iv}_i}, 0_{κ} \mid m \bigr) \end{array} `$ @@ -1370,3 +1378,376 @@ The construction MUST proceed as follows: δ_{\mathrm{iv}_i}, δ_{i+1} \bigr) \end{array} `$ + + Note that the length of $\delta_i$, $0 \leq i \leq L-1$ is $|m| + κ$ bytes. + + Given that the derived size of $\delta_i$ is $3984$ bytes as defined in + [Section 8.3.2](#832-payload-size), this allows $m$ to be of length + $3984-16 = 3968$ bytes as defined in Step 1. + + e. **Assemble Final Packet** + The final Sphinx packet is structured as defined in + [Section 8.3](#83-packet-component-sizes): + + ```text + α = α_0 // 32 bytes + β = β_0 // 576 bytes + γ = γ_0 // 16 bytes + δ = δ_0 // 3984 bytes + ``` + + Serialize the final packet using a consistent format and + prepare it for transmission. + + f. **Transmit Packet** + - Sample a randomized delay from the same distribution family used for + per-hop delays (in Step 3.e.) with an independently chosen mean. + + This delay prevents timing correlation when multiple Sphinx packets are + sent in quick succession. Such bursts may occur when an upstream protocol + fragments a large message, or when several messages are sent close together. + + - After the randomized delay elapses, transmit the serialized packet to + the first hop via a libp2p stream negotiated under the + `"/mix/1.0.0"` protocol identifier. + + Implementations MAY reuse an existing stream to the first hop as + described in [Section 5.5](#55-stream-management-and-multiplexing), if + doing so does not introduce any observable linkability between the + packets. + +Once a Sphinx packet is constructed and transmitted by the initiating node, it is +processed hop-by-hop by the remaining mix nodes in the path. Each node receives +the packet over a libp2p stream negotiated under the `"/mix/1.0.0"` protocol. +The following subsection defines the per-hop packet handling logic expected of +each mix node, depending on whether it acts as an intermediary or an exit. + +### 8.6 Sphinx Packet Handling + +Each mix node MUST implement a handler for incoming data received over +libp2p streams negotiated under the `"/mix/1.0.0"` protocol identifier. +The incoming stream may have been reused by the previous hop, as described +in [Section 5.5](#55-stream-management-and-multiplexing). Implementations +MUST ensure that packet handling remains stateless and unlinkable, +regardless of stream reuse. + +Upon receiving the stream payload, the node MUST interpret it as a Sphinx packet +and process it in one of two roles—intermediary or exit— as defined in +[Section 7.3](#73-sphinx-packet-receiving-and-processing). This section defines +the exact behavior for both roles. + +#### 8.6.1 Shared Preprocessing + +Upon receiving a stream payload over a libp2p stream, the mix node MUST first +deserialize it into a Sphinx packet `(α, β, γ, δ)`. + +The deserialized fields MUST match the sizes defined in [Section 8.5.2](#852-construction-steps) +step 3.e., and the total packet length MUST match the fixed packet size defined in +[Section 8.3.2](#832-payload-size). + +If the stream payload does not match the expected length, it MUST be discarded and +the processing MUST terminate. + +After successful deserialization, the mix node performs the following steps: + +1. **Derive Session Key** + + Let $x \in \mathbb{Z}_q^*$ denote the node's X25519 private key. + Compute the shared secret $s = α^x$. + +2. **Check for Replays** + + - Compute the tag $H(s)$. + - If the tag exists in the node's table of previously seen tags, + discard the packet and terminate processing. + - Otherwise, store the tag in the table. + + The table MAY be flushed when the node rotates its private key. + Implementations SHOULD perform this cleanup securely and automatically. + +3. **Check Header Integrity** + + - Derive the MAC key from the session secret $s$: + + $` + \begin{array}{l} + \mathrm{mac\_key} = + \mathrm{KDF}(\text{"mac\_key"} \mid s) + \end{array} + `$ + + - Verify the integrity of the routing header: + + $` + \begin{array}{l} + γ \stackrel{?}{=} \mathrm{HMAC\text{-}SHA\text{-}256}(\mathrm{mac\_key}, + β) + \end{array} + `$ + + If the check fails, discard the packet and terminate processing. + +4. **Decrypt One Layer of the Routing Header** + + - Derive the routing header AES key and IV from the session secret $s$: + + $` + \begin{array}{l} + β_{\mathrm{aes\_key}} = + \mathrm{KDF}(\text{"aes\_key"} \mid s)\\ + β_{\mathrm{iv}} = + \mathrm{KDF}(\text{"iv"} \mid s) + \end{array} + `$ + + - Decrypt the suitably padded $β$ to obtain the routing block $B$ for this hop: + + $` + \begin{array}{l} + B = \mathrm{AES\text{-}CTR}\bigl(β_{\mathrm{aes\_key}}, + β_{\mathrm{iv}}, β \mid 0_{(t+1)κ} + \bigr) + \end{array} + `$ + + This step removes the filler string appended during header encryption in + [Section 8.5.2](#852-construction-steps) step 3.c. and + yields the plaintext routing information for this hop. + + The routing block $B$ MUST be parsed according to the rules and field layout + defined in [Section 8.6.2](#862-node-role-determination) to determine + whether the current node is an intermediary or the exit. + +5. **Decrypt One Layer of the Payload** + + - Derive the payload AES key and IV from the session secret $s$: + + $` + \begin{array}{l} + δ_{\mathrm{aes\_key}} = + \mathrm{KDF}(\text{"δ\_aes\_key"} \mid s)\\ + δ_{\mathrm{iv}} = + \mathrm{KDF}(\text{"δ\_iv"} \mid s) + \end{array} + `$ + + - Decrypt one layer of the encrypted payload $δ$: + + $` + \begin{array}{l} + δ' = \mathrm{AES\text{-}CTR}\bigl(δ_{\mathrm{aes\_key}}, + δ_{\mathrm{iv}}, δ \bigr) + \end{array} + `$ + + The resulting $δ'$ is the decrypted payload for this hop and MUST be + interpreted depending on the parsed node's role, determined by $B$, as + described in [Section 8.6.2](#862-node-role-determination). + +#### 8.6.2 Node Role Determination + +As described in [Section 8.6.1](#861-shared-preprocessing), the mix node +obtains the routing block $B$ by decrypting one layer of the encrypted +header $β$. + +At this stage, the node MUST determine whether it is an intermediary +or the exit based on the prefix of $B$, in accordance with the construction of +$β_i$ defined in [Section 8.5.2](#852-construction-steps) step 3.c.: + +- If the first $(tκ - 2)$ bytes of $B$ contain a nonzero-encoded + address, immediately followed by a two-byte zero delay, + and then $((t + 1)(r - L) + t + 2)κ$ bytes of all-zero padding, + process the packet as an exit. +- Otherwise, process the packet as an intermediary. + +The following subsections define the precise behavior for each case. + +#### 8.6.3 Intermediary Processing + +Once the node determines its role as an intermediary following the steps in +[Section 8.6.2](#862-node-role-determination), it MUST perform the following +steps to interpret routing block $B$ and decrypted payload $δ'$ obtained in +[Section 8.6.1](#861-shared-preprocessing): + +1. **Parse Routing Block** + + Parse the routing block $B$ according to the $β_i$, $i \neq L - 1$ construction + defined in [Section 8.5.2](#852-construction-steps) step 3.c.: + + - Extract the first $(tκ - 2)$ bytes of $B$ as the next hop address $\mathrm{addr}$ + + $` + \begin{array}{l} + \mathrm{addr} = B_{[0\ldots(tκ - 2) - 1]} + \end{array} + `$ + + - Extract next two bytes as the mean delay $\mathrm{delay}$ + + $` + \begin{array}{l} + \mathrm{delay} = B_{[(tκ - 2)\ldots{tκ} - 1]} + \end{array} + `$ + + - Extract next $κ$ bytes as the next hop MAC $γ'$ + + $` + \begin{array}{l} + γ' = B_{[tκ\ldots(t + 1)κ - 1]} + \end{array} + `$ + + - Extract next $(r(t+1)+1)κ$ bytes as the next hop routing information $β'$ + + $` + \begin{array}{l} + β' = B_{[(t + 1)κ\ldots(r(t +1 ) + t + 2)\kappa - 1]} + \end{array} + `$ + + If parsing fails, discard the packet and terminate processing. + +2. **Update Header Fields** + + Update the header fields according to the construction steps + defined in [Section 8.5.2](#852-construction-steps): + + - Compute the next hop ephemeral public value $α'$, deriving the blinding factor + $b$ from the shared secret $s$ computed in + [Section 8.6.1](#861-shared-preprocessing) step 1. + + $` + \begin{aligned} + b &= H(α\ |\ s) \\ + α' &= α^b + \end{aligned} + `$ + + - Use the $β'$ and $γ'$ extracted in Step 1. as the routing information and + MAC respectively in the outgoing packet. + +3. **Update Payload** + + Use the decrypted payload $δ'$ computed in + [Section 8.6.1](#861-shared-preprocessing) step 5. as the payload in the + outgoing packet. + +4. **Assemble Final Packet** + The final Sphinx packet is structured as defined in + [Section 8.3](#83-packet-component-sizes): + + ```text + α = α' // 32 bytes + β = β' // 576 bytes + γ = γ' // 16 bytes + δ = δ' // 3984 bytes + ``` + + Serialize $α'$ using the same format used in + [Section 8.5.2](#852-construction-steps). The remaining fields are + already fixed-length buffers and do not require further + transformation. + +5. **Transmit Packet** + + - Interpret the $\mathrm{addr}$ and $\mathrm{delay}$ extracted in + Step 1. according to the encoding format used during construction in + [Section 8.5.2](#852-construction-steps) Step 3.c. + + - Sample the actual forwarding delay from the configured delay distribution, + using the decoded mean delay value as the distribution parameter. + + - After the forwarding delay elapses, transmit the serialized packet to + the next hop address via a libp2p stream negotiated under the `"/mix/1.0.0"` + protocol identifier. + + Implementations MAY reuse an existing stream to the next hop as + described in [Section 5.5](#55-stream-management-and-multiplexing), if + doing so does not introduce any observable linkability between the + packets. + +6. **Erase State** + + - After transmission, erase all temporary values securely from memory, + including session keys, decrypted content, and routing metadata. + + - If any error occurs—such as malformed header, invalid delay, or + failed stream transmission—silently discard the packet and do not + send any error response. + +#### 8.6.4 Exit Processing + +Once the node determines its role as an exit following the steps in +[Section 8.6.2](#862-node-role-determination), it MUST perform the following +steps to interpret routing block $B$ and decrypted payload $δ'$ obtained in +[Section 8.6.1](#861-shared-preprocessing): + +1. **Parse Routing Block** + + Parse the routing block $B$ according to the $β_i$, $i = L - 1$ + construction defined in [Section 8.5.2](#852-construction-steps) step 3.c.: + + - Extract first $(tκ - 2)$ bytes of $B$ as the destination address $Δ$ + + $` + \begin{array}{l} + Δ = B_{[0\ldots(tκ - 2) - 1]} + \end{array} + `$ + +2. **Recover Padded Application Message** + + - Verify the decrypted payload $δ'$ computed in + [Section 8.6.1](#861-shared-preprocessing) step 5.: + + $` + \begin{array}{l} + δ'_{[0\ldots{κ} - 1]} \stackrel{?}{=} 0_{κ} + \end{array} + `$ + + If the check fails, discard $δ'$ and terminate processing. + + - Extract rest of the bytes of $δ'$ as the padded application message $m$: + + $` + \begin{array}{l} + m = δ'_{[κ\ldots]},\; \; \; + \text{where notation $X_{[a \ldots]}$ denotes the substring of $X$ + from byte offset $a$ to the end of the string using zero-based indexing.} + \end{array} + `$ + +3. **Extract Application Message** + + Interpret recovered $m$ according to the construction steps + defined in [Section 8.5.2](#852-construction-steps) step 1.: + + - First, unpad $m$ using the deterministic padding scheme defined during + construction. + + - Next, parse the unpadded message deterministically to extract: + + - optional spam protection proof + - zero or more SURBs + - the origin protocol codec + - the serialized application message + + - Parse and deserialize the metadata fields required for spam validation, + SURB extraction, and protocol codec identification, consistent with the + format and extensions applied by the initiator. + The application message itself MUST remain serialized. + + - If parsing fails at any stage, discard $m$ and terminate processing. + +4. **Handoff to Exit Layer** + + - Hand off the serialized application message, the origin protocol codec, and + destination address $Δ$ (extracted in step 1.) to the local Exit layer for + further processing and delivery. + + - The Exit Layer is responsible for establishing a client-only connection and + forwarding the message to the destination. Implementations MAY reuse an + existing stream to the destination, if doing so does not introduce any + observable linkability between forwarded messages.