Low-level ADNL
Abstract Datagram Network Layer (ADNL) is the core protocol of TON, which helps network peers communicate.
Peer identity
Each peer must have at least one identity; while it's possible to use multiple identities, it is not required. Each identity consists of a keypair used for performing the Diffie-Hellman exchange between peers. An abstract network address is derived from the public key in the following way: address = SHA-256(type_id || public_key). Note that the type_id must be serialized as a little-endian uint32.
Public-key cryptosystems list
| type_id | cryptosystem |
|---|---|
| 0x4813b4c6 | ed255191 |
- To perform x25519, the keypair must be generated in "x25519" format. However, the public key is transmitted over the network in ed25519 format, so you have to convert the public key from x25519 to ed25519, examples of such conversions can be found here for Kotlin.
Client-server protocol (ADNL over TCP)
The client connects to the server using TCP and sends an ADNL handshake packet. This packet contains a server abstract address, a client public key, and encrypted AES-CTR session parameters, which the client determines.
Handshake
First, the client must perform a key agreement protocol (for example, x25519) using their private key and server public key, taking into account the server key's type_id. As a result, the client will gain secret, which is used to encrypt session keys in future steps.
Then, the client has to generate AES-CTR session parameters, a 16-byte nonce and 32-byte key, both for TX (client->server) and RX (server->client) directions and serialize it into a 160-byte buffer as follows:
| Parameter | Size |
|---|---|
| rx_key | 32 bytes |
| tx_key | 32 bytes |
| rx_nonce | 16 bytes |
| tx_nonce | 16 bytes |
| padding | 64 bytes |
The purpose of padding is unknown; it is not used by server implementations. It is recommended that the whole 160-byte buffer be filled with random bytes. Otherwise, an attacker may perform an active MitM attack using compromised AES-CTR session parameters.
The next step is to encrypt the session parameters using the secret through the key agreement protocol outlined above. To achieve this, AES-256 needs to be initialized in CTR mode with a 128-bit big-endian counter. This will utilize a (key, nonce) pair that is computed as follows (note that aes_params is a 160-byte buffer that was created earlier):
hash = SHA-256(aes_params)
key = secret[0..16] || hash[16..32]
nonce = hash[0..4] || secret[20..32]
After encrypting aes_params, noted as E(aes_params), remove AES as it is no longer needed. We are now ready to serialize all this information into the 256-byte handshake packet and send it to the server.
| Parameter | Size | Notes |
|---|---|---|
| receiver_address | 32 bytes | Server peer identity as described in the Peer identity section |
| sender_public | 32 bytes | Client public key |
| SHA-256(aes_params) | 32 bytes | Integrity proof of session parameters |
| E(aes_params) | 160 bytes | Encrypted session parameters |
The server must decrypt session parameters using a secret derived from the key agreement protocol, just as the client does. After decryption, the server must perform the following checks to ensure the security properties of the protocol:
-
The server must possess the corresponding private key for
receiver_address. Without this key, it cannot execute the key agreement protocol. -
The condition
SHA-256(aes_params) == SHA-256(D(E(aes_params)))must hold true. If this condition is not met, it indicates that the key agreement protocol has failed and thesecretvalues on both sides are not equal.
If any of these checks fail, the server will immediately drop the connection without responding to the client. If all checks pass, the server must issue an empty datagram (see the Datagram section) to the client in order to prove that it owns the private key for the specified receiver_address.
Datagram
Both the client and server must initialize two AES-CTR instances each for both transmission (TX) and reception (RX) directions. The AES-256 must be used in CTR mode with a 128-bit big-endian counter. Each AES instance is initialized using a (key, nonce) pair, which can be taken from the aes_params during the handshake.
To send a datagram, either the client or the server must construct the following structure, encrypt it, and send it to the other peer:
| Parameter | Size | Notes |
|---|---|---|
| length | 4 bytes (LE) | Length of the whole datagram, excluding length field |
| nonce | 32 bytes | Random value |
| buffer | length - 64 bytes | Actual data to be sent to the other side |
| hash | 32 bytes | SHA-256(nonce || buffer) to ensure integrity |
The whole structure must be encrypted using the corresponding AES instance (TX for client -> server, RX for server -> client).
The receiving peer must fetch the first 4 bytes, decrypt it into the length field, and read exactly the length bytes to get the full datagram. The receiving peer may start to decrypt and process buffer earlier, but it must take into account that it may be corrupted, intentionally or occasionally. Datagram hash must be checked to ensure the integrity of the buffer. In case of failure, no new datagrams can be issued and the connection must be dropped.
The first datagram in the session always goes from the server to the client after a handshake packet is successfully accepted by the server and its actual buffer is empty. The client should decrypt it and disconnect from the server in case of failure because it means that the server has not followed the protocol properly and the actual session keys differ on the server and client side.
Communication details
If you want to dive into communication details, you could check the article ADNL TCP - liteserver to see some examples.