I2P transport protocols were developed almost 15 years ago, when the main goal was to hide the content of traffic, and not the fact that a particular protocol was used. Nobody took DPI (deep packets inspection) and traffic blocking into account at that time. However, times are changing and although existing I2P protocols are still quite secure, there is a need for a new transport protocol that responds to existing and future threats, and, first of all, DPI, which analyzes the length of packets. In addition, the new protocol uses the latest advances in cryptography. Full description of the protocol Here. Based on Noise, in which SHA256 is used as the hash function, and x as the DH (in Noise terminology)25519.
For NTCP2, in addition to those already existing in I2P, it is necessary to implement the following cryptographic algorithms:
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 in the near future. For compatibility with openssl 1.0.2, which is included in most operating systems currently in use, i2pd was added with its own implementations written by one of the i2pd developers Jeff Becker-om, 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 public key calculation procedure has become more complex, with many HMAC-SHA calls256.
To work using the NTCP2 protocol, in addition to the two existing keys (encryption and signature), a third x25519 key is introduced, called a static key, which must be present in some RouterInfo address as the “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 only contain “s” and not contain the “host” and “port” parameters. Also a required NTCP2 parameter is “v”, currently always equal to «2».
The NTCP2 address can be specified as an “NTCP” type address with additional parameters - in this case, the connection can be established via both NTCP and NTCP2, or as an “NTCP2” type address that supports only NTCP2 connections. Java I2P uses the first method, i2pd uses the second.
If a node accepts incoming NTCP2 connections, then it must publish the parameter "i" with the value IV to encrypt the public key when establishing a connection.
During the connection establishment process, the parties generate x25519 temporary key pairs, and based on them and the static keys, they calculate key sets for data transmission. Static keys are also verified against the contents of RouterInfo..
The parties exchange three messages:
SessionRequest ------------------->
< — SessionCreated
SessionConfirmed ----------------->
for each of which a common x25519 key called “input key material” is calculated and then a message encryption key is generated using the MixKey operation, with the value ck (chaining key) stored between messages and being the result on the basis of which keys for data transmission are calculated . The MixKey implementation looks something like this:
SessionRequest consists of a 32-byte public key x25519 of the client, and an AEAD/Chacha20/Poly1305 encrypted 16-byte data block + 16 bytes of hash, as well as a set of random data (padding), the length of which is transmitted in the 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 the MixKey is set to SHA256 (“Noise_XKaesobfse+hs2+hs3_25519_ChaChaPoly_SHA256»).
Since the 32 bytes of the x25519 public key can be recognized by dpi, they are encrypted using AES-256-CBC, where the key is a hash of the server address, and the IV is taken from the “i” parameter of the address in RouterInfo.
SessionCreated similar in structure to SessionRequest, except that the key is calculated based on the temporary keys of both parties, and the IV after decryption/encryption of the public key from SessionRequest is taken as the IV for encrypting/decrypting the public key.
SessionConfirmed consists of two parts: the client's static public key and the client's RouterInfo. Unlike previous posts, the public key is encrypted by 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, calculated based on the server’s temporary key and the client’s static key. A block of random data can also be added to the RouterInfo, but this is generally not necessary because the length of the RouterInfo varies.
If all checks of hashes and keys during the connection setup process were successful, then after the last MixKey there should be the same ck on both sides, from which 2 sets of key triples will be generated in each direction, where k is the AEAD/Chaha20/Poly1305 key, sipk is the key for Siphash, sipiv is the initial IV value for Siphash, which changes after each use.
The first 16 bytes of the sipkeys array represent the Siphash key, the second 8 bytes represent the IV.
Siphash actually requires two 8 byte keys, but i2pd treats them as 1 16 byte key.
Data is transmitted in frames, each frame consists of 3 parts:
The maximum length of transmitted data in one frame is 65519 bytes.
The message length is encrypted by XORing the first two bytes of the current IV Siphash.
The data consists of blocks, each block is preceded by a 3-byte header with the block type and length. Mostly I2NP type blocks are transmitted, containing I2NP messages with a modified header. Several I2NP blocks can be transmitted in one frame.
Another important block type is the random data block, which is recommended to be added to each frame. He can only be one and the last.
In addition to them, in the current implementation of NTCP2 there are 3 more block types:
Thus, the new transport protocol allows not only to effectively resist DPI, but also significantly reduces the load on the processor 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.
New cryptography
For NTCP2, in addition to those already existing in I2P, it is necessary to implement the following cryptographic algorithms:
- 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 in the near future. For compatibility with openssl 1.0.2, which is included in most operating systems currently in use, i2pd was added with its own implementations written by one of the i2pd developers Jeff Becker-om, 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 public key calculation procedure has become more complex, with many HMAC-SHA calls256.
Changes to RouterInfo
To work using the NTCP2 protocol, in addition to the two existing keys (encryption and signature), a third x25519 key is introduced, called a static key, which must be present in some RouterInfo address as the “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 only contain “s” and not contain the “host” and “port” parameters. Also a required NTCP2 parameter is “v”, currently always equal to «2».
The NTCP2 address can be specified as an “NTCP” type address with additional parameters - in this case, the connection can be established via both NTCP and NTCP2, or as an “NTCP2” type address that supports only NTCP2 connections. Java I2P uses the first method, i2pd uses the second.
If a node accepts incoming NTCP2 connections, then it must publish the parameter "i" with the value IV to encrypt the public key when establishing a connection.
Establishing a connection
During the connection establishment process, the parties generate x25519 temporary key pairs, and based on them and the static keys, they calculate key sets for data transmission. Static keys are also verified against the contents of RouterInfo..
The parties exchange three messages:
SessionRequest ------------------->
< — SessionCreated
SessionConfirmed ----------------->
for each of which a common x25519 key called “input key material” is calculated and then a message encryption key is generated using the MixKey operation, with the value ck (chaining key) stored between messages and being the result on the basis of which keys for data transmission are calculated . The MixKey implementation looks something like this:
MixKey code
void NTCP2Establisher::MixKey (const uint8_t * inputKeyMaterial, uint8_t * derived)
{
// temp_key = HMAC-SHA256(ck, input_key_material)
uint8_t tempKey[32]; unsigned int len;
HMAC(EVP_sha256(), m_CK, 32, inputKeyMaterial, 32, tempKey, &len);
// ck = HMAC-SHA256(temp_key, byte(0x01))
static uint8_t one[1] = { 1 };
HMAC(EVP_sha256(), tempKey, 32, one, 1, m_CK, &len);
// derived = HMAC-SHA256(temp_key, ck || byte(0x02))
m_CK[32] = 2;
HMAC(EVP_sha256(), tempKey, 32, m_CK, 33, derived, &len);
}
SessionRequest consists of a 32-byte public key x25519 of the client, and an AEAD/Chacha20/Poly1305 encrypted 16-byte data block + 16 bytes of hash, as well as a set of random data (padding), the length of which is transmitted in the 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 the MixKey is set to SHA256 (“Noise_XKaesobfse+hs2+hs3_25519_ChaChaPoly_SHA256»).
Since the 32 bytes of the x25519 public key can be recognized by dpi, they are encrypted using AES-256-CBC, where the key is a hash of the server address, and the IV is taken from the “i” parameter of the address in RouterInfo.
SessionCreated similar in structure to SessionRequest, except that the key is calculated based on the temporary keys of both parties, and the IV after decryption/encryption of the public key from SessionRequest is taken as the IV for encrypting/decrypting the public key.
SessionConfirmed consists of two parts: the client's static public key and the client's RouterInfo. Unlike previous posts, the public key is encrypted by 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, calculated based on the server’s temporary key and the client’s static key. A block of random data can also be added to the RouterInfo, but this is generally not necessary because the length of the RouterInfo varies.
Generating keys for data transfer
If all checks of hashes and keys during the connection setup process were successful, then after the last MixKey there should be the same ck on both sides, from which 2 sets of key triples will be generated in each direction, where k is the AEAD/Chaha20/Poly1305 key, 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;
// temp_key = HMAC-SHA256(ck, zerolen)
HMAC(EVP_sha256(), m_Establisher->GetCK (), 32, nullptr, 0, tempKey, &len);
static uint8_t one[1] = { 1 };
// k_ab = HMAC-SHA256(temp_key, byte(0x01)).
HMAC(EVP_sha256(), tempKey, 32, one, 1, m_Kab, &len);
m_Kab[32] = 2;
// k_ba = HMAC-SHA256(temp_key, k_ab || byte(0x02))
HMAC(EVP_sha256(), tempKey, 32, m_Kab, 33, m_Kba, &len);
static uint8_t ask[4] = { 'a', 's', 'k', 1 }, master[32];
// ask_master = HMAC-SHA256(temp_key, "ask" || byte(0x01))
HMAC(EVP_sha256(), tempKey, 32, ask, 4, master, &len);
uint8_t h[39];
memcpy (h, m_Establisher->GetH (), 32);
memcpy (h + 32, "siphash", 7);
// temp_key = HMAC-SHA256(ask_master, h || "siphash")
HMAC(EVP_sha256(), master, 32, h, 39, tempKey, &len);
// sip_master = HMAC-SHA256(temp_key, byte(0x01))
HMAC(EVP_sha256(), tempKey, 32, one, 1, master, &len);
// temp_key = HMAC-SHA256(sip_master, zerolen)
HMAC(EVP_sha256(), master, 32, nullptr, 0, tempKey, &len);
// sipkeys_ab = HMAC-SHA256(temp_key, byte(0x01)).
HMAC(EVP_sha256(), tempKey, 32, one, 1, m_Sipkeysab, &len);
m_Sipkeysab[32] = 2;
// sipkeys_ba = HMAC-SHA256(temp_key, sipkeys_ab || byte(0x02))
HMAC(EVP_sha256(), tempKey, 32, m_Sipkeysab, 33, m_Sipkeysba, &len);
}
The first 16 bytes of the sipkeys array represent the Siphash key, the second 8 bytes represent the IV.
Siphash actually requires two 8 byte keys, but i2pd treats them as 1 16 byte key.
Data transfer
Data is transmitted in frames, each frame consists of 3 parts:
- 2 byte frame length encrypted by Siphash
- data encrypted by Chacha20
- 16 Poly hash byte1305
The maximum length of transmitted data in one frame is 65519 bytes.
The message length is encrypted by XORing the first two bytes of the current IV Siphash.
The data consists of blocks, each block is preceded by a 3-byte header with the block type and length. Mostly I2NP type blocks are transmitted, containing I2NP messages with a modified header. Several I2NP blocks can be transmitted in one frame.
Another important block type is the random data block, which is recommended to be added to each frame. He can only be one and the last.
In addition to them, in the current implementation of NTCP2 there are 3 more block types:
- RouterInfo - usually contains the RouterInfo of the 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 operation of floodfills, for which a flag field is provided in the message.
- Termination - sent by the node when the connection is terminated on its initiative, indicating the reason.
- DateTime - current time in seconds.
Thus, the new transport protocol allows not only to effectively resist DPI, but also significantly reduces the load on the processor 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.