Noise
- Author: Trevor Perrin (curves @ trevp.net)
- Date: 2015-06-12
- Revision: 00 (work in progress)
- Copyright: This document is placed in the public domain
1. Introduction
Noise is a framework for DH-based crypto protocols. Noise can describe protocols that consist of a single message as well as interactive protocols.
Noise messages are described in a language that specifies the exchange of DH public keys and DH calculations. The DH outputs are accumulated into a session state. This allows the construction of multi-message protocols.
The resulting protocols can be instantiated based on ciphersuites that fill in the details of the DH calculation and other crypto primitives.
2. Overview
2.1. Messages, sessions, and descriptors
Noise messages are ciphertext objects exchanged between parties. Noise messages can be created and consumed.
Each Noise party will have a session which contains the state used to process messages. Each Noise message corresponds to a descriptor which describes the contents of a message and the rules for processing it.
Creating a message requires prologue and payload data, a descriptor, and a session. The output is a message and an updated session.
Consuming a message requires a message, a descriptor, and a session. The output is prologue and payload data, and an updated session.
2.2. Prologue and payload
Noise messages will contain prologue and payload data. The payload is typically (but not always) encrypted. The prologue is an unencrypted header that can be used for versioning.
Prologue data is authenticated but ignored by default, so version numbers and other data can be added into the prologue. Older implementations that don't recognize these fields will ignore them.
2.3. Key agreement
Noise can implement protocols where each party has a static and/or ephemeral DH key pair. The static keypair is a longer-term key pair that exists prior to the protocol. Ephemeral key pairs are short-term key pairs that are created and destroyed during the protocol.
3. Crypto functions
Noise depends on the following functions, which are supplied by a ciphersuite:
-
DH(privkey, pubkey): Performs a DH or ECDH calculation and returns an output sequence of bytes.
-
ENCRYPT(k, authtext, plainttext), DECRYPT(k, authtext, ciphertext): Encrypts or decrypts data using the cipher key
k
, using authenticated encryption with the additional authenticated dataauthtext
. Returns an updated cipher key that may be used for subsequent calls to encrypt, so the encryption should be randomized or use an IV that's stored as part of the key. -
KDF(k, input): Takes a cipher key and some input data and returns a new cipher key. The KDF should be a PRF when keyed by
k
. The samek
may be used in a call toKDF
and toENCRYPT
orDECRYPT
, so these functions should use nonces or internal key derivation steps to make this secure. -
HASH(input): Hashes some input and returns the output.
4. Structures
4.1. Session variables
A Noise session contains several variables. Any of these variables may be empty.
Each session has two variables for DH (or ECDH) key pairs:
-
s
: The local static key pair -
e
: The local ephemeral key pair
Each session has two variables for DH (or ECDH) public keys:
-
rs
: The remote party's static public key -
re
: The remote party's ephemeral public key
The following variables are used for symmetric cryptography:
-
k
: A symmetric key for the cipher algorithm specified in the ciphersuite. -
h
: A hash output from the hash algorithm specified in the ciphersuite.
4.2. Messages
A Noise message has the following structure:
-
A 1-byte prologue length.
-
0-255 bytes of prologue data.
-
A sequence of public keys (perhaps encrypted), as determined by the message's descriptor.
-
A message payload which is either encrypted or in clear.
5. Processing
5.1. Reading and writing
While processing messages, a Noise party will perform writes into the message (for a creator) or reads from the message (for a consumer). While processing a message, the processor will maintain a local pointer to the last byte written (or read), and will write (or read) after that, and advance the pointer.
"Clear" reads and writes are performed without encryption. "Encrypted" reads
and writes will encrypt or decrypt the value using k
if k
is non-empty, and
replace k
. When encrypting or decrypting, the additional authenticated data
(authtext
) is set to h
followed by all preceding bytes of the message.
5.1. Descriptors
A descriptor is a comma-separated list containing some of the following tokens. The tokens describe the sequential actions taken by the creator or consumer of a message.
-
e
: The creator generates an ephemeral key pair, stores it in here
variable, and then performs a clear write of her ephemeral public key. The consumer performs a clear read of the ephemeral public key into herre
variable. -
s
: The creator performs an encrypted write of her static public key. The consumer performs an encrypted read of the static public key into herrs
variable. -
dhss, dhee, dhse, dhes
: A DH calculation is performed between the creator's static or ephemeral key (specified by the first character) and the consumer's static or ephemeral key (specified by the second character). The output is used to updatek
by calculatingk = KDF(k, output)
.
5.2. Session operations
A Noise session supports the following operations:
-
Initialize: Initializes a session.
-
Create message: Takes a prologue and payload (either of which may be empty) and a descriptor and returns a message.
-
Consume message: Takes a message and descriptor and returns a prologue and payload.
-
Split: Takes a session and returns two derived sessions. This is called at the end of a handshaking protocol to create sending and receiving sessions for each party and delete ephemeral private keys.
5.3. Initializing a session
Takes an ASCII label and writes its bytes into h
sequentially, zero-filling
any unused bytes. The label should be unique to the particular ciphertext,
descriptor, and protocol.
All other variables are set to empty.
5.3. Creating a message
On input of some prologue and payload data and a descriptor, a message is constructed with the following steps:
1) The length of the prologue data is written in the first byte, followed by prologue data. This write is performed in clear.
2) The descriptor is processed sequentially, as described above.
3) The payload is written into the message via an encrypted write (so
ciphertext is written if k
is not empty).
4) If the descriptor was not empty, h
is set to HASH(h || message)
.
5.4. Consuming a message
On input of a message and descriptor, the message is consumed with the following steps:
1) The prologue data is returned via some callback. The caller can examine the prologue data to see if the message is requesting different processing (e.g., requesting a different ciphersuite or protocol - however the details of this negotation is out of scope).
2) The descriptor is processed sequentially, as described above.
3) The payload is read via an encrypted read (so the ciphertext is decrypted if
k
is not empty).
4) If the descriptor was not empty, h
is set to HASH(h || message)
.
5.5. Splitting a session
A session is split with the following steps:
1) All private keys are deleted from the session.
2) The session is copied into two child sessions.
3) The first child's k
is set to k = KDF(k, zeros)
where zeros
is a
string of length equal to a DH output filled with zeros. The second child's
k
is set the same way except using a string filled with 0x01 bytes.
The two children are returned. Splitting typically happens after a handshake protocol is complete. The initiator of a protocol should use the first session for sending messages, and the second session for receiving them.
6. Protocols
6.1. Box protocols
The following "Box" protocols represent one-shot messages from a sender to a
recipient. The descriptor for the message is given, and whether the message
requires initialization with only the recipient's public key (rs
) or also the
sender's key pair (s
).
Box naming:
N = no static key for sender
K = static key for sender known to recipient
X = static key for sender transmitted to recipient
BoxN:
<- s
-----
-> e, dhes
BoxK:
<- s
-----
-> e, dhes, dhss
BoxX:
<- s
-----
-> e, dhes, s, dhss
Handshake protocols
The following "Handshake" protocols represent handshakes where the initiator and responder exchange messages.
Handshake naming:
N_ = no static key for initiator
X_ = static key for initiator transmitted to responder without forward secrecy
F_ = static key for initiator transmitted to responder with forward secrecy
_N = no static key for responder
_K = static key for responder known to initiator
_F = static key for responder transmitted to initiator with forward secrecy
HandshakeNN:
-> e
<- e, dhee
HandshakeNK:
<- s
-> e, dhes
<- e, dhee
HandshakeNF:
-> e
<- e, dhee, s, dhse
HandshakeXK:
<- s
-----
-> e, dhes, s, dhss
<- e, dhee, dhes
HandshakeFK:
<- s
-----
-> e, dhes
<- e, dhee
-> s, dhse
HandshakeFF:
-> e
<- e, dhee, s, dhse
-> s, dhse
BoxSS sAuth rAuth ( = not KCI-resistant) BoxNS sFS rAuth - BoxXS sFS sAuth rAuth sIDhide BoxNE sFS rFS rAuth - BoxXE sFS rFS sAuth rAuth sIDhide
For handshakes, I only show messages up to the point that features "stabilize", all subsequent messages in same direction have same properties:
HandshakeNX-> - HandshakeNX<- sFS rFS sAuth sIDhide ( = anyone can solicit) HandshakeNX-> sFS rFS rAuth -
HandshakeXX-> HandshakeXX<- sFS rFS sAuth sIDhide HandshakeXX-> sFS rFS sAuth rAuth sIDhide HandshakeXX<- sFS rFS sAuth rAuth sIDhide
HandshakeNS-> sFS rAuth - HandshakeNS<- sFS rFS sAuth sIDhide HandshakeNS-> sFS rFS rAuth -
HandshakeXS-> sFS rAuth - HandshakeXS<- sFS rFS sAuth sIDhide HandshakeXS-> sFS rFS sAuth rAuth sIDhide HandshakeXS<- sFS rFS sAuth rAuth sIDhide
HandshakeNE-> sFS rFS rAuth - HandshakeNE<- sFS rFS sAuth sIDhide
HandshakeXE-> sFS rFS sAuth rAuth sIDhide HandshakeXE<- sFS rFS sAuth sAuth sIDhide
4. Algorithms
4.1. Ciphersuite variables
SUITE_NAME = ? # 24-byte string uniquely naming the ciphersuite
K_LEN = Length of PRF key in bytes
OK_LEN = Length of output key in bytes
GENERATE_KEY():
# Returns a DH keypair
DH(privkey, pubkey):
# Calculates a DH result.
# Returns a DH secret of length DH_LEN.
ENCRYPT(cc, plaintext, authtext):
# Takes a cipher context, some additional authenticated data, and plaintext.
# Returns an "authenticated encryption" ciphertext of length equal to
# plaintext plus MAC_LEN.
# Modifies the value of 'cc'.
PRF(k, input):
# Takes a PRF key and some input data and returns a new PRF key and output key.
HASH(h, input):
# Takes a hash context and some input data, and hashes the input data. The eventual
# value of the hash context is the hash of all input data.
IPR
The Noise specification (this document) is hereby placed in the public domain.
Acknowledgements
Noise is inspired by the NaCl and CurveCP protocols from Dan Bernstein et al., and also by HOMQV from Hugo Krawzcyk.
Moxie Marlinspike and Christian Winnerlein assisted in designing the key derivation process. The Noise KDF has some similarity with HKDF from Hugo Krawzcyk, who also provided some feedback.
Additional feedback on spec and pseudocode came from: Jonathan Rudenberg, Stephen Touset, and Tony Arcieri.
Jeremy Clark, Thomas Ristenpart, and Joe Bonneau gave feedback on earlier versions.
2.1. PRF chains
A PRF is a "pseudorandom function" which takes a secret key and some input data and returns output data. The output data is indistinguishable from random provided the key isn't known. HMAC-SHA2-512 is an example.
We use the term "PRF chain" when some of the output from a PRF is used as an
"output key", and some is used as a new PRF key to process another input. The
below diagram represents a PRF chain processing inputs i0...i2
and producing
output keys ok0...ok2
, with a starting PRF key k0
and a final PRF key k3
:
k0
i0 ->|
v
k1 ok0
i1 ->|
v
k2 ok1
i2 ->|
v
k3 ok2
(k1, ok0) = PRF(k0, i0)
(k2, ok1) = PRF(k1, i1)
(k3, ok2) = PRF(k2, i2)t