Files
specs/noise/README.md
2022-12-14 17:09:28 +01:00

495 lines
22 KiB
Markdown

# 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]
[@yusefnapora]: https://github.com/yusefnapora
[@raulk]: https://github.com/raulk
[@tomaka]: https://github.com/tomaka
[@romanb]: https://github.com/romanb
[@shahankhatch]: https://github.com/shahankhatch
[@Mikerah]: https://github.com/Mikerah
[@djrtwo]: https://github.com/djrtwo
[@dryajov]: https://github.com/dryajov
[@mpetrunic]: https://github.com/mpetrunic
[@AgeManning]: https://github.com/AgeManning
[@morrigan]: https://github.com/morrigan
[@araskachoi]: https://github.com/araskachoi
[@mhchia]: https://github.com/mhchia
See the [lifecycle document][lifecycle-spec] for context about the maturity level
and spec status.
[lifecycle-spec]: https://github.com/libp2p/specs/blob/master/00-framework-01-spec-lifecycle.md
## Table of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Overview](#overview)
- [Negotiation](#negotiation)
- [The Noise Handshake](#the-noise-handshake)
- [Static Key Authentication](#static-key-authentication)
- [libp2p Data in Handshake Messages](#libp2p-data-in-handshake-messages)
- [The libp2p Handshake Payload](#the-libp2p-handshake-payload)
- [Handshake Pattern](#handshake-pattern)
- [XX](#xx)
- [Cryptographic Primitives](#cryptographic-primitives)
- [Noise Protocol Name](#noise-protocol-name)
- [Wire Format](#wire-format)
- [Encryption and I/O](#encryption-and-io)
- [Design Considerations](#design-considerations)
- [No Negotiation of Noise Protocols](#no-negotiation-of-noise-protocols)
- [Why the XX handshake pattern?](#why-the-xx-handshake-pattern)
- [Why ChaChaPoly?](#why-chachapoly)
- [Distinct Noise and Identity Keys](#distinct-noise-and-identity-keys)
- [Why Not Noise Signatures?](#why-not-noise-signatures)
- [Changelog](#changelog)
- [r1 - 2020-01-20](#r1---2020-01-20)
- [r2 - 2020-03-30](#r2---2020-03-30)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Overview
The [Noise Protocol Framework][npf] 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](#design-considerations) 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][conn-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](#negotiation) 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](#the-noise-handshake) describes the
[handshake pattern](#handshake-pattern) and [how libp2p-specific data is
exchanged during the handshake](#libp2p-data-in-handshake-messages).
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](#static-key-authentication).
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](#wire-format). The cryptographic primitives used to secure the channel
are described in the [Cryptographic Primitives
section](#cryptographic-primitives).
## 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][conn-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](#cryptographic-primitives) used to construct it.
This section covers the method of [authenticating the Noise static
key](#static-key-authentication), the [libp2p-specific
data](#libp2p-data-in-handshake-messages) that is exchanged in handshake message
payloads, and the [supported handshake pattern](#handshake-pattern).
### Static Key Authentication
The [Security Considerations section of the Noise spec][npf-security] 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][peer-id-spec], 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](#the-libp2p-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](#the-libp2p-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][protobuf]
`NoiseHandshakePayload` message with the following schema:
``` protobuf
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][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][peer-id-spec-signing-rules]. 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][rfc-7748-sec-5].
The `extensions` field contains Noise extensions and is described in
[Noise Extensions](#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][npf-fundamental-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](#xx), 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](#why-the-xx-handshake-pattern) 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](#the-libp2p-handshake-payload), which contains a signature
authenticating the sender's static Noise key as described in the [Static Key
Authentication section](#static-key-authentication) 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](https://www.rfc-editor.org/rfc/rfc6066) (for TLS) and
[RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000#section-19.21)
(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][npf-dh-25519], [ChaChaPoly cipher functions][npf-cipher-chachapoly],
and [SHA256 hash function][npf-hash-sha256] as defined in the Noise spec.
## Noise Protocol Name
A Noise `HandshakeState` is initialized with the hash of a [Noise protocol
name][npf-protocol-names], 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][npf-message-format], 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](#encrypted-payloads).
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][npf-handshake-state] with the [Noise protocol
name](#noise-protocol-name) `Noise_XX_25519_ChaChaPoly_SHA256`.
Alice and Bob exchange handshake messages, during which they [authenticate each
other's static Noise keys](#static-key-authentication). Handshake messages are
framed as described in the [Wire Format section](#wire-format), and if a
handshake message contains a payload, it will have the structure described in
[Encrypted Payloads](#encrypted-payloads).
Following a successful handshake, each peer will possess two Noise
[`CipherState` objects][npf-cipher-state]. 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](#wire-format). 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][npf-message-format].
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][npf-security].
## 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][npf-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][issue-rm-noise-pipes] 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][npf-security] 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][noise-signatures-spec] 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
[peer-id-spec]: ../peer-ids/peer-ids.md
[peer-id-spec-signing-rules]: ../peer-ids/peer-ids.md#how-keys-are-encoded-and-messages-signed
[conn-spec]: ../connections/README.md
[multiselect-2-pr]: https://github.com/libp2p/specs/pull/95
[npf]: https://noiseprotocol.org/noise.html
[npf-prologue]: https://noiseprotocol.org/noise.html#prologue
[npf-handshake-basics]: https://noiseprotocol.org/noise.html#handshake-pattern-basics
[npf-fundamental-patterns]: https://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental
[npf-compound-protocols]: https://noiseprotocol.org/noise.html#compound-protocols
[npf-noise-pipes]: https://noiseprotocol.org/noise.html#noise-pipes
[npf-protocol-names]: https://noiseprotocol.org/noise.html#protocol-names-and-modifiers
[npf-dh-25519]: https://noiseprotocol.org/noise.html#the-25519-dh-functions
[npf-cipher-chachapoly]: https://noiseprotocol.org/noise.html#the-chachapoly-cipher-functions
[npf-hash-sha256]: https://noiseprotocol.org/noise.html#the-sha256-hash-function
[npf-security]: https://noiseprotocol.org/noise.html#security-considerations
[npf-message-format]: https://noiseprotocol.org/noise.html#message-format
[npf-alice-and-bob]: https://noiseprotocol.org/noise.html#alice-and-bob
[npf-handshake-indistinguishability]: https://noiseprotocol.org/noise.html#handshake-indistinguishability
[npf-handshake-state]: https://noiseprotocol.org/noise.html#the-handshakestate-object
[npf-cipher-state]: https://noiseprotocol.org/noise.html#the-cipherstate-object
[npf-channel-binding]: https://noiseprotocol.org/noise.html#channel-binding
[rfc-7748-sec-5]: https://tools.ietf.org/html/rfc7748#section-5
[protobuf]: https://developers.google.com/protocol-buffers/
[noise-socket-spec]: https://github.com/noisesocket/spec
[noise-signatures-spec]: https://github.com/noiseprotocol/noise_sig_spec/blob/master/output/noise_sig.pdf
[issue-rm-noise-pipes]: https://github.com/libp2p/specs/issues/249