From f10e5fe59f1aadf44f06ecd34df3fedf4427f09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Sat, 24 Aug 2019 23:50:12 +0200 Subject: [PATCH 1/4] noise-libp2p: introduce "handshake seal"; more. The current construction is vulnerable to replay attacks by a MITM agent that records previous handshakes, and transplants old user data from those handshakes into new ones, as long as the static key hasn't changed. We fix this vulnerability by leveraging Noise channel binding, in order to "seal" the handshake and assert that the exchanged data was witnessed equally by both parties. This commit also simplifies protobuf field naming. Finally, we formalise in which Noise messages of IK and XX the message payload is to be shared, to guarantee secrecy, integrity and authentication. --- noise/README.md | 183 +++++++++++++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 71 deletions(-) diff --git a/noise/README.md b/noise/README.md index cbf389e61..b8cf8963a 100644 --- a/noise/README.md +++ b/noise/README.md @@ -4,7 +4,7 @@ | Lifecycle Stage | Maturity | Status | Latest Revision | |-----------------|---------------|--------|-----------------| -| 1A | Working Draft | Active | r0, 2019-08-05 | +| 1A | Working Draft | Active | r1, 2019-12-04 | Authors: [@yusefnapora] @@ -23,6 +23,7 @@ Interest Group: [@raulk], [@tomaka], [@romanb], [@shahankhatch], [@Mikerah], [@d [@morrigan]: https://github.com/morrigan [@araskachoi]: https://github.com/araskachoi + See the [lifecycle document][lifecycle-spec] for context about maturity level and spec status. @@ -32,35 +33,36 @@ and spec status. ## Table of Contents - [noise-libp2p - Secure Channel Handshake](#noise-libp2p---secure-channel-handshake) - - [Table of Contents](#table-of-contents) - - [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 Signed Handshake Payload](#the-libp2p-signed-handshake-payload) - - [Supported Handshake Patterns](#supported-handshake-patterns) - - [XX](#xx) - - [Optimistic 0-RTT with Noise Pipes](#optimistic-0-rtt-with-noise-pipes) - - [IK](#ik) - - [XXfallback](#xxfallback) - - [Noise Pipes Message Flow](#noise-pipes-message-flow) - - [Cryptographic Primitives](#cryptographic-primitives) - - [Valid Noise Protocol Names](#valid-noise-protocol-names) - - [Wire Format](#wire-format) - - [Encrypted Payloads](#encrypted-payloads) - - [Encryption and I/O](#encryption-and-io) - - [libp2p Interfaces and API](#libp2p-interfaces-and-api) - - [Initialization](#initialization) - - [Secure Transport Interface](#secure-transport-interface) - - [NoiseConnection](#noiseconnection) - - [SecureOutbound](#secureoutbound) - - [SecureInbound](#secureinbound) - - [Design Considerations](#design-considerations) - - [No Negotiation of Noise Protocols](#no-negotiation-of-noise-protocols) - - [Why ChaChaPoly?](#why-chachapoly) - - [Distinct Noise and Identity Keys](#distinct-noise-and-identity-keys) - - [Why Not Noise Signatures?](#why-not-noise-signatures) + - [Table of Contents](#table-of-contents) + - [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 sealing](#handshake-sealing) + - [Supported Handshake Patterns](#supported-handshake-patterns) + - [XX](#xx) + - [Optimistic 0-RTT with Noise Pipes](#optimistic-0-rtt-with-noise-pipes) + - [IK](#ik) + - [XXfallback](#xxfallback) + - [Noise Pipes Message Flow](#noise-pipes-message-flow) + - [Cryptographic Primitives](#cryptographic-primitives) + - [Valid Noise Protocol Names](#valid-noise-protocol-names) + - [Wire Format](#wire-format) + - [Encrypted Payloads](#encrypted-payloads) + - [Encryption and I/O](#encryption-and-io) + - [libp2p Interfaces and API](#libp2p-interfaces-and-api) + - [Initialization](#initialization) + - [Secure Transport Interface](#secure-transport-interface) + - [NoiseConnection](#noiseconnection) + - [SecureOutbound](#secureoutbound) + - [SecureInbound](#secureinbound) + - [Design Considerations](#design-considerations) + - [No Negotiation of Noise Protocols](#no-negotiation-of-noise-protocols) + - [Why ChaChaPoly?](#why-chachapoly) + - [Distinct Noise and Identity Keys](#distinct-noise-and-identity-keys) + - [Why Not Noise Signatures?](#why-not-noise-signatures) ## Overview @@ -71,9 +73,8 @@ 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. +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 @@ -98,9 +99,9 @@ traffic. The [Noise Handshake section](#the-noise-handshake) describes the 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). +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 @@ -207,56 +208,95 @@ handshake payload](#the-libp2p-signed-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 Signed Handshake Payload +#### 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 the first message of the handshake pattern +**that guarantees secrecy**. -libp2p-specific data, including the signature used for static key -authentication, is transmitted in Noise handshake message payloads. When -decrypted, the message payload has the structure described in [Encrypted +* 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 signed payload has the structure described in [Encrypted Payloads](#encrypted-payloads), consisting of a length-prefixed `body` field -followed by optional padding. The `body` of the payload contains a serialized -[protobuf][protobuf] message with the following schema: +followed by optional padding. + +The `body` of the payload contains a serialized [protobuf][protobuf] +`NoiseHandshakePayload` message with the following schema: ``` protobuf message NoiseHandshakePayload { - bytes libp2p_key = 1; - bytes noise_static_key_signature = 2; - bytes libp2p_data = 3; - bytes libp2p_data_signature = 4; + bytes identity_key = 1; + bytes identity_sig = 2; + bytes data = 3; } ``` -The `libp2p_key` field contains a serialized `PublicKey` message as defined in -the [peer id spec][peer-id-spec]. +The `identity_key` field contains a serialized `PublicKey` message as defined +in the [peer id spec][peer-id-spec]. -The `noise_static_key_signature` field is produced using the libp2p identity -private key according to the [signing rules in the peer id +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 `libp2p_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 expected to be defined in a future iteration of the -connection establishment spec. - -If `libp2p_data` is non-empty, the `libp2p_data_signature` field MUST contain a -signature produced with the libp2p identity key. The data to be signed is the -UTF-8 string `noise-libp2p-early-data:` followed by the contents of the -`libp2p_data` field. +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 -`libp2p_key` field into a usable form. The key MUST be used to validate the -`noise_static_key_signature` field against the static Noise key received in the -handshake. If the signature is invalid, the connection MUST be terminated -immediately. - -If the `libp2p_data` field is non-empty, the `libp2p_data_signature` MUST be -validated against the supplied `libp2p_data`. If the signature is invalid, the -connection MUST be terminated immediately. +`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. If a noise-libp2p implementation does not expose an API for early data, they -MUST still validate the signature upon receiving a non-empty `libp2p_data` -field and abort the connection if it is invalid. +MUST still validate the signature upon receiving a non-empty `data` field and +abort the connection if it is invalid. + +#### Handshake sealing + +In the above scheme, the `data` field is not guarded against tampering. +Consequently, a MITM agent could alter the data in transit, or even record the +handshake to replay it at a later time with mutated or transplanted data. + +An option to counteract such attacks is to sign the data too. However, for +efficiency and enhanced security purposes, we choose to adopt the [channel +binding][npf-channel-binding] technique defined in the Noise Protocol Framework. + +The Noise Protocol state machine tracks the handshake hash throughout the entire +handshake exchange. This is a rolling digest of all messages witnessed by each +peer, and it's exposed by the state machine via an accessor. + +Once the Noise handshake has concluded, and the shared secret has been derived, +each party performs the following actions: + +1. Obtains the `HandshakeHash` from the Noise state machine. +2. Signs the `HandshakeHash` with its libp2p identity key. +3. Writes the result on the encrypted channel, prefixed with a Protobuf varint + length prefix. We call this payload the "handshake seal". +4. Awaits to receive the handshake seal from its peer. +5. Verifies the signature against the libp2p public key of its peer, and its + local value of `HandshakeHash`. +6. If the signature verification passes, the Noise state machine is destroyed + and the encrypted channel is handed over to the libp2p stack. If the + verification fails, the party terminates the network connection immediately. ### Supported Handshake Patterns @@ -482,7 +522,8 @@ 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: +Implementations that support Noise Pipes will also support the following Noise +protocols: - `Noise_IK_25519_ChaChaPoly_SHA256` - `Noise_XXfallback_25519_ChaChaPoly_SHA256` @@ -754,7 +795,7 @@ unsupported types like RSA. [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/ From 1d253683f97635cffe308fcba8ea1428c246fec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Fri, 6 Dec 2019 12:26:24 +0000 Subject: [PATCH 2/4] apply editorial suggestions from code review. Co-Authored-By: Jacob Heun --- noise/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noise/README.md b/noise/README.md index b8cf8963a..471230d2e 100644 --- a/noise/README.md +++ b/noise/README.md @@ -218,7 +218,7 @@ messages. We leverage this construct to transmit: 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 the first message of the handshake pattern +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 @@ -242,8 +242,8 @@ The `body` of the payload contains a serialized [protobuf][protobuf] ``` protobuf message NoiseHandshakePayload { - bytes identity_key = 1; - bytes identity_sig = 2; + bytes identity_key = 1; + bytes identity_sig = 2; bytes data = 3; } ``` From 82d5b0afecdb622d7b251545d5b405b136462b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Fri, 6 Dec 2019 12:29:45 +0000 Subject: [PATCH 3/4] remove vestiges of previous version. --- noise/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/noise/README.md b/noise/README.md index 471230d2e..e47ac7866 100644 --- a/noise/README.md +++ b/noise/README.md @@ -233,7 +233,7 @@ These payloads MUST be inserted into the first message of the handshake pattern the fresh cryptographic material is necessary. This is performed in message 3. -When decrypted, the signed payload has the structure described in [Encrypted +When decrypted, the payload has the structure described in [Encrypted Payloads](#encrypted-payloads), consisting of a length-prefixed `body` field followed by optional padding. @@ -266,10 +266,6 @@ Upon receiving the handshake payload, peers MUST decode the public key from the the `identity_sig` field against the static Noise key received in the handshake. If the signature is invalid, the connection MUST be terminated immediately. -If a noise-libp2p implementation does not expose an API for early data, they -MUST still validate the signature upon receiving a non-empty `data` field and -abort the connection if it is invalid. - #### Handshake sealing In the above scheme, the `data` field is not guarded against tampering. From a42a214b4e5eb786d8944098a47fda58bb5dbfd5 Mon Sep 17 00:00:00 2001 From: Shahan Khatchadourian Date: Fri, 6 Dec 2019 07:35:05 -0500 Subject: [PATCH 4/4] Fixes to section links and some minor wording changes (#235) --- noise/README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/noise/README.md b/noise/README.md index e47ac7866..0d7d7ce58 100644 --- a/noise/README.md +++ b/noise/README.md @@ -178,7 +178,7 @@ 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-signed-handshake-payload). This signature is produced with +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. @@ -203,8 +203,8 @@ 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 [signed -handshake payload](#the-libp2p-signed-handshake-payload) as a byte string +Any early data provided to noise-libp2p MUST be included in a [handshake +payload](#the-libp2p-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. @@ -288,8 +288,8 @@ each party performs the following actions: 3. Writes the result on the encrypted channel, prefixed with a Protobuf varint length prefix. We call this payload the "handshake seal". 4. Awaits to receive the handshake seal from its peer. -5. Verifies the signature against the libp2p public key of its peer, and its - local value of `HandshakeHash`. +5. Verifies the peer's seal signature against the peer's libp2p public key, and + its local value of `HandshakeHash`. 6. If the signature verification passes, the Noise state machine is destroyed and the encrypted channel is handed over to the libp2p stack. If the verification fails, the party terminates the network connection immediately. @@ -333,8 +333,8 @@ 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 [signed handshake -payload](#the-libp2p-signed-handshake-payload), which contains a signature +The second and third handshake messages include a +[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. @@ -398,9 +398,8 @@ key has changed, they may initiate an [`XXfallback`](#xxfallback) handshake, using the ephemeral public key from the failed `IK` handshake message as pre-message knowledge. -Each handshake message will include a [libp2p signed handshake -payload](#the-libp2p-signed-handshake-payload) that identifies the sender and -authenticates the static Noise key. +Each handshake message will include a [payload](#the-libp2p-handshake-payload) +that identifies the sender and authenticates the static Noise key. #### XXfallback @@ -429,9 +428,8 @@ 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 signed handshake -payload](#the-libp2p-signed-handshake-payload) that identifies the sender and -authenticates the static Noise key. +Each handshake message will include a [payload](#the-libp2p-handshake-payload) +that identifies the sender and authenticates the static Noise key. #### Noise Pipes Message Flow