To design such a protocol, we need 4 primitives:

  • for Signatures (identity keypairs).
  • for encryption (jobs and results)
  • for key exchange (prekeys and ephemeral keys)
  • and a last one, a Key Derivation Function.

Signatures

Ed25519 is an industry standard

Encryption

We basically have 3 choices for encryption:

  • AES-GCM
  • ChaCha20Poly1305
  • XChaCha20Poly1305

AES-GCM

The Galois/Counter Mode (GCM) for the famous AES block cipher is certainly the safest and most commonly recommended choice if you want to use AES. It’s widely used principally thanks to its certifications and hardware support, which make it extremely fast on modern, mainstream CPUs. Unfortunately, being a mode for AES, it’s extremely hard to understand and easy to misuse or implement vulnerabilities when implementing it.

ChaCha20-Poly1305

ChaCha20-Poly1305 is a combination of both a stream cipher (ChaCha20) and MAC (Poly1305) which combined, make one of the fastest AEAD primitive available today, which does not require special CPU instructions. That being said, with Vector SIMD instructions, such as AVX-512, the algorithm is even faster.

It is particularly appreciated by cryptographers due to its elegance, simplicity, and speed. This is why you can find it in a lot of modern protocols such as TLS or WireGuard®.

Benchmarking

It’s not that easy to benchmark crypto algorithms (people often end up with different num- bers), but ChaCha20-Poly1305 is generally as fast or up to 1.5x slower than AES-GCM-256 on modern hardware.

XChaCha20-Poly1305

The X before ChaCha20-Poly1305 means eXtended nonce : instead of a 12 bytes (96 bits) nonce, it uses a longer one of 24 bytes (192 bits).

A larger nonce allows to avoid reusing a nonce with the same key, which is fatal for the security of the algorithm.

The birthday paradox

Due to the birthday paradox, when using random nonces with ChaCha20Poly1305 , “only” 2 ^ (96 / 2) = 2 ^ 48 = 281,474,976,710,656 messages can be encrypted using the same secret key, it’s a lot, but it can happen rapidly for network packets for example.

Draft RFC online: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-

Key exchange

Like Ed25519 , because it’s an industry standard, we are going to use X25519 for key exchange.

The problem with X25519 is that the shared secret is not a secure random vector of data, so it can’t be used securely as a secret key for our AEAD. Instead, it’s a really big number encoded on 32 bytes. Its entropy is too low to be used securely as an encryption key.

Key Derivation Function

There are a lot of Key Derivation functions available. blake2b is the simplest to understand and hardest to misuse.

Code example

pub fn register(api_client: &ureq::Agent) -> Result<config::Config, Error> {
    let register_agent_route = format!("{}/api/agents", config::SERVER_URL);
    let mut rand_generator = rand::rngs::OsRng {};
 
    let identity_keypair = ed25519_dalek::Keypair::generate(&mut rand_generator);
 
    let mut private_prekey = [0u8; crypto::X25519_PRIVATE_KEY_SIZE];
    rand_generator.fill_bytes(&mut private_prekey);
    let public_prekey = x25519(private_prekey.clone(), X25519_BASEPOINT_BYTES);
 
    let public_prekey_signature = identity_keypair.sign(&public_prekey);
 
    let register_agent = RegisterAgent {
        identity_public_key: identity_keypair.public.to_bytes(),
        public_prekey: public_prekey.clone(),
        public_prekey_signature: public_prekey_signature.to_bytes().to_vec(),
    };
 
    let api_res: api::Response<api::AgentRegistered> = api_client
        .post(register_agent_route.as_str())
        .send_json(ureq::json!(register_agent))?
        .into_json()?;
 
	panic!("Handling the server response excluded")
}
 

The Need for Two Keypairs

In this example, we use two keypairs: an Ed25519 for authentication, i.e. to prove who are cryptographically, and X25519 to establish a shared secret we can use for the conversation.

The idea is that if we use our identitykeypairs (Ed25519) to generate a shared secret, we might lose our forward secrecy, i.e. in case the private key is compromised, all conversations become immediately readable from the attacker. Instead, by using a separate pair of keys, an ephemeral /temporal prekey is generated for each conversation. Additionally:

  • The identity keypair has properties that make it optimal for signing because it has determinsitic nature
  • X25519, on the other hand, is specifically designed for fast and secure Diffie-Hellman key exchanges. Using it as a prekey aligns with the best practices for secure encryption.

The Signal protocol

This approach is similar to the Signal protocol:

  1. Each user has a long-term Ed25519 identity keypair.
  2. Temporary X25519 prekeys are generated for each session to establish forward secrecy.
  3. The X25519 prekey is signed with the Ed25519 private key to link it to the user’s identity.