Last updated


This proposal builds on and requires the changes from proposal 152: ECIES Tunnels.

Only tunnels built through hops supporting the BuildRequestRecord format for ECIES-X25519 tunnels can implement this specification.

This specification requires the Tunnel Build Options format for indicating tunnel layer encryption type, and transmitting layer AEAD keys.


The goals of this proposal are to:

  • Replace AES256/ECB+CBC with ChaCha20 for established tunnel IV and layer encryption
  • Use ChaCha20-Poly1305 for inter-hop AEAD protection
  • Be undetectable from existing tunnel layer encryption by non-tunnel participants
  • Make no changes to overall tunnel message length

Established Tunnel Message Processing

This section describes changes to:

  • Outbound and Inbound Gateway preprocessing + encryption
  • Participant encryption + postprocessing
  • Outbound and Inbound Endpoint encryption + postprocessing

For an overview of current tunnel message processing, see the [Tunnel-Implementation] spec.

Only changes for routers supporting ChaCha20 layer encryption are discussed.

No changes are considered for mixed tunnel with AES layer encryption, until a safe protocol can be devised for converting a 128-bit AES IV to a 64-bit ChaCha20 nonce. Bloom filters guarantee uniqueness for the full IV, but the first half of unique IVs could be identical.

This means layer encryption must be uniform for all hops in the tunnel, and established using tunnel build options during the tunnel creation process.

All gateways and tunnel participants will need to maintain a Bloom filter for validating the two independent nonces.

The nonceKey mentioned throughout this proposal takes the place of the IVKey used in AES layer encryption. It is generated using the same KDF from proposal 152.

AEAD Encryption of Hop-to-Hop Messages

An additional unique AEADKey will need to be generated for each pair of consecutive hops. This key will be used by consecutive hops to ChaCha20-Poly1305 encrypt and decrypt the inner ChaCha20 encrypted tunnel message.

Tunnel messages will need to reduce the length of the inner encrypted frame by 16 bytes to accommodate the Poly1305 MAC.

AEAD cannot be used on the messages directly, since iterative decryption is needed by outbound tunnels. Iterative decryption can only be achieved, in the way it's used now, using ChaCha20 without AEAD.

|    Tunnel ID      |   tunnelNonce     |
| tunnelNonce cont. |    obfsNonce      |
|  obfsNonce cont.  |                   |
+----+----+----+----+                   +
|                                       |
+           Encrypted Data              +
~                                       ~
|                                       |
+                   +----+----+----+----+
|                   |    Poly1305 MAC   |
+----+----+----+----+                   +
|                                       |
+                   +----+----+----+----+
|                   |

Tunnel ID :: TunnelId
       4 bytes
       the ID of the next hop

tunnelNonce ::
       8 bytes
       the tunnel layer nonce

obfsNonce ::
       8 bytes
       the tunnel layer nonce encryption nonce

Encrypted Data ::
       992 bytes
       the encrypted tunnel message

Poly1305 MAC ::
       16 bytes

total size: 1028 Bytes

Inner hops (with preceding and following hops), will have two AEADKeys, one for decrypting the AEAD layer of the previous hop, and encrypting the AEAD layer to the following hop.

All inner hop participants will thus have 64 additional bytes of key material included in their BuildRequestRecords.

The Outbound Endpoint and Inbound Gateway will only require an additional 32 bytes of keydata, since they do not tunnel layer encrypt messages between each other.

The Outbound Gateway generates its outAEAD key, which is the same as the first outbound hop's inAEAD key.

The Inbound Endpoint generates its inAEAD key, which is the same as the final inbound hop's outAEAD key.

Inner hops will receive and inAEADKey and outAEADKey which will be used to AEAD decrypt incoming messages and encrypt outgoing messages, respectively.

As an example, in a tunnel with inner hops OBGW, A, B, OBEP:

  • A's inAEADKey is the same as the OBGW's outAEADKey
  • B's inAEADKey is the same as A's outAEADKey
  • B's outAEADKey is the same as OBEP's inAEADKey

Keys are unique to hop pairs, so OBEP's inAEADKey will be different than A's inAEADKey, A's outAEADKey different than B's outAEADKey, etc.

Gateway and Tunnel Creator Message Processing

Gateways will fragment and bundle messages in the same way, reserving space after the instructions-fragment frame for the Poly1305 MAC.

Inner I2NP messages containing AEAD frames (including the MAC) can be split across fragments, but any dropped fragments will result in failed AEAD decryption (failed MAC verification) at the endpoint.

Gateway Preprocessing & Encryption

When tunnels support ChaCha20 layer encryption, gateways will generate two 64-bit nonces per message set.

Inbound tunnels:

  • Encrypt the IV and tunnel message(s) using ChaCha20
  • Use 8-byte tunnelNonce and obfsNonce given the lifetime of tunnels
  • Use 8-byte obfsNonce for tunnelNonce encryption
  • Destroy tunnel before 2^(64 - 1) - 1 sets of messages: 2^63 - 1 = 9,223,372,036,854,775,807
    • Nonce limit in place to avoid collision of the 64-bit nonces
    • Nonce limit nearly impossible to ever be reached, given this would be over ~15,372,286,728,091,294 msgs/second for 10 minute tunnels
  • Tune the Bloom filter based on a reasonable number of expected elements (128 msgs/sec, 1024 msgs/sec? TBD)

The tunnel's Inbound Gateway (IBGW), processes messages received from another tunnel's Outbound Endpoint (OBEP).

At this point, the outermost message layer is encrypted using point-to-point transport encryption. The I2NP message headers are visible, at the tunnel layer, to the OBEP and IBGW. The inner I2NP messsages are wrapped in Garlic cloves, encrypted using end-to-end session encryption.

The IBGW preprocesses the messages into the appropriately formatted tunnel messages, and encrypts as following:

// IBGW generates random nonces, ensuring no collision in its Bloom filter for each nonce
tunnelNonce = Random(len = 64-bits)
obfsNonce = Random(len = 64-bits)
// IBGW ChaCha20 "encrypts" each of the preprocessed tunnel messages with its tunnelNonce and layerKey
encMsg = ChaCha20(msg = tunnel msg, nonce = tunnelNonce, key = layerKey)

// ChaCha20-Poly1305 encrypt each message's encrypted data frame with the tunnelNonce and outAEADKey
(encMsg, MAC) = ChaCha20-Poly1305-Encrypt(msg = encMsg, nonce = tunnelNonce, key = outAEADKey)

Tunnel message format will slightly change, using two 8-byte nonces instead of a 16-byte IV. The obfsNonce used for encrypting the nonce is appended to the 8-byte tunnelNonce, and is encrypted by each hop using the encrypted tunnelNonce and the hop's nonceKey.

After the message set has be pre-emptively decrypted for each hop, the Outbound Gateway ChaCha20-Poly1305 AEAD encrypts the ciphertext portion of each tunnel message using the tunnelNonce and its outAEADKey.

Outbound tunnels:

  • Iteratively decrypt tunnel messages
  • ChaCha20-Poly1305 encrypt preemptively decrypted tunnel message encrypted frames
  • Use the same rules for layer nonces as Inbound tunnels
  • Generate random nonces once per set of tunnel messages sent
// For each set of messages, generate unique, random nonces
tunnelNonce = Random(len = 64-bits)
obfsNonce = Random(len = 64-bits)

// For each hop, ChaCha20 the previous tunnelNonce with the current hop's IV key
tunnelNonce = ChaCha20(msg = prev. tunnelNonce, nonce = obfsNonce, key = hop's nonceKey)

// For each hop, ChaCha20 "decrypt" the tunnel message with the current hop's tunnelNonce and layerKey
decMsg = ChaCha20(msg = tunnel msg(s), nonce = tunnelNonce, key = hop's layerKey)

// For each hop, ChaCha20 "decrypt" the obfsNonce with the current hop's encrypted tunnelNonce and nonceKey
obfsNonce = ChaCha20(msg = obfsNonce, nonce = tunnelNonce, key = hop's nonceKey)

// After hop processing, ChaCha20-Poly1305 encrypt each tunnel message's "decrypted" data frame with the first hop's encrypted tunnelNonce and inAEADKey
(encMsg, MAC) = ChaCha20-Poly1305-Encrypt(msg = decMsg, nonce = first hop's encrypted tunnelNonce, key = first hop's inAEADKey / GW outAEADKey)

Participant Processing

Participants will track seen messages in the same way, using decaying Bloom filters.

Tunnel nonces will each need to be encrypted once per-hop, to prevent confirmation attacks by non-consecutive, colluding hops.

Hops will encrypt the received nonce to prevent confirmation attacks between prior and later hops, i.e. colluding, non-consecutive hops being able to tell they belong to the same tunnel.

To validate received tunnelNonce and obfsNonce, participants check each nonce individually against their Bloom filter for duplicates.

After validation, the participant:

  • ChaCha20-Poly1305 decrypts each tunnel message's AEAD ciphertext with the received tunnelNonce and its inAEADKey
  • ChaCha20 encrypts the tunnelNonce with its nonceKey and received obfsNonce
  • ChaCha20 encrypts the each tunnel message's encrypted data frame with the encrypted tunnelNonce and its layerKey
  • ChaCha20-Poly1305 encrypts each tunnel message's encrypted data frame the encrypted tunnelNonce and its outAEADKey
  • ChaCha20 encrypts the obfsNonce with its nonceKey and encrypted tunnelNonce
  • Sends the tuple {nextTunnelId, encrypted (tunnelNonce || obfsNonce), AEAD ciphertext || MAC} to the next hop.
// For verification, tunnel hops should check Bloom filter for each received nonce's uniqueness
// After verification, unwrap the AEAD frame(s) byChaCha20-Poly1305 decrypt each tunnel message's encrypted frame
// with the received tunnelNonce and inAEADKey
encTunMsg = ChaCha20-Poly1305-Decrypt(msg = received encMsg \|\| MAC, nonce = received tunnelNonce, key = inAEADKey)

// ChaCha20 encrypt the tunnelNonce with the obfsNonce and hop's nonceKey
tunnelNonce = ChaCha20(msg = received tunnelNonce, nonce = received obfsNonce, key = nonceKey)

// ChaCha20 encrypt each tunnel message's encrypted data frame with the encrypted tunnelNonce and hop's layerKey
encMsg = ChaCha20(msg = encTunMsg, nonce = tunnelNonce, key = layerKey)

// For AEAD protection, also ChaCha20-Poly1305 encrypt each message's encrypted data frame
// with the encrypted tunnelNonce and the hop's outAEADKey
(encMsg, MAC) = ChaCha20-Poly1305-Encrypt(msg = encMsg, nonce = tunnelNonce, key = outAEADKey)

// ChaCha20 encrypt the received obfsNonce with the encrypted tunnelNonce and hop's nonceKey
obfsNonce = ChaCha20(msg = obfsNonce, nonce = tunnelNonce, key = nonceKey)

Inbound Endpoint Processing

For ChaCha20 tunnels, the following scheme will be used to decrypt each tunnel message:

  • Validate the received tunnelNonce and obfsNonce independently against its Bloom filter
  • ChaCha20-Poly1305 decrypt the encrypted data frame using the received tunnelNonce and inAEADKey
  • ChaCha20 decrypt the encrypted data frame using the received tunnelNonce & the hop's layerKey
  • ChaCha20 decrypt the obfsNonce using the hop's nonceKey and received tunnelNonce to get the preceding obfsNonce
  • ChaCha20 decrypt the received tunnelNonce using the hop's nonceKey and decrypted obfsNonce to get the preceding tunnelNonce
  • ChaCha20 decrypt the encrypted data using the decrypted tunnelNonce & the preceding hop's layerKey
  • Repeat the steps for nonce and layer decryption for each hop in the tunnel, back to the IBGW
  • The AEAD frame decryption is only needed in the first round
// For the first round, ChaCha20-Poly1305 decrypt each message's encrypted data frame + MAC
// using the received tunnelNonce and inAEADKey
msg = encTunMsg \|\| MAC
tunnelNonce = received tunnelNonce
encTunMsg = ChaCha20-Poly1305-Decrypt(msg, nonce = tunnelNonce, key = inAEADKey)

// Repeat for each hop in the tunnel back to the IBGW
// For every round, ChaCha20 decrypt each hop's layer encryption on each message's encrypted data frame
// Replace the received tunnelNonce w/ the prior round's decrypted tunnelNonce for each hop
decMsg = ChaCha20(msg = encTunMsg, nonce = tunnelNonce, key = layerKey)
obfsNonce = ChaCha20(msg = obfsNonce, nonce = tunnelNonce, key = nonceKey)
tunnelNonce = ChaCha20(msg = tunnelNonce, nonce = obfsNonce, key = nonceKey)

Security Analysis for ChaCha20+ChaCha20-Poly1305 Tunnel Layer Encryption

Switching from AES256/ECB+AES256/CBC to ChaCha20+ChaCha20-Poly1305 has a number of advantages, and new security considerations.

The biggest security considerations to account for, are that ChaCha20 and ChaCha20-Poly1305 nonces must be unique per-message, for the life of the key being used.

Failing to use unique nonces with the same key on different messages breaks ChaCha20 and ChaCha20-Poly1305.

Using an appended obfsNonce allows the IBEP to decrypt the tunnelNonce for each hop's layer encryption, recovering the previous nonce.

The obfsNonce alongside the tunnelNonce doesn't reveal any new information to tunnel hops, since the obfsNonce is encrypted using the encrypted tunnelNonce. This also allows the IBEP to recover the previous obfsNonce in a similar way to tunnelNonce recovery.

The biggest security advantage is that there are no confirmation or oracle attacks against ChaCha20, and using ChaCha20-Poly1305 between hops adds AEAD protection against ciphertext manipulation from out-of-band MitM attackers.

There are practical oracle attacks against AES256/ECB + AES256/CBC, when the key is reused (as in tunnel layer encryption).

The oracle attacks against AES256/ECB won't work, because of the double-encryption used, and encryption is over a single block (the tunnel IV).

The padding oracle attacks against AES256/CBC won't work, because no padding is used. If tunnel message length ever changed to non-mod-16 lengths, AES256/CBC would still not be vulnerable due to rejected duplicate IVs.

Both attacks are also blocked by disallowing multiple oracle calls using the same IV, since duplicate IVs are rejected.