A libp2p transport secure channel handshake built with the Noise Protocol Framework.
Lifecycle Stage | Maturity | Status | Latest Revision |
---|---|---|---|
3A | Recommendation | Active | r5, 2022-12-07 |
Authors: @yusefnapora
Interest Group: @raulk, @tomaka, @romanb, @shahankhatch, @Mikerah, @djrtwo, @dryajov, @mpetrunic, @AgeManning, @morrigan, @araskachoi, @mhchia
See the lifecycle document for context about the maturity level and spec status.
- Overview
- Negotiation
- The Noise Handshake
- Cryptographic Primitives
- Noise Protocol Name
- Wire Format
- Encryption and I/O
- Design Considerations
- Changelog
The Noise Protocol Framework is a framework for building security protocols by composing a small set of cryptographic primitives into patterns with verifiable security properties.
This document specifies noise-libp2p, a libp2p channel security handshake built using the Noise Protocol Framework. As a framework for building protocols rather than a protocol itself, Noise presents a large decision space with many tradeoffs. The Design Considerations section goes into detail about the choices made when designing the protocol.
Secure channels in libp2p are established with the help of a transport upgrader, a component that layers security and stream multiplexing over "raw" connections like TCP sockets. When peers connect, the upgrader uses a protocol called multistream-select to negotiate which security and multiplexing protocols to use. The upgrade process is described in the connection establishment spec.
The transport upgrade process is likely to evolve soon, as we are in the process of designing multiselect 2, a successor to multistream-select. Some noise-libp2p features are designed to enable proposed features of multiselect 2, however noise-libp2p is fully compatible with the current upgrade process and multistream-select. See the Negotiation section for details about protocol negotiation.
Every Noise connection begins with a handshake between an initiating peer and a responding peer, or in libp2p terms, a dialer and a listener. Over the course of the handshake, peers exchange public keys and perform Diffie-Hellman exchanges to arrive at a pair of symmetric keys that can be used to efficiently encrypt traffic. The Noise Handshake section describes the handshake pattern and how libp2p-specific data is exchanged during the handshake.
During the handshake, the static DH key used for Noise is authenticated using the libp2p identity keypair, as described in the Static Key Authentication section.
Following a successful handshake, peers use the resulting encryption keys to send ciphertexts back and forth. The format for transport messages and the wire protocol used to exchange them is described in the Wire Format section. The cryptographic primitives used to secure the channel are described in the Cryptographic Primitives section.
libp2p has an existing protocol negotiation mechanism which is used to reach agreement on the secure channel and multiplexing protocols used for new connections. A description of the current protocol negotiation flow is available in the libp2p connections spec.
noise-libp2p is identified by the protocol ID string /noise
. Peers using
multistream-select for protocol negotiation may send this protocol ID during
connection establishment to attempt to use noise-libp2p.
Future versions of this spec may define new protocol IDs using the /noise
prefix, for example /noise/2
.
During the Noise handshake, peers perform an authenticated key exchange according to the rules defined by a concrete Noise protocol. A concrete Noise protocol is identified by the choice of handshake pattern and cryptographic primitives used to construct it.
This section covers the method of authenticating the Noise static key, the libp2p-specific data that is exchanged in handshake message payloads, and the supported handshake pattern.
The Security Considerations section of the Noise spec says:
* Authentication: A Noise protocol with static public keys verifies that the
corresponding private keys are possessed by the participant(s), but it's up to
the application to determine whether the remote party's static public key is
acceptable. Methods for doing so include certificates which sign the public key
(and which may be passed in handshake payloads), preconfigured lists of public
keys, or "pinning" / "key-continuity" approaches where parties remember public
keys they encounter and check whether the same party presents the same public
key in the future.
All libp2p peers possess a cryptographic keypair which is used to derive their peer id, which we will refer to as their "identity keypair." To avoid potential static key reuse, and to allow libp2p peers with any type of identity keypair to use Noise, noise-libp2p uses a separate static keypair for Noise that is distinct from the peer's identity keypair.
A given libp2p peer will have one or more static Noise keypairs throughout its lifetime. Because the static key is authenticated using the libp2p identity key, it is not necessary for the key to actually be "static" in the traditional sense, and implementations MAY generate a new static Noise keypair for each new session. Alternatively, a single static keypair may be generated when noise-libp2p is initialized and used for all sessions. Implementations SHOULD NOT store the static Noise key to disk, as there is no benefit and a hightened risk of exposure.
To authenticate the static Noise key used in a handshake, noise-libp2p includes a signature of the static Noise public key in a handshake payload. This signature is produced with the private libp2p identity key, which proves that the sender was in possession of the private identity key at the time the payload was generated.
In addition to authenticating the static Noise key, noise-libp2p implementations MAY send additional "early data" in the handshake message payload. The contents of this early data are opaque to noise-libp2p, however it is assumed that it will be used to advertise supported stream multiplexers, thus avoiding a round-trip negotiation after the handshake completes.
The use of early data MUST be restricted to internal libp2p APIs, and the early data payload MUST NOT be used to transmit user or application data. Some handshake messages containing the early data payload may be susceptible to replay attacks, therefore the processing of early data must be idempotent. The noise-libp2p implementation itself MUST NOT process the early data payload in any way during the handshake, except to produce and validate the signature as described below.
Early data provided by a remote peer should only be made available to other libp2p components after the handshake is complete and the payload signature has been validated. If the handshake fails for any reason, the early data payload MUST be discarded immediately.
Any early data provided to noise-libp2p MUST be included in the handshake payload as a byte string without alteration by the noise-libp2p implementation.
The Noise Protocol Framework caters for sending early data alongside handshake messages. We leverage this construct to transmit:
- the libp2p identity key along with a signature, to authenticate each party to the other.
- extensions used by the libp2p stack.
The extensions are inserted into the first message of the handshake pattern that guarantees secrecy. Specifically, this means that the initiator MUST NOT send extensions in their first message. The initiator sends its extensions in message 3 (closing message), and the responder sends theirs in message 2 (their only message). It should be stressed, that while the second message of the handshake pattern has forward secrecy, the sender has not authenticated the responder yet, so this payload might be sent to any party, including an active attacker.
When decrypted, the payload contains a serialized protobuf
NoiseHandshakePayload
message with the following schema:
syntax = "proto2";
message NoiseExtensions {
repeated bytes webtransport_certhashes = 1;
repeated string stream_muxers = 2;
}
message NoiseHandshakePayload {
optional bytes identity_key = 1;
optional bytes identity_sig = 2;
optional NoiseExtensions extensions = 4;
}
The identity_key
field contains a serialized PublicKey
message as defined
in the peer id spec.
The identity_sig
field is produced using the libp2p identity private key
according to the signing rules in the peer id
spec. The data to be signed is the UTF-8 string
noise-libp2p-static-key:
, followed by the Noise static public key, encoded
according to the rules defined in section 5 of RFC 7748.
The extensions
field contains Noise extensions and is described in
Noise Extensions.
Upon receiving the handshake payload, peers MUST decode the public key from the
identity_key
field into a usable form. The key MUST then be used to validate
the identity_sig
field against the static Noise key received in the handshake.
If the signature is invalid, the connection MUST be terminated immediately.
Noise defines twelve fundamental interactive handshake patterns for exchanging public keys between parties and performing Diffie-Hellman computations. The patterns are named according to whether static keypairs are used, and if so, by what means each party gains knowledge of the other's static public key.
noise-libp2p
supports the XX handshake pattern, which provides mutual
authentication and encryption of static keys and handshake payloads and is
resistant to replay attacks.
Prior revisions of this spec included a compound protocol involving the IK
and
XXfallback
patterns, but this was removed due
to the benefits not justifying the considerable additional complexity.
XX:
-> e
<- e, ee, s, es
-> s, se
In the XX
handshake pattern, both parties send their static Noise public keys
to the other party.
The first handshake message contains the initiator's ephemeral public key, which allows subsequent key exchanges and message payloads to be encrypted.
The second and third handshake messages include a handshake payload, which contains a signature authenticating the sender's static Noise key as described in the Static Key Authentication section and may include other internal libp2p data.
The XX handshake MUST be supported by noise-libp2p implementations.
Since the Noise handshake pattern itself doesn't define any extensibility mechanism, this specification defines an extension registry, modeled after RFC 6066 (for TLS) and RFC 9000 (for QUIC).
Note that this document only defines the NoiseExtensions
code points, and
leaves it up to the protocol using that code point to define semantics
associated with these code point.
Code points above 1024 MAY be used for experimentation. Code points up to this value MUST be registered in this document before deployment.
The Noise framework allows protocol designers to choose from a small set of Diffie-Hellman key exchange functions, symmetric ciphers, and hash functions.
For simplicity, and to avoid the need to explicitly negotiate Noise protocols, noise-libp2p defines a single "cipher suite".
noise-libp2p implementations MUST support the 25519 DH functions, ChaChaPoly cipher functions, and SHA256 hash function as defined in the Noise spec.
A Noise HandshakeState
is initialized with the hash of a Noise protocol
name, which defines the handshake pattern and cipher suite
used. Because noise-libp2p
supports a single cipher suite and handshake
pattern, the Noise protocol name MUST be: Noise_XX_25519_ChaChaPoly_SHA256
.
noise-libp2p defines a simple message framing format for sending data back and forth over the underlying transport connection.
All data is segmented into messages with the following structure:
noise_message_len |
noise_message |
---|---|
2 bytes | variable length |
The noise_message_len
field stores the length in bytes of the noise_message
field, encoded as a 16-bit big-endian unsigned integer.
The noise_message
field contains a Noise Message as defined in the Noise
spec, which has a maximum length of 65535 bytes.
During the handshake phase, noise_message
will be a Noise handshake message.
Noise handshake messages may contain encrypted payloads. If so, they will have
the structure described in the Encrypted Payloads
section.
After the handshake completes, noise_message
will be a Noise transport
message, which is defined as an AEAD ciphertext consisting of an encrypted
payload plus 16 bytes of authentication data.
During the handshake phase, the initiator (Alice) will initialize a Noise
HandshakeState
object with the Noise protocol
name Noise_XX_25519_ChaChaPoly_SHA256
.
Alice and Bob exchange handshake messages, during which they authenticate each other's static Noise keys. Handshake messages are framed as described in the Wire Format section, and if a handshake message contains a payload, it will have the structure described in Encrypted Payloads.
Following a successful handshake, each peer will possess two Noise
CipherState
objects. One is used to encrypt outgoing
data to the remote party, and the other is used to decrypt incoming data.
After the handshake, peers continue to exchange messages in the format described
in the Wire Format section. However, instead of containing a
Noise handshake message, the contents of the noise_message
field will be Noise
transport message, which is an AEAD ciphertext consisting of an encrypted
payload plus 16 bytes of authentication data, as defined in the Noise
spec.
In the unlikely event that peers exchange more than 2^64 - 1
messages, they
MUST terminate the connection to avoid reusing nonces, in accordance with the
Noise spec.
Supporting a single cipher suite allows us to avoid negotiating which concrete Noise protocol to use for a given connection. This removes a huge source of incidental complexity and makes implementations much simpler. Changes to the cipher suite will require a new version of noise-libp2p, but this should happen infrequently enough to be a non-issue.
Users who require cipher agility are encouraged to adopt TLS 1.3, which supports negotiation of cipher suites.
An earlier draft of this spec included a compound protocol called Noise
Pipes that uses the IK
and XXfallback
handshake patterns
to enable a slightly more efficient handshake when the remote peer's static
Noise key is known a priori. During development of the Go and JavaScript
implementations, this was determined to add too much complexity to be worth the
benfit, and the benefit turned out to be less than originally hoped. See the
discussion on github for more context.
We debated supporting AESGCM in addition to or instead of ChaChaPoly. The desire for a simple protocol without explicit negotiation of ciphers and handshake patterns led us to support a single cipher, so the question became which to support.
While AES has broad hardware support that can lead to significant performance improvements on some platforms, secure and performant software implementations are hard to come by. To avoid excluding runtime platforms without hardware AES support, we chose the ChaChaPoly cipher, which is possible to implement in software on all platforms.
Using a separate keypair for Noise adds complexity to the protocol by requiring signature validation and transmission of libp2p public keys during the handshake.
However, none of the key types supported by libp2p for use as identity keys are fully compatible with Noise. While it is possible to convert an ed25519 key into the X25519 format used with Noise, it is not possible to do the reverse. This makes it difficult to use any libp2p identity key directly as the Noise static key.
Also, Noise recommends only using Noise static keys with other Noise protocols using the same hash function. Since we can't guarantee that users won't also use their libp2p identity keys in other contexts (e.g. SECIO handshakes, signing pubsub messages, etc), requiring separate keys seems prudent.
Since we're using signatures for authentication, the Noise Signatures extension is a natural candidate for adoption.
Unfortunately, the Noise Signatures spec requires both parties to use the same signature algorithm, which would prevent peers with different identity key types to complete a Noise Signatures handshake. Also, only Ed25519 signatures are currently supported by the spec, while libp2p identity keys may be of other unsupported types like RSA.
- Renamed protobuf fields
- Edited for clarity
- Removed Noise Pipes and related handshake patterns
- Removed padding within encrypted payloads
- Change Protobuf definition to proto2 (due to the layout of the protobuf used, this is backwards-compatible change)
- Add Noise extension registry