[noise] Simple 1-RTT protocol

Trevor Perrin trevp at trevp.net
Sat Jun 10 18:04:34 PDT 2017

We've talked about a "NoiseSocket" protocol that would be the default
usage for Noise.  I suggested a complicated versioning scheme which
allows clients to offer initial messages from different handshakes,
and the server chooses one of them:


Alexey has continued to explore this, and his latest draft is worth reading:


However, I wonder if complicated versioning is the wrong track.  I'd
like this to be easy to understand and add to every Noise library, so
we might need more simplicity.

Here's a simpler proposal:

Protocol messages

We'd use the Noise_XX pattern:
 -> e
 <- e, ee, s, es
 -> s, es

We'd give these messages names and arguments:

 -> ClientHello(client_version: byte, options: bytes[])
 <- ServerAuth(server_version: byte, server_credentials: bytes[])
 -> ClientAuth(client_credentials: bytes[])

The "options" is used as the initial, cleartext payload.  Its usage is
up to the application, but it is recommended that applications ignore
any contents they don't understand.  This allows applications to later
advertise support for new ciphers, or send ephemerals for different
curves, or advertise anything else.  However we don't specify how any
of that works, and just leave it for the application.

The "version" fields allow the parties to signal
compatibility-breaking choices, i.e. if a new client refuses to accept
the old client_version=0 cipher, it can signal client_version=1, and
old servers will error immediately.  If the client used "options" to
offer incompatible choices, servers can use server_version to indicate
which they are choosing.

The "server_credentials" and "client_credentials" are used as
handshake payloads and are intended to contain certificates, though
it's up to the application.  For example, they might be empty if a
party is using a "dummy" static public key, and not authenticating.

Message framing

ClientHello and ServerAuth message:
 - 1 byte: version
 - 2 bytes: length of following message
 - <message>

Other messages:
 - 2 bytes: length of following message
 - <message>

Since we can't know how all applications will use the version fields,
we should probably bind them, so we could do a prologue like:

"NoiseSocket" || client_version || server_version


Because we're trying to make this easy to use, we might be more
prescriptive about the API.  I'd suggest:

WriteClientHello(client_version=0, options=empty) -> handshake_message
ReadClientHello(version_window=(0,0), handshake_message) ->
(client_version, options)

WriteServerAuth(client_version, server_version, server_key_pair=empty,
server_credentials=empty) -> handshake_message
ReadServerAuth(version_window=(0,0), handshake_message) ->
(server_version, server_pub_key, server_credentials)

WriteClientAuth(client_key_pair=empty, client_credentials=empty) ->
ReadClientAuth(handshake_message) -> (client_pub_key, client_credentials)

Write(payload) -> transport_message
Read(transport_message) -> payload

The API reads and writes whole messages, instead of dealing with byte
streams, so that the caller can handle buffering and have full control
over packetizing.

If the client or server specifies an empty static key pair, the
library generates a "dummy" single-use key pair for them.

By requiring clients and servers to specify version windows =
(min_version, max_version), we hope that unrecognized versions will
cause immediate errors.

Does this have enough extensibility / future-proofing?

I'll argue that "options" lets clients advertise features that are
consistent with the original first message, "server_version" lets
servers indicate choice of some option, and "client_version" lets
clients indicate a break in compatibility in the first message.

Is that enough?


More information about the Noise mailing list