Investigated Sphinx packet specification
This specification contains all details of Sphinx packet data structures and algorithms for constructing/unwrapping Sphinx packets.
Please note that this specification is exactly the same as what defined in the Sphinx paper below and what implemented by Nym: https://github.com/nymtech/sphinx.
Therefore, this document covers only an overview of Sphinx packet construction with easy-to-understand diagrams. For more details, please see the executable specification written in Python.
Sphinx Packet Construction
Header Construction
A diagram below describes how a Sphinx header is constructed.
- A Sphinx header contains encapsulated routing information.
- Given a mix route with three mix nodes $m_1, m_2, m_3$, and a mix destination $m_D$, a routing information of each $m_2, m_3, m_D$ is encapsulated in a Sphinx packet as below.
- A routing information of $m_1$ is not encapsulated in the packet because a packet sender, who chooses a mix route, always know the information of $m_1$.
Routing Information Encryption
- A routing information (
RoutingInfo
orPaddedFinalRoutingInfo
) is encrypted by XOR operation with a pseudo-random bytes generated by AES128-CTR using a private key of $m_l$ and a constant nonce. - The reason why XOR is used separated with AES128-CTR is making zero fillers can be re-appended seamlessly as if they have not been truncated.
- For details of fillers, please see Routing Information Filler Construction.
Routing Information Filler Construction
In the Header Construction diagram, two zero fillers (encrypted; colored) are appended to the EncryptedPaddedFinalRoutingInfo
to make it have the same size as EncryptedRoutingInfo
(300 bytes).
Also, when constructing a RoutingInfo
of $m_3$, the last filler is truncated. And, the remaining filler in the RoutingInfo
is encrypted again with a stream key of $m_2$ to build a new EncryptedRoutingInfo
(300 bytes).
That is, the filler is for preventing adversaries from distinguishing packets emitted from different mix layers (i.e. packet-layer undistinguishability).
The diagram below shows how fillers can be constructed and encrypted. The zero-filler
is a 60-byte x00
byte array.
Because an additional XOR is used separately with AES128-CTR for encryption, the truncated filler can be easily recovered when a mix node decrypts a EncryptedRoutingInfo
. For example,
- $m_1$ appends a zero filler to the
EncryptedRoutingInfo
, and decrypts it using XOR with the same pseudo-random bytes that was used when theEncryptedRoutingInfo
was created. - This XOR converts the zero filler appended to an encrypted-once filler (the sky-blue filler in the diagram) because of the characteristics of XOR and the position of the pseudo-random bytes that is XOR-ed.
- In the same manner, $m_2$ can also recover the blue filler and the yellow filler, which can be forwarded to the $m_3$ finally.
The more detailed explanation can be found below:
-
RND: $A_{60}B_{60}C_{60}D_{60}E_{60}F_{60} \leftarrow \text{PRNG}(key)$
\[\text{PRNG}(key) = \text{AES128CTR}(key, iv_{const}, 0_{60}0_{60}0_{60}0_{60}0_{60}0_{60})\] - Filler (60*2 bytes): $(F \oplus E)F$
- encrypted by $m_1$: $XXX$
- and, $m_1$ adds the filler: $XXX \rightarrow XXX(F \oplus E)F$
- encrypted by $m_2$: $YX’X’X’(F \oplus E \oplus E)=YX’X’X’F$
-
encrypted by $m_3$: $ZY’X’X’X’$
- $m_1$ decrypts: $ZY’X’X’X’0 \rightarrow M_2YXXXF$
- So, $YXXXF$ is passed to $m_2$ because $m_1$ truncates $M_2$ that is the $m_2$ info.
- $m_2$ decrypts: $YXXXF0 \rightarrow M_3XXX(F \oplus E)F$
- So, $XXX(F \oplus E)F$ is passed to $m_3$ because $m_2$ truncates $M_3$ that is the $m_3$ info.
- $m_3$ removes the filler: $XXX(F \oplus E)F \rightarrow XXX$
- and decrypts $XXX$
Payload Construction
Payload construction is simpler than the header construction.
A payload is leading/trailing padded as below:
padded_payload = zero_bytes(16) + payload + b"\x01" + zero_bytes(T)
The T
is determined to make the size of padded_payload
1024 bytes.
If payload
is too large to make padded_payload
1024 bytes, the algorithm fails.
The padded_payload
is layered-encrypted by Lioness with ChaCha20 and Blake2b using a payload key of each $m_1, m_2$ and $m_3$.