We implement a secure VPN protocol

SSH (can it work with TUN / TAP devices and be used for VPN), TLS , OpenVPN or IPsec ? The complexity of both protocol and code. And hence the complexity of review, security is in question. Dependence on US organizations dictating which algorithms can be used. Only SSH out of the box offers such fast, secure, intelligence-independent algorithms as ChaCha20 , Poly1305 and Curve25519 . TLS libraries and the protocol have shown themselves in large numbers from the bad side due to their complexity. OpenVPN, when it does not use PSK (pre-shared key - a key known in advance by both parties), it also depends on TLS with all the consequences.
Transport protocol
To solve the problem of creating just a VPN, modern OSs offer such a convenient thing as TUN / TAP : virtual network interfaces. It is very simple to write a client-server application that reads frames from the interface, wraps them in UDP packets and sends them to the remote side. But we want to ensure the confidentiality of the transmitted data, authentication by both sides of the connection (to make sure that they impersonate who we expect).
The entire protocol globally consists of two parts: the handshake process and transport. For now, we will consider the handshake as a black box at the output from which we will receive: an IP address with a port on the opposite side for sending UDP packets and a common symmetric encryption key. Any communication begins with a handshake, and it is enough to authenticate the parties only at this stage.
Transport is responsible for the encrypted (ensuring confidentiality of the payload) message transmission and reception with decryption. As the algorithm / encryption function, select Salsa20(or a variant of this ChaCha20 algorithm). At the moment, it has very good cryptographic security (no fatal flaws in cryptanalysis have been identified), the highest performance and ease of implementation in the code. This is a stream cipher: that is, we can assume that the result of its work is a pseudo-random sequence of bytes that need to be XOR with data that requires encryption.
With stream ciphers it is worth being very careful. Unlike block ones, they are much more difficult to analyze (therefore, popular strong ciphers are now block ones), and if at least somewhere in the key (respectively, the pseudo-random output of this function) was used twice, this instantly leads to the possibility of easy decryptionpreviously intercepted messages of this key. However, there are some nice simple aspects: the size of the plaintext is equal in length to the size of the encrypted one, there is no need to think about additions (up to the multiplicity of the size of the cipher block), about various encryption modes (CBC, ECB, CTR and others).
In addition to the data key, Salsa20 also accepts nonce: a number that should be used only once for a given key. Using twice is fatal. As nonce we use a simple, constantly incrementing counter. Each incoming packet increases it. This requires storing the state of the transport connection on both sides. As an alternative, you could use a pseudo-random number generator (PRNG) and access it every time, but it is much slower and find a quality one (which during the operation of a cipher with one key is guaranteed not to spit out the same number) PRNG is problematic. The host for decryption should also know this nonce. If the packages were not lost and arrived guaranteed in the given order, it’s quite simple to store the nonce state of the counter of the opposite side and increment it with the arrival of the packet. But UDP packets are lost and therefore we assign nonce to each packet at the beginning.
This is already a workable protocol that ensures the confidentiality of data transmission, however, it is vulnerable to attacks inherent in all stream ciphers: you can consciously change the ciphertext and make the required substitution of plaintext after decryption. To prevent them, it is necessary to authenticate the transmitted data packets: confirm and make sure that this is the data set that was transmitted. Typically, message authentication codes (MAC) are used. The input MAC receives the key and the data that needs to be authenticated, and the output receives the so-called tag attached to the message during transmission.
As MAC we choose Poly1305. Like Salsa20 from the same author, it has the highest security ceiling, performance and ease of implementation. Poly1305 has a different API in different libraries, but in the simplest case, we can assume that its keys are one-time and one key is used to authenticate only one message. As a key, you can have some kind of secret (known only to two parties) part with a counter attached to it. But it was already normal practice to generate an authentication key for each message based on the Salsa20 PRNG output of this packet. That is, at the time of encrypting the packet, we take the first 256 bits of the Salsa20 output and use them as the Poly1305 authentication key. Of course, we no longer use them to encrypt them. Since the internal state of the Salsa20 is 512 bits, we ignore the next 256 bits of output for every fireman. Yes, this is a performance loss, but a minor one, and giving a simple (and therefore potentially more reliable in terms of security) code. This is already in full practice in SSH and TLS protocols.
Thus, we get an authenticated encryption mode that can really be applied in practice. The only thing that we can add a little is the obfuscation / scrambling of the transmitted nonce. From the point of view of security, there are no complaints about it, but we are looking to ensure that, ideally, our protocol is indistinguishable from noise and random data, in order to complicate the life of all the traffic DPI DPI systems.
Nonce is a 64 bit data block. Ideally, you can simply encrypt it, turning it into a kind of noise. We already have a Salsa20 cipher, however, it is a PRF function (pseudorandom function - a pseudorandom function), and we want to have PRP (pseudorandom permutation - pseudorandom permutations): just mix the bits. You can make PRP from PRF and Luby-Rackoffproved it. However, we still introduce another primitive: the XTEA block cipher function . The choice fell on her because of the ease of implementation, so as not to write the Feistel network from Salsa20 on her own. XTEA is not the fastest, not the safest (although it has a high threshold), but this is not so critical due to the fact that it will be called exactly once when sending a packet. Since nonce is not repeated, it is not necessary to think about encryption modes, initialization vectors, and the like. Since nonce is a multiple of the size of the cipher block, no addition is necessary.
In our case, the nonce encryption key will be the Salsa20 output with a zero (which is not used by both parties) nonce. It is calculated only once (after a handshake) and remains so constantly.
The last thing to remember is what to do with the replay attack.(replay attack). Its essence is that no one forbids to intercept a packet and send it again. There are many solution options: remember packets and see if dubbing occurs (this requires memory), look at time stamps (this requires good clock synchronization), or, in our case, just remember the last received nonce of the opposite side. All packets with nonce below the last will be ignored. Unfortunately, due to the nature of the UDP packets during the transmission process, they can be reordered and therefore will be lost with this approach. In our implementation (this does not concern the protocol as such), we allow optionally to have a certain window of permissible variations of nonce values: a compromise between absolute protection against repeated packets and a relatively small window when such an attack can be carried out in practice.
The resulting transport protocol looks like this:

Handshake
Now consider the handshake protocol, as a result of which the most important thing is to get a common encryption key and trust in the opposite side (authenticate it). As a result, this part of the protocol will work only once, in total it will transmit four packets, but all the security of the protocol completely depends on this stage of the handshake.
We do not use and do not want to use PSK for encryption: a long-lived key is, of course, simple, but if it is lost or compromised, we will be able to decrypt all previously intercepted traffic. I would like to have such a property as perfect direct secrecy. To do this, each time, for each new session, with each connection, we generate new keys. They are temporary, session and are only in the program memory and are forgotten when they are turned off. In addition, there are restrictions on the maximum data size that should be encrypted with one key. For Salsa20, this border is quite large, but for peace of mind, it is worth remembering it and, for example, exchanging keys (shaking hands from scratch) every N gigabytes of encrypted traffic.
For both parties to agree on the use of the same key, a protocol / key exchange algorithm such as Diffie-Helman must be used. The essence of his work from the point of view of the user is simple: both parties generate a pair of public and private keys, exchange public parts via an unsecured communication channel, perform calculations and they have a common secret key. Having intercepted the public parts of the keys, it is not trivial to find out their private parts or to find out the common one.
In our protocol we will use Curve25519: again, one of the simplest from the point of view of implementation in the program, which has excellent (possibly at the moment and unsurpassed) cryptographic security, amazing performance. The algorithm is based on working with elliptic curves and therefore has a compact public key size: 256 bits. A private key is generated simply by taking 256 bits of data from the PRNG of the operating system. Curve25519 this function, roughly speaking, allows you to multiply points on a curve. The public key is obtained by multiplying the point, which is the private key, with a constant embedded in the code. A common secret is obtained by multiplying the private key point of one side with the public key point of the opposite.
By transmitting one packet with the public key from each side, we are already able to establish a shared key, a shared secret. However, we do not authenticate the parties: we do not know with whom we communicate. Very often, in modern protocols, they exchange keys, then they are used to encrypt the communication channel, and they already use a separate authentication protocol: for example, passing passwords or using CHAP, or using asymmetric cryptography like RSA. Firstly, this means a large number of round-trip packets over the network, which is not very fast. Secondly, if asymmetric cryptography is used, then it is resource-intensive and complicated.
In our protocol, we use a common authentication key of the parties. Actually a password, but only highly entropic. If the task was to authenticate only the client, then on top of the channel encrypted with the session key, you could simply send this password and compare it with what is in the database. However, if there is an attacker server, then it will know the password. You can use the CHAP protocol: it is simple, fast, but the attacker server recognizes the password / key hash. On the one hand, the loss is not great, but this makes it possible to attack a hash function already.
The best choice for us is EKE: Encrypted Key Exchange - encrypted key exchange. This is a subspecies of the PAKE protocol family: password authenticated key exchange - password-authenticated key exchange. The essence of the protocols is that it is at the moment of key exchange that one or two parties are authenticated. In the simplest case, each of the two DH packets is symmetrically encrypted; a common password known to each other is used as a key. If the password does not match, then the parties receive incorrectly decrypted public keys, which will not give a matching shared key when multiplying.
There is such a property as zero-knowledge proof, in which no bit of information about the PSK key will be known to the attacker: he receives encrypted data (noise) and the only thing he can do is try to decrypt, but he has no way of knowing if he found the public key correctly (decrypted it), since it is also a noise. It is this property that is decisive in our choice of the DH-EKE protocol and popular SSH, TLS and others do not possess it - they all have attacks either on asymmetric cryptography, or on hash functions, or on symmetric ciphers.
All the same Salsa20 is used for encryption. Since it has nonce at the input, which should never be repeated, each time we generate a random nonce R for it and send along with the encrypted public key DH from the client.
R, enc(PSK, R, ClientDHPubKey) --> Server
The server, knowing the shared authentication key, is able to correctly decrypt the message, immediately calculate the shared secret.
R, enc(PSK, R, ClientDHPubKey) --> Server
enc(PSK, R+1, ServerDHPubKey) --> Client
For authentication of the parties, we will transmit random numbers ( RS from the server side) and expect to receive them again in the response after re-encryption:
R, enc(PSK, R, ClientDHPubKey) --> Server
enc(PSK, R+1, ServerDHPubKey), enc(K, R, RS) --> Client
enc(K, R+1, RS) --> Server
After receiving the public key of the server, the client is able to calculate the common key K , respectively, correctly decrypt RS, encrypt it and send it to the server. The server compares RS with what it sent (it must save the state in memory) and thereby make sure that the client really knows the PSK. To authenticate the server, a similar action is taken with a random number from the RC client :
R, enc(PSK, R, ClientDHPubKey) --> Server
enc(PSK, R+1, ServerDHPubKey), enc(K, R, RS) --> Client
enc(K, R+1, RS + RC) --> Server
enc(K, 0, RC) --> Client
In the latter case, we use zero as nonce, or R + 2 or something else like that. It doesn’t matter: if only it were not repeated when using one key, and the key K is already unique for each session. The client, having received the correctly decrypted RC from the server, is convinced that he and the server really have a common key and the server knows the PSK.
The common key K may not have the best entropy. Curve25519 still does not have all 256 bits involved, respectively, its cryptographic strength is slightly less than the symmetrical analogue of the 128 bit threshold. In addition, the unscrupulous side can use specially created public keys, which, when multiplied, can give weak K keys: in theory, this is possible, although in practice for Curve25519 it has not been noticed. Still, paranoid moods can be satisfied by separately generating each side a high-quality high-entropy key and transmitting it in packets encrypted with a K-key. The resulting shared key, which will already be used in the transport protocol, is obtained by the XOR-th parts created by the client and server. If either side is dishonorable, all the same, one highly entropy part of the key is enough for a good key. In the final diagram below, part of the client key is indicated asSC , and the server - SS .
And the final touch will be sending the customer ID. This part is not related to protocol security: it is for the convenience of administration only. When we have several clients, we need to somehow determine which PSK keys to use. You can distinguish between clients and servers by port, but we decided to send the identifier explicitly. The identifier is not secret information, but sending it explicitly, we do not have anonymity and give the opportunity to use DPI and clearly see which user establishes the connection.
Currently, the identifier is sent in the form of encrypted R (which is already present in the initial handshake from the user), where the client identifier is used as the key. The cipher is XTEA. Since R is different each time, then again, there is no need to think about initialization vectors, encryption modes. The server cannot immediately deterministically determine which key the encryption was performed and it simply iterates over all the client identifiers it knows and finds the one whose decryption result matches R. A symmetric cipher is a quick operation and therefore costs are not significant here, in fact by orders of magnitude smaller than DH work. In this case, of course, we provide the attacker with both plaintext and ciphertext, but XTEA cryptanalysis shows that there is nothing to worry about.
The final version of the handshake looks like this:

What else is worth doing or fixing?
At the moment, handshake packets have an explicit label (ends with zero bytes): the server can immediately understand that this is not a transport packet. If we want to resist DPI, we need to remove this mark and make handshake packets indistinguishable from noise. Fixed in version 2.3. The
protocol at the stage of shaking hands and generating PSK keys depends on high-quality PRNG. If PRNG is predictable, weak, then there is no need to talk about security. Running software under a closed proprietary operating system and using the word "security" is pointless. Popular Microsoft Windows and Apple OS X are known to have entropy and PRNG sources unsuitable for cryptography. One could build in one's own PRNG, for example, based on Fortuna, but this is only a delay of the inevitable leak of keys, since the OS still has full access to the program memory and no one guarantees that the information from them is not used for loopholes. Fixed in version 3.4 : you can use third-party EGD-compatible PRNG sources.
The protocol provides confidentiality, authenticity of the payload, two-way zero-knowledge authentication of the parties, but data on the size and time of occurrence of packets leak into the network. You can solve this by fragmenting them and adding “noise”: extraneous, not related to the real packet traffic. Fixed in version 3.0.
Also read the continuation of the article. We implement an even more secure VPN protocol !
All the best, do not switch!
© Sergey Matveev, Python and Go developer ivi.ru
Our previous publications:
» Extra elements or how we balance between servers
» Blowfish on guard ivi
» Non-personalized recommendations: association method
» By city and by weight or how we balance between CDN nodes
» I am Groot. We do our analytics on events
» All for one or how we built CDN