Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a multiselect 2.0 spec #227

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added connections/handshake.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions connections/multiselect2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Multiselect 2.0

## Introduction

Multiselect 2.0 replaces the Multistream protocol. Compared to its predecessor, it offers:

1. Downgrade protection for the security protocol negotiation.
2. Zero-roundtrip stream multiplexer negotiation for handshake protocols that take advantage of early data mechanisms (one-roundtrip negotiation for protocols / implementations that don't).
3. Compression for the protocol identifiers of frequently used protocols.

By using protobufs for all control messages, Multiselect 2.0 provides an easy path for future protocol upgrades. The protobuf format guarantees that unknown fields in a message will be skipped, thus future versions of the protocol can add new fields that signal support for new protocol features.

## High-Level Overview

### Secure Channel Selection

Conversely to multistream-select 1.0, secure channel protocols are not dynamically negotiated in-band. Instead, they are announced upfront in the peer multiaddrs (<add link to multiaddr spec>). This way, implementations can jump straight into a cryptographic handshake, thus curtailing the possibility of packet-inspection-based censorship and dynamic downgrade attacks.

It is up to the implementers to decide whether each secure channel is exposed over a different port, or if a single port handles all secure channels, and a demultiplexing strategy is used to identify which protocol is being used.

**TODO**: Do we need to describe the format here? I guess we don't, but we will probably need another document for that change, and we can link to it from here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a link to some documentation on this would be nice! I don't believe it exists at the moment, so may be worth keeping this here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need an extension to the multiaddr spec + multicodec table to add secure channels as an atom. Also, @yusefnapora was writing this document to specify the semantics of multiaddrs: #191.


Peers advertising a multiaddr that includes a handshake protocol MUST support Multiselect 2.0 as described in this document.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we had discussed this assumption as a way to simplify things, but I do think it'll end up being short-sighted, and in some ways, a regression compared to multistream-select 1.0, which does announce its version (although admittedly the implementations are not ready to support multiple versions, but the protocol is).

Something I've been thinking about is to create an extension to multistream-select 1.0 / upgraders that would allow us to go straight into a cryptographic handshake, as a way to deliver censorship resistance to downstream users that require it before we realistically ship ms2.0.

Copy link
Contributor Author

@marten-seemann marten-seemann Nov 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how this would work without either defining another multicoded or requiring an additional round-trip to negotiate.


#### TCP Simultaneous Open
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved

TCP allows the establishment of a connection if two endpoints start initiating a connection at the same time. This is called TCP Simultaneous Open. For libp2p, on the one hand, this poses a problem, since most stream multiplexers assign stream IDs based on the role (client or server) of an endpoint. On the other hand, it can be used as a hole punching technique to facilitate NAT traversal.

TLS as well as Noise will fail the handshake if both endpoints act as clients. In case of such a handshake failure, the two endpoints need to restart the handshake. Endpoints MUST NOT close the underlying TCP connection in this case. Implementations SHOULD specifically test for this type of handshake failure, and not treat any handshake failure as a potential Simultaneous Open.

To determine the roles in the second handshake attempt, endpoints compare the SHA-256 hashes of their peer IDs. The peer with the numerically smaller hash value acts as a client in the second handshake attempt, the peer with the numerically larger hash value acts as a server.
Copy link
Member

@tomaka tomaka Nov 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This forces us to transfer our peer ID in the initial handshake message, right?
While this is normally good, it also means we can't for example fool proxies by pretending that our traffic in regular HTTP3.

Copy link
Member

@tomaka tomaka Nov 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling TCP simultaneous connections seems a bit off-topic to me for multistream-select, and could be specific to each transport and/or encryption protocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you dial a peer, you already know it’s peer ID. And of course you also know your own peer ID, so we don’t have to send anything extra on the wire.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomaka I tend to agree with this being slightly off-topic. The reason it's in here is because ms-2.0 also changes the connection bootstrapping, and #196 won't work any more (at least not if we want to be able to mask our traffic).

What do you think of saying in this document

Secure Channels MUST define how to handle TCP simultaneous open, if they can be used over TCP.

and then moving the text here to the TLS document?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marten-seemann

When you dial a peer, you already know it’s peer ID. And of course you also know your own peer ID, so we don’t have to send anything extra on the wire.

I may be missing something, but how does the responder learn the peer ID without a hard requirement for the crypto handshake to transmit it on the first message?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a simultaneous dial, you have both peer IDs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this layer, you only know you have established a connection and have exchanged some bytes. You think you know the other side’s peer ID, but it’s not authenticated. After exchanging those initial bytes, you notice the other party sends you a message you didn’t expect. Does each party work on their assumption of the peer ID of the other party? That leaves the system open to a series of attacks where a peer advertises a very large/small peer ID for itself, then responds to all SYN packets with another SYN + the initiator message, to force a conflict resolution pathway it knows it’ll always win. I much rather introduce a vector of randomness per-session.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raulk What's the attack here? Sure, an attacker can go through the hassle you're describing to make sure it ends up in the client role (from the viewpoint of the cryptographic handshake). During the cryptographic handshake both peer validate each other's peer IDs, so it seems to me that the attacker gains nothing from this attack at all.

Copy link
Member

@raulk raulk Nov 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not worried about peers lying about their identity. I'm worried about making the conflict resolution 100% deterministic, and the transitive attack surface that might expose going forward. We're specifying against our future selves, not against the situations we foresee now.

If there's a bug exploitable in this circumstance, an attack can be crafted that works 100% of the time, by precomputing a large/small peer identity.

I admit I may be thinking too far. But if we want to make conflict resolution probabilistic (which I think is the right way, thinking from first principles), then we can make the protocol can send a random nonce and have peers XOR their nonces to calculate who wins, or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure I understand the argument here. The security of our handshake relies on the TLS handshake. If TLS is broken, we don’t gain a lot from reducing the cost of an attack from succeeding in 100% of the cases to 50% of the cases.


Since secio assign roles during the handshake, it is not possible to detect a Simultaneous Open in this case. Therefore, secio MUST NOT be used with Multiselect 2.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, this is not quite right; we need something for secio too, unless we are actively deprecating it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which part of it is not right?
We've been talking about deprecating it for a long time, and the main reason we've been sticking to it is for backwards compatibility. Since Multiselect 2.0 is not backwards compatible anyways, this would be a good opportunity to finally phase it out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the push towards deprecation, as seems to be the growing consensus. Multiselect 2.0 presents a convenient opportunity to upgrade our secure channels.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no path for backwards compatibility here with multistream 1 correct? Pairing the secio deprecation with multiselect 2 is going to put a lot of stress on limited environments (browsers) to upgrade, or segregate them from the rest of the network. I don't think it warrants blocking the spec, especially if providing that compatibility hampers the performance/feature gains, but we should be cognizant and clear of the network rollout time table and its impact on the network.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secio is actively being deprecated and removed from the network, so this no longer needs to be a consideration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jacobheun @raulk

IIUC, even Noise needs an initiator and a responder. So, even after deprecating SecIO, we still need to assign roles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to update the document and remove any mention of secio, provided there's interest in moving forward with this document. It's been quiet for long time...


### Stream Multiplexer Selection
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved

This section only applies if Multiselect 2 is run over a transport that is not natively multiplexed. Transports that provide stream multiplexing on the transport layer (e.g. QUIC) don't need to do anything described in this section.

Some handshake protocols (TLS 1.3, Noise) support sending of *Early Data*.

In Multiselect 2 endpoints make use of Early Data to speed up stream multiplexcer selection. As soon as an endpoints reaches a state during the handshake where it can send encrypted application data, it sends a list of supported stream multiplexers. The first entry of the client's list of stream multiplexers is selected, thus the client SHOULD send its list ordered by preference.

When using TLS 1.3, the server can send Early Data after it receives the ClientHello. Early Data is encrypted, but at this point of the handshake the client's identity is not yet verified.
While Noise in principle allows sending of unencrypted data, endpoints MUST NOT use this to send their list of stream multiplexers. An endpoint MAY send it as soon it is possible to send encrypted data, even if the peers' identity is not verified at that point.
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
While Noise in principle allows sending of unencrypted data, endpoints MUST NOT use this to send their list of stream multiplexers. An endpoint MAY send it as soon it is possible to send encrypted data, even if the peers' identity is not verified at that point.
While Noise in principle allows sending of unencrypted data, endpoints MUST NOT use this to send their list of stream multiplexers. An endpoint MAY send it as soon as it is possible to send encrypted data, even if the peers' identity is not verified at that point.


The stream multiplexer that is used in the connection is determined by intersection the lists sent by both endpoints, as follows: First all stream multiplexers that aren't supported by both endpoints are removed from the clients' list of stream multiplexers. The stream multiplexer chosen is then the first element of this list.
If there is no overlap between the two lists, it is not possible to communicate with the peer, and an endpoint MUST close the connection.

Note that this negotiation scheme allows peers to negotiate a "monoplexed" connection, i.e. a connection that doesn't use any stream multiplexer. Endpoints can offer support for monoplexed connections by offering the `/monoplex` stream multiplexer.

**TODO**: Do we need to define a way to send an error code / error string? Or do we have something like that in libp2p already?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to be able to send an error message if, e.g., the responder doesn't support any of the initiator's multiplexers. Perhaps that would be a potential use-case for a Reject message, even in the streaming case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we have any libp2p-wide error codes. I think they should be protocol specific.


![](handshake.png)

Handshake protocols (or implementations of handshake protocols) that don't support sending of Early Data will have to run the stream multiplexer selection after the handshake completes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, I see that you added the fallback. We need to define how that'll work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there's anything we need to define. "Early data" is not a separate byte stream, it's the same byte stream as the rest of the connection. The only difference is that the data is sent earlier (and, depending on the handshake protocol, might use a different set of keys).
To the application Early Data and Late (?) Data is not distinguishable at all.


#### 0-RTT

When using 0-RTT session resumption as offered by TLS 1.3 and Noise, the endpoints MUST remember the negotiated stream multiplexer used on the original connection. This ensures that the client can send application data in the first flight when resuming a connection.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Stream multiplexers can change over time.
  2. Peers may forget other peers' protocols.

We may need some form of opaque token with which we can verify that our assertions about the other party still remain valid, upon reconnecting. If the other party NACKs, we fall back to full connection bootstrapping.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TLS session ticket is exactly that opaque token.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But multiselect 2.0 doesn't know about concrete secure channels. If there's a handshake-specific token that we can bind to, it needs to percolate up to this layer.


## Protocol Specification

### Wire Encoding

All messages are Protobuf messages using the `proto3` syntax. Every message is wrapped by the `Multiselect` message:

```protobuf
# Wraps every message
message Multiselect {
oneof message {
OfferMultiplexer offerMultiplexer = 1;
Offer offer = 2;
Use use = 3;
}
}
```
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved

This document defines three messages. The first one is the `OfferMultiplexer` message:

```protobuf
# Offer a list of stream multiplexers.
message OfferMultiplexer {
repeated string name = 1;
}
```

The second one is the `Offer` message:

```protobuf
# Select a list of protocols.
message Offer {
message Protocol {
oneof protocol {
string name = 1;
uint64 id = 2;
}
}
repeated Protocol protocols = 1;
}
```

And the third one is the `Use` message:

```protobuf
# Declare that a protocol is used on this stream.
# By using an id (instead of a name), an endpoint can provide the peer
# an abbreviation to use for future uses of the same protocol.
message Use {
message Protocol {
uint64 id = 1;
string name = 2;
}
Protocol protocol = 1;
}
```

### Protocol Description

The `OfferMultiplexer` message is used to select a stream multiplexer to use on a connection. Each endpoint MUST send this message exactly once as the first message on a transport that does not support native stream multiplexing. This message MUST NOT be sent on transports that support native stream multiplexing (e.g. QUIC), and it MUST NOT be sent at any later moment during the connection.
Once an endpoint has both sent and received the `OfferMultiplexer` message, it determines the stream multiplexer to use on the connection as described in {{stream-multiplexer-selection}}. From this moment, it now has a multiplexed connection that can be used to exchange application data.

An endpoint uses the `Offer` message to initiate a conversation on a new stream. The `Offer` message can be used in two distinct scenarios:
1. The endpoint knows exactly which protocol it wants to use. It then lists this protocol in the `Offer` message.
2. The endpoint wants to use any of a set of protocols, and lets the peer decide which one. It then lists the set of protocols in the `Offer` message.

A `Protocol` is the application protocol spoken on top of an ordered byte stream. The `name` of a protocol is the protocol identifier, e.g. `/ipfs/ping/1.0.0`. The `id` is a numeric abbreviation for this protocol (see below for details how `id`s are assigned).
If the endpoint only selects a single protocol, it MAY start sending application data right after the protobuf message. Since it has not received confirmation if the peer actually supports the protocol, any such data might be lost in that case. If the endpoint selects multiple protocols, it MUST wait for the peer's choice of the application protocol (see description of the `Use` message) before sending application.

The `Use` message is sent in response to the `Offer`. An endpoint MUST treat the receipt of a `Use` message before having sent an `Offer` message on the stream as a connection error.
If none of the protocol(s) listed in the `Offer` message are acceptable, an endpoint MUST reset both the send- and the receive-side of the stream.

If an endpoint receives an `Offer` message that only offers a single protocol, it accepts this protocol by sending an empty `Use` message (i.e. a message that doesn't list any `protocol`), or a `Use` message that assigns a protocol id (see below). Sending an empty `Use` message in response to an `Offer` message that offers multiple protocols is not permitted, and MUST be treated as a connection error by an endpoint.

If an endpoint receives an `Offer` message that offers multiple protocols, it chooses an application protocol that it would like to speak on this stream. It informs the peer about its choice by sending its selection in the `protocol` field of the `Use` message.

When choosing a protocol, an endpoint can allow its peer to save bytes on the wire for future use of the same protocol by assigning a numeric identifier for the protocol by sending an `id`. The identifier is valid for the lifetime of the connection. The identifier must be unique for the protocol, an endpoint MUST NOT use the same identifier for different protocols.