UDP-WG Implementation
Loading...
Searching...
No Matches
crypto Namespace Reference

The cryptographic implementations for WireGuard. More...

Classes

class  keypair
 A simple private-public keypair. More...
 
class  string
 A cryptographically secure string. More...
 

Functions

string DH (const string &priv, const string &pub)
 
keypair DH_GENERATE ()
 Generate a Curve25519 keypair.
 
string IV (uint64_t counter)
 Format the IV array given the WireGuard Counter.
 
string ENCRYPT (string key, const uint64_t &counter, const string &plain, const string &data)
 Encrypt with ChaCha20-Poly1305.
 
string DECRYPT (string key, const uint64_t &counter, const string &cipher, const string &data)
 Decrypt with ChaCha20-Poly1305.
 
keypair XENCRYPT (string key, const string &plain, const string &data)
 Encrypt with XChaCha20-Poly1305.
 
string XDECRYPT (string key, const keypair &pair, const string &data)
 Decrypt with XChaCha20-Poly1305.
 
string HASH (const string &in)
 Generate a BLAKE2s256 Hash of the input.
 
string HMAC (const string &key, const string &input, const size_t &size=32)
 Compute an HMAC using BLAKE and a key.
 
string MAC (const string &key, const string &input)
 OpenSSL makes no difference between HMAC-BLAKE2s256 and Keyed BLAKE2s256 Besides setting the size. Therefore, we can reuse the same code, and just return the required 16 bytes, as opposed to the 32 expected from HMAC proper.
 
std::vector< stringKDF (const size_t &n, const string &key, const string &input)
 Perform the HKDF scheme on our HMAC function.
 

Detailed Description

The cryptographic implementations for WireGuard.

Remarks
Like the main file at wireguard.h: this implementation was created in reference to the WireGuard Whitepaper (https://www.wireguard.com/papers/wireguard.pdf) herein referred to as the "Reference"
Originally, I intended to implement all the cryptographic algorithms using OpenSSL. This would not only reduce the amount of dependencies, but effectively eliminate them as most Linux systems already have it. However, OpenSSL neither support XChaCha20-Poly1305, and it's Curve25519 was not giving me correct results. Therefore, these parts of the program have been implemented using libsodium, which, rather unfortunately, was also unable to provide all the required functions (It doesn't have BLAKE2s support).
This namespace has the perk of showing both how to work with OpenSSL, and Sodium. The latter is much easier to work with, as OpenSSL's design of leasing dynamically allocated objects, checking the result of every call, and then having to free those objects is usually a pain to work with. Most solutions I've seen pick the lesser of two evils: Free all the resources within each error check, which clogs the code with duplication, or–worse–use goto statements to just jump to the end of the function. The approach I've taken is have each function be a wrapper for a lambda that makes all the OpenSSL calls (Or at least the ones that can give errors). The main function can then allocate everything, run the function, and then free everything afterwards, and the lambda is free to abort at any point back to the caller. This isn't really what lambdas were made for, but it makes the code cleaner, doesn't clutter the namespace with auxiliary functions,and ensures proper memory management and error checking.

Function Documentation

◆ DECRYPT()

string crypto::DECRYPT ( string key,
const uint64_t & counter,
const string & cipher,
const string & data )

Decrypt with ChaCha20-Poly1305.

Parameters
keyThe 32 byte key.
counterThe WireGuard counter.
cipherThe ciphertext.
dataThe AAD
Returns
the plaintext string
Exceptions
std::runtime_errorIf the key/data are invalid.
Remarks
See https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/original_chacha20-poly1305_construction

◆ DH()

string crypto::DH ( const string & priv,
const string & pub )

Perform a Curve25519 point multiplication on the public and private key.

Parameters
keypairThe 32 byte public key and private key
Returns
The 32 byte product.
Remarks
See 5.4 of the Reference
Curve25519 Point Multiplication has this really interesting property where if you have a two keypairs A and B, the result of DH(A_priv, B_pub) is equal to DH(B_priv, A_pub). This means, that by solely exchanging public keys, two peers can come to the same value by simply multiplying their private key against the received key. This is akin to how standard DH works (Hence the name), Where we share a public value which is a generator raised to our private key, and by raising this received value by our own private key, we come to the same value.
Why does this work? In essence, the private key is a very large number that gets multiplied. to a generator (In this case the Curve25519 generator on its curve), which returns us a point on that curve that is our public key. The idea is that while multiplying the curve generator against the private number is very quick, trying to derive the original private key from only the public point requires tedious addition and is akin to the discrete logarithm problem. Let's try expanding our values (Note that operations like * and + are performed in GF(2**255 - 19):
  1. A_pub = G * A_priv; B_pub = G * B_priv
  2. DH(A_priv, B_pub) = DH(A_priv, G * B_priv) = A_priv * G * B_priv
  3. DH(B_priv, A_pub) = DH(B_priv, G * A_priv) = B_priv * G * A_priv. And, thanks to the associative property of multiplication, we reach the same value.

◆ DH_GENERATE()

keypair crypto::DH_GENERATE ( )

Generate a Curve25519 keypair.

Returns
{public,private} keypair of 32 bytes each.
Remarks
WireGuard identifies peers and the interface (IE the server) Via a Curve25519, 32-byte public key. This function uses Sodium to generate such a keypair.
See Section 2 of the Reference.
Curve25519 is what is called an "Elliptic Curve," and is the EC in ECC. It is literally a curve, derived from a mathematica functional: such as y = x**2. This curve in particular is: y**2 = x**3 + 486662(x**2) +x. So, how does this curve somehow generate a keypair? In my own personal readings, I found this video: https://www.youtube.com/watch?v=NF1pwjL9-DE to be very helpful, but I'll give you the summary: To generate a keypair, we first take a point on the curve, G which is the generator. Everyone knows this value, and its akin to the public g in Diffie-Hellman (Which is where the DH in these functions come from, if you're interested). Then, we generate a massive random number as our private key, and the idea is that you continually add the generator to itself which, due to the properties of the curve and the way you add (It's not complicated, I just don't want to make this more verbose than it already is), will leave you with a public value which is a point on that curve, which is your private key multiplied by the generator. We can easily calculate our public value from our private value and generator by simply doing pub = priv * gen (Curve Multiplication is very fast), but with only the public and generator values, the only way to find the private is by brute forcing adding the generator over and over again until you get the public value.
The mathematics behind this is very similar to AES's GF(256), in fact, the name Curve25519 is because it's in GF(2**255 -19), and just like GF(256), this field requires special functions for mathematical operations, but is very fast (Hence why we can so quickly calculate pub*gen). I say this because when this used to be implemented in OpenSSL, I had to manually implement these operations. I based it off of another excellent source on the subject: https://martin.kleppmann.com/papers/curve25519.pdf but a combination of misusing OpenSSL's Curve25519 implementation, and trying to implement the operations myself, led to it not working, and I was able to reduce > 200 lines and a custom class into four lines of code with libsodium.

◆ ENCRYPT()

string crypto::ENCRYPT ( string key,
const uint64_t & counter,
const string & plain,
const string & data )

Encrypt with ChaCha20-Poly1305.

Parameters
keyThe 32 byte key key to use for encryption.
counterThe WireGuard Counter, for a nonce.
plainThe plaintext.
dataThe AAD.
Returns
The ciphertext
Remarks
This encryption scheme is like AES-CTR + HMAC, in that ChaCha20 is a cipher (stream, not block), and Poly1305 is a MAC that provides authentication. According to: https://en.wikipedia.org/wiki/ChaCha20-Poly1305 It's actually faster than AES-GCM. The delightful name of ChaCha is in reference to the scheme that it is based on: Salsa20. They use Pseudo-Random number generators alongside Add-Rotate-XOR operations. Wikipedia provides a sample implementation (https://en.wikipedia.org/wiki/Salsa20) that is a whole 32 lines.
A stream cipher, like ChaCha20, differs from a block cipher, like AES, in that rather than applying a set of operations to blocks, like breaking a message into 16 byte states for AES, we instead generate a "keystream", which we treat as one, massive one-time pad that we can XOR against the entire message. So, rather than working on blocks, we work on each bit of the input, typically just XORing the bit against the corresponding position in the keystream. This lets us stream values, such as incoming packets, with a continuous keystream, which is perfect for network applications, where data is received in such streams.
See https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/original_chacha20-poly1305_construction

◆ HASH()

string crypto::HASH ( const string & in)

Generate a BLAKE2s256 Hash of the input.

Parameters
inThe string to hash.
Returns
the 32 byte hash
Remarks
See 5.4 of the Reference.
BLAKE2 is neat, because it's a hashing algorithm that's based on the original BLAKE, which is based on an algorithm we've already seen: ChaCha. Wikipedia states what makes BLAKE different: "[A] permuted copy of the input block, XORed with round constants, is added before each ChaCha round." So, WireGuard is almost entirely based on the ChaCha algorithm, with Curve25519 for Key generation. BLAKE2 is also a really impressive algorithm: it's faster than MD5 and provides better security than SHA-2. There are two types: BLAKE2b, and BLAKE2s; Sodium does not implement the latter, but WireGuard requires it and as such we used OpenSSL for the implementation. Blake2s is specifically optimized for 32 bit computers.
Using BLAKE2s is really a baffling decision, and despite my best efforts could not come up with a reason for why it was chosen. For context, BLAKE2b, despite using more rounds, is FASTER than BLAKE2s. The only advantage of BLAKE2s is that it's for 8-32 bit platforms, whereas BLAKE2b is 64 bits. This is the reason that Sodium doesn't have BLAKE2s support: It has no real advantage: https://github.com/jedisct1/libsodium/issues/531 "I'm very reluctant [to BLAKE2s], or more generally to adding anything that provides no value over what is already available." Now, perhaps the reason is to provide support for older, 32 bit systems. That's all well and good, except for the fact that we proflifically use 64 bit values, such as the WireGuard counter. Perhaps they did some benchmarking, but I struggle to see how using an algorithm designed for 64 bits on a 32 bit at handshake and rekey would be slower than doubling the needed clock cycles to update a counter that is updated on every message. Given that WireGuard is also touted as a modern VPN, I feel like supporting 32 bit machines is anthesis to that design philosophy. At the end of the day, it doesn't really make that much of a difference, it just means that I can't implement WireGuard exclusively through Sodium, which I was hoping for.

◆ HMAC()

string crypto::HMAC ( const string & key,
const string & input,
const size_t & size = 32 )

Compute an HMAC using BLAKE and a key.

Parameters
keyThe 32 byte key
inputThe arbitrary sized input to hash.
Returns
The 32 byte HMAC
Remarks
See 5.4 of the Reference.
A very neat part of BLAKE2 is that it is a "Keyed-Hash." This means that BLAKE2 directly supports providing a key alongside the data, returning a MAC without needing to use algorithms like HMAC that use a hash under the hood.
This leads to a rather confusing section of the Reference, where it stipulates two functions: HMAC, which uses BLAKE in an HMAC construction, and MAC, which is BLAKE using it's self-keying functionality. OpenSSL's functionality is here: https://docs.openssl.org/3.3/man7/EVP_MAC-BLAKE2/ OpenSSL also has an HMAC section: https://docs.openssl.org/master/man3/HMAC Which politely tells you that it's deprecated and to instead use... https://docs.openssl.org/master/man3/EVP_MAC/ So we're back at EVP_MAC-BLAKE2. I couldn't figure out why WireGuard stipulates the need for two separate MAC algorithms, especially when both are using the same Hashing algorithm under the hood, and the only thing I could think of is that HMAC returns 32 bytes, whereas MAC returns 16. I had a separate function that actually used the BLAKE2s keyed for MAC, and HMAC-BLAKE2s for HMAC, but I noticed that it returned the exact same thing, just truncated. So, to save myself the redundant code, MAC just returns a truncated HMAC.

◆ IV()

string crypto::IV ( uint64_t counter)

Format the IV array given the WireGuard Counter.

Parameters
counterThe current WireGuard counter.
Remarks
As per Section 5.4 of the Reference: "[The nonce is] composed of 32 bits of zeros followed by the 64-bit little-endian value of counter"
I have a suspicion that the reason for this format of the IV is the same reason as the counter used in AES-GCM. We want a 96 bit nonce as per ChaCha20's requirements, but there isn't exactly a 12 byte register for easily adding such a massive number. Therefore, we use the largest value that can actually be executed on in a modern system, 64 bits. Unlike GCM, however, 2**64 is such a monstrous number that there isn't much of a risk of the WireGuard counter overflowing.

◆ KDF()

std::vector< string > crypto::KDF ( const size_t & n,
const string & key,
const string & input )

Perform the HKDF scheme on our HMAC function.

Parameters
nThe amount of rounds to run
keyThe 32 byte key to use for the HMAC
inputThe arbitrary sized input data for the HMAC.
Returns
a Vector containing N elements of 32 bytes each.
Remarks
See 5.4 of the Reference.
This is the nice thing about having all the algorithms implemented. Since these build off each other, we don't even need to interface with OpenSSL to implement this.
You may be concerned about how quickly this could become a monstrous output, but WireGuard never uses KDF with a n greater than 3.
KDF works by taking an initial key and an input to derive a generation, and then subsequently creates new iterations from that initial state (Sort of like how AES' Key Schedule worked). The main idea between the algorithm is to get multiple keys from a single input, but another use I've seen is intentionally slowing down encryption to thwart attackers from repeatedly guessing. For example, the GRUB bootloader uses PBKDF for encrypting the passwords of users which is not to derive 50,000 separate keys, but to require more computation to get the password at the end.

◆ MAC()

string crypto::MAC ( const string & key,
const string & input )

OpenSSL makes no difference between HMAC-BLAKE2s256 and Keyed BLAKE2s256 Besides setting the size. Therefore, we can reuse the same code, and just return the required 16 bytes, as opposed to the 32 expected from HMAC proper.

Parameters
keyThe 32-byte key
inputThe arbitrary input
Returns
THe 16 byte MAC.
Remarks
My understanding of OpenSSL, and BLAKE2s, was that the 16 byte MAC was a fundamentally different operation than the normal 32 byte HMAC. We can tell OpenSSL the size of the resulting MAC via "size": https://docs.openssl.org/3.0/man7/EVP_MAC-BLAKE2/#supported-parameters But there was two problems with that: first, setting "size" caused a double-free, and second, printing out the MAC's revealed that OpenSSL was just truncating the output. Rather than use OpenSSL's unintuitive functions for setting size, we can do it ourselves and truncate the result to 16.

◆ XDECRYPT()

string crypto::XDECRYPT ( string key,
const keypair & pair,
const string & data )

Decrypt with XChaCha20-Poly1305.

Parameters
keyThe 32 byte key.
pairThe cipher + nonce
dataThe AAD
Returns
the plaintext string
Remarks
See https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction

◆ XENCRYPT()

keypair crypto::XENCRYPT ( string key,
const string & plain,
const string & data )

Encrypt with XChaCha20-Poly1305.

Parameters
keyThe 32 byte key key to use for encryption.
plainThe plaintext.
dataThe AAD.
Returns
The ciphertext + nonce
Remarks
As per the reference, the nonce used for the X variants are just randomly generated, and thus carted around with the return.
The X stands for Extended, because the Nonce is 192-bits. When choosing nonces at random, it's got better security than the original ChaCha20-Poly1305, but it isn't technically standardized, so OpenSSL doesn't implement it.
See https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction