I2P transport protocols were developed almost 15 years ago, when the main task was to conceal the contents of the traffic, and not the fact of using one or another protocol. DPI (deep packets inspection) and traffic blocking at the time no one took into account. However, times are changing, and although the existing I2P protocols are still fairly well protected, there is a need for a new transport protocol that responds to existing and future threats, and, above all, the DPI, which analyzes packet lengths. In addition, the new protocol uses the latest advances in cryptography. Full protocol description is
here . The basis is
Noise , in which SHA256 is used as a hash function, and as DH (in Noise terminology) - x25519.
New cryptography
For NTCP2, in addition to the already existing ones in I2P, the following cryptographic algorithms must be implemented:
- x25519
- HMAC-SHA256
- Chacha20
- Poly1305
- AEAD
- Siphash
All of them, with the exception of Siphash, are implemented in openssl 1.1.0. In turn, Siphash will appear in openssl 1.1.1, which will be released soon. For compatibility with openssl 1.0.2, which is included in most of the currently used operating systems, i2pd added its own implementations, written by one of i2pd developers
Jeff Becker , known in I2P as psi.
Compared to NTCP, x25519 replaces DH, AEAD / Chaha20 / Poly1305 replaces AES-256-CBC / Adler32, and Siphash is used to encrypt the length of transmitted messages. The procedure for computing the shared key has become more complex: with a lot of HMAC-SHA256 calls.
Changes in RouterInfo
To work on the NTCP2 protocol, in addition to the two already existing keys (encryption and signature), a third key x25519, called a static key, must be entered, which must be present in some RouterInfo address as an “s” parameter for both clients and servers. If more than one address supports NTCP2, for example, ipv4 and ipv6, then “s” must be the same everywhere. For clients, the address can contain only “s” and not contain the parameters “host” and “port”. Also mandatory parameter NTCP2 is "v", currently always equal to "2".
The NTCP2 address can be specified as an “NTCP” address with additional parameters - in this case, the connection can be established either via NTCP or NTCP2, or also as an “NTCP2” address that supports only NTCP2 connections. In Java I2P, the first method is used, in i2pd - the second.
If the host accepts incoming NTCP2 connections, then it must publish the “i” parameter with the value IV for encrypting the public key when the connection is established.
Connection setup
In the process of establishing a connection, the parties generate x25519 temporary key pairs, and based on these and static keys, the sets of keys for data transfer are calculated. It also authenticates static keys and matches the contents of RouterInfo.
The parties exchange three messages:
SessionRequest ------------------->
<- SessionCreated
SessionConfirmed ----------------->
for each of which, the common key x25519, called the “input key material” is calculated, and then the message encryption key is generated using the MixKey operation, the value of ck (chaining key) is stored between the messages and is the result from which the keys for data transmission are calculated . The implementation of MixKey looks like this:
MixKey Codevoid NTCP2Establisher::MixKey (const uint8_t * inputKeyMaterial, uint8_t * derived) {
SessionRequest consists of a 32-byte x25519 client public key, and an encrypted AEAD / Chacha20 / Poly1305 16-byte data block + 16 byte hash, as well as a set of random data (padding), which is transmitted in an encrypted block. The length of the second half of the SessionConfirmed message is also transmitted there. The block is encrypted and signed with a key based on the client's temporary key and the server's static key. The initial ck for MixKey is set to SHA256 (“Noise_XKaesobfse + hs2 + hs3_25519_ChaChaPoly_SHA256”).
Since 32 bytes of the x25519 public key can be recognized by dpi, they are encrypted using AES-256-CBC, where the key is the hash of the server address, and IV is taken from the “i” parameter of the address in RouterInfo.
SessionCreated is similar in structure to SessionRequest, except that the key is calculated based on the temporary keys of both sides, and IV is used to encrypt / decrypt the public key IV after decrypting / encrypting the public key from SessionRequest.
SessionConfirmed consists of two parts: the client’s static public key and the client’s RouterInfo. Unlike previous messages, the public key is encrypted with AEAD / Chaha20 / Poly1305 with the same key as SessionCreated. Therefore, the length of the first part is not 32, but 48 bytes. The second part is also encrypted with AEAD / Chaha20 / Poly1305, but with a new key, we calculate it based on the temporary server key and the client's static key. Also, a block of random data can be added to RouterInfo, but, as a rule, this is not necessary, because the length of RouterInfo is different.
Key generation for data transfer
If all checks of the hashes and keys are successful during the connection, then after the last MixKey, both sides should have the same ck, from which 2 sets of triples of keys <k, sipk, sipiv> will be generated on each side, where k is the AEAD key / Chaha20 / Poly1305, sipk is the key for Siphash, sipiv is the initial IV value for Siphash, which changes after each use.
Code that implements key generation void NTCP2Session::KeyDerivationFunctionDataPhase () { uint8_t tempKey[32]; unsigned int len;
The first 16 bytes of the sipkeys array are the Siphash key, the second 8 bytes are IV.
In fact, Siphash requires two keys of 8 bytes each, but in i2pd they are treated as 1 key 16 bytes long.
Data transfer
Data is transmitted by frames, each frame consists of 3 parts:
- 2 bytes of Siphash encrypted frame length
- data encrypted by chacha20
- 16 bytes of Poly1305 hash
The maximum length of transmitted data in one frame is 65519 bytes.
The message length is encrypted using the XOR operation with the first two bytes of the current IV Siphash.
The data consists of blocks, each block is preceded by a 3-byte header with a block type and length. Mostly I2NP type blocks are transmitted that contain I2NP messages with a modified header. Multiple I2NP blocks can be transmitted in one frame.
Another important block type is a random data block, which is recommended to be added to each frame. He can be only one and the last.
In addition to them, in the current implementation of NTCP2 there are 3 more types of block:
- RouterInfo - usually contains the RouterInfo server immediately after the connection is established, but the RouterInfo of an arbitrary node can be transmitted at any time in order to speed up the work of the floodfills, for which the flags field is provided in the message.
- Termination - sent by the node when the connection is broken on its initiative, indicating the reason.
- DateTime - the current time in seconds.
Thus, the new transport protocol not only effectively resists DPI, but also significantly reduces the CPU load due to more modern and faster cryptography, which is especially important when working on weak devices such as smartphones and routers. Currently, NTCP2 support is fully implemented in both the official I2P and i2pd and will appear officially in the next releases, 0.9.36 and 2.20, respectively. To enable ntcp2 in i2pd, you must specify the configuration parameter ntcp2.enabled = true, and ntcp2.published = true and ntcp2.port = <port> for incoming connections.