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

WebSockets: add opt-in delegation of encryption to TLS #625

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions noise/README.md
achingbrain marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ syntax = "proto2";
message NoiseExtensions {
repeated bytes webtransport_certhashes = 1;
repeated string stream_muxers = 2;
optional bool handshake_only = 3;
optional string tls_common_name = 4;
Comment on lines +224 to +225
Copy link
Member

@Stebalien Stebalien Sep 3, 2024

Choose a reason for hiding this comment

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

Can we make this less specific to websocket/TLS? E.g., one way to do this is to:

  1. Setup a noise stream.
  2. Over the noise stream, send the TLS common name, etc.
  3. Close the noise stream but leave the connection open.
  4. Use the connection without encryption.

To support a better upgrade path, this method can be advertised as a new "stream muxer" (more like a "next protocol"). I.e.:

  1. Initiator negotiates the "/drop-noise" protocol (or something like that) along with the expected TLS common name (or nothing?).
  2. Receiver returns their TLS common name and terminates their end of the noise stream.
  3. Client terminates their end of the noise stream.

Copy link
Member Author

Choose a reason for hiding this comment

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

This seems like it would add quite a lot of round trips? Part of the joy of the extensions is they arrive as part of the handshake message.

I know connection establishment with WebSockets has a bunch of round trips already but is this a reason to make it slower, particularly when we already have WebTransport-specific stuff in the extensions registry?

Copy link
Member

Choose a reason for hiding this comment

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

Hm. You're right, this'll add a round trip.

}

message NoiseHandshakePayload {
Expand Down
97 changes: 97 additions & 0 deletions websockets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# libp2p WebSockets

| Lifecycle Stage | Maturity | Status | Latest Revision |
|-----------------|--------------------------|--------|-----------------|
| 2A | Candidate Recommendation | Active | r0, 2022-10-12 |

Authors: [@achingbrain]

Interest Group: [@MarcoPolo]

[@achingbrain]: https://github.com/achingbrain
[@MarcoPolo]: https://github.com/MarcoPolo

See the [lifecycle document](../00-framework-01-spec-lifecycle.md) for context about maturity level
and spec status.

## Introduction

[WebSockets](https://websockets.spec.whatwg.org/) are a way for web applications to maintain bidirectional communications with server-side processes.

All major browsers have shipped WebSocket support and the implementations are both robust and well understood.

A WebSocket request starts as a regular HTTP request, which is renegotiated as a WebSocket connection using the [HTTP protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism).

## Drawbacks

WebSockets suffer from [head of line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking) and provide no mechanism for stream multiplexing, encryption or authentication so additional features must be added by the developer or by libp2p.

In practice they only run over TCP so are less effective with [DCuTR Holepunching](../relay/DCUtR.md).

## Certificates

With [some exceptions](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure) browsers will prevent making connections to unencrypted WebSockets when the request is made from a [Secure Context](https://www.w3.org/TR/secure-contexts/).

Given that libp2p makes extensive use of the [SubtleCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto), and that API is only available in Secure Contexts, it's safe to assume that any incoming libp2p connections initiated over WebSockets originate from a Secure Context.

Consequently server-side processes listening for incoming libp2p connections via WebSockets must use TLS certificates that can be verified by the connecting user agent.

These must be obtained externally and configured in the same way as you would for an HTTP server.

The only exception to this is if both server and client are operating exclusively on loopback or localhost addresses such as in a testing or offline environment. Such addresses should not be shared outside of these environments.

## Stream Multiplexing

WebSockets have no built in stream multiplexing. Server-side processes listening for incoming libp2p connections via WebSockets should support [multi-stream select](https://github.com/multiformats/multistream-select) and negotiate an appropriate stream multiplexer such as [yamux](../yamux/README.md).

## Authentication

WebSockets have no built in authentication mechanism. Server-side processes listening for incoming libp2p connections via WebSockets should support [multi-stream select](https://github.com/multiformats/multistream-select) and negotiate an appropriate authentication mechanism such as [noise](../noise/README.md).

## Encryption

Server-side processes listening on WebSocket addresses should use TLS certificates to secure transmitted data at the transport level.

This does not provide any assurance that the remote peer possesses the private key that corresponds to their public key, so an additional handshake is necessary.

During connection establishment over WebSockets, before the connection is made available to the rest of the application, if all of the following criteria are met:

1. `noise` is negotiated as the connection encryption protocol
Copy link
Contributor

Choose a reason for hiding this comment

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

So despite noise being negotiated as the encryption protocol, no encryption is done at the libp2p level, because of the handshake_only which overrules it?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's correct. Noise does a handshake and encryption, and here we only want the handshake since encryption is done by TLS at the transport layer. This is similar to how the WebTransport and WebRTC transports work.

We still negotiate a connection encryption protocol for backwards compatibility and to allow for alternatives to noise in the future.

1. An initial handshake is performed with:
1. the `handshake_only` boolean extension set to true
1. a `tls_common_name` extension value that matches the domain name being connected to
1. The transport layer is secured by TLS

Then all subsequent data is sent without encrypting it at the libp2p level, instead relying on TLS encryption at the transport layer.

If any of the above is not true, all data is encrypted with the negotiated connection encryption method before sending.

This prevents double-encryption but only when both ends opt-in to ensure backwards compatibility with existing deployments.

Note that by opting-in to single encryption, peers are also opting-in to trusting the [CA](https://en.wikipedia.org/wiki/Certificate_authority) system.

### MITM mitigation
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't work unless the server somehow includes their domain name in their noise handshake. Otherwise, an attacker can simply announce that some peer id QmVictim is available at /dns/attacker.com/....

Furthermore, by including the domain name in the noise handshake, the server is opting-in to trusting the CA system.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that this also doesn't work unless the server trusts all owners of the given domain name. If the server operator is the sole owner of the domain name, this is fine. But if another party controls the domain name as well, but not the private key to the peer ID, they would be able to MITM/intercept this connection.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good points both.

This doesn't work unless the server somehow includes their domain name in their noise handshake

I've added a further tls_common_name extension to the noise handshake to prevent an attacker publishing an address with a bogus domain and then simply relaying the handshake.

by including the domain name in the noise handshake, the server is opting-in to trusting the CA system

I've added a note to this effect.

Note that this also doesn't work unless the server trusts all owners of the given domain name

I've copied the Security Considerations section from #564 as it seems like they have the same problems with third party root certificates installed on the machine.

Do you think this covers it?


The TLS certificate used should be signed by a trusted certificate authority, the host name should correspond to the common name contained within the certificate, and the domain being connected to should match the common name sent as part of the noise handshake.

This requires trusting the certificate authority to issue correct certificates, but is necessary due to limitations of certain user agents, namely web browsers which do not allow use of self-signed certificates that could be otherwise be verified via preshared certificate fingerprints.
Copy link
Contributor

Choose a reason for hiding this comment

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

I’d include a sentence that mentions Keying Material Exporters (RFC 5705), which, in a perfect world, would be the preferred solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added a sentence - please suggest an edit if you think it needs more exposition.


### Security Considerations

Protection against man-in-the-middle (MITM) type attacks is through Web [PKI](https://en.wikipedia.org/wiki/Public_key_infrastructure). If the client is in an environment where Web PKI can not be fully trusted (e.g. an enterprise network with a custom enterprise root CA installed on the client), then this authentication scheme can not protect the client from a MITM attack.

This authentication scheme is also not secure in cases where you do not own your domain name or the certificate. If someone else can get a valid certificate for your domain, you may be vulnerable to a MITM attack.

## Addressing

A WebSocket address contains `/ws`, `/tls/ws` or `/wss` and runs over TCP. If a TCP port is omitted, a secure WebSocket (e.g. `/tls/ws` or `/wss` is assumed to run on TCP port 443), an insecure WebSocket is assumed to run on TCP port 80 similar to HTTP addresses.

Examples:

* `/ip4/192.0.2.0/tcp/1234/ws` (an insecure address with a TCP port)
* `/ip4/192.0.2.0/tcp/1234/tls/ws` (a secure address with a TCP port)
* `/ip4/192.0.2.0/ws` (an insecure address that defaults to TCP port 80)
* `/ip4/192.0.2.0/tls/ws` (a secure address that defaults to TCP port 443)
* `/ip4/192.0.2.0/wss` (`/tls` may be omitted when using `/wss`)
* `/dns/example.com/wss` (a DNS address)
* `/dns/example.com/wss/http-path/path%2Fto%2Fendpoint` (an address with a path)