Repository navigation
Conversation
| In RFC 9849, clients can only use the server provided `retry_configs` if the | ||
| outer handshake authenticates successfully with the given | ||
| ECHConfig.contents.public_name. This means that if servers wish to support the | ||
| `retry_configs` fallback they MUST use a valid domain name and hold the | ||
| corresponding Server Certificate. This is the retry mechanism in RFC 9849; | ||
| there is nothing libp2p specific about this. |
There was a problem hiding this comment.
| In RFC 9849, clients can only use the server provided `retry_configs` if the | |
| outer handshake authenticates successfully with the given | |
| ECHConfig.contents.public_name. This means that if servers wish to support the | |
| `retry_configs` fallback they MUST use a valid domain name and hold the | |
| corresponding Server Certificate. This is the retry mechanism in RFC 9849; | |
| there is nothing libp2p specific about this. | |
| Servers MUST use a valid domain name and hold the corresponding Server Certificate to support the `retry_configs` fallback. This is the retry mechanism in RFC 9849; there is nothing libp2p specific about this. |
There was a problem hiding this comment.
+1, and it needs a rule on public_name: it goes out as cleartext SNI on every ECH connection (RFC 9849, section 6.1, step 5). Reusing a /tls/ws cert whose name embeds the peer ID or a libp2p-specific domain would leak what ECH is meant to hide.
List caveats explicitly:
| In RFC 9849, clients can only use the server provided `retry_configs` if the | |
| outer handshake authenticates successfully with the given | |
| ECHConfig.contents.public_name. This means that if servers wish to support the | |
| `retry_configs` fallback they MUST use a valid domain name and hold the | |
| corresponding Server Certificate. This is the retry mechanism in RFC 9849; | |
| there is nothing libp2p specific about this. | |
| Servers MUST use a valid domain name and hold the corresponding Server Certificate | |
| to support the `retry_configs` fallback. This is the retry mechanism in RFC 9849; | |
| there is nothing libp2p specific about this. | |
| `public_name` is sent in cleartext on every ECH connection. It MUST NOT contain | |
| the peer ID or a libp2p-specific domain. It MUST be a host name with at least | |
| two labels, even without a certificate, as some TLS stacks reject single-label | |
| names. |
| If the server does not have a valid public_name and certificate, the client can | ||
| only fail the connection. |
There was a problem hiding this comment.
| If the server does not have a valid public_name and certificate, the client can | |
| only fail the connection. | |
| The client MUST fail the connection If the server does not have a valid public_name and certificate pair. |
There was a problem hiding this comment.
Strong +1 to the MUST here.
Failing this connection does not end the dial, though: go-libp2p moves on to the peer's other addresses and starts TCP dials some ms after QUIC unless QUIC already connected.
👉 RFC 9848, section 5.1 forbids falling back to a non-ECH connection when all DNS-advertised endpoints have ECH, because it "would negate the privacy benefits of ECH". Section 8 calls a mix of ECH and non-ECH endpoints a downgrade risk.
Building on the suggestion above:
| If the server does not have a valid public_name and certificate, the client can | |
| only fail the connection. | |
| If the server does not have a valid public_name and certificate, the client MUST | |
| fail the connection. | |
| Failing the connection does not end the dial: libp2p dialers try a peer's other | |
| addresses, often in parallel. A non-ECH libp2p TLS handshake sends the `libp2p` ALPN in | |
| cleartext, so an on-path observer only has to drop ECH handshakes to see it. A | |
| client that holds an `/ech` address for a peer MUST NOT dial that peer's non-ECH | |
| addresses, in parallel or after an ECH failure. |
| Servers SHOULD rotate their keys once a month, and keep the prior ECH Config | ||
| keys around for 1 week to assist stale clients. |
There was a problem hiding this comment.
Maybe add the reasoning behind the time windows here? (i.e. once a month, 1 week)
There was a problem hiding this comment.
+1
| Servers SHOULD rotate their keys once a month, and keep the prior ECH Config | |
| keys around for 1 week to assist stale clients. | |
| Servers SHOULD rotate their ECH key pairs once a month, and keep the prior ECH Config | |
| keys around for 1 week to assist stale clients. | |
| Servers MUST persist ECH private keys across restarts until the rotation | |
| window above retires them: an `/ech` value has no TTL and ends up where multiaddrs live for months: | |
| bootstrap lists, static peering configs, and | |
| caller-provided addresses. Without a certificate for `public_name` clients cannot use | |
| `retry_configs`, so dials with a config for a dropped key fail until the client | |
| learns a fresh `/ech` address. |
|
|
||
| ## Client ECH Config Caching | ||
|
|
||
| Clients SHOULD cache the ECHConfigList for no more than 48 hours. Note that a |
|
|
||
| ## Server Key Rotation | ||
|
|
||
| Servers SHOULD rotate their keys once a month, and keep the prior ECH Config |
There was a problem hiding this comment.
| Servers SHOULD rotate their keys once a month, and keep the prior ECH Config | |
| Servers SHOULD rotate their ECH key pairs once a month, and keep the prior ECH Config |
| ## Overview | ||
|
|
||
| This document specifies minor changes to the [libp2p TLS Handshake](./tls.md) to | ||
| enable support for [RFC 9849]: TLS Encrypted Client Hello. The primary benefit |
There was a problem hiding this comment.
| enable support for [RFC 9849]: TLS Encrypted Client Hello. The primary benefit | |
| enable support for [RFC 9849]: TLS Encrypted ClientHello. The primary benefit |
|
|
||
| #### Authenticated Rejection | ||
|
|
||
| In RFC 9849, clients can only use the server provided `retry_configs` if the |
There was a problem hiding this comment.
| In RFC 9849, clients can only use the server provided `retry_configs` if the | |
| In RFC 9849, clients can only use the server-provided `retry_configs` if the |
| The `/ech` component SHOULD appear after the `/tls` or `/quic-v1` component. | ||
| Servers use this multiaddr to advertise ECH support and their ECHConfigList. | ||
|
|
||
| ## Caller Provided ECHConfigList |
There was a problem hiding this comment.
| ## Caller Provided ECHConfigList | |
| ## Caller-Provided ECHConfigList |
|
|
||
| ## Overview | ||
|
|
||
| This document specifies minor changes to the [libp2p TLS Handshake](./tls.md) to |
There was a problem hiding this comment.
For TCP, how would this work? because with TCP the flow is:
1 - TCP connection
2 - Multistream negotiation in plaintext sending /multistream/1.0.0 and /tls/1.0.0
3 - Then the TLS handshake happen
meaning that an observer should be able to recognize libp2p being used. Would using /tls/ech mean that we gotta start TLS handshake directly, and skip the multistream select negotiation? if so, probably worth mentioning in the spec
There was a problem hiding this comment.
+1. The multiaddr has the same gap: in today's specs /tls under /tcp is only defined for /tls/ws or /tls/http, which run WebPKI TLS rather than the libp2p handshake, so /tcp/<port>/tls/ech has no defined meaning. Scoping this spec to QUIC until a TLS-first TCP mode exists may be simplest, but also defer solving problem. Better to, at very least, clearly state it in the spec.
There was a problem hiding this comment.
Thank you for kicking this off, it is worth doing. Honestly though, given where ipfs/libp2p maintenance is heading, and the TLS stacks are today, getting it right will take a while, so worth setting expectations low and focus on specs being realistic.
FWIW:
- (Bad) Sadly, in its current form this is a no-op in Go and Rust 🙈 IIUC stock Go
crypto/tls(also used by quic-go) and rustls send the same ALPN list in the inner and outer ClientHello, so thelibp2pALPN still goes out in cleartext while ECH reports success (details). Users would thinklibp2pis hidden when it is not ( ❗ )- Upstream:
- golang/go#71220 tracks the outer ALPN
- rustls has no issue for it yet but also lacks server-side ECH it seems? (rustls/rustls#1980).
- Upstream:
- (Good) Once TLS stacks allow a separate outer ALPN, ECH does beat deep packet inspection matching on the
libp2pstring.
See the inline comments, they mostly narrow what the spec promises and add rules where a quick LLM-assisted implementation would leak or break.
I'd suggest parking this spec until Go and Rust implementations exist and the upstream gaps above are closed, so it describes something real rather than security theater :)
ps. two caveats worth writing down for drive-by reader in case someone thinks ECH is a silver bullet for "privacy in libp2p":
- a censor that drops ECH handshakes gets a plain
libp2phandshake via the peer's other addresses, unless the spec forbids that fallback (suggestion) - peers announced via the DHT can be labeled by destination alone by anyone with a DHT crawl (prior art: https://github.com/dennis-tra/nebula); ECH cannot fix that, but a short section noting it would set honest expectations, e.g. that ECH inspection-resistance truly works only when peer addresses are not publicly discoverable
| for QUIC connections. The ALPN MUST NOT include `libp2p` or the muxer protocol | ||
| IDs used by [inlined muxer negotiation]. |
There was a problem hiding this comment.
@MarcoPolo FYSA stock Go crypto/tls (and quic-go on top of it) builds ClientHelloOuter by cloning the inner hello and keeps its ALPN list, so libp2p and, on TCP, the muxer IDs still go out in cleartext while ECH reports success (golang/go#71220).
This is not "golang bad" type of thing, rustls has the same single ALPN list (code, cc @jxs for rust-libp2p: afaik no rustls issue tracks this yet) 🙃. So.. without a rule, the straightforward^W LLM implementation ships a silent no-op 🙈
Spec should clearly state this known and common problem:
| for QUIC connections. The ALPN MUST NOT include `libp2p` or the muxer protocol | |
| IDs used by [inlined muxer negotiation]. | |
| for QUIC connections. The ALPN MUST NOT include `libp2p` or the muxer protocol | |
| IDs used by [inlined muxer negotiation]. | |
| Some TLS stacks send the same ALPN list in ClientHelloOuter and ClientHelloInner. | |
| On such a stack the `libp2p` ALPN stays in cleartext even when ECH is | |
| accepted. Implementations MUST NOT offer ECH unless their TLS stack can set the | |
| ClientHelloOuter ALPN independently of the ClientHelloInner ALPN. |
| If the server does not have a valid public_name and certificate, the client can | ||
| only fail the connection. |
There was a problem hiding this comment.
Strong +1 to the MUST here.
Failing this connection does not end the dial, though: go-libp2p moves on to the peer's other addresses and starts TCP dials some ms after QUIC unless QUIC already connected.
👉 RFC 9848, section 5.1 forbids falling back to a non-ECH connection when all DNS-advertised endpoints have ECH, because it "would negate the privacy benefits of ECH". Section 8 calls a mix of ECH and non-ECH endpoints a downgrade risk.
Building on the suggestion above:
| If the server does not have a valid public_name and certificate, the client can | |
| only fail the connection. | |
| If the server does not have a valid public_name and certificate, the client MUST | |
| fail the connection. | |
| Failing the connection does not end the dial: libp2p dialers try a peer's other | |
| addresses, often in parallel. A non-ECH libp2p TLS handshake sends the `libp2p` ALPN in | |
| cleartext, so an on-path observer only has to drop ECH handshakes to see it. A | |
| client that holds an `/ech` address for a peer MUST NOT dial that peer's non-ECH | |
| addresses, in parallel or after an ECH failure. |
| In RFC 9849, clients can only use the server provided `retry_configs` if the | ||
| outer handshake authenticates successfully with the given | ||
| ECHConfig.contents.public_name. This means that if servers wish to support the | ||
| `retry_configs` fallback they MUST use a valid domain name and hold the | ||
| corresponding Server Certificate. This is the retry mechanism in RFC 9849; | ||
| there is nothing libp2p specific about this. |
There was a problem hiding this comment.
+1, and it needs a rule on public_name: it goes out as cleartext SNI on every ECH connection (RFC 9849, section 6.1, step 5). Reusing a /tls/ws cert whose name embeds the peer ID or a libp2p-specific domain would leak what ECH is meant to hide.
List caveats explicitly:
| In RFC 9849, clients can only use the server provided `retry_configs` if the | |
| outer handshake authenticates successfully with the given | |
| ECHConfig.contents.public_name. This means that if servers wish to support the | |
| `retry_configs` fallback they MUST use a valid domain name and hold the | |
| corresponding Server Certificate. This is the retry mechanism in RFC 9849; | |
| there is nothing libp2p specific about this. | |
| Servers MUST use a valid domain name and hold the corresponding Server Certificate | |
| to support the `retry_configs` fallback. This is the retry mechanism in RFC 9849; | |
| there is nothing libp2p specific about this. | |
| `public_name` is sent in cleartext on every ECH connection. It MUST NOT contain | |
| the peer ID or a libp2p-specific domain. It MUST be a host name with at least | |
| two labels, even without a certificate, as some TLS stacks reject single-label | |
| names. |
| `retry_configs` fallback they MUST use a valid domain name and hold the | ||
| corresponding Server Certificate. This is the retry mechanism in RFC 9849; | ||
| there is nothing libp2p specific about this. | ||
|
|
There was a problem hiding this comment.
hm.. not sure if i got this right, but holding the cert here feels to be not enough: after rejecting ECH the server continues with ClientHelloOuter, and a server that keeps the libp2p TLS ALPN rule aborts with no_application_protocol before retry_configs are sent.
Should below missing steps be added here?
| On ECH rejection, if the ClientHelloOuter ALPN does not offer `libp2p`, the server MUST complete the outer handshake without the libp2p | |
| TLS ALPN rules: accept the ClientHelloOuter ALPN, present the `public_name` | |
| certificate, send `retry_configs`, and let the client abort with `ech_required`. | |
This document specifies minor changes to the libp2p TLS Handshake to enable support for RFC 9849: TLS Encrypted Client Hello. The primary benefit is to make identifying libp2p connections harder to passive network observers by hiding the “libp2p” ALPN in the encrypted ClientHelloInner.
Related and prereq PRs:
/echmultiformats/multiaddr#183