Visitar URL original
server encrypted client hello support by jellevandenhooff · Pull Request #2993 · rustls/rustls · GitHub
Skip to content

server encrypted client hello support - #2993

Open
jellevandenhooff wants to merge 10 commits into
rustls:mainfrom
jellevandenhooff:ech-server-pr
Open

jellevandenhooff wants to merge 10 commits into
rustls:mainfrom
jellevandenhooff:ech-server-pr

Conversation

@jellevandenhooff

@jellevandenhooff jellevandenhooff commented Mar 21, 2026 •

Copy link
Copy Markdown

Current state

This PR implements support for TLS encrypted hello, or ECH, as described in RFC 9849 for rustls servers. It implements support for ECH in shared mode where the rustls server decrypts the ECH (frontend) and also handles the TLS connection (backend). The code is setup for later implementation of split mode where the frontend runs on a SNI-inspecting reverse proxy.

The code tries to implement the spec faithfully and to adhere to existing rustls style. The logic implemented in this PR, roughly split out by commit, is all that is necessary to run ECH on the server:

  • Wire format wrangling of ECH. The server-side implementation of ECH must do byte-level surgery to reconstruct the inner client hello. This PR introduces new machinery for working with raw client hello messages.
  • ECH key management. Trial decryption of ECH messages requires finding the right key given a configuration ID, and the PR has an API for both providing keys and internal O(1) lookup of the right key.
  • Trial decryption of ECH messages. To decrypt ECH you need to construct AAD, try the appropriate key(s) and gracefully fail.
  • A state machine to handle ECH decryption with hello retry support, where the key gets re-used and state carries over.
  • Integration of ECH with the existing TLS 1.3 handshake building on the state machine.
  • A new status and configuration API (see below).
  • API tests and support in the bogo shim for running ECH tests. All tests pass, except those expect split-mode support.

For servers that do not have ECH configured this PR changes behavior from before slightly: if ECH is offered in a GREASE extension, the support will respond with GREASE ECH in a hello retry request. This is so an observer on the wire cannot tell if the server does or does not have ECH configured. I think making this a non-configurable default is good for the TLS ecosystem.

New API surface

// --- Configuration ---

  // New field on ServerConfig:
  pub struct ServerConfig {
      /// ECH server keys for decrypting Encrypted Client Hello offers.
      pub ech_keys: Arc<dyn EchKeyResolver>,
      // ...
  }

  // --- Key management ---

  /// Trait for dynamic ECH key resolution (e.g. hot-swap on rotation).
  pub trait EchKeyResolver: Debug + Send + Sync {
      fn resolve(&self) -> EchKeys;
  }

  /// Immutable set of ECH server keys, cheap to clone (Arc'd).
  #[derive(Clone, Debug)]
  pub struct EchKeys;

  impl EchKeys {
      pub fn new(keys: Vec<EchServerKey>) -> Self;
      pub fn empty() -> Self;
  }

  /// Simplest EchKeyResolver: wraps a fixed EchKeys.
  pub struct FixedEchKeys;

  impl FixedEchKeys {
      pub fn new(keys: Vec<EchServerKey>) -> Self;
      pub fn empty() -> Self;
  }

  impl EchKeyResolver for FixedEchKeys { ... }
  impl From<Vec<EchServerKey>> for FixedEchKeys { ... }

  /// A server-side ECH key: ECHConfig + HPKE private key.
  pub struct EchServerKey;

  impl EchServerKey {
      /// Parse from raw ECHConfig bytes + private key.
      pub fn from_raw(
          config_bytes: &[u8],
          private_key_bytes: Vec<u8>,
          hpke_suites: &[&'static dyn Hpke],
      ) -> Result<Self, Error>;

      /// Exclude from retry_configs on rejection (for key rotation).
      pub fn with_retry(self, is_retry: bool) -> Self;
  }

  // --- Handshake introspection ---

  /// New method on ClientHello (in cert resolver callback):
  impl ClientHello<'_> {
      /// True when this is the decrypted ECH inner hello.
      /// Returning Err triggers ECH rewind to the outer hello.
      pub fn is_ech_inner(&self) -> bool;
  }

  /// New method on ServerConnection:
  impl ServerConnection {
      /// Returns ECH frontend info if ECH was accepted (shared mode).
      pub fn ech_accepted(&self) -> Option<&EchFrontendInfo>;
  }

  /// Same on quic::ServerConnection:
  impl quic::ServerConnection {
      pub fn ech_accepted(&self) -> Option<&EchFrontendInfo>;
  }

  /// Information from successful ECH termination.
  #[non_exhaustive]
  #[derive(Clone, Debug)]
  pub struct EchFrontendInfo;

  impl EchFrontendInfo {
      pub fn config_id(&self) -> u8;
      pub fn outer_server_name(&self) -> Option<&DnsName<'_>>;
  }

Old description

This PR introduces encrypted client hello support addressing #1980. Caveat: I wrote a lot of this PR with Claude... but I think it's pretty good.

This implementation supports ECH in both shared mode and split mode and works with tls-over-tcp and quic. A demo showing all this runs at https://ech.jelle.dev and works with Chrome and Firefox on my laptop (when I point them at a DoH resolver). The code passes all ECH bogo tests, and each commit should pass format, lint, and tests by itself. Support for ECH is built up over small commits that slowly add more features. Let me know if you'd like more work on this, I'd be interested to get it to a place that it might get merged. At the same time, I also understand it's a lot of code you might want to instead work on yourselves.

Design decisions and questions

There's a couple of things that I kind of decided along the way that might or might not be good ideas. Happy to change those.

  • The backend always supports running as a split-mode backend, accepting decrypted inner client hellos. The RFC says not to do this but I think other libraries also do this. Making this a choice would require a configuration knob specifying to run as a backend or not.

  • Rewind support, which is what happens when the application decides it does not like the SNI in the inner hello and rustls reverts back to the outer hello, is implemented by treating any errors by the certificate resolver as a request to rewind. I think that's a fine choice and I'd rather not introduce new APIs to otherwise approve or reject an inner hello.

  • There is no check on the outer hello's SNI, so if a client sends eg. google.com, even though the backend has no certificate for that domain, it'd still get accepted.

  • There is a somewhat fairly detailed status API to get information on the ECH handshake (eg. it shows the used config ID.) That seems quite nice for monitoring how, for example, HTTPS DNS records get propagated, but I don't know exactly what the API should look like.

  • The API for getting ECH keys mirrors the certificate getting API.

  • There is proxy support in this PR. Maybe that should not yet go in there. Although the API seems slim and good enough for the demo perhaps it's a bit early.

  • The (re-)constructing of the ECH is a bunch of gnarly wrangling of bytes. I don't quite know how to make that cleaner or less scary.

Demo details

The demo's code is a bit of a mess but still perhaps helpful to understand how the proxy API in this PR is used. The code lives at https://github.com/jellevandenhooff/rustls-server-ech-demo and depends on some small changes to tokio-rustls, quinn and h3. The split mode proxy for TCP is quite simple (https://github.com/jellevandenhooff/rustls-server-ech-demo/blob/main/src/split_proxy.rs#L111): to proxy a connection you decode one or two hello messages, pass them to rustls, and get back the message you should send to the backend. Proxying quic is quite a bit more hairy and I don't fully trust the code yet but it seems to work.

If I go to https://proxied-a.ech.jelle.dev/ in Chrome I get the following status:

Proxy metadata:
Outer SNI: public.ech.jelle.dev
Inner SNI (decrypted): proxied-a.ech.jelle.dev
Proxy ECH result: decrypted (ECH accepted)
Route mode: split (inner CH forwarded)

Backend metadata:
Protocol: QUIC (HTTP/3)
server_name (SNI): proxied-a.ech.jelle.dev
ECH status: accepted (split mode)

This means that the connection is over quic and is using ECH ("ECP accepted", different outer SNI from inner SNI). This is running in split routing mode, which means the proxy has decrypted the ECH and forwarded the inner ECH to the appropriate backend. That backend does know it's using ECH because the hello has a different shape from a normal hello.
(And the reason I am excited about split mode is because it splits responsibilities and secrecy nicely: the backends do not get the ECH keys, so they cannot decode other requests going into the proxy; meanwhile, the proxy does not decrypt the actual traffic.)

@jellevandenhooff
jellevandenhooff marked this pull request as draft March 21, 2026 07:34
@djc

djc commented Mar 21, 2026

Copy link
Copy Markdown
Member

Do you have a (serious) use case for this? What are you trying to achieve?

@codecov

codecov Bot commented Mar 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.95765% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.30%. Comparing base (65e3f66) to head (3e80839).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
rustls/src/server/ech.rs 95.26% 25 Missing and 11 partials ⚠️
rustls/src/quic.rs 0.00% 3 Missing ⚠️
rustls/src/msgs/client_hello.rs 96.66% 1 Missing and 1 partial ⚠️
rustls/src/msgs/mod.rs 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2993      +/-   ##
==========================================
+ Coverage   93.13%   93.30%   +0.17%     
==========================================
  Files          98       99       +1     
  Lines       22332    23348    +1016     
  Branches      642      684      +42     
==========================================
+ Hits        20798    21785     +987     
- Misses       1384     1399      +15     
- Partials      150      164      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jellevandenhooff

Copy link
Copy Markdown
Author

Serious is a tricky question. I would like to run a reverse proxy, and I would like for it to use ECH. Is my anonymity set going to be anywhere near as large as Cloudflare? If this code merged, I would use it. If it did not, I might use a fork, or I might decide to not care.

My specific use case is that I would like to offer people personal (sub-)domains that I reverse proxy to their personal servers (think self-hosting), without me decrypting their TLS, and without anyone inspecting SNI being able to see who is trying to access anyone else's page.

@jellevandenhooff
jellevandenhooff force-pushed the ech-server-pr branch 4 times, most recently from 94a960e to 1115a88 Compare March 21, 2026 18:34
@jellevandenhooff

Copy link
Copy Markdown
Author

I cleaned up the commit stack a little more. Now the "server: integrate ECH into TLS 1.3 handshake" commit is a bit smaller because I moved out the application-facing status API and some HelloRetryRequest plumbing.

Like I tried to say before, I am open to all kinds of feedback and/or approaches with this PR. If you want to skim the commits and their structure and give high-level feedback, that works, or if you'd want me to read everything line by line and make sure there's no more LLM-isms before you take a look at it, that works also. I can split things into several PRs so that each commit goes by itself and we can talk about them on their own; I haven't done that yet because I figured high-level feedback would be helpful earlier. I would be excited to contribute something meaningful to the TLS ecosystem and am flexible in approach.

Thanks.

@jellevandenhooff
jellevandenhooff marked this pull request as ready for review March 21, 2026 18:52
@djc

djc commented Mar 21, 2026

Copy link
Copy Markdown
Member

In skimming some of the early commits, here is some high-level feedback:

  • should fix code style to match the style guide in CONTRIBUTING.md, notably on item ordering (top-down)
  • looks like there are lots of free functions, where it might make more sense to attach things to types
  • lots of u8 slices as opposed to more strongly typed things
  • for adding code, splitting into different commits tends to have limited value
  • on other hand, changes to existing code should ideally be factored out into separate commits where possible

@jellevandenhooff
jellevandenhooff force-pushed the ech-server-pr branch 3 times, most recently from b835916 to 4a48615 Compare March 21, 2026 23:29
@jellevandenhooff

Copy link
Copy Markdown
Author

Thanks for taking a look. I believe I addressed your comments:

  • The ordering in ech.rs is now top-down (or at least my understanding of top-down! sorry, I am used to go ordering).
  • The free functions are gone and logic now lives in appropriate impl blocksl
  • The u8 slices are gone (though some in tests remain). If this is too much new types happy to change.
  • All code in ech.rs is now in one big commit (besides the proxy stuff). The wiring up of the ECH logic in the actual handshake is still spread over multiple commits (first basic, then API, retry, and rewind). If you'd prefer those also combined I can do that as well, though at that point maybe the whole PR view would show the same.

I appreciate the feedback; I think ech.rs is a lot more legible now.

@jellevandenhooff
jellevandenhooff force-pushed the ech-server-pr branch 5 times, most recently from 0b5e682 to 2864aab Compare March 22, 2026 05:41
@iadev09

iadev09 commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

We built a small local testbed around this PR and got the server-side ECH path working successfully, including a reqwest-based H3 client path and a simpler H2 comparison path.

Looking forward to this being merged. We used this local harness while testing, in case it is useful:

https://github.com/iadev09/ech-server

@jellevandenhooff

Copy link
Copy Markdown
Author

@iadev09 cool!

Pushed some more improvements. Most of the manual byte wrangling now goes through a ClientHelloFields struct so that there is only one new ClientHello parser. It would be great to integrate with the existing message parsing code but I don't quite know how to structure that or if it would make sense. Also tried to make the handshake -> ech state machine API a bit more obvious.

@iadev09

iadev09 commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

I think fully folding this into the existing typed ClientHello parsing may be a bit awkward, because the ECH path seems to need a wire-level view too: exact bytes for AAD, extension ordering, and raw extension bodies.

A smaller step that might still help would be to move the zero-copy ClientHelloFields / raw extension iterator pieces into msgs/client_hello.rs as a crate-private wire-level helper, and let server/ech.rs use that instead of keeping its own local parser.

@jellevandenhooff
jellevandenhooff force-pushed the ech-server-pr branch 2 times, most recently from 90e1527 to d21258c Compare March 22, 2026 18:59
@jellevandenhooff

Copy link
Copy Markdown
Author

Moved the message parsing code into msgs/client_hello.rs. It is a nice improvement, thanks for the suggestion. Also got rid of the new-types for byte slices that now seemed unnecessary.

@jellevandenhooff
jellevandenhooff force-pushed the ech-server-pr branch 3 times, most recently from 4660932 to 84f79b7 Compare March 22, 2026 19:51
@jellevandenhooff

Copy link
Copy Markdown
Author

I split the ECH implementation in several commits, which I think makes reviewing easier, and I added a high-level description of the structure of the changes in the PR message at the top, and added commit messages.

The PR should now be ready for review. Thanks.

@jellevandenhooff
jellevandenhooff marked this pull request as ready for review March 24, 2026 23:13
@frochet

frochet commented Apr 1, 2026

Copy link
Copy Markdown

Hey,

I am chiming in this PR to discuss a potential issue; this is more a conceptual review of the proposal rather than a review of the code, with hopefully good suggestions to solve the stated problem.

So, if I understand correctly what you aim to do: you want a reverse proxy that does not terminate the TLS or QUIC connection, and forward the decrypted ECH towards the real server behind the frontend server.

The remark from @djc is meaningful:

Do you have a (serious) use case for this? What are you trying to achieve?

To what you answer:

My specific use case is that I would like to offer people personal (sub-)domains that I reverse proxy to their personal servers (think self-hosting), without me decrypting their TLS, and without anyone inspecting SNI being able to see who is trying to access anyone else's page.µ

which dwelves on your original comment:

(And the reason I am excited about split mode is because it splits responsibilities and secrecy nicely: the backends do not get the ECH keys, so they cannot decode other requests going into the proxy; meanwhile, the proxy does not decrypt the actual traffic.)

Another question of importance link to @djc's question would be: what is your threat model? If people getting (sub-)domains trust your reverse proxy, then terminating the TLS or QUIC connection at the reverse proxy, and having long-lived connections towards people's (sub-) domains does the job fine, and this PR is not needed in the sense that it does not bring any new use case.

However, if people getting (sub-)domains do not trust your reverse proxy, which is kinda what I suppose you mean by "the proxy does not decrypt the actual traffic", then the use case you have cannot be secure as of now. That is, your people would not have any guarantee that your reverse proxy is not MITM'ing the connections. One way to do that would be for your proxy to request certificates for your people's (sub-) domains and intercepting the challenge towards these domains. You'd get valid cryptographic material that your reverse proxy could use to terminate the connection, and opening another channel towards the people's (sub-)domain. Neither the backend servers, nor the clients would see a problem with that.

So, in my view, the current PR either does not admit a use case or could lead people building misleading, insecure reverse proxy with incorrect guarantees.

There might be a way to mitigate the insecurity problem, but it seems like a long road. We would need a new method to acquire a certificate that could only be challenged to the backend server and its owner. One idea I can think of would be DNS extensions: tell CAs that the domain can only be validated through a DNS record (or higher tier validation, like EV certs). That works assuming you are not as well in control of the domain(s) behind your reverse proxy (so sub-domain case is likely off the table). It is a long road because Browsers need to support this extension and refuse any certificate behind such a reverse proxy that was not acquired through a DNS record challenge. This would need standardization as well, and have people on board with this idea. I am afraid that without this little dance you would end up to nothing useful or secure.

@jellevandenhooff

Copy link
Copy Markdown
Author

@frochet you’re wrong; the ECH RFC split mode is explicitly designed to have a proxy do the ECH handshake while the backend servers do the TLS termination. Backend servers cannot decrypt other ECH handshakes because they do not have the ECH keys and the proxy cannot MITM the actual TLS connection because it does not have certificates for the actual domains.

That’s about an earlier version of the PR anyway. In response to feedback I removed split mode support from the API and instead this PR now implements ECH where a single server does both the ECH decryption and the TLS termination.

@frochet

frochet commented Apr 1, 2026 •

Copy link
Copy Markdown

@frochet you’re wrong; the ECH RFC split mode is explicitly designed to have a proxy do the ECH handshake while the backend servers do the TLS termination.

I agree. My statements do not contradict this.

Backend servers cannot decrypt other ECH handshakes because they do not have the ECH keys

I agree, but I did not discuss this. Only the the client-facing server can decrypt ECH.

and the proxy cannot MITM the actual TLS connection because it does not have certificates for the actual domains.

I disagree. If the backend domain configures their domain to point towards the client-facing server (the reverse proxy), then this server, as an active attacker, can request new valid certificates to CAs for the backend domain and use these certificates to terminate the connection. Ask yourself: how could the split mode prevent that? If a domain owner configures the domain to point to another machine that it does not trust, then no possible privacy guarantee can be possibly formulated given how origin validation works on the Internet.

Coming back to my original question: what is your threat model? I think it is alright for a passive honest-but-curious reverse proxy; in that case the privacy stands, but arguably, this is not a serious threat model for the Internet.

Skimming through the ECH RFC, it appears completely unclear what are the security guarantees, quoting section 10.1:

Passive and active attackers can exist anywhere in the network,
including between the client and client-facing server, as well as
between the client-facing and backend servers when running ECH in
split mode. However, for split mode in particular, ECH makes two
additional assumptions:

  1. The channel between each client-facing and each backend server is
    authenticated such that the backend server only accepts messages
    from trusted client-facing servers. The exact mechanism for
    establishing this authenticated channel is out of scope for this
    document.

The text seems to exclude the reverse proxy as an attacker, and the point "1." says 'trusted'; which means that indeed that no privacy guarantee is formulated from the reverse proxy.

Further bellow, the RFC says this:

Given this threat model, the primary goals of ECH are as follows.

  1. Security preservation. Use of ECH does not weaken the security
    properties of TLS without ECH.

In split mode, this seems true only if we assume that the reverse proxy is trusted.

My understanding of this situation is as follows: security-wise, against active attackers, the split-mode brings not added value compared to the shared mode. I am unsure whether the RFC fumbled it or if there is more subtle nuance I am not seeing. Maybe I should move this discussion to the TLS working group mailing list.

That’s about an earlier version of the PR anyway. In response to feedback I removed split mode support from the API and instead this PR now implements ECH where a single server does both the ECH decryption and the TLS termination.

Oh, sorry, I missed this information. I would however kindly advice precautions with security claims from potential future split mode usage. If anything is unclear, please let me know, I am happy to further elaborate.

@jellevandenhooff

Copy link
Copy Markdown
Author

Off list seems better, yes. It’s true that a malicious proxy could try to acquire certificates through a SNI challenge with letsencrypt, but you can prevent that with CAA records and an account binding (see https://letsencrypt.org/docs/caa/). Additionally, such certificates would show up in the CT log so you would have evidence of the proxy misbehaving.

@iadev09

iadev09 commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

@jellevandenhooff

I know split mode has been removed from this PR (which makes sense), but I’m still curious about the earlier design:

Was split mode inherently limited to SNI/DNS boundaries, or could it support single-domain indirection while keeping the privay of the backend identity — similar in spirit to a tiny URL-style entrypoint acting as a proxy?

@jellevandenhooff

Copy link
Copy Markdown
Author

The ECH proxy can see the SNI information (and all other information in the hello) but nothing else. What you can do, with a wildcard certificate, stick information in the subdomain (like the bsky domains), and the browser will reuse the connection if it resolves to the same IP.

@yaroslavros

Copy link
Copy Markdown

Split mode would be very valuable. It is a very viable way to implement privacy-preserving proxies.
Hopefully it will be brought back.

@jellevandenhooff

Copy link
Copy Markdown
Author

@yaroslavros I agree split mode is valuable. Restoring it is a natural extension of this PR.

Preparation for adding new ECH-related PeerMisbehaved variants.
Sorting first makes the subsequent diffs cleaner and avoids
mixing refactoring with functional changes.
The ECH confirmation signal derivation (RFC 9849 section 7.2) needs
to be callable from the new server ECH module without access to the
KeySchedule. Extract it as a standalone function that takes an
hkdf_provider, matching the existing server_ech_hrr_confirmation_secret.
Server-side ECH decryption needs raw byte access to the ClientHello
for AAD computation (zeroing the ECH payload in-place) and inner hello
reconstruction (splicing outer extensions by byte offset). The existing
parsed ClientHelloPayload re-encodes on serialization, which may not
produce byte-identical output.

RawClientHello provides a zero-copy parsed view of the ClientHello wire
format, with RawExtensionIter for forward-only extension traversal.
The ECH inner ClientHello marker (type 0x01) should be exactly one
byte with no payload. Previously, trailing bytes after the type byte
were silently ignored. Reject them as InvalidMessage::TrailingData
to match the RFC 9849 wire format strictly.
Add the public API types and internal key index for server-side ECH
(Encrypted Client Hello, RFC 9849) key management:

- EchServerKey: pairs an ECHConfig with its HPKE private key
- EchKeys / FixedEchKeys / EchKeyResolver: key set and resolver trait
  for static or dynamic (hot-swappable) key configurations
- EchKeyIndex: flattens key × HPKE suite combinations into a lookup
  table indexed by config_id for O(1) candidate selection during
  decryption
Add the core ECH decryption logic for server-side Encrypted Client Hello
(RFC 9849 sections 5.1, 5.2, 7.1):

- OuterClientHello: parsed + raw wire view for AAD computation
- decrypt_ech: HPKE trial decryption loop over candidate keys
- decode_inner_hello / reconstruct_inner_bytes: inner ClientHello
  reconstruction with ech_outer_extensions splicing
- compute_aad: zero the ECH payload in the ClientHello body for HPKE AAD
- DecryptedEch: holds the decrypted inner hello and its wire encoding
  for transcript hashing
Add EchServerState that ties together key resolution, HPKE decryption,
and confirmation signal computation across the TLS 1.3 handshake.

EchFrontend holds the HPKE opener (reused across HelloRetryRequest).
EchBackend carries the inner random for ServerHello/HRR confirmation
signals (RFC 9849 section 7.2). EchServerState::rewind() supports
falling back to the outer ClientHello when the cert resolver cannot
serve the inner SNI.
Wire up EchServerState into the TLS 1.3 server handshake:

- Resolve ECH on each ClientHello (initial and after HRR)
- Splice the ECH confirmation signal into ServerHello random[24..32]
- Splice the HRR confirmation into the encrypted_client_hello extension
- Include retry_configs in EncryptedExtensions on ECH rejection
- Support ECH rewind: if the cert resolver fails for the inner SNI,
  fall back to the outer ClientHello
- Expose ech_accepted() on ServerConnection and QUIC ServerConnection
- Add is_ech_inner() to ClientHello for the cert resolver
Integration tests exercising the server-side ECH API end-to-end:
accepts, rejects (wrong config_id, no keys, keys with retry=false),
not-offered, is_ech_inner flag, custom EchKeyResolver, and the
ECH rewind path where the cert resolver rejects the inner SNI.
Wire up the bogo test runner to pass ECH server configs, keys, and
retry flags to rustls. Adds error mappings for the new ECH-specific
PeerMisbehaved variants and removes the blanket TLS-ECH-Server* skip
(keeping only the two inner-direct split-mode tests as NYI).
@jellevandenhooff

Copy link
Copy Markdown
Author

rebased

@iadev09

iadev09 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

@jellevandenhooff

I think @ctz’s earlier comment still captures the main design risk here:

implementing all possible options (in this case, both shared and split mode) is usually not a good idea

My outside read is that keeping this PR focused on shared-mode server ECH gives it the clearest merge path, while split/proxy support can be handled in a later PR.

@jellevandenhooff

Copy link
Copy Markdown
Author

I don’t quite know how to think about code contributions in the time of LLMs. I sympathize with maintainers getting a lot of contributions of unknown quality.

The path for now is I think someone wanting to use this in their own codebase doing so with a fork and/or a more concrete use case and perhaps sponsoring the work in rustls.

@iadev09

iadev09 commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

To me, this is mostly about holding both views at once: your concrete need, and the public API shape rustls can review and own confidently.

@djc has also pointed later parallel server-side ECH work back here, so this does not read to me like fork-only work.

@rootful

rootful commented Jun 7, 2026

Copy link
Copy Markdown

How is the progress on this so far? I'm working on a Rust HTTP(S) daemon and have been wanting to implement ECH, and have considered resorting to forking rustls for this until I stumbled upon this PR.

@djc

djc commented Jun 8, 2026

Copy link
Copy Markdown
Member

How is the progress on this so far? I'm working on a Rust HTTP(S) daemon and have been wanting to implement ECH, and have considered resorting to forking rustls for this until I stumbled upon this PR.

I don't think we'll get to this before 0.24 is released (tentatively planned for ~3 months from now).

@johalun

johalun commented Jun 12, 2026

Copy link
Copy Markdown

Thank you all for working on this. I very much would like this feature as well.

tdortman added a commit to tdortman/agent-sandbox that referenced this pull request Aug 6, 2026
rustls implements client ECH only, so the downstream HTTP/3
listener cannot decrypt `ClientHelloInner`.  ECH under HTTP/3 has
been the one remaining rollout gate.

The vendored crate adds RFC 9849 shared-mode server ECH: a new
`ServerConfig::with_ech_keys` API, HPKE decryption of the outer
hello, and a byte-exact reconstruction of the inner hello (session
id restore and `ech_outer_extensions` decompression) that feeds
SNI, ALPN, key share and the handshake transcript.
`ServerHello.random` carries the section 7.2 accept confirmation,
`HelloRetryRequest` carries the section 7.2.1 confirmation, and
rejected offers advertise `retry_configs` in
`EncryptedExtensions` only when the client offered ECH.

The patch is modelled on the open upstream PR
`rustls/rustls#2993`.  Upstream server ECH has no release target,
so the vendored crate stays pinned at 0.23.43 (matching the
workspace version requirement, so quinn and rama keep resolving to
it unchanged) until upstream offers an equivalent API.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants