Noise-C
 All Data Structures Files Functions Variables Typedefs Macros Groups Pages
Using Noise-C: Client/Server Echo Example

Table of Contents

This page describes how to use Noise-C to create a simple client/server echo system. This example uses the same wire protocol as the echo example from cacophony, for testing interoperability between Noise implementations.

The source code for the example is under the examples/echo directory in the Noise-C repository. There are three programs that make up the example: echo-client, echo-server, and echo-keygen. Here we describe the main points of the client code to demonstrate how to use Noise-C. The server side is similar.

Creating a Noise-C client application

Creating the HandshakeState

The first step in using a HandshakeState is to create it with either the noise_handshakestate_new_by_name() or noise_handshakestate_new_by_id() function:

NoiseHandshakeState *handshake;
(&handshake, protocol, NOISE_ROLE_INITIATOR);
if (err != NOISE_ERROR_NONE) {
noise_perror(protocol, err);
return 1;
}

Here the "protocol" variable is the name of the Noise protocol, such as "Noise_XX_25519_ChaChaPoly_BLAKE2s". Because we are writing a client, the role is NOISE_ROLE_INITIATOR. The server side uses NOISE_ROLE_RESPONDER instead.

If an error occurs, the noise_perror() function can be used to print a simple message to the standard error output in the same way as the perror() function in C. If you want to report errors through some means other than the standard error output, you can use noise_strerror() to obtain the error string directly.

For the rest of this page we will elide the calls to noise_perror() to make it easier to understand the code snippets. Error handling is very important. Noise sessions can fail for any number of reasons and forgetting to check an error return could lead the application to continue operating when it should stop.

When you no longer need the HandshakeState, the memory should be returned to the system with noise_handshakestate_free():

Setting keys

After the HandshakeState object has been created, the next step is to specify the prologue, pre shared key, and any public or private keys that are needed for the handshake. We start with the prologue:

err = noise_handshakestate_set_prologue(handshake, prologue, prologue_len);

We can also set the pre shared key (PSK) value:

err = noise_handshakestate_set_pre_shared_key(handshake, psk, sizeof(psk));

Next is the local client private key. The function noise_handshakestate_needs_local_keypair() tells us if the handshake requires a local keypair.

Once we know if we need a keypair, we obtain the local Diffie-Hellman key object using noise_handshakestate_get_local_keypair_dh() and set the key with noise_dhstate_set_keypair_private():

if (!echo_load_private_key(client_private_key, key, key_len))
return 0;
err = noise_dhstate_set_keypair_private(dh, key, key_len);
...
}

In this case we are using a private key only. Noise-C internally derives the public key from the private key. If we had both keys, then noise_dhstate_set_keypair() can be used to set both.

The public key for the remote server is set in a similar fashion:

if (!echo_load_public_key(server_public_key, key, key_len))
return 0;
err = noise_dhstate_set_public_key(dh, key, key_len);
...
}

Running the handshake phase

At this point we have a fully configured HandshakeState object. The next step is to start the handshake:

err = noise_handshakestate_start(handshake);

If there are any missing keys, then this function will fail with an error.

The handshake runs until there are no more packets to be processed. Each step of the way, the noise_handshakestate_get_action() function tells the application what needs to be done next. The two most important actions are NOISE_ACTION_WRITE_MESSAGE and NOISE_ACTION_READ_MESSAGE, which indicates whether the application should write or read the next handshake message. A typical handshake processing loop looks like this:

for (;;) {
int action = noise_handshakestate_get_action(handshake);
if (action == NOISE_ACTION_WRITE_MESSAGE) {
...
} else if (action == NOISE_ACTION_READ_MESSAGE) {
...
} else {
break;
}
}

When the application receives NOISE_ACTION_WRITE_MESSAGE, it should format the next handshake payload and call noise_handshakestate_write_message() to prepare it for transmission. The application then transmits what it was given over the transport.

When the application receives NOISE_ACTION_READ_MESSAGE, it should wait for an incoming message from the transport and pass it to noise_handshakestate_read_message() to process it and recover the payload.

This process continues until the action is either NOISE_ACTION_FAILED or NOISE_ACTION_SPLIT. An action of NOISE_ACTION_SPLIT indicates that the handshake has completed successfully and the application should move onto the data transport phase.

Running the data transport phase

Once the handshake completes successfully, we can "split" the HandshakeState into two CipherState objects, one for sending and the other for receiving:

err = noise_handshakestate_split(handshake, &send_cipher, &recv_cipher);

If the remote side provided its public key during the handshake, then noise_handshakestate_get_remote_public_key_dh() and noise_dhstate_get_public_key() can be used to retrieve it at this point.

The HandshakeState is now no longer required and we can discard it:

From now on, whenever the application has data to send, it calls noise_cipherstate_encrypt() or noise_cipherstate_encrypt_with_ad() on the sending CipherState, and then writes the resulting packet to the transport.

Whenever the application receives an incoming packet, it calls noise_cipherstate_decrypt() or noise_cipherstate_decrypt_with_ad() on the receiving CipherState to decrypt it, and then processes the contents if the decryption succeeded.

This process continues until the connection is terminated or an error occurs. The final step is to clean up the two CipherState objects:

Using the echo example

This section describes how to use the echo example: how to generate keys, how to start an echo server running, and how to connect to it using an echo client.

Generating keys

The echo example uses a very simple key format. Private key files are binary data containing the 32 or 56 bytes of the private key, for Curve25519 and Curve448 respectively. Public key files contain the base64 encoding of the 32 or 56 byte public key values.

The echo-keygen example can be used to generate new keys or you can use the keys from cacophony. To generate a new key for Curve25519, you would invoke the key generator as follows:

echo-keygen 25519 client_key_25519 client_key_25519.pub

The arguments are the curve type ("25519" or "448"), the name of the private key file, and the name of the public key file.

The server needs a full set of keys, so here's how to generate them all:

echo-keygen 25519 client_key_25519 client_key_25519.pub
echo-keygen 25519 server_key_25519 server_key_25519.pub
echo-keygen 448 client_key_448 client_key_448.pub
echo-keygen 448 server_key_448 server_key_448.pub

Starting an echo server

A new echo server can be started with the following command:

echo-server --key-dir=../keys 7000

The server will look in key-dir for the keys that were generated earlier. If –key-dir is omitted, it defaults to the current directory.

The pre-shared key value is loaded from the "psk" file in the key directory. It is assumed to be a 32 byte value encoded in base64. Pre-shared keys are only used if the client selects them.

The final argument is the port number for the server to bind to.

Running the echo client

To use the client, specify the Noise protocol name, server hostname, and server port number on the command-line:

echo-client Noise_NN_25519_AESGCM_SHA256 hostname 7000

In this case we are using the "NN" pattern which does not require any additional local or remote static keys. If keys are required, they can be provided via options:

echo-client --client-private-key=client_key_448 \
--server-public-key=server_key_448.pub \ Noise_KK_448_ChaChaPoly_BLAKE2b hostname 7000
echo-client --client-private-key=client_key_448 \
--server-public-key=server_key_448.pub \
--psk=pskfile \
NoisePSK_KK_448_ChaChaPoly_BLAKE2b hostname 7000

After performing the handshake, the client reads lines of text from its standard input and sends them to the server. After each line, the client will wait for a packet from the server and then will write its contents to standard output prefixed by Received:

Hello!
Received: Hello!
What day is it?
Received: What day is it?

If the keys supplied to the client do not match those used by the server, the connection will abort with a "MAC failure".

The –padding option can be supplied to cause the client to pad all outgoing messages to a uniform size with random data. The padding will be stripped when the echo returns from the server.

The –verbose or -v option can be supplied to dump the verbose packet contents that are transmitted or received. The –fixed-ephemeral or -f option will force the client or the server to use a fixed ephemeral key instead of the usual random ephemeral key. These options can help diagnose interoperability issues between different implementations of the echo protocol.