mirror of
https://github.com/logos-storage/transport-over-mix.git
synced 2026-07-28 13:53:13 +00:00
improved documentation
This commit is contained in:
parent
bc723fb4e7
commit
2097378dbc
@ -1,122 +0,0 @@
|
||||
Transport layer abstraction over Mix
|
||||
------------------------------------
|
||||
|
||||
Mix supports the following basic communication pattern:
|
||||
|
||||
- sending a fixed size packet (approx 4kb) to someone with a public IP address, but hiding the sender's identity
|
||||
- optionally (?) getting back a reply of the same size (via a SURB)
|
||||
|
||||
This is not enough for many (most?) real-world use cases:
|
||||
|
||||
- we want to send larger messages
|
||||
- we want to get back larger replies
|
||||
- we may want "acknowledge" messages when the full larger message is received intact
|
||||
- some form of message integrity check would be useful
|
||||
- we may not know the size of reply a priori
|
||||
- we may want to allow the reply being a stream
|
||||
- we may want to get back multiple, time-separated replies
|
||||
- we may want socket-like behaviour
|
||||
- as Mix is expected to lose at least a small fraction of packets, it would be nice to have some built-in fault-tolerance
|
||||
|
||||
While mix already has very rudimentary support for chunking up an outgoing message, it doesn't yet support all these more sophisticated use cases.
|
||||
|
||||
Our suggestion is to build an "application-level" transport abstraction layer to handle all the above.
|
||||
|
||||
### Basic workflow
|
||||
|
||||
The basic workflow of how something like this could work is pretty straightforward:
|
||||
|
||||
1. The user wanting to communicate with a remote party first generates a unique (random?) session id
|
||||
2. The user takes their message payload, generates a number of SURB paths, and bundles the message and the SURBs into a single bytestring, from which the list of surbs, the payload, and the message type ("initial payload") can be reconstructed.
|
||||
3. A checksum of this extended payload is added
|
||||
4. The user erasure codes this bytestring into uniform chunks (of the maximum mix packet size minus a fixed header), with a configurable overhead (eg. 10% extra size for redundancy)
|
||||
5. For each erasure coded chunk, a header consisting of the session id, a message sequence number (eg. 1st message in a socket-like abstraction), an EC chunk index, and the erasure coding parameters are prepended.
|
||||
6. The resulting extended chunks are sent over Mix using different paths
|
||||
|
||||
Now on the receiver side:
|
||||
|
||||
1. The receipent starts to get some of the above mix packets
|
||||
2. They need to identify that the packets are sent according this protocol. This should be probably encoded in the Mix final destination address (eg. it could be in the destination protocol byte?)
|
||||
3. After identified as such, as each decoded packet contains the session id and message sequence number, they can put them into buckets according to those
|
||||
4. As each of those also includes the erasure coding parameters, they know when enough packets arrived for decoding
|
||||
5. Each EC chunk also include their chunk index, so they can decode the original extended payload
|
||||
6. They can check integrity be recomputing and comparing the checksum
|
||||
7. Parse the extended payload into the original message, message type, and set of SURBs
|
||||
8. Do whatever they want the payload, and reply using the the above (sender) protocol, but using the SURBs
|
||||
|
||||
Note that maybe there aren't enough SURBs to send the reply. In this case, they can construct a partial reply, which also include a request for more SURBs. The encoding of this must be also standardized.
|
||||
|
||||
While the original sender doesn't need SURBs for themselves, because they know the receiver's IP address, this situation will change when we also introduce hidden services, so both parties are anonymous. For this reason, it's better to make the package formats symmetric from the start, and include the request for more SURBs in the initial message too (but it can be set to zero).
|
||||
|
||||
### Mix abstraction
|
||||
|
||||
At first approximation, we assume the following things from Mix:
|
||||
|
||||
- Mix can deliver fixed sized packets (of size `M` bytes, where `3072 <= M < 65536` is relatively small) to some destination address (which can be an IP address or later maybe a pseudonymous identity)
|
||||
- a destination address can also be a SURB, which we just assume an opaque blob of say `S <= 512` bytes.
|
||||
- users can generate an arbitrary number of fresh SURBs, whose final destination is themselves
|
||||
- packet delivery is not guaranteed (but expected with some probability, eg. 95%)
|
||||
|
||||
Note that in practice, Mix may provide some other guarantees, eg. message integrity (except that the implementations may mess that up...).
|
||||
|
||||
We also assume that the destination server also has a public key.
|
||||
|
||||
### Abstract data encoding
|
||||
|
||||
We use Haskell syntax to specify our data structures.
|
||||
|
||||
-- what the parties want to actually send
|
||||
type Payload = ByteString
|
||||
|
||||
-- we use an ephemeral public key as the session id
|
||||
type SessionId = PublicKey
|
||||
|
||||
-- message sequence inside a session
|
||||
type SequenceNumber = Int
|
||||
|
||||
type ChunkIdx = Int
|
||||
|
||||
-- the erasure coding algorithm
|
||||
-- encodes K chunks into N chunks
|
||||
data ECParams = MkECParams
|
||||
{ ecK :: Int
|
||||
, ecN :: Int
|
||||
}
|
||||
|
||||
-- we assume fixed size of `S` bytes
|
||||
type SURB = ByteString
|
||||
|
||||
data Destination
|
||||
= IPAddress String
|
||||
| SingleUse SURB
|
||||
| Pseudonym PublicKey
|
||||
|
||||
data MessageType
|
||||
= IntialMsg
|
||||
| FinalReply
|
||||
| Streaming -- ??
|
||||
...
|
||||
|
||||
data ExtendedPayload = MkExtPayload
|
||||
{ msgType :: MessageType
|
||||
, requestAck :: Bool
|
||||
, requestSurbs :: Int
|
||||
, surbs :: [Surb]
|
||||
, payload :: Payload
|
||||
}
|
||||
|
||||
type CheckSum = HMAC
|
||||
|
||||
type ECChunk = ByteString
|
||||
|
||||
data ExtendedChunk = MkExtChunk
|
||||
{ sessionId :: SessionId
|
||||
, seqNum :: SequenceNumber
|
||||
, chunkIdx :: ChunkIdx
|
||||
, ecParams :: ECParams
|
||||
, chunk :: ECChunk
|
||||
}
|
||||
|
||||
|
||||
### Concrete encoding (wire format)
|
||||
|
||||
190
docs/sphinx/SphinxAbstract.md
Normal file
190
docs/sphinx/SphinxAbstract.md
Normal file
@ -0,0 +1,190 @@
|
||||
Abstract Sphinx Description
|
||||
---------------------------
|
||||
|
||||
It turns out that if we abstract away the details, and consider addresses to be just arbitrary data, describing the Sphinx packet construction becomes much simpler and more intuitive (and also more general).
|
||||
|
||||
### Security parameters
|
||||
|
||||
Let $\lambda=128$ be the targeted security level in bits, and $\kappa = \lambda/8 = 16$ the same but measured in bytes.
|
||||
|
||||
Let $r\in\mathbb{N}$ denote the maximum number of hops, and $1\le\ell\le r$ the actual number of hops (mix nodes in the path).
|
||||
|
||||
### Sphinx packets
|
||||
|
||||
A Sphinx packet is a pair $(\mathcal{H},\delta)$ where $\mathcal{H}=(\alpha,\beta,\gamma)$ is the header, and $\delta$ is the (encrypted) payload. Each of the four component of the packet must be constant size: $|\alpha|=2\kappa$, $|\beta|=N_\beta$, $|\gamma|=\kappa$, and $|\delta|=N_\delta$. For example, if we have 16-byte mix node addresses and 48 byte destination addresses, then $N_\beta=176$ bytes, and thus the overhead of the header is $|\mathcal{H}|=224$ bytes.
|
||||
|
||||
The header construction is the interesting part; the payload is simply encrypted in multiple layers, each hop removing (or adding, in case of reply messages) one layer of encryption.
|
||||
|
||||
#### Message integrity
|
||||
|
||||
Note: Since for reply messages we need to create the headers before the actual message payload is known (resulting in SURBs, "Single Use Reply Blocks"), the header and the payload are handled separately.
|
||||
|
||||
This is a problem because if header and payload are decoupled, how to guarantee message integerity? Sphinx solves this rather elegantly: The payload MUST be encrypted using a so-called pseudo-random-permutation (PRP):
|
||||
|
||||
$$
|
||||
\pi \;:\; K\times \{0,1\}^{N_\delta} \;\to
|
||||
\{0,1\}^{N_\delta}
|
||||
$$
|
||||
|
||||
which is practically indistinguishable from a random permutation. In particular, even if a single bit of the input is flipped, one would expect approximately half of the bits flip in the output - it should completely scramble the input. The original Sphinx paper recommends using the Lioness "wide-block cipher" for this purpose (with the whole payload consisting a single "wide" block!).
|
||||
|
||||
Then, if the payload is prepended by $\kappa$ zero bytes, if any mix node (or middle-man) tries to tamper with it, the final decrypted payload will have $2^{-128}$, that is, negligible chance for these zero bytes to reaming intact.
|
||||
|
||||
### Computing the shared secrets
|
||||
|
||||
Sphinx uses layered encryption while the packet travels through the mix network; for this we need to derive a (different) shared secret for each mix node.
|
||||
|
||||
We assume all mix nodes have a (relatively) long-term private-public keypair $(\mathsf{sk}_i,\mathsf{pk}_i)$. Note: In practice, these keys are usually rotated with some frequency, which can be for example daily, weekly or monthly; but we assume we know the relevant public keys.
|
||||
|
||||
The user creating the header first generates an ephemeral secret key $x$; in practice this is just a random number $x\in\mathbb{Z}_q^\times$. It can then iteratively derive a sequence of keys (one set per hop) consisting of:
|
||||
|
||||
- a per-node secret key $x_i\in\mathbb{Z}_q^\times$
|
||||
- a per-node public key $\alpha_i:=x_i*\mathbf{g}\in \mathbb{G}$
|
||||
- a per-node shared secret $s_i\in \mathbb{G}$, derived using Diffie-Hellman: $s_i:=x_i*\mathsf{pk}_i = \mathsf{sk}_i*\alpha_i$
|
||||
- a blinding factor $b_i\in\mathbb{Z}_q^\times$, computed from $\mathsf{KDF}_{\mathsf{blind}}(\alpha_i,s_i)$
|
||||
|
||||
Remark: The blinding factor is supposed to be uniform in $\mathbb{Z}_q^\times$ (recall that $q\approx 2^{256}$). However it's not a big deal if it's a little bit bigger, or if there is a tiny bias from uniformity; hence implementations however usually simply say $b_i:=H(\alpha_i,s_i)\in [0,2^{256}-1]$ where $H$ is eg. SHA256. Same care should be taken in an actual implementation to avoid possible corner cases (?).
|
||||
|
||||
The secret key sequence is then defined iteratively:
|
||||
|
||||
- $x_0:=x$
|
||||
- $x_{i+1}:=b_i\cdot x_i$
|
||||
|
||||
All the rest can be computed from $x_i$:
|
||||
|
||||
- the public key is $\alpha_i = x_i * \mathbf{g}$
|
||||
- the shared secret is $s_i=x_i*\mathsf{pk}_i$
|
||||
- and the blinding factor is $b_i=H(\alpha_i,s_i)$
|
||||
|
||||
The idea behind this construction is that each hop will have a unique ephemeral sender public key, from which a shared secret can be derived, and then using a KDF further symmetric keys are derived from that ($m_i$ for MAC, $e_i$ for header encryption and $p_i$ for payload encryption). When composing the forwarded packet for the next hop, the node can then "tweak" this public key using their own blinding factor: $\alpha_{i+1}=b_i * \alpha_i$.
|
||||
|
||||
Thus each hop can only decrypt their own header, and if they try to break the protocol, the message will be ruined.
|
||||
|
||||
Sidenote: With X25519, not all 32-byte integers are valid secret keys. However, we cannot simply apply the standard "masking" algorithm to the blinded private key, because the mix nodes doing the processing won't have access to the secret keys. At least the cofactor 8 subgroup is kept invariant by multiplication. I think that the remaining of the mask is only there to ensure uniformity of randomly generated secret keys; so this is probably still OK at the end.
|
||||
|
||||
### Sphinx headers
|
||||
|
||||
A Sphinx header $\mathcal{H}$ consist of a triple $\mathcal{H}=(\alpha,\beta,\gamma)$, where
|
||||
|
||||
- $\alpha$ is a (blinded) public key (that is, a group element; $\alpha\in\mathbb{G})$.
|
||||
- $\beta$ is the (encrypted) routing information
|
||||
- $\gamma$ is a MAC (message authentication code) of $\beta$
|
||||
|
||||
The header must have a fixed size of $N_{\mathcal{H}}$ bytes. As $|\alpha|=2\kappa$ and $|\gamma| = \kappa$, we have $N_{\mathcal{H}} = N_{\beta} + 3\kappa$, where $N_\beta := |\beta|$ is the size of the encrypted routing info.
|
||||
|
||||
Let $(A_0,A_1,\dots,A_{\ell-1},A_\ell=\Delta)$ be a mix path of length $\ell$, that is, a sequence of $\ell+1 \le r$ generalized addresses. $A_0$ is the "entry node", the mix node we will send the packet to; $A_{\ell-1}$ is the "exit node", the final mix node; and $A_\ell=\Delta$ is the final destination. These can have different types and even different sizes, as long as:
|
||||
|
||||
- they are prefix-parseable
|
||||
- the total size fits in the header
|
||||
|
||||
The routing information $\beta:=\beta_0$ will contain the sequence of addresses and MACs:
|
||||
|
||||
$$(A_1,\gamma_1,A_2,\gamma_2,A_3,\gamma_3,\;\dots\;, A_{\ell-1},\gamma_{\ell-1},A_{\ell})$$
|
||||
|
||||
but encrypted in several layers (note that there is one less MAC than addresses!). The first address $A_0$ we directly send to, and the first MAC $\gamma_0$ is stored in the "$\gamma$" section of the header (while the above sequence is encoded in "$\beta$", the routing piece).
|
||||
|
||||
Hence the size condition means that:
|
||||
|
||||
$$
|
||||
\underbrace{(\ell-1)\kappa}_{\textrm{MACs}} \;+\; \underbrace{\sum_{i=1}^{\ell} |A_i|}_{\textrm{addresses}} \;\;\le\;\; N_\beta \;=\; N_\mathcal{H} - 3\kappa
|
||||
$$
|
||||
|
||||
that is,
|
||||
|
||||
$$
|
||||
\sum_{i=1}^{\ell} |A_i| \; \le \; |\mathcal{H}| - (\ell+2)\kappa
|
||||
$$
|
||||
|
||||
Apart from this, we have no restriction on the addresses.
|
||||
|
||||
### Constructing Sphinx headers
|
||||
|
||||
We construct a Sphinx header iteratively, going backwards from the last hop.
|
||||
|
||||
The routing info for the final hop is
|
||||
|
||||
$$
|
||||
\beta_{\ell-1} := \mathsf{encdec}_{\ell-1} \big(\, \Delta \;\|\; 0^{\mathrm{pad}} \,\big) \;\big\|\; \phi_{\ell-1}
|
||||
$$
|
||||
|
||||
where $\Delta = A_\ell$ is the final destination;
|
||||
|
||||
$$\mathsf{encdec}_{i}(x)\;:=\; x \oplus \mathsf{STREAM}(e_i)$$
|
||||
|
||||
is a XOR-based stream cipher using the encryption key $e_i$ derived for the $i$-th hop; and $\phi_{\ell-1}$ is a "filler string", to be defined below. Note that the filler string is _not encrypted_ (in fact it will be encrypted, but using the previous key $e_{\ell-2}$). The required padding length can be then computed easily as $\mathrm{pad} := N_\beta - |\Delta| - |\phi_{\ell-1}|$.
|
||||
|
||||
The header of the final hop will be then simply $\mathcal{H}_{\ell-1} = (\alpha_{\ell-1}, \beta_{\ell-1}, \gamma_{\ell-1})$ where $\gamma_{\ell-1}:=\mathsf{MAC}_{\ell-1}(\beta_{\ell-1})$ is the MAC of $\beta$ (using the corresponding MAC key).
|
||||
|
||||
To construct an earlier header $i<\ell-1$, we can form
|
||||
|
||||
$$
|
||||
\beta_{i} := \mathsf{encdec}_{i}\big[\, A_{i+1} \;\|\; \gamma_{i+1} \;\|\; \mathsf{trunc}(\beta_{i+1}) \,\big]
|
||||
$$
|
||||
|
||||
where $\mathsf{trunc}()$ simply truncates to the required length $N_\beta - |A_{i+1}| - |\gamma_{i+1}|$.
|
||||
|
||||
As before, then $\gamma_{i} := \mathsf{MAC_{i}}(\beta_{i})$ is the corresponding MAC, and then the header is $\mathcal{H}(\alpha_i,\beta_i,\gamma_i)$.
|
||||
|
||||
### Filler strings
|
||||
|
||||
Expanding the above construction, the routing info $\beta$ in the intitial header looks like this (see also the illustration below):
|
||||
|
||||
$$
|
||||
\beta_0 =
|
||||
\mathsf{E}_0\Big[ A_1 \big\| \gamma_1 \big\|
|
||||
\mathsf{E}_1\Big[ A_2 \big\| \gamma_2 \big\|
|
||||
\mathsf{E}_2\Big[ A_3 \big\| \gamma_3 \big\| \cdots
|
||||
\mathsf{E}_{\ell-1}\Big[ \Delta \big\| \mathsf{pad} \Big]\cdots \Big]\Big]\Big]
|
||||
$$
|
||||
|
||||
where we use the shorthand $\mathsf{E}_i:=\mathsf{encdec}_i$ so that it fits into a line.
|
||||
|
||||
However, when this get processed by the first mix node (with address $A_0$), the first layer of encyption and also the address and MAC $A_1,\gamma_1$ are removed, resulting in:
|
||||
|
||||
$$
|
||||
\beta'=
|
||||
\mathsf{E}_1\Big[ A_2 \big\| \gamma_2 \big\|
|
||||
\mathsf{E}_2\Big[ A_3 \big\| \gamma_3 \big\| \cdots
|
||||
\mathsf{E}_{\ell-1}\Big[ \Delta \big\| \mathsf{pad} \Big]\cdots \Big]\Big]
|
||||
$$
|
||||
|
||||
We now have two problems: This is shorter than the required length $N_\beta$ (the difference is of course $|A_1|+|\gamma_1|$ bytes), and furthermore we cannot just pad it with arbitrary bytes, because then the MAC $\gamma_1$ won't match when checked by the next node.
|
||||
|
||||
So we have to construct the intermediate headers in such a way, that each node can reconstruct the last bytes of the next header exactly. Sphinx solves this by the following construction of "filler strings" $\phi_0,\dots, \phi_{\ell-1}$:
|
||||
|
||||
$$
|
||||
\begin{align*}
|
||||
\phi_0 &:= \emptyset \\
|
||||
\phi_i &:= S_i\oplus \Big[\, \phi_{i-1} \;\|\;0^{|A_i|+|\gamma_i|} \,\Big] \\
|
||||
S_i &:= \mathsf{STREAM}(e_{i-1})_{U_i\dots V_i-1} \\
|
||||
U_i &:= N_\beta-|\phi_{i-1}| \\
|
||||
V_i &:= N_\beta+|A_i|+|\gamma_i|
|
||||
\end{align*}
|
||||
$$
|
||||
|
||||
Here the funny thing in the definition of $\phi_i$ means basically the stream cipher $\mathsf{encdec}_{i-1}$ (with key $e_{i-1}!)$, except that it's _shifted_: The separator between $\phi_{i-1}$ and the zero bytes $0^{|A_i|+|\gamma_i|}$ should fall exactly at the $N_\beta$ position.
|
||||
|
||||
Only the last filler, $\phi_{\ell-1}$ is actually used to construct the header, but in fact $\phi_i$ are the last few bytes of the changing headers $\mathcal{H}_i$, as they are processed and forwarded by the mix nodes.
|
||||
|
||||
Now when the $i-1$-th node receives its $\beta_{i-1}$, then if before decryption with $\mathsf{encdec}_i$, they append $|A_i|+|\gamma_i|$ zero bytes, then after decryption and removing $A_i$ and $\gamma_i$ from the beginning, the result is exactly $\beta_i$, which they can use to construct the next hop's header.
|
||||
|
||||
### Illustration of the process
|
||||
|
||||
Given a mix path $(A_0,A_1,A_2,A_3)$ consisting of 4 mix nodes, with a final destination $\Delta$, the $\beta$ part of the header looks like this for the four nodes (omitting the layered encryption for brevity):
|
||||
|
||||
+------------+------------+------------+-------+-----+
|
||||
| Addr1 | M1 | Addr2 | M2 | Addr3 | M3 | Delta | pad | <-- key0
|
||||
+------------+------------+------------+-------+-----+
|
||||
|
||||
+------------+------------+-------+-----+------------+
|
||||
| Addr2 | M2 | Addr3 | M3 | Delta | pad | filler1 | <-- key1
|
||||
+------------+------------+-------+-----+------------+
|
||||
|
||||
+------------+-------+-----+------------+------------+
|
||||
| Addr3 | M3 | Delta | pad | filler2 | <-- key2
|
||||
+------------+-------+-----+------------+------------+
|
||||
|
||||
+-------+-----+------------+------------+------------+
|
||||
| Delta | pad | filler3 | <-- key3
|
||||
+-------+-----+------------+------------+------------+
|
||||
|
||||
@ -81,7 +81,7 @@ Remark \#2: With X25519, not all scalar field elements are valid secret keys. Ho
|
||||
The key sequence is defined iteratively:
|
||||
|
||||
- $x_0:=x$
|
||||
- $x_{i+1}:=\mathsf{mask}(b_i\cdot x_i)$
|
||||
- $x_{i+1}:=b_i\cdot x_i$
|
||||
|
||||
All the rest can be computed from $x_i$:
|
||||
|
||||
@ -97,7 +97,7 @@ Thus each hop can only decrypt their own header, and if they try to break the pr
|
||||
|
||||
The size of the mix header must be constant (otherwise, mix nodes could guess where they are in the path), and its processing uniform (except for the final hop).
|
||||
|
||||
With the usual parameters, we have $|\alpha|=32$ and $|\gamma|=16$. Thus $N_\beta:=|\beta|$ determines the header size. This must be big enough to fit $(r-1)$ mix node addresses $A_i$, and also $(r-1)$ MACs $\gamma_i$ (both are the set $i>0)$, and the destination address and message id pair $(\Delta,J)$.
|
||||
With the usual parameters, we have $|\alpha|=32$ and $|\gamma|=16$. Thus $N_\beta:=|\beta|$ determines the header size. This must be big enough to fit $(r-1)$ mix node addresses $A_i$ and MACs $\gamma_i$ (for $1\le i<r$), and the destination address and message id pair $(\Delta,J)$.
|
||||
|
||||
In the original Sphinx paper, we have
|
||||
|
||||
370
docs/transport/TransportSpec.md
Normal file
370
docs/transport/TransportSpec.md
Normal file
@ -0,0 +1,370 @@
|
||||
Transport layer over Mix - draft spec
|
||||
-------------------------------------
|
||||
|
||||
### Motivation
|
||||
|
||||
Mix supports the following basic communication pattern:
|
||||
|
||||
- sending a fixed size packet (currently about 4kb) to someone with a public IP address, but (hopefully) hiding the sender's identity;
|
||||
- optionally getting back a reply of the same size (via a SURB, "Single Use Reply Block"), while still staying anonymous (at least most of the time).
|
||||
- there is no guarantee of delivery or ordering
|
||||
- however, as long as both endpoints is "part of the Mix" (so no Tor-like exit relays), we can assume that packets an encrypted, and their integrity is protected
|
||||
|
||||
This is simply not enough for most application, in particular definitely not enough for Logos Storage, where an anonymous user wants to download (and possibly also upload) large files.
|
||||
|
||||
Hence this proposal for an abstraction layer which sits between "raw" Mix and the application, allowing for a more conventional communication interface.
|
||||
|
||||
### Socket abstraction
|
||||
|
||||
The application would use the transport layer abstraction, which should mostly hide the fact that there is an underlying mixnet.
|
||||
|
||||
As most network communication already uses a socket API, it seems to be a natural fit to offer a similar abstraction here too.
|
||||
|
||||
A socket is a bidirectional channel of streams, into which both parties can send information (bytes), and which is assumed to be ordered and more-or-less reliable.
|
||||
|
||||
This abstraction is very simple:
|
||||
|
||||
- one party (typically the client) opens a _session_ with another party (typically the server)
|
||||
- both parties can send data to the other party, without waiting for any synchronization to happen
|
||||
- finally the session can be closed (either manually or by some automation, eg. a timeout)
|
||||
|
||||
Probably the biggest difference between our proposal and usual Unix sockets is that the streams are not _streams of bytes_, but streams of _larger messages_ (still consisting bytes) instead. This fits the mechanics of the underlying Mix network much better.
|
||||
|
||||
### Transport layer API
|
||||
|
||||
In pseudocode (using Haskell syntax), the API could look something like this:
|
||||
|
||||
```haskell
|
||||
data Result a
|
||||
= Success a
|
||||
| Failure String
|
||||
|
||||
type IOResult a = IO (Result a)
|
||||
|
||||
-- session ids are 16 bytes, and assumed to be random
|
||||
type SessionId = ByteString
|
||||
|
||||
-- options for a connection
|
||||
data ConnectionOpts
|
||||
{ sessionTimeout :: TimeInterval
|
||||
, messageTimeout :: TimeInterval
|
||||
, messageRedundancy :: ...
|
||||
, ...
|
||||
}
|
||||
|
||||
openConnection
|
||||
:: ConnectionOpts
|
||||
-> ApplicationProtocol
|
||||
-> Either SURB NetworkAddress
|
||||
-> IOResult SessionId
|
||||
|
||||
closeConnection :: SessionId -> IOResult ()
|
||||
|
||||
-- messages are just blobs of bytes
|
||||
type Message = [Byte]
|
||||
|
||||
-- message indices are just serial numbers 0,1,2...
|
||||
type MsgIdx = Int
|
||||
|
||||
sendMessage :: SessionId -> Message -> IOResult MsgIdx
|
||||
|
||||
-- we can poll if a new message was received
|
||||
--
|
||||
-- (alternatively, a callback could be installed;
|
||||
-- we leave that out here for simplicty)
|
||||
poll :: SessionId -> IOResult (Maybe Message)
|
||||
|
||||
-- we can query the status of the connection
|
||||
--
|
||||
-- (eg. number of un-sent messages, un-acknowledged messages
|
||||
-- we sent, partially received messages, number of SURBs, etc)
|
||||
getConnectionState :: SessionId -> IOResult ConnectionStatus
|
||||
|
||||
-- we can check whether a sent message was acknowledged by the other party
|
||||
isAcknowledged :: SessionId -> MsgIdx -> IOResult Bool
|
||||
|
||||
-- if we know our expected communication pattern,
|
||||
-- eg. for example that we want ot receive a large amount
|
||||
-- of data, then for example we can pro-actively send more
|
||||
-- SURBs
|
||||
provideHints :: SessionId -> Hint -> IOResult ()
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
To initiate a connection, we need either a network address, or a SURB to the other party.
|
||||
|
||||
SURBs are Mix headers prepared by the other party, which can be used to send them Mix packets without revealing their network address (note though that a patient attacker can always reveal the real IP behind the SURB with some non-negligible probability! So this is only really useful for communication with ephemeral identities...).
|
||||
|
||||
A SURB has three components:
|
||||
|
||||
- a Sphinx header
|
||||
- a network address (the first hop in the Mix path, where the packet should be sent)
|
||||
- and an ephemeral symmetric key (this protects the message against the first hop)
|
||||
|
||||
As at least one of the parties want to stay anonymous, they can only receive packets via SURBs. Each SURB can be only used once, hence we need to manage their supply. The main tasks of the transport layer is then to:
|
||||
|
||||
- chunk large messages into Mix packets (preferably with some redundancy using erasure coding)
|
||||
- assemble the received Mix packets back into messages (taking also care of the ordering)
|
||||
- manage the supply of SURBs, so that ideally they never run out
|
||||
- handle acknowledgments and resend lost messages
|
||||
|
||||
So, the transport layer needs keep the current state of:
|
||||
|
||||
- open connections
|
||||
- partially received messages
|
||||
- sent but un-acknowledged messages
|
||||
- supply of SURBs (if the other party is anonymous)
|
||||
|
||||
|
||||
### Chunking
|
||||
|
||||
Given a message $\mathsf{msg}$ of an arbitrary size, we need to chunk it up to uniform sized pieces or "chunks", and send those to the other party via different mix routes (in the payload of mix packets). The other party then needs to recognize these as pieces of the same message, and reconstruct the message (to be processed by the application layer).
|
||||
|
||||
As in Mix, we expect some percentage of the packets not arriving the destination, and because the latency is high, we add redundancy by employing [erasure coding](https://en.wikipedia.org/wiki/Erasure_code) together with the chunking, so that any $K$ packets out of the $N$ transmitted packets can reconstruct the message.
|
||||
|
||||
Each transport packet will then look like:
|
||||
|
||||
- Sphinx header
|
||||
- Sphinx payload
|
||||
- Sphinx integrity check (16 zeros bytes)
|
||||
- message metadata:
|
||||
- session id (16 bytes, random)
|
||||
- message index (4 bytes, sequential numbering)
|
||||
- number of original chunks $K$ (2 bytes)
|
||||
- total number of redundant chunks $N$ (2 bytes)
|
||||
- chunk index (2 bytes; a number `0 <= idx < N`)
|
||||
- raw chunk data (fixed size)
|
||||
|
||||
Remarks:
|
||||
|
||||
- We use network byte ordering for wire format, that is, numbers are represented as big-endian sequence of bytes.
|
||||
- We plan to use the [fast erasure coding library `leopard`](https://github.com/catid/leopard/) for EC. This has a limitation of $1 < K < N < 2^{16}$ (and also $N \le 2K$), which justifies using 16 bit numbers for the chunk indices.
|
||||
- In an ideal world, the total number of (redundant) chunks $N$ wouldn't be necessary to include (as we only need _any_ $K$ packets; an implementation in theory could just generate an infinite stream of new parity chunks); however, this is not true for the Leopard implementation. As the overhead is minimal, maybe it's best to include it (another solution would be a deterministic function $N(K)$; but that's less flexible).
|
||||
- If the original message fits into a single chunk, we don't do erasure coding; instead if necessary, we just re-transmit it.
|
||||
|
||||
#### Payload encoding
|
||||
|
||||
To encode and chunk a raw payload (a sequence of bytes):
|
||||
|
||||
1. we first prepend a header;
|
||||
2. then pad it to the next multiple of the raw chunk size $R$ (which is the raw Sphinx payload size minus `42 = 16 + 26 + 2` bytes);
|
||||
3. split it into $K = \mathsf{length} / R$ pieces;
|
||||
4. erasure code the pieces into $N$;
|
||||
5. finally, prepend the chunk header (message metadata and chunk index).
|
||||
|
||||
The payload header consists of:
|
||||
|
||||
- payload header version number (1 byte)
|
||||
- length of the raw payload in bytes (8 bytes)
|
||||
- additional payload info depending on the version
|
||||
|
||||
The additional info can be:
|
||||
|
||||
- empty (payload version `0x00`)
|
||||
- SHA256 checksum, truncated to 16 bytes (payload version `0x01`)
|
||||
- HMAC-SHA256 checksum, truncated to 16 bytes (payload version `0x02`; requires a shared secret between sender and recipient)
|
||||
|
||||
Note: Ideally, Mix should provide message integrity. However, if we are paranoid, we can add extra integrity check here.
|
||||
|
||||
|
||||
### Transport message format
|
||||
|
||||
The actual messages need to contain not only what the user intends to send (the payload), but also information needed by the transport layer itself. Hence, a transport message will have three parts:
|
||||
|
||||
- administration (eg. acknowledgments)
|
||||
- fresh SURBs
|
||||
- the user's payload
|
||||
|
||||
The administrative part will contain the following:
|
||||
|
||||
- session control
|
||||
- acknowledgements
|
||||
- request for fresh SURBs
|
||||
|
||||
In Haskell code:
|
||||
|
||||
```Haskell
|
||||
data SessionControl
|
||||
= InitialMessage AppProtocol -- the first message estabilishing a connection
|
||||
| Continue -- proceed normally
|
||||
| CloseSession -- closing down the session
|
||||
|
||||
data AppProtocol = MkAppProtocol
|
||||
{ appProtocolName :: String
|
||||
, appProtocolInfo :: ByteString
|
||||
}
|
||||
|
||||
data AckEntry
|
||||
= MsgReceived MsgIdx -- message received intact
|
||||
| MsgFailed MsgIdx FailReason -- message failed (eg. decoding, checksum or timeout)
|
||||
| NeedMoreChunks MsgIdx ChunkIdx -- we need more chunks
|
||||
| ResendChunks MsgIdx [ChunkIdx] -- re-transmit particular chunks
|
||||
|
||||
data FailReason
|
||||
= FailTimeout -- couldn't decode within the expected time
|
||||
| FailDecode -- erasure code decoding failed
|
||||
| FailChecksum -- checksum failed
|
||||
|
||||
data MsgMeta = MkMsgMeta
|
||||
{ msgControl :: SessionControl -- where we are in the session
|
||||
, msgReqSURBs :: Int -- how many more SURBs we need
|
||||
, msgAcks :: [AckEntry] -- status of previous messages
|
||||
}
|
||||
|
||||
data Message = MkMessage
|
||||
{ msgMeta :: MsgMeta -- message metadata
|
||||
, msgSURBs :: [SURB] -- the new SURBs we send
|
||||
, msgPayload :: ByteString -- the actual payload
|
||||
}
|
||||
```
|
||||
|
||||
#### Wire format
|
||||
|
||||
Transport message:
|
||||
|
||||
- transport layer version (1 byte; for this version `0x01`)
|
||||
- session control (1 byte, enum)
|
||||
- if it was the initial message, then the application protocol:
|
||||
- application layer protocol name, as a string (1 byte length, followed by that many bytes)
|
||||
- protocol-dependent additional info (4 byte length, followed by that many bytes); can be empty.
|
||||
- number of fresh SURBs we request (2 bytes)
|
||||
- acknowledges:
|
||||
- number of acknowledge entries (2 bytes)
|
||||
- list of acknowledge entries
|
||||
- fresh SURBs we send:
|
||||
- number of SURBs included (2 bytes)
|
||||
- the size of a single Sphinx header (2 bytes)
|
||||
- list of raw SURBs
|
||||
- payload
|
||||
|
||||
Acknowledge entries:
|
||||
|
||||
- entry type (1 byte, enum)
|
||||
- message index (2 bytes, int)
|
||||
- depending on entry type value:
|
||||
- `0x00 = MsgReceived`: empty
|
||||
- `0x01 = MsgFailed`: failure reason (1 byte, enum) - `0x02 = NeedMoreChunks`: number of chunks requested (2 bytes, int)
|
||||
- `0x03 = ResendChunks`:
|
||||
- number of chunks to retransmit (2 bytes, int)
|
||||
- that many chunk indices (2 bytes each)
|
||||
|
||||
SURB entries:
|
||||
|
||||
- network address of the first hop
|
||||
- address type (1 byte, enum)
|
||||
- address length (1 byte, int)
|
||||
- followed by that many bytes
|
||||
- secret key (16 bytes)
|
||||
- Sphinx header (the length is fixed, and included above for safe parsing)
|
||||
|
||||
### State management
|
||||
|
||||
The transport layer needs to maintain the state of all "unfinished business", and transparently handle things like acknowledgments, timeouts, re-transmits, etc.
|
||||
|
||||
The required state:
|
||||
|
||||
- set of open connections (or sessions)
|
||||
- for each connection:
|
||||
- the session id
|
||||
- network address (if applicable)
|
||||
- the application protocol
|
||||
- sent and received message counters
|
||||
- set of unused SURBs
|
||||
- sent but unacknowledged messages
|
||||
- partially received messages
|
||||
- outgoing mix packet queue (eg. we may have rate limits or just not enough bandwidth)
|
||||
- optionally, network quality estimations
|
||||
- can be global, per-connection or both
|
||||
- estimations for:
|
||||
- bandwidth
|
||||
- latency
|
||||
- packet loss
|
||||
|
||||
Then we can describe the expected behaviour via a relatively straightforward (if somewhat complex) state machine.
|
||||
|
||||
|
||||
### Requirements from the Mix protocol
|
||||
|
||||
For the above to work, we require some things from the underlying Mix protocol itself:
|
||||
|
||||
- ability to query the set of Mix relay nodes (their network addresses and public keys)
|
||||
- optionally: the ability of generating SURBs and Sphinx headers (but we could also do that ourselves)
|
||||
- send and receive packets, and handle any rate-limiting
|
||||
- a flag _in the Sphinx header_ (inside the destination address field) indicating that this packet is a transport layer packet
|
||||
- if bidirectional SURBs are used (that is, both sides want to hide their identity), then "SURB proxies" (see below)
|
||||
|
||||
#### Sphinx header format proposal
|
||||
|
||||
We propose the revise the Mix packet format to better support both this and other use cases, while also being more flexible and potentially also more compact.
|
||||
|
||||
Recall that a Sphinx packet consists of:
|
||||
|
||||
- ephemeral public key $\alpha$
|
||||
- encrypted routing info $\beta$
|
||||
- MAC (authentication code) $\gamma$ of the routing info
|
||||
- encrypted payload $\delta$
|
||||
|
||||
The routing info contains a sequence of `(address, MAC)` pairs, which are address of the next hop and the MAC for the next layer's routing info. The destination address doesn't need the MAC because their is no more info.
|
||||
|
||||
In ASCII art:
|
||||
|
||||
beta = [ Addr1 | MAC1 | Addr2 | MAC2 | Addr3 | MAC3 | Dest | padding ]
|
||||
gamma = MAC0
|
||||
|
||||
We propose that the address field should be variable length, and contain the following data:
|
||||
|
||||
- process handling behaviour (enum)
|
||||
- optional additional info required for the given behaviour
|
||||
- if one of the "destination behaviours", then a low-level protocol id (enum)
|
||||
- address format type (enum)
|
||||
- if the given address format is variable length (eg. a libp2p address), then its length (1 byte)
|
||||
- the address itself
|
||||
|
||||
Of course the Sphinx header itself needs to have a fixed size, but padding was already necessary because the routing info for different layers have different sizes anyway. So this only means that we can fit more hops if the addresses are shorter.
|
||||
|
||||
Example address formats:
|
||||
|
||||
- IP address (fixed length)
|
||||
- libp2p address (variable length)
|
||||
- compressed Mix node address (fixed length; a truncated hash of the Mix public key)
|
||||
|
||||
Example packet processing behaviours:
|
||||
|
||||
- you are a normal Mix relay, remove one layer and forward as usual
|
||||
- you are the destination, and it was a forward message
|
||||
- you are the destination, and it was a reply message (sent via SURB). The address field also contains a unique ID of the SURB.
|
||||
- you are the destination, and it was a proxied replay message (sent via SURB by a proxy, see below for details; this requires different payload handling)
|
||||
- you are an exit relay; decrypt and forward to an "external" address
|
||||
- you are a SURB proxy; forward using the proxy behavour
|
||||
|
||||
Example of destination protocols:
|
||||
|
||||
- default Mix protocol
|
||||
- transport layer protocol
|
||||
- exit node protocol (the final destination is "external")
|
||||
|
||||
#### SURB proxies
|
||||
|
||||
SURBs are more-or-less "safe" for the SURB _creator_, but they are _extremely unsafe_ for the _user_ of the SURB: The SURB creator can trivially figure out their identity simply by putting a mix node under their control as the first hop.
|
||||
|
||||
A mitigation against this is that both parties control "their side" of a path, meeting in the middle, similar to Tor randevuous points.
|
||||
|
||||
As Sphinx headers can be created only in one direction, this means that the (randomly selected) randevous point needs to extract a SURB and encrypted payload, and use those to forward to the final destination. This way it's the randevous point whose identity the attacker could find out, but that's just a random Mix node.
|
||||
|
||||
In more details:
|
||||
|
||||
Suppose we use 3 hops, and party `A` wants to send a packet to party `B` via a SURB prepared by party `B`.
|
||||
|
||||
Party `A` prepares a special payload, which wrap both the SURB and the actual payload (hence the actual payload must be smaller than the usual payload). It then selects a random path of 4 hops, and prepares a Mix header with the destination being _the 4rd hop_, and a destination processing behaviour indicating that it's a SURB proxy.
|
||||
|
||||
In ASCII art:
|
||||
|
||||
A -> a1 -> a2 -> a3 -> P => b1 -> b2 -> b3 -> B
|
||||
\_________________/ \_________________/
|
||||
A's path B's SURB
|
||||
|
||||
When the Mix node `P` (for proxy) receives and decrypts the packet, they see the flag indicating that it's a special proxy packet and they need to proxy it. They thus decode the payload into the SURB and the smaller payload; pad the smaller payload; and use the extracted SURB to send the padded payload to `b1` (whose address is part of the SURB).
|
||||
|
||||
When `B` receives the final packet, they will see another flag in the payload, indicating that it was proxied. This is necessary because it needs to be decrypted differently (?). And `A` cannot put it into header, because the header was prepared by `B`...
|
||||
|
||||
16
reference/README.md
Normal file
16
reference/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
Transport layer abstraction over Mix
|
||||
------------------------------------
|
||||
|
||||
This is a WIP reference implementation of our proposed transport layer abstraction
|
||||
over Mix, written in Haskell.
|
||||
|
||||
### Source code layout
|
||||
|
||||
- `Crypto` - cryptography primitives (symmetric crypto, X25519 key exchange, Lioness PRP)
|
||||
- `Data` - data structures (octetstrings)
|
||||
- `Mix` - modelling Mix itself
|
||||
- `Simulate` - WIP Mix simulator
|
||||
- `Sphinx` - Sphinx packet constructor
|
||||
- `Transport` - transport layer
|
||||
|
||||
@ -97,9 +97,10 @@ partitionLazyByteString m = go where
|
||||
-- doesn't receive enough data to reconstruct, we can simply send more parity chunks
|
||||
--
|
||||
data ChunkMeta = MkChunkMeta
|
||||
{ _cmSessionId :: SessionId -- ^ session id
|
||||
, _cmMessageIdx :: Word32 -- ^ message index within the session
|
||||
, _cmNOrigChunks :: Word16 -- ^ K = number of chunks containing the original data
|
||||
{ _cmSessionId :: SessionId -- ^ session id
|
||||
, _cmMessageIdx :: Word32 -- ^ message index within the session
|
||||
, _cmNOrigChunks :: Word16 -- ^ K = number of chunks containing the original data
|
||||
, _cmNTotalChunks :: Word16 -- ^ N = number total (redundant) chunks
|
||||
}
|
||||
deriving (Eq,Show)
|
||||
|
||||
@ -141,6 +142,7 @@ putChunkMeta (MkChunkMeta{..}) = do
|
||||
putSessionId _cmSessionId
|
||||
putWord32be _cmMessageIdx
|
||||
putWord16be _cmNOrigChunks
|
||||
putWord16be _cmNTotalChunks
|
||||
|
||||
putSessionId :: SessionId -> Put
|
||||
putSessionId (MkSessionId bytes) = mapM_ putWord8 bytes
|
||||
@ -169,6 +171,7 @@ getChunkMeta = MkChunkMeta
|
||||
<$> getSessionId
|
||||
<*> getWord32be
|
||||
<*> getWord16be
|
||||
<*> getWord16be
|
||||
|
||||
getSessionId :: Get SessionId
|
||||
getSessionId = MkSessionId <$> replicateM sessionIdSize getWord8
|
||||
|
||||
@ -16,30 +16,44 @@ import Transport.Misc
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
data SessionControl
|
||||
= InitialMessage
|
||||
| Continue
|
||||
| CloseSession
|
||||
= InitialMessage AppProtocol -- ^ the first message estabilishing a connection
|
||||
| Continue -- ^ proceed normally
|
||||
| CloseSession -- ^ closing down the session
|
||||
deriving (Eq,Show,Enum)
|
||||
|
||||
-- | Application layer protocol
|
||||
data AppProtocol = MkAppProtocol
|
||||
{ appProtocolName :: String -- ^ name of the application layer protocol
|
||||
, appProtocolInfo :: ByteString -- ^ additional protocol-dependent info (eg. key exchange)
|
||||
}
|
||||
deriving (Eq,Show)
|
||||
|
||||
data AckEntry
|
||||
= MsgReceived MsgIdx -- ^ message received intact
|
||||
| MsgFailed MsgIdx -- ^ message failed (eg. decoding or checksum)
|
||||
| NeedMoreChunks MsgIdx Int -- ^ we need more chunks
|
||||
= MsgReceived MsgIdx -- ^ message received intact
|
||||
| MsgFailed MsgIdx FailReason -- ^ message failed (eg. decoding, checksum or timeout)
|
||||
| NeedMoreChunks MsgIdx Int -- ^ we need more chunks
|
||||
| ResendChunks MsgIdx [Int] -- ^ re-transmit particular chunks
|
||||
deriving (Eq,Show)
|
||||
|
||||
data FailReason
|
||||
= FailTimeout -- ^ couldn't decode within the expected time
|
||||
| FailDecode -- ^ erasure code decoding failed
|
||||
| FailChecksum -- ^ checksum failed
|
||||
deriving (Eq,Show,Enum)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
data MsgMeta = MkMsgMeta
|
||||
{ _msgControl :: SessionControl -- ^ where we are in the session
|
||||
, _msgReqSURBs :: Int -- ^ how many more SURBs we need
|
||||
, _msgAcks :: [AckEntry] -- ^ status of previous messages
|
||||
{ _msgControl :: SessionControl -- ^ where we are in the session
|
||||
, _msgReqSURBs :: Int -- ^ how many more SURBs we need
|
||||
, _msgAcks :: [AckEntry] -- ^ status of previous messages
|
||||
}
|
||||
deriving (Eq,Show)
|
||||
|
||||
data Message = MkMessage
|
||||
{ _msgMeta :: MsgMeta -- ^ message metadata
|
||||
, _msgSURBs :: [SURB] -- ^ the new SURBs we send
|
||||
, _msgPayload :: ByteString -- ^ the actual payload
|
||||
{ _msgMeta :: MsgMeta -- ^ message metadata
|
||||
, _msgSURBs :: [SURB] -- ^ the new SURBs we send
|
||||
, _msgPayload :: ByteString -- ^ the actual payload
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user