Skip to content

Latest commit

 

History

History
494 lines (374 loc) · 22 KB

README.md

File metadata and controls

494 lines (374 loc) · 22 KB

noise-libp2p - Secure Channel Handshake

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.

Table of Contents

Overview

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.

Negotiation

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.

The Noise Handshake

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.

Static Key Authentication

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.

libp2p Data in Handshake Messages

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 libp2p Handshake Payload

The Noise Protocol Framework caters for sending early data alongside handshake messages. We leverage this construct to transmit:

  1. the libp2p identity key along with a signature, to authenticate each party to the other.
  2. 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.

Handshake Pattern

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

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.

Noise Extensions

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.

Cryptographic Primitives

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.

Noise Protocol Name

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.

Wire Format

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.

Encryption and I/O

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.

Design Considerations

No Negotiation of Noise Protocols

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.

Why the XX handshake pattern?

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.

Why ChaChaPoly?

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.

Distinct Noise and Identity Keys

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.

Why Not Noise Signatures?

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.

Changelog

r1 - 2020-01-20

  • Renamed protobuf fields
  • Edited for clarity

r2 - 2020-03-30

  • Removed Noise Pipes and related handshake patterns
  • Removed padding within encrypted payloads

r3 - 2022-09-20

  • Change Protobuf definition to proto2 (due to the layout of the protobuf used, this is backwards-compatible change)

r4 - 2022-09-22

  • Add Noise extension registry