Skip to content

Commit

Permalink
Merge branch 'release/0.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
doitian committed Sep 25, 2024
2 parents 69010ba + 8172737 commit debc6bc
Show file tree
Hide file tree
Showing 9 changed files with 762 additions and 144 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.svg -whitespace
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fiber-sphinx"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
license-file = "COPYING.md"
description = "A Rust implementation of the Sphinx mix network."
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

A Rust implementation of [Sphinx][] (a.k.a. Onion Message) for [Fiber][].

- [Specification](docs/spec.md)
- [Contributing Guidelines](docs/CONTRIBUTING.md)
- [Development](docs/development.md)

Expand Down
29 changes: 29 additions & 0 deletions docs/Fiber Sphinx Specification - Construction.excalidraw.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions docs/Fiber Sphinx Specification - Peeling.excalidraw.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
218 changes: 218 additions & 0 deletions docs/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
## Synopsis

Fiber Sphinx implements [Sphinx][Sphinx] using the same configuration as Bitcoin Lightning [BOLT#4][BOLT4]. As said in the Sphinx paper:

> Sphinx is a cryptographic message format used to relay anonymized messages within a mix network. It is more compact than any comparable scheme, and supports a full set of security features: indistinguishable replies, hiding the path length and relay position, as well as providing unlinkability for each leg of the message’s journey over the network.
[Sphinx]: http://ieeexplore.ieee.org/document/5207650/
[BOLT4]: https://github.com/lightning/bolts/blob/master/04-onion-routing.md

Fiber uses Sphinx to facilitate the transmission of messages from an initial node to a destination node by utilizing a sequence of forwarding nodes. These nodes create a singly linked list, and the message transmitted between consecutive nodes is referred to as a hop.

Intermediate nodes can verify the packet's integrity and determine the next hop for forwarding. However, they have limited knowledge of the route. They only know their predecessor and successor nodes, and cannot learn about other nodes or the route's length. Additionally, the packet is obfuscated at each hop, preventing network-level attackers from associating packets on the same route.

The route is established by the origin node, which possesses the public keys of all intermediate nodes as well as the final node. With knowledge of these public keys, the origin node is able to generate a shared secret for each intermediate node and the final node using ECDH. This shared secret is utilized to create a pseudo-random stream of bytes, which is employed to obfuscate the packet. Additionally, the shared secret is used to generate multiple keys that are used for payload encryption and HMAC computation. These HMACs are then used to ensure packet integrity at each hop in the route.

## Definition

Assume that the origin node wants to send the message $m_{v-1}$ to node $n_{v-1}$ and it has found a path $(n_0, n_1, \cdots, n_{v-1})$ where $n_0, n_1, \cdots, n_{v-2}$ are forwarding nodes. The data $m_0, m_1, \cdots, m_{v-2}$ are control messages sent to forwarding nodes where $m_i$ is the control message for the node $n_i$. The origin node can optionally attach the associated data $A$ that all the nodes can verify the integrity of the associated data $A$.

Each node $n_i$ has a secp256k1 private key $x_i$. The origin node knows the corresponding public keys $y_i = g^{x_i}$ for each node.

$L$ is a setup constant that the origin node must create a packet of length $L$ for $n_0$, and each forwarding node will construct the forwarding packet of length $L$ as well. $L$ must be larger enough to hold the messages $m_0, m_1, \cdots, m_{v-1}$ plus $32v$ bytes of HMACs for integrity verification.

## Packet Construction

### Keys Generation

Pick a random secp256k1 private key $x$ as the session key.

Compute a sequence of $v$ tuples $(\alpha_i, s_i, b_i, \mu_i, \rho_i)$ for each node $n_i$. For the first hop:

$$
\begin{array}{rl}
\alpha_0 =& g^x \\
s_0 =& y_{0}^x \\
b_0 =& h_b(\alpha_0, s_0) \\
\mu_0 =& h_\mu(s_0) \\
\rho_0 =& h_\rho(s_0) \\
\end{array}
$$

Denote the product of $b_0, b_1, \cdots, b_{i-1}$ as $\prod_{k=1}^{i-1}{b_k}$. For the following hops:

$$
\begin{array}{rl}
\alpha_i =& g^{x \prod_{k=1}^{i-1}{b_k} } \\
s_i =& y_{i}^{x \prod_{k=1}^{i-1}{b_k} } \\
b_i =& h_b(\alpha_i, s_i) \\
\mu_i =& h_\mu(s_i) \\
\rho_i =& h_\rho(s_i) \\
\end{array}
$$

The $\alpha_i$ are ephemeral secp256k1 public keys, the $s_i$ are the Diffie-Hellman shared secrets, $b_i$ are the blinding factors, $\mu_i$ are keys to compute HMACs, and $\rho_i$ are keys to generate Chacha20 cipher stream to encrypt messages. The $x\prod_{k=1}^{i-1}{b_k}$ are corresponding private keys of $\alpha_i$, and $g$ is the generator of secp256k1.

The $\alpha_i$ are secp256k1 points, the $b_i$ are integers modulo the secp256k1 prime order $q$, and $s_i, \mu_i, \rho_i$ are 32-byte binaries.

There are there hash function used in the equations: $h_b$, $h_\mu$, and $h_\rho$.

- $h_b: (\alpha_i, s_i) \to b_i$ computes SHA256 on the concatenation of the $\alpha_i$ compressed serialization and $s_i$, decodes the SHA256 result into an integer in big-endian and gets the modulus of the decoded value with $q$.
- $h_\mu: (s_i) \to \mu_i$ computes HMAC-SHA256 on $s_i$ by using 2 bytes `0x6d75` (utf8 encoding of the text "mu") as the key.
- $h_\rho: (s_i) \to \rho_i$ computes HMAC-SHA256 on $s_i$ by using 3 bytes `0x72686F` (utf8 encoding of the text "rho") as the key.

### Filler Generation

Filler is used to fill the blanks when a hop removes its message from the packet. It is generated by using the $\mu_i$ from $\mu_0$ to $\mu_{v-2}$ incrementally.

Let $\phi_0$ be an empty string. For $0 \lt i \lt v$:

$$
\phi_i = \mathsf{Chacha20}(\rho_{i-1})[(L-\lvert \phi_{i-1} \rvert)..(L + \lvert m_{i-1} \rvert)] \oplus \\{\phi_{i-1} \Vert 0_{\lvert m_{i-1} \rvert} \\}
$$

Where
- $0_b$ means the string of 0 bits of length $b$ bytes.
- $\mathsf{Chacha20}(\rho_{i-1})$ is a Chacha20 cipher stream with the key $\rho_{i-1}$ and iv $0_{12}$.
- $\mathsf{Chacha20}(\rho_{i-1})[a..b]$ means the substring of is the string of $\mathsf{Chacha20}$ consisting of bytes a (inclusively) through b (exclusively). The substring has the length $b - a$. The string index starts from 0.
- $\Vert$ denotes concatenation
- $\lvert s \rvert$ is the length of string $s$
- $a \oplus b$ applies XOR on strings $a$ and $b$.

This step is illustrated in Figure 1.

![Figure 1](Fiber%20Sphinx%20Specification%20-%20Filler%20Generation%20v2.excalidraw.svg)

### Creating a Forwarding Message

Create a Chacha20 cipher stream $P$ of length $L$ for the initial noise. The Chacha20 stream key is $h_{\mathit{pad}}(x)$, the HMAC-SHA256 on the session key $x$ by using 3 bytes `0x706164` (utf8 encoding of the text "pad") as the HMAC key. The cipher stream iv is $0_{12}$.

$$
P = \mathsf{Chacha20}(h_{\mathit{pad}}(x))
$$

Compute a sequence of $v$ tuples $(\beta_i, \gamma_i)$ for each node $n_i$ incrementally in the reverse order. For the last node $n_{v-1}$:

$$
\begin{array}{rl}
\beta' _{v-1} =& \\{m _{v-1} \Vert 0 _{32} \Vert P[0..(L-\lvert \phi _{v-1}\rvert -\lvert m _{v-1}\rvert - 32)]\\} \oplus \mathsf{Chacha20}(\rho _{v-1})[0..(L-\lvert \phi _{v-1}\rvert)] \\
\beta _{v-1} =& \beta' _{v-1} \Vert \phi _{v-1} \\
\gamma _{v-1} =& \mathsf{HMAC\textrm{-}SHA256}(\mu _{v-1}, \beta _{v-1} \Vert A)
\end{array}
$$

Where
- $0_b$ means the string of 0 bits of length $b$ bytes.
- $\mathsf{Chacha20}(\rho_{i-1})$ is a Chacha20 cipher stream with the key $\rho_{i-1}$ and iv $0_{12}$.
- $\mathsf{Chacha20}(\rho_{i-1})[a..b]$ means the substring of is the string of $\mathsf{Chacha20}$ consisting of bytes a (inclusively) through b (exclusively). The substring has the length $b - a$. The string index starts from 0.
- $\Vert$ denotes concatenation
- $\lvert s \rvert$ is the length of string $s$
- $a \oplus b$ applies XOR on strings $a$ and $b$.
- $\mathsf{HMAC\textrm{-}SHA256}(k, s)$ computes HMAC-SHA256 on the string $s$ by using $k$ as the HMAC key.

See the annotations description in the previous section Filler Generation.

For node $n_i$ that $0 \le i \lt v-1$:

$$
\begin{array}{rl}
\beta_{i} =& \\{m_{i} \Vert \gamma_{i+1} \Vert \beta_{i+1}[0..(L-\lvert m_{i} \rvert - 32)] \\} \oplus \mathsf{Chacha20}(\rho_{i})[0..L] \\
\gamma_{i} =& \mathsf{HMAC\textrm{-}SHA256}(\mu_{i}, \beta_{i} \Vert A) \\
\end{array}
$$

The forward message is the tuple $(\alpha_0, \beta_0, \gamma_0)$, and should be sent to $n_0$.

This step is illustrated in Figure 2.

![Figure 2](Fiber%20Sphinx%20Specification%20-%20Construction.excalidraw.svg)

## Peeling and Forwarding

Input: The node $n_i$ who possesses the private key $x_i$ has received the message $(\alpha_i, \beta_i, \gamma_i)$.

The node $n_i$ can get the Diffie-Hellman shared secret $s_i$ from $x_i$ and $\alpha_i$.

$$
\begin{array}{rl}
s_i =& y_{i}^{x \prod_{k=1}^{i-1}{b_k} } \\
=& {(g^{x_{i}})}^{x \prod_{k=1}^{i-1}{b_k} } \\
=& {(g^{x \prod_{k=1}^{i-1}{b_k} })}^{x_{i}} \\
=& \alpha_{i}^{x_i}
\end{array}
$$

From the shared secret, the node can derive $b_i$, $\mu_i$ and $\rho_i$:

$$
\begin{array}{rl}
b_i =& h_b(\alpha_i, s_i) \\
\mu_i =& h_\mu(s_i) \\
\rho_i =& h_\rho(s_i) \\
\end{array}
$$

Compute the HMAC of $\beta_i$ using the key $\mu_i$ and verify whether it is $\gamma_i$. If they does not match, discard the message. Otherwise, decrypt $\beta_i$ by XORing it with the output of $\mathsf{Chacha20}(\rho_i)[0..L]$.

Attention that, the decrypted content starts with $m_i$ and $\gamma_{i+1}$ but how to know the length of $m_i$ is not a part of Fiber Sphinx Specification. The applications must add their own mechanisms to get the length. For example, the message can have a fixed length or has embed the length in itself. Using the length, the node can extracted $m_i$ and $\gamma_{i+1}$ from the decrypted content. The node $n_i$ is the final node if $\gamma_{i+1}$ is $0_{32}$, otherwise it should create the forwarding message $(\alpha_{i+1}, \beta_{i+1}, \gamma_{i+1})$ for $n_{i+1}$:

- $\alpha_{i+1}$ can be derived from $\alpha_i$ and $b_i$ since $g^{x \prod_{k=1}^{i-1}{b_k} } = {g^{x \prod_{k=1}^{i-2}{b_k}}}^{b_i} = \alpha_i^{b_i}$
- Delete $m_i$ and $\gamma_{i+1}$ from the decrypted content, and append $\lvert m_i \rvert + 32$ bytes of zeros in the end. XOR the new appended $\lvert m_i \rvert + 32$ bytes with $\mathsf{Chacha20}(\rho)[L..(L+\lvert m_i \rvert+32)]$. This step will recreate the content of $\beta_{i+1}$.
- $\gamma_{i+1}$ is in the decrypted $\beta_i$.

This step is illustrated in Figure 3

![Figure%203](Fiber%20Sphinx%20Specification%20-%20Peeling.excalidraw.svg)

The Fiber Sphinx dose not define how $n_i$ knows the address of $n_{i+1}$ to send the forwarding message. Usually such information can be obtained from $m_i$.

## Returning Errors

The Sphinx protocol allows encrypted error messages to be returned to the origin node from any hop, including the final node.

The forwarding nodes and the final node must store the shared secret from the forward path and reuse it to obfuscate any corresponding return packet. In addition, each node locally stores data regarding its own sending peer in the route, so it knows where to forward the error packets.

The forwarding and final nodes both store the shared secret from the forward path. They reuse this secret to obfuscate any return packet. Additionally, each node keeps data about its own sending peer in the route, enabling it to know where to forward the error-returning packet.

The node generating the error (erring node) creates a return packet that includes two parts:

- `hmac`: 32 bytes of the HMAC authenticating the `payload`
- `payload`: variable length bytes of the failure message, usually padded to a constant size to obfuscate the message length.

The erring node then generates two keys from the shared secret:

$$
\begin{array}{rl}
\bar{\mu} _ i =& h _ \bar{\mu}(s _ i) \\
\bar{\gamma} _ i =& h _ \bar{\gamma}(s _ i) \\
\end{array}
$$

Where

- $h_\bar{\mu}: (s_i) \to \bar{\mu}_i$ computes HMAC-SHA256 on $s_i$ by using 2 bytes `0x756d` (utf8 encoding of the text "um") as the key.
- $h_\bar{\gamma}: (s_i) \to \bar{\gamma}_i$ computes HMAC-SHA256 on $s_i$ by using 3 bytes `0x616d6d6167` (utf8 encoding of the text "ammag") as the key.

Finally, the erring node computes the HMAC and encrypt the packet:

- Computes the HMAC of `payload` to get `hmac`: $\mathsf{HMAC\textrm{-}SHA256}(\bar{\mu} _{i}, \textrm{payload})$
- Concatenate `hmac` and `payload`, then XOR it with the Chacha20 stream: $\\{\textrm{hmac} \Vert \textrm{payload}\\} \oplus \mathsf{Chacha20}(\bar{\gamma}_i)$

When a node $n_{i-1}$ receives the error-returning packet $e_i$ and it is not the original node, it encrypt it with its own $\bar{\gamma}_i$:

$$
e_{i-1} = e_{i} \oplus \mathsf{Chacha20}(\bar{\gamma}_{i-1})
$$

The origin node must store the session key $x$ and the route path $(y_0, y_1, \ldots, y_{v-1})$ locally to decrypt the error packet. Using the same procedure in the section Key Generation, the node can get the keys $\bar{\mu}_i$ and $\bar{\gamma}_i$ for each hop.

The origin node must try to decrypt the message until it gets a valid `payload` and the `hmac` matches.

- Let $e$ be the error-returning packet.
- For $i$ from 0 to $v-1$:
- Let $e = e \oplus \mathsf{Chacha20}(\bar{\gamma}_{i})$
- If $e[32..\lvert e \rvert]$ is a valid error payload, and $\mathsf{HMAC\textrm{-}SHA256}(\bar{\mu} _{i}, e[32..\lvert e \rvert])$ matches $e[0..32]$, return $e[32..\lvert e \rvert]$ as the decrypted error payload, otherwise continue.

## Test Vectors

TODO
Loading

0 comments on commit debc6bc

Please sign in to comment.