Skip to content
Branch: master
Find file History

Latest commit

Fetching latest commit…
Cannot retrieve the latest commit at this time.

Files

Permalink
Type Name Latest commit message Commit time
..
Failed to load latest commit information.
README.md

README.md

noise-libp2p - Secure Channel Handshake

A libp2p transport secure channel handshake built with the Noise Protocol Framework.

Lifecycle Stage Maturity Status Latest Revision
1A Working Draft Active r1, 2020-01-20

Authors: @yusefnapora

Interest Group: @raulk, @tomaka, @romanb, @shahankhatch, @Mikerah, @djrtwo, @dryajov, @mpetrunic, @AgeManning, @morrigan, @araskachoi, @mhchia

See the lifecycle document for context about 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 supported handshake patterns 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.

The libp2p Interfaces and API section goes into detail about how noise-libp2p integrates with the libp2p framework and offers a suggested API for implementations to adapt to their respective language idioms.

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 set of supported handshake patterns.

A brief overview of the payload security and identity hiding properties of each handshake pattern is included in the description of each pattern, however, readers are strongly encouraged to refer to the Noise spec for a full understanding.

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. Implementations MAY allow persisting static Noise keys across process restarts, or they may generate new static Noise keys when initializing the noise-libp2p module.

Systems which enable the Noise Pipes pattern are likely to benefit from a longer lifetime for static Noise keys, as the static key is used in the optimistic case. Other systems may prefer to cycle static Noise keys frequently to reduce 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, and a valid signature of the early data MUST be included as described below.

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. arbitrary data private to the libp2p stack. This facility is not exposed to userland. Examples of usage include streamlining muxer selection.

These payloads MUST be inserted into the first message of the handshake pattern that guarantees secrecy.

  • In XX-initiated handshakes, the initiator will send its payload in message 3 (closing message), whereas the responder will send theirs in message 2 (their only message).
  • In IK-initiated handshakes, the initiator will optimistically send its payload in message 1 (as it satisfies the guarantee). Next, this case bifurcates:
    • If the responder continues the IK handshake, it will send its payload in message 2. The handshake ends.
    • If the responder fall backs to XXfallback, it will have failed to decrypt the payload in message 1. A retransmission from the initiator with the fresh cryptographic material is necessary. This is performed in message 3.

When decrypted, the payload has the structure described in Encrypted Payloads, consisting of a length-prefixed body field followed by optional padding.

The body of the payload contains a serialized protobuf NoiseHandshakePayload message with the following schema:

message NoiseHandshakePayload {
  bytes identity_key = 1;
  bytes identity_sig = 2;
  bytes data         = 3;
}

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 data field contains the "early data" provided to the Noise module when initiating the handshake, if any. The structure of this data is opaque to noise-libp2p and is defined in the connection establishment specs.

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.

Supported Handshake Patterns

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 two fundamental handshake patterns, one of which is optional and may be enabled for efficiency.

The XX handshake pattern provides mutual authentication and encryption of static keys and handshake payloads and is resistant to replay attacks. It is the most "expensive" handshake, requiring 1.5 round trips in order to be sound, however, the cost of sending the final handshake message may be amortized by sending the initiator's first transport message within the same transmission unit as the final handshake message. Implementations MUST support the XX handshake pattern.

The IK handshake pattern is used in the context of Optimistic 0-RTT with Noise Pipes and is described in that section along with the XXfallback variation on the XX pattern.

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.

A variation on the XX handshake, XXfallback can be optionally enabled to support Optimistic 0-RTT with Noise Pipes and is described in that context below.

Optimistic 0-RTT with Noise Pipes

The Noise spec describes a compound protocol called Noise Pipes, which enables 0-RTT encryption in the optimistic case, while allowing peers to fallback to a different Noise protocol if their initial handshake attempt fails.

The Noise Pipes protocol consists of the XX and IK handshake patterns, as well as a variation on XX called XXfallback.

The XX pattern is used for a full handshake when two peers have not communicated using Noise before. Once the handshake completes, Alice can cache Bob's static Noise key.

Later, Alice can open a new Noise connection to Bob using the IK pattern. This is a zero-RTT handshake that uses the cached static key to encrypt the initial handshake message.

If Alice attempts an IK handshake but Bob has changed his static Noise key, Bob will fail to decrypt the handshake message. However, Bob may use the ephemeral key from Alice's IK message to initiate a switch handshake with Alice using the XXfallback pattern. Bob effectively treats Alice's IK message as if it were the first message in an XX handshake and proceeds accordingly.

The handshake patterns unique to Noise Pipes, IK and XXfallback, are described below. Noise Pipes is an optional feature of noise-libp2p, and implementations that do support it SHOULD offer a single configuration option to enable Noise Pipes, rather than separate options for enabling IK and XXfallback.

IK

IK:
      <- s
      ...
      -> e, es, s, ss
      <- e, ee, se

In the IK handshake pattern, the initiator has prior knowledge of the responder's static Noise public key, indicated by the <- s token prior to the ... separator. This allows the initial handshake payload to be encrypted using the known static key, and hides the identity of the initiator from passive observers.

If the responder is unable to complete the IK handshake because their static key has changed, they may initiate an XXfallback handshake, using the ephemeral public key from the failed IK handshake message as pre-message knowledge.

Each handshake message will include a libp2p handshake payload that identifies the sender and authenticates the static Noise key.

XXfallback

XXfallback:
  -> e
  ...
  <- e, ee, s, es
  -> s, se

The XXfallback handshake pattern is used when a peer fails to decrypt an incoming IK handshake message that was prepared using a static Noise public key that is no longer valid.

The responder for a failed IK handshake becomes the initiator of the subsequent XXfallback handshake. For example, if Alice initiated an IK handshake that Bob was unable to decrypt, Bob will initiate the XXfallback handshake to Alice. This is reflected in the arrow direction above; fallback handshake patterns are notated in the so-called "Bob-initiated form," with arrows reversed from the canonical (Alice-initiated) form.

The handshake pattern is the same as in XX, however, Alice's ephemeral public key is obtained from her initial IK message, moving it to the pre-message section of the handshake pattern. Essentially, the failed IK message serves the same role as the first handshake message in the standard XX pattern.

Each handshake message will include a libp2p handshake payload that identifies the sender and authenticates the static Noise key.

Noise Pipes Message Flow

Noise Pipes is a compound protocol, and peers supporting Noise pipes need to be able to distinguish between handshake messages from each pattern. We also wish to impose no additional overhead on peers that do not support Noise Pipes.

There are four cases to support:

  • Neither party supports Noise Pipes.
  • Alice and Bob both support Noise Pipes.
  • Bob supports Noise Pipes but Alice does not.
  • Alice supports Noise Pipes but Bob does not.

If neither party supports Noise Pipes, they both use the XX handshake and life is easy.

If Alice and Bob both support Noise Pipes, Alice's initial handshake message to Bob may be either an XX or IK message. Bob, supporting Noise Pipes, will attempt to handle all initial handshake messages as IK messages.

If Alice sends an IK message to Bob to initiate a zero-RTT handshake and Bob has not changed his static Noise key, Bob will successfully decrypt the initial message and will respond with the next message in the IK sequence.

If Alice sends an XX message to initiate a full handshake, or if Bob's static key has changed, Bob will fail to decrypt the initial message as an IK message. Bob will then re-initialize his Noise handshake state using the XXfallback pattern, using the ephemeral key from the initial message as pre-message knowledge. This is semantically equivalent to re-initializing with the XX pattern and re-processing Alice's message as the first in the XX sequence.

If Alice sends an XX message, she will always receive an XX-compatible response. However, if Alice sends an IK message, Bob may reply with either the second IK message, or the first message in the XXfallback sequence (aka the second message in XX).

Alice will always attempt to process Bob's response to an IK handshake attempt as an IK response. If this succeeds, the handshake is complete. If Alice fails to decrypt Bob's response as an IK message, she will re-initialize her Noise handshake state using the XXfallback pattern and re-process Bob's reply. She will then respond with the final message in the XXfallback pattern, which also corresponds to the final message in XX.

If Bob supports Noise Pipes but Alice does not, Alice's initial handshake message will always be an XX message. Bob will first attempt to decrypt the initial message as an IK message, which will fail. He will then re-initialize his Noise state and respond with the first message in XXfallback, which is equivalent to the second XX message that Alice was expecting. Alice will complete the handshake by sending the final message in the XX sequence.

If Alice supports Noise Pipes but Bob does not, Alice may send an initial IK message to Bob. Bob, not knowing anything about Noise Pipes, will treat this as the initial message in the XX sequence. This will succeed, because the only required information from the initial XX message is the ephemeral public key, which is also present in the IK message. Bob's response will be the second message in the XX sequence. Alice will first try to decrypt this as an IK response, which will fail. She then re-initializes her Noise state to use XXfallback as in the case where Bob also supports Noise Pipes but cannot complete an IK handshake. She then completes the handshake by sending the third message in the XX sequence that Bob was expecting.

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.

Valid Noise Protocol Names

This section lists the Noise protocol names that are valid according to the definitions in this spec.

Because only a single set of cryptographic primitives is supported, the Noise protocol name depends on the handshake pattern in use.

The Noise_XX_25519_ChaChaPoly_SHA256 protocol MUST be supported by all implementations.

Implementations that support Noise Pipes will also support the following Noise protocols:

  • Noise_IK_25519_ChaChaPoly_SHA256
  • Noise_XXfallback_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. The decrypted plaintext of the encrypted payload will have the structure described in the Encrypted Payloads section.

Encrypted Payloads

All Noise transport messages have a single encrypted payload. Noise handshake messages may or may not have an encrypted payload.

Once decrypted, the plaintext of an encrypted payload will have this structure:

body_len body padding
2 bytes variable length variable length

The body_len field stores the length in bytes of the body field as an unsigned 16-bit big-endian integer.

All data following the body field consists of padding bytes, which must be ignored by the recipient. Senders SHOULD use a source of random data to populate the padding field and may use any length of padding that does not cause the total length of the Noise message to exceed 65535 bytes.

Encryption and I/O

During the handshake phase, the initiator (Alice) will initialize a Noise HandshakeState object with their preferred concrete Noise protocol.

If Alice does not support Noise Pipes, this will be Noise_XX_25519_ChaChaPoly_SHA256. With Noise pipes, the initial protocol may use the IK handshake pattern instead of XX.

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.

When decrypted, the payload of a Noise transport message will have the structure described in Encrypted Payloads. Receivers MUST decode the body_len field from the decrypted payload, and MUST ignore any additional padding following the body field.

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.

libp2p Interfaces and API

This section describes an abstract API for noise-libp2p. Implementations may alter this API to conform to language idioms or patterns used by the targeted libp2p implementation. Examples are written in pseudo-code that vaguely resembles Swift.

Initialization

The noise-libp2p module accepts the following inputs at initialization.

  • The private libp2p identity key
  • [optional] An early data payload to be sent in handshake messages
  • [optional] The private Noise static key
  • [optional] If Noise Pipes is supported, a flag to enable at runtime

The private libp2p identity key is required for static key authentication and signing of early data (if provided).

Implementations that support sending early data in handshake messages should accept this data at initialization time, rather than accepting an early data payload for each new connection. This ensures that no user or connection-specific data can be present in the early data payload.

If a noise-libp2p implementation supports persisting the static Noise key, the constructor for the noise-libp2p module must accept a stored key.

If a noise-libp2p implementation supports Noise Pipes, they may expose a configuration flag to selectively enable Noise Pipes at runtime.

A minimal constructor could look like:

init(libp2pKey: PrivateKey) -> NoiseLibp2p

While one supporting all options might look like:

init(libp2pKey: PrivateKey, noiseKey: ByteString, earlyData: ByteString,
useNoisePipes: bool) -> NoiseLibp2p

Secure Transport Interface

noise-libp2p is designed to work with libp2p's transport upgrade pattern. libp2p security modules conform to a secure transport interface, which provides the SecureOutbound and SecureInbound methods described below.

SecureOutbound and SecureInbound each accept an InsecureConnection and return a NoiseConnection on success.

The details of the InsecureConnection type are libp2p-implementation dependent, but it is assumed to expose a bidirectional, reliable streaming interface.

NoiseConnection

A NoiseConnection must conform to the libp2p secure transport interface in the noise-libp2p implementation language by defining SecureOutbound and SecureInbound connections, described below.

In addition to the secure transport interface defined by the libp2p framework, a NoiseConnection MAY have an additional method to expose early data transmitted by the remote peer during the handshake phase, if any. For example:

remoteEarlyData() -> ByteString?

Following a successful handshake, a NoiseConnection will transmit and receive data over the InsecureConnection as described in Encryption and I/O.

SecureOutbound

SecureOutbound(insecure: InsecureConnection, remotePeer: PeerId) -> Result<NoiseConnection, Error>

SecureOutbound initiates a noise-libp2p connection to remotePeer over the provided InsecureConnection.

The remotePeer PeerId argument MUST be validated against the libp2p public identity sent by the remote peer during the handshake. If a remote peer sends a public key that is not capable of deriving their expected peer id, the connection MUST be aborted.

Note that the interface does not allow the user to choose the Noise handshake pattern. Implementations that support Noise Pipes must decide whether to use an XX or IK handshake based on whether they possess a cached static Noise key for the remote peer.

SecureInbound

SecureInbound(insecure: InsecureConnection) -> Result<NoiseConnection, Error>

SecureInbound attempts to complete a noise-libp2p handshake initiated by a remote peer over the given InsecureConnection.

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 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.

You can’t perform that action at this time.