[noise] Simple 1-RTT protocol strawman
Trevor Perrin
trevp at trevp.net
Sat Jun 24 22:12:19 PDT 2017
Hi all,
We've been tossing around ideas for a user-friendly 1-RTT protocol
based on Noise_XX. I think the goals are:
* An easy-to-use starting point for working with Noise.
* An alternative to 1-RTT protocols like TLS and SSH that is simpler
and more efficient.
I think the unresolved issues are padding, versioning/negotiation, and
the name. I'll suggest some resolutions, leading to a "strawman"
proposal as a basis for further discussion.
Padding
========
We've discussed whether padding should be a responsibility of the
application or this protocol.
I'll propose we handle it in the protocol, because:
* Having an API will encourage application designers to use it.
* If padding is required to be zero-filled by sender, and ignored by
recipient, then it becomes a reserved area which could be useful.
Suppose an application wants to transmit out-of-band data inside of
payloads (e.g. requesting rekey, or sending keep-alives). If the
application has "painted itself into a corner" and didn't use an
extensible data format for its payloads (like JSON, Protobufs, etc),
then data can be stuffed into the "padding".
* If we allow padding in the ClientHello's cleartext payload, then
the previous bullet could be particularly useful for
versioning/negotiation (see next section).
So every payload's plaintext - including the ClientHello cleartext -
could contain a "body" followed by padding:
- 2 bytes: body_len
- " bytes: body
- ? bytes: padding
Versioning and negotiation
===========================
If clients only support a single version, then versioning is easy:
clients just send a version indicator, and servers accept or reject.
Things get complicated when clients support multiple versions, or
multiple orthogonal options:
* Client have to advertise what they support. This could be as
simple as (min_version, max_version), or as complicated as a list of
TLS-style extensions.
* In addition to advertising what they support, clients might want to
send associated data. Examples are:
- Different ephemerals if the client is advertising different curves
- 0-RTT encrypted payload(s)
- The server name the client is contacting (like TLS SNI)
* Servers have to indicate which client options they are accepting.
Again, this could be as simple as a single version field, or as
complicated as a list of extensions.
* The simple protocol could choose not to have a special place for
advertisements / associated data, so the application has to transmit
them inside the initial cleartext payload.
So there's a lot of options, for example:
SINGLE VERSION
C->S: version
SERVER CHOICE
C->S: client_version
C<-S: server_version
SERVER CHOICE WITH CLIENT RESERVED AREA
C->S: client_version, client_reserved
C<-S: server_version
CLIENT RANGE WITH SERVER CHOICE
C->S: client_version, client_max_version
C<-S: server_version
MUTUAL EXTENSIONS
C->S: list of client_extensions
C<-S: list of server_extensions
I think we can rule out SINGLE VERSION, because we want the ability
for clients to advertise and servers to choose.
I would also rule out MUTUAL EXTENSIONS. I think that ends up in two
different but undesirable places. Either: A centrally-managed set of
extensions, like TLS, making the protocol difficult to evolve and
customize; or extension lists exposed through an API, which is
probably more parsing and API complexity than we want.
I'll argue for the middle option: SERVER CHOICE WITH CLIENT RESERVED
AREA. This allows the client to easily send it's minimum version in
client_version, and it can advertise and send associated data in the
cleartext payload. If the application designer forgets to make the
cleartext payload extensible, they still can advertise via some sort
of client "reserved "area".
Given this, what should versions and the reserved area look like? I'd
suggest a generous 32-bit size for versions, and giving the
application full control over its contents. The server_version might
have to encode a large number of choices, and it could be useful for
the application to subdivide client_version or server_version, e.g.
client_version = (max_version, min_version), or server_version = (dh,
cipher, hash).
The padding space in the cleartext payload could be used for the
client_reserved area.
It's a little weird to overload an encrypted-padding mechanism as
cleartext-reserved space. But one problem with reserved / extension
mechanisms is that they don't get tested so don't work when you need
them. The padding mechanism is more likely to be tested and
exercised.
Anyways, proposed message headers:
ClientHello and ServerAuth:
- 4 bytes: version
- 2 bytes: noise_message_len
- " bytes: noise_message
Other messages
- 2 bytes: noise_message_len
- " bytes: noise_message
Versioning and fallback
========================
Suppose the client is initialized with Noise protocol X, then the
server chooses Noise protocol Y. I think we'll want to handle this as
XXfallback, where the client uses the handshake hash from X as a
prologue for new protocol Y. (The alternative is that the client
re-initializes with protocol Y and simulates sending the same initial
message, but that requires buffering the whole initial message,
instead of just the handshake hash).
So we'll have to reflect that in the API (see later).
Naming
=======
NoiseSocket, NoiseTransport, and NoiseLink have been proposed.
I think NoiseSocket implies a byte-stream API, but we should probably
have a message API, so that the caller can control buffering. Also,
it's a better name for an API object, rather than a protocol.
NoiseTransport clashes with the Noise "transport phase" and "transport
messages".
We probably can't use names like "tubes" or "conduits", because
they're too close to "Pipe".
I'm liking "NoiseLink" because "link" is a small and simple word for a
small and simple protocol. But Alexey thinks it sounds like a techno
group.
I'm still leaning towards it, but any other ideas?
Protocol
=========
Pulling this together, we have:
Noise_XX:
-> e
<- e, ee, s, es
-> s, se
Message names:
-> ClientHello
<- ServerAuth
-> ClientAuth
ClientHello and ServerAuth headers:
- 4 bytes: version
- 2 bytes: noise_message_len
- " bytes: noise_message
Other message headers:
- 2 bytes: noise_message_len
- " bytes: noise_message
All payloads:
- 2 bytes: body_len
- " bytes: body
- ? bytes: padding
Prologue is different for the initial versus fallback case:
"NoiseLinkInit" || client_version
"NoiseLinkReinit" || client_version || server_version ||
preceding_handshake_hash
We'll use the second (fallback) case whenever server_version != client_version.
The recommended API has a "session object" with the following methods.
Note that the reserved/padding contents are not accessible through the
API. We want to discourage their use, except for emergencies.
The 'padded_len' parameter specifies the simulated length that the
encrypted plaintext will be padded to, so 65517 is the max value:
65535 (noise_message_len) - 16 (for authentication tag) - 2 (for
padding_len)
Client functions
-----------------
The client calls these in sequence. If the client only supports a
single version, it skips PeekServerAuth and ReinitializeClient. If it
supports multiple versions but PeekServerAuth returns a server_version
== client_version it can skip ReinitializeClient. The client can use
a specified ephemeral key pair in Reinitialize, e.g. if the client
sends multiple different ephemerals in the cleartext body.
InitializeClient
INPUT: client_version, dh, cipher, hash
OUTPUT: session object
WriteClientHello
INPUT: [cleartext_body]
- cleartext_body is zero-length if omitted
OUTPUT: client_hello_message
PeekServerAuth
INPUT: server_auth_message
OUTPUT: server_version
ReinitializeClient
INPUT: server_version, dh, cipher, hash[, new_client_ephemeral_key_pair]
- If server_version != client_version, fall back is used
OUPUT: updated session object
ReadServerAuth
INPUT: server_auth_message
- Errors if server_version does not match session version
OUTPUT: server_public_key, server_auth_body
WriteClientAuth
INPUT: [client_key_pair], [client_auth_body], [padded_len]
- client_key_pair is randomly generated (dummy) if omitted
- client_auth_body is zero-length if omitted
- padded_len is zero (no padding) if omitted
OUTPUT: client_auth_message
Server functions
-----------------
The server calls these in sequence. If the server only supports a
single version, or ReadClientHello returns client_version ==
server_version, then it skips ReinitializeServer. The server can use
a specified client ephemeral public key in Reinitialize, e.g. if the
client sends multiple different ephemerals in the cleartext_body.
InitializeServer
INPUT: server_version, dh, cipher, hash
OUTPUT: session object
ReadClientHello
INPUT: client_hello_message
OUTPUT: client_version, cleartext_body
ReinitializeServer
INPUT: server_version, dh, cipher, hash[, new_client_ephemeral_public_key]
- If server_version != client_version, fallback is used
OUTPUT: updated session object
WriteServerAuth
INPUT: [server_key_pair], [server_auth_body], [padded_len]
- Errors if server_version != client_version and no re-initialization
- server_key_pair is randomly generated (dummy) if omitted
- server_auth_body is zero-length if omitted
- padded_len is zero (no padding) if omitted
OUTPUT: server_auth_message
ReadClientAuth
INPUT: client_auth_message
OUTPUT: client_public_key, client_auth_body
Functions for both
-------------------
After WriteClientAuth / ReadClient, both parties can call Write and Read:
Write
INPUT: transport_body[, padded_len]
- padded_len is zero (no padding) if omitted
OUTPUT: transport_message
Read
INPUT: transport_message
OUTPUT: transport_body
Thoughts?
Looking at the API, it's still kind of low-level. But perhaps it's
easier to wrap into a high-level API then raw Noise?
Trevor
More information about the Noise
mailing list