# Secure a request-reply exchange with CURVE and Noise_XX

**For:** Rust developers who have an rzmq service working and now need it encrypted.

You will take a small REQ/REP price lookup over TCP, generate keypairs for it with the `rzmq` CLI and turn on CURVE encryption. Then you run the same exchange over Noise_XX. Along the way you will point a client at the wrong server key and watch the handshake fail in the socket monitor.

Before you start: you need Rust with Cargo. rzmq comes from crates.io as `rzmq = "0"`.

## Set up the project

Start a new binary crate.

```sh
cargo new secure-quotes
cd secure-quotes
```

CURVE and Noise_XX are both optional features of rzmq, so you name them in the dependency. The default features (`ipc`, `inproc` and `plain`) stay on. Your `[dependencies]` section should look like this:

```toml title="Cargo.toml"
[dependencies]
rzmq = { version = "0", features = ["curve", "noise_xx"] }
tokio = { version = "1", features = ["full"] }
```

rzmq has four security mechanisms and each socket picks one through its options:

- NULL is the default and does no security at all.
- PLAIN (default feature) sends a username and password in the clear, which is authentication without encryption.
- CURVE (`curve` feature) is the CurveZMQ protocol. It encrypts the session, authenticates the server to the client and interoperates with libzmq's CURVE.
- Noise_XX (`noise_xx` feature) uses the Noise Protocol Framework's XX handshake. It also encrypts and authenticates but only talks to other rzmq peers.

This tutorial covers the two that encrypt.

## Generate the keys

Both mechanisms use Curve25519 keypairs: a 32-byte secret key and the 32-byte public key derived from it. The `rzmq` command line tool generates them with `keygen curve` for CURVE and `keygen noise-xx` for Noise_XX. Install it once:

```sh
cargo install rzmq_cli
```

Make a keypair for the server and one for the client, for each mechanism. `--file-format binary` writes the raw 32 bytes, which is what rzmq's key options take, so your program can read the files as they are.

```sh
rzmq keygen curve --name server --output-dir keys --file-format binary
rzmq keygen curve --name client --output-dir keys --file-format binary
rzmq keygen noise-xx --name server --output-dir keys --file-format binary
rzmq keygen noise-xx --name client --output-dir keys --file-format binary
```

For each keypair the tool prints both keys in hex along with the file names. The first command prints:

```text
Generating CURVE (Curve25519) keypair with name: 'server' in directory: 'keys'
File content format: binary, Stdout display format: hex
Secret key (binary) saved to: "keys/server.curve.sk"
Public key (binary) saved to: "keys/server.curve.pk"

--- Generated Keys (Display Format: hex) ---
Secret Key (Hex): <64 hex characters>
Public Key (Hex): <64 hex characters>

IMPORTANT: Store the secret key file ("keys/server.curve.sk") securely!
The public key file ("keys/server.curve.pk") can be shared with peers.

Remember: rzmq socket options expect RAW BYTES for keys.
If your key files are in hex or base64 format, your application will need to decode them before setting the socket options.
```

Your keys are random, so the hex is shown as placeholders here. The secret key only needs to exist in the `.sk` file, so don't paste the hex anywhere.

The `.sk` files are created readable by your user only and the `.pk` files are world-readable. Keep the whole directory out of git before you do anything else:

```shell-session
$ echo "/keys" >> .gitignore
$ ls -l keys
-rw-r--r--  1 you  staff  32 Sep 24 09:00 client.curve.pk
-rw-------  1 you  staff  32 Sep 24 09:00 client.curve.sk
-rw-r--r--  1 you  staff  32 Sep 24 09:00 client.noise.pk
-rw-------  1 you  staff  32 Sep 24 09:00 client.noise.sk
-rw-r--r--  1 you  staff  32 Sep 24 09:00 server.curve.pk
-rw-------  1 you  staff  32 Sep 24 09:00 server.curve.sk
-rw-r--r--  1 you  staff  32 Sep 24 09:00 server.noise.pk
-rw-------  1 you  staff  32 Sep 24 09:00 server.noise.sk
```

In a real deployment each side only holds what it needs. The server machine has its own `.sk` files. Each client has its own `.sk` files plus copies of the server's `.pk` files. Public keys are safe to ship with the client or commit to a repo. Secret keys belong in a file owned by the service's user or in your secret store, loaded from a path in config at startup. Here both ends run in one process, so both sets sit in `keys/`.

## Start with an open exchange

Before adding security, here is the exchange you are going to protect. A REP server answers one price question and a REQ client asks it. The client opens a monitor before connecting and waits for `HandshakeSucceeded` before it sends, which is the point where rzmq says the connection is ready for application data. With NULL security that handshake is just the ZMTP greeting.

Replace `src/main.rs` with this:

```rust title="src/main.rs"
use rzmq::socket::SocketEvent;
use rzmq::{Context, Msg, Socket, SocketType, ZmqError};

const QUOTES: &str = "tcp://127.0.0.1:5670";

fn text(msg: &Msg) -> String {
    String::from_utf8_lossy(msg.data().unwrap_or_default()).into_owned()
}

async fn serve(server: Socket) -> Result<(), ZmqError> {
    let request = server.recv().await?;
    println!("server got: {}", text(&request));
    server.send(Msg::from_static(b"widget costs 42")).await?;
    Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), ZmqError> {
    let ctx = Context::new()?;

    let server = ctx.socket(SocketType::Rep)?;
    server.bind(QUOTES).await?;
    let serving = tokio::spawn(serve(server));

    let client = ctx.socket(SocketType::Req)?;
    let events = client.monitor_default().await?;
    client.connect(QUOTES).await?;

    while let Ok(event) = events.recv().await {
        if let SocketEvent::HandshakeSucceeded { endpoint } = event {
            println!("client handshake done with {endpoint}");
            break;
        }
    }

    client.send(Msg::from_static(b"price of widget?")).await?;
    let reply = client.recv().await?;
    println!("client got: {}", text(&reply));

    serving.await.expect("server task panicked")?;
    ctx.term().await?;
    Ok(())
}
```

Run it:

```shell-session
$ cargo run
client handshake done with tcp://127.0.0.1:5670
server got: price of widget?
client got: widget costs 42
```

Anyone on the network path can read those bytes and anyone can connect to that port and ask for a price. The next step fixes the first problem and lets the client prove it is talking to the real server.

## Turn on CURVE

CURVE needs options on both sockets, all set before `bind` or `connect`:

- The server sets `CURVE_SERVER` to `true` and gives its own secret key in `CURVE_SECRET_KEY`.
- The client gives its own secret key in `CURVE_SECRET_KEY` and the server's public key in `CURVE_SERVER_KEY`.

The client uses the server's public key to check that whoever answers holds the matching secret key. The keys go in as `&[u8; 32]`, which `set_option` accepts directly. A small `key` helper reads a key file and panics with the path if it is missing or the wrong size.

Replace `src/main.rs`:

```rust title="src/main.rs" {1,4,6-9,27-30,35-40,46}
use rzmq::socket::{CURVE_SECRET_KEY, CURVE_SERVER, CURVE_SERVER_KEY, SocketEvent};
use rzmq::{Context, Msg, Socket, SocketType, ZmqError};

const QUOTES: &str = "tcp://127.0.0.1:5671";

fn key(path: &str) -> [u8; 32] {
    let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
    bytes.try_into().expect("a key file holds exactly 32 bytes")
}

fn text(msg: &Msg) -> String {
    String::from_utf8_lossy(msg.data().unwrap_or_default()).into_owned()
}

async fn serve(server: Socket) -> Result<(), ZmqError> {
    let request = server.recv().await?;
    println!("server got: {}", text(&request));
    server.send(Msg::from_static(b"widget costs 42")).await?;
    Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), ZmqError> {
    let ctx = Context::new()?;

    let server = ctx.socket(SocketType::Rep)?;
    server.set_option(CURVE_SERVER, true).await?;
    server
        .set_option(CURVE_SECRET_KEY, &key("keys/server.curve.sk"))
        .await?;
    server.bind(QUOTES).await?;
    let serving = tokio::spawn(serve(server));

    let client = ctx.socket(SocketType::Req)?;
    client
        .set_option(CURVE_SECRET_KEY, &key("keys/client.curve.sk"))
        .await?;
    client
        .set_option(CURVE_SERVER_KEY, &key("keys/server.curve.pk"))
        .await?;
    let events = client.monitor_default().await?;
    client.connect(QUOTES).await?;

    while let Ok(event) = events.recv().await {
        if let SocketEvent::HandshakeSucceeded { endpoint } = event {
            println!("client handshake done with {endpoint} (CURVE)");
            break;
        }
    }

    client.send(Msg::from_static(b"price of widget?")).await?;
    let reply = client.recv().await?;
    println!("client got: {}", text(&reply));

    serving.await.expect("server task panicked")?;
    ctx.term().await?;
    Ok(())
}
```

Run it:

```shell-session
$ cargo run
client handshake done with tcp://127.0.0.1:5671 (CURVE)
server got: price of widget?
client got: widget costs 42
```

The application code didn't change at all. `send` and `recv` look the same and the encryption happens underneath once the handshake is done. That is why waiting for `HandshakeSucceeded` matters more here: it is the event that tells you the keys checked out.

The server never names any client key. rzmq doesn't support ZAP, the ZeroMQ protocol a server would use to accept or reject clients by public key, so a CURVE server takes any client that completes the handshake. CURVE here gives you an encrypted session and a server the client can trust. If the server also has to know who the client is, do that in your own messages on top.

## Point the client at the wrong server key

To see what failure looks like, make a third keypair that stands in for a server the client shouldn't trust:

```sh
rzmq keygen curve --name impostor --output-dir keys --file-format binary
rzmq keygen noise-xx --name impostor --output-dir keys --file-format binary
```

The real server keeps its real key. The client is told to expect `impostor.curve.pk`, as if it had been handed a stale or wrong key. There is no point sending anything, so this version only watches the monitor and reports the first handshake outcome.

Replace `src/main.rs`:

```rust title="src/main.rs" {2,4,27,33-45}
use rzmq::socket::{CURVE_SECRET_KEY, CURVE_SERVER, CURVE_SERVER_KEY, SocketEvent};
use rzmq::{Context, SocketType, ZmqError};

const QUOTES: &str = "tcp://127.0.0.1:5672";

fn key(path: &str) -> [u8; 32] {
    let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
    bytes.try_into().expect("a key file holds exactly 32 bytes")
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), ZmqError> {
    let ctx = Context::new()?;

    let server = ctx.socket(SocketType::Rep)?;
    server.set_option(CURVE_SERVER, true).await?;
    server
        .set_option(CURVE_SECRET_KEY, &key("keys/server.curve.sk"))
        .await?;
    server.bind(QUOTES).await?;

    let client = ctx.socket(SocketType::Req)?;
    client
        .set_option(CURVE_SECRET_KEY, &key("keys/client.curve.sk"))
        .await?;
    client
        .set_option(CURVE_SERVER_KEY, &key("keys/impostor.curve.pk"))
        .await?;
    let events = client.monitor_default().await?;
    client.connect(QUOTES).await?;

    while let Ok(event) = events.recv().await {
        match event {
            SocketEvent::HandshakeSucceeded { endpoint } => {
                println!("client handshake done with {endpoint}");
                break;
            }
            SocketEvent::HandshakeFailed {
                endpoint,
                error_msg,
            } => {
                println!("client handshake with {endpoint} failed: {error_msg}");
                break;
            }
            _ => {}
        }
    }

    ctx.term().await?;
    Ok(())
}
```

Run it:

```shell-session
$ cargo run
client handshake with tcp://127.0.0.1:5672 failed: Connection closed by peer or transport
```

The server is the side that finds the key wrong. The client encrypts its first handshake message to the key it was given, the server cannot decrypt it and closes the connection, so all the client learns is that the connection closed. A client that sees this right after connecting should suspect its server key before the network.

When the handshake fails, rzmq drops the connection and no application message crosses it. The client keeps its `connect` and tries again on its reconnect interval. Each attempt fails the same way until the client has the right key, so a real client should log `HandshakeFailed` where someone will see it.

`ctx.term()` still shuts everything down cleanly here, with the client's connection never established and the REP socket never having seen a request.

## Switch to Noise_XX

Noise_XX uses the same shape of options with different names:

- Both sides set `NOISE_XX_ENABLED` to `true` and their own secret key in `NOISE_XX_STATIC_SECRET_KEY`.
- The client sets `NOISE_XX_REMOTE_STATIC_PUBLIC_KEY` to the server's public key.

There is no separate server flag. In the XX handshake both sides send their static public keys encrypted, so the server learns the client's key during the handshake and the client checks the server's against the one it was given.

Replace `src/main.rs`:

```rust title="src/main.rs" {1-4,6,11,13-15,17-21,29,31,34,37,39,42-45,51-53,57-59,61}
use rzmq::socket::{
    NOISE_XX_ENABLED, NOISE_XX_REMOTE_STATIC_PUBLIC_KEY, NOISE_XX_STATIC_SECRET_KEY, SocketEvent,
};
use rzmq::{Context, Msg, Socket, SocketType, ZmqError};

const QUOTES: &str = "tcp://127.0.0.1:5673";

fn key(path: &str) -> [u8; 32] {
    let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
    bytes.try_into().expect("a key file holds exactly 32 bytes")
}

fn text(msg: &Msg) -> String {
    String::from_utf8_lossy(msg.data().unwrap_or_default()).into_owned()
}

async fn serve(server: Socket) -> Result<(), ZmqError> {
    let request = server.recv().await?;
    println!("server got: {}", text(&request));
    server.send(Msg::from_static(b"widget costs 42")).await?;
    Ok(())
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), ZmqError> {
    let ctx = Context::new()?;

    let server = ctx.socket(SocketType::Rep)?;
    server.set_option(NOISE_XX_ENABLED, true).await?;
    server
        .set_option(NOISE_XX_STATIC_SECRET_KEY, &key("keys/server.noise.sk"))
        .await?;
    server.bind(QUOTES).await?;
    let serving = tokio::spawn(serve(server));

    let client = ctx.socket(SocketType::Req)?;
    client.set_option(NOISE_XX_ENABLED, true).await?;
    client
        .set_option(NOISE_XX_STATIC_SECRET_KEY, &key("keys/client.noise.sk"))
        .await?;
    client
        .set_option(
            NOISE_XX_REMOTE_STATIC_PUBLIC_KEY,
            &key("keys/server.noise.pk"),
        )
        .await?;
    let events = client.monitor_default().await?;
    client.connect(QUOTES).await?;

    while let Ok(event) = events.recv().await {
        if let SocketEvent::HandshakeSucceeded { endpoint } = event {
            println!("client handshake done with {endpoint} (Noise_XX)");
            break;
        }
    }

    client.send(Msg::from_static(b"price of widget?")).await?;
    let reply = client.recv().await?;
    println!("client got: {}", text(&reply));

    serving.await.expect("server task panicked")?;
    ctx.term().await?;
    Ok(())
}
```

Run it:

```shell-session
$ cargo run
client handshake done with tcp://127.0.0.1:5673 (Noise_XX)
server got: price of widget?
client got: widget costs 42
```

The wrong-key test works the same way. Take the program from the previous section, swap the CURVE options for the Noise_XX ones on both sockets and give the client `impostor.noise.pk`:

```rust title="src/main.rs"
use rzmq::socket::{
    NOISE_XX_ENABLED, NOISE_XX_REMOTE_STATIC_PUBLIC_KEY, NOISE_XX_STATIC_SECRET_KEY, SocketEvent,
};
use rzmq::{Context, SocketType, ZmqError};

const QUOTES: &str = "tcp://127.0.0.1:5674";

fn key(path: &str) -> [u8; 32] {
    let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
    bytes.try_into().expect("a key file holds exactly 32 bytes")
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), ZmqError> {
    let ctx = Context::new()?;

    let server = ctx.socket(SocketType::Rep)?;
    server.set_option(NOISE_XX_ENABLED, true).await?;
    server
        .set_option(NOISE_XX_STATIC_SECRET_KEY, &key("keys/server.noise.sk"))
        .await?;
    server.bind(QUOTES).await?;

    let client = ctx.socket(SocketType::Req)?;
    client.set_option(NOISE_XX_ENABLED, true).await?;
    client
        .set_option(NOISE_XX_STATIC_SECRET_KEY, &key("keys/client.noise.sk"))
        .await?;
    client
        .set_option(
            NOISE_XX_REMOTE_STATIC_PUBLIC_KEY,
            &key("keys/impostor.noise.pk"),
        )
        .await?;
    let events = client.monitor_default().await?;
    client.connect(QUOTES).await?;

    while let Ok(event) = events.recv().await {
        match event {
            SocketEvent::HandshakeSucceeded { endpoint } => {
                println!("client handshake done with {endpoint}");
                break;
            }
            SocketEvent::HandshakeFailed {
                endpoint,
                error_msg,
            } => {
                println!("client handshake with {endpoint} failed: {error_msg}");
                break;
            }
            _ => {}
        }
    }

    ctx.term().await?;
    Ok(())
}
```

```text
client handshake with tcp://127.0.0.1:5674 failed: Security error: Noise decrypt/authentication failed (e.g., bad MAC or ciphertext).
```

Pick CURVE when any peer might be libzmq or another ZeroMQ implementation. Pick Noise_XX when every peer is rzmq and you want the server to learn each client's static key as part of the handshake. The two don't mix: both ends of a connection have to use the same mechanism.

## Shut down cleanly

Every program above ends the same way. The server task returns after its one reply, `main` awaits it and then `ctx.term()` closes both sockets and waits for their background tasks:

```rust title="src/main.rs"
serving.await.expect("server task panicked")?;
ctx.term().await?;
```

For a service that runs until told to stop, the security options don't change anything about shutdown. Load the keys once at startup, set them before `bind` or `connect` and keep the monitor running in its own task so a `HandshakeFailed` reaches your logs whenever a client shows up with the wrong key.

## Where to go next

The [rzmq usage guide](https://github.com/excsn/rzmq/blob/main/core/README.USAGE.md) lists the security options for PLAIN, CURVE and Noise_XX together with the rest of the monitor events. The full API reference, including every option constant and the `SocketEvent` enum, is on [docs.rs](https://docs.rs/rzmq).
