Skip to content

no_std compatible, pure Rust implementation of the Noise protocol framework with post-quantum support

License

Notifications You must be signed in to change notification settings

jmlepisto/clatter

Repository files navigation

Clatter 🔊

Crates.io Version docs.rs GitHub branch check runs

no_std compatible, pure Rust implementation of the Noise protocol framework with support for Post Quantum (PQ) extensions as presented by Yawning Angel, Benjamin Dowling, Andreas Hülsing, Peter Schwabe, and Fiona Johanna Weber.

Main targets of this crate are correctness, extensibility, and strict no_std compatibility and those come with the small drawback of more verbose user experience with some boilerplate. If you don't need PQ functionality and are developing for a regular target, you probably are better off using these instead:

Basis of this implementation relies heavily on the abovementioned crates and I'm extending huge thanks to the developers for their effort!

⚠️ Warning ⚠️

  • This library has not received any formal audit
  • While we enable some cryptographic providers by default, it is up to you to get familiar with those and decide if they meet your security and integrity requirements
  • Post-Quantum cryptography generally is not as established and mature as classical cryptography. Users are encouraged to implement hybrid encryption schemes with classical crypto primitives incorporated to provide additional security in case of a catastrophic flaw in the post-quantum algorithms. Clatter provides DualLayerHandshake for this purpose.

Noise Protocol

This crate tracks Noise protocol framework revision 34. As of now we omit support for the following features:

  • Handshake pattern parsing support - Handshakes have to be instantiated with the correct primitives compile-time
  • Curve 448 DH support - No suitable Rust implementation exists for our requirements
  • Deferred pattern support - Can be implemented by the user
  • Fallback pattern support - Can be implemented by the user

Basics

From user perspective, everything in this crate is built around three types:

  • NqHandshake - Classical, non-post-quantum Noise handshake
  • PqHandshake - Post-quantum Noise handshake
  • DualLayerHandshake - Dual layer handshake, which combines two Noise handshakes and allows a naive hybrid encryption approach

Users will pick and instantiate the desired handshake state machine with the crypto primitives they wish to use (supplied as generic parameters) and complete the handshake using the methods provided by the common Handshaker trait:

  • write_message(...) - Write next handshake message
  • read_message(...) - Read next handshake message
  • is_finished() - Is the handshake ready?
  • finalize() - Move to transport state

Once the handshake is complete, the handshake state machine can be moved to transport state by calling .finalize(). This finishes the handshake and the returned TransportState can be used for encrypting and decrypting communication with the peer. Voilà!

As already mentioned, this crate is quite verbose due to no_std compatibility requirements, so it's a good idea to take a look at the examples for a better view of the handshake process.

Example

Simplified example with the most straightforward (and most unsecure) interactive PQ handshake pattern and no handshake payload data at all:

use clatter::crypto::cipher::ChaChaPoly;
use clatter::crypto::hash::Sha512;
use clatter::crypto::kem::rust_crypto_ml_kem::MlKem512;
use clatter::handshakepattern::noise_pqnn;
use clatter::traits::Handshaker;
use clatter::PqHandshake;

fn main() {
    let mut rng_alice = rand::thread_rng();

    // Instantiate initiator handshake
    let mut alice = PqHandshake::<MlKem512, MlKem512, ChaChaPoly, Sha512, _>::new(
        noise_pqnn(),   // Handshake pattern
        &[],            // Prologue data
        true,           // Are we the initiator
        None,           // Pre-shared keys..
        None,           // ..
        None,           // ..
        None,           // ..
        &mut rng_alice, // RNG instance
    ).unwrap();

    let mut buf_alice_send = [0u8; 4096];
    let mut buf_alice_receive = [0u8; 4096];

    // Send and receive handshake messages until the handshake is completed
    loop {
        if alice.is_write_turn() {
            // Write handshake message to buf_alice_send
            let n = alice.write_message(&[], &mut buf_alice_send).unwrap();
            // --> Deliver buf_alice_send[..n] to peer
            my_send_function(&buf_alice_send[..n]);
        } else {
            // <-- Receive message from peer to &buf_alice_receive
            let n = my_receive_function(&mut buf_alice_receive);
            // Process received handshake message
            let _ = alice.read_message(&buf_alice_receive[..n], &mut[]).unwrap();
        }

        if alice.is_finished() {
            break;
        }
    }

    // Move to transport state
    let mut alice = alice.finalize().unwrap();

    // All done! Use .send() and .receive() on the transport state to encrypt
    // and decrypt communication with the peer
    let n = alice.send(b"Hello from Alice", &mut buf_alice_send).unwrap();
    my_send_function(&buf_alice_send[..n]);   
}

Selectable Features

Clatter allows user to pick the crypto primitives they wish to use via feature flags. Below is a table of all the configurable features supported by Clatter:

Feature flag Description Default Details
use-25519 Enable X25519 DH yes
use-aes-gcm Enable AES-GCM cipher yes
use-chacha20poly1305 Enable ChaCha20-Poly1305 cipher yes
use-sha Enable SHA-256 and SHA-512 hashing yes
use-blake2 Enable BLAKE2 hashing yes
use-rust-crypto-ml-kem Enable ML-KEM (Kyber) KEMs by RustCrypto yes
use-pqclean-kyber Enable Kyber KEMs by PQClean yes
std Enable standard library support no Enables std for supported dependencies
alloc Enable allocator support no

PQ? NQ? Why should I care?

This crate refers to classical Noise handshakes as NQ handshakes (non-post-quantum). But what does a PQ (post-quantum) handshake actually mean?

Key encapsulation mechanism or KEM is a public-key encryption system that allows a sender to securely transmit a short shared secret to a receiver using the receivers public key. This shared secret can then be used as a basis for further symmetric encrypted communication.

Classical Noise uses Diffie-Hellman or DH key exchanges to establish a shared secret between the parties. During a DH key exchange the shared secret is generated by both parties through mutual computations on the publicly transmitted data - whereas KEMs are used to transmit the shared secret directly.

The motivation to use KEMs lies in the fact that there are KEM algorithms that are currently thought to be secure against cryptoanalytic attacks by quantum computers. The DH algorithms used by Noise rely on the difficulty of mathematical problems that can be easily solved on a powerful quantum computer. Such quantum computers do not exist yet, but the world is already shifting towards quantum-safe cryptography.

Post Quantum Noise by Yawning Angel et al. introduced methods and rules for substituting DH key exchanges from classical Noise with KEMs, while maintaining a similar level of secrecy. This crate provides a safe Rust based implementation for the post-quantum handshakes proposed by PQNoise - so that we can keep on benefitting from the clarity and formal security guarantees of Noise even in post-quantum era.

PQ Handshake Notation

Noise uses a simple pattern language for defining the handshake patterns. PQ patterns follow these same rules, only substituting DH tokens with ekem and skem operations, which indicate sending of a ciphertext that was encapsulated to the ephemeral/static key of the receiving party.

Differences to PQNoise paper

  • PQNoise presents the possibility to use different KEMs for ephemeral, initiator, and responder. With Clatter the same KEM is used for both initiator and responder operations, while it is still possible to configure a separate KEM for ephemeral use.
  • PQNoise presents SEEC, a method for improving RNG security in bad randomness settings. Clatter does not currently implement SEEC.

Protocol Naming and Interoperability

Noise uses the protocol name as a basis for the handshake hash and for this reason it is important for cross-implementation compatibility to have consistent naming schemes for the crypto primitives. For all the classical ones Noise spec defines the naming but there is no absolute source for naming the PQ ones.

On top of this, there's also the fact that Kyber KEM was renamed to "ML-KEM" during the selection process and some crypto crates still use the term "Kyber" while others have migrated to "ML-KEM". Clatter uses whichever name the underlying crate has chosen to use.

Thus Clatter proposes and uses the following naming scheme:

Primitive Protocol Name
Kyber 512 Kyber512
Kyber 768 Kyber768
Kyber 1024 Kyber1024
ML-KEM-512 MLKEM512
ML-KEM-768 MLKEM768
ML-KEM-1024 MLKEM1024

Clatter also includes the possibility to pick different KEMs for ehpemeral and static operations. If the same KEM is used for both, the name of the KEM is simply placed in the protocol name in place of the DH algorithm.

Examples:

Noise_pqNN_Kyber512_ChaChaPoly_BLAKE2s
Noise_pqNN_MLKEM512_ChaChaPoly_BLAKE2s

If, however, a different KEM is used for ephemeral and static operations, the resulting name will include both KEMs joined together with a + symbol - ephemeral KEM first.

Examples:

Noise_pqNN_Kyber512+Kyber1024_ChaChaPoly_BLAKE2s
Noise_pqNN_MLKEM512+Kyber768_ChaChaPoly_BLAKE2s

Verification

Caltter is verified by:

Roadmap

Planned features:

  • Dedicated TransportState variant for unreliable transport protocols (UDP)
    • Some form of encryption nonce n would be delivered along the transport message
    • Receiving party would validate the nonce and use it to decrypt the message
    • This would provide (more) robust transport in case of dropped or out-of-order messages
  • Built-in support for more KEM variants
  • Benchmarking and optimization

Changelog

Please see the releases page