@marcel91200 / caddy Mirror · read-only / commits / 700d6032
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 03:30 UTC

caddytls: Send ECH retry configs when rejecting ECH (#8142)

* caddytls: Send ECH retry configs when rejecting ECH * chore: add licence header to ECH test

videah committed Oct 5, 2026 at 23:54 UTC 700d60327d77d56ba81455acb989a698f1819640
2 files changed +142 -1
caddytest/integration/ech_test.go new
+131
@@ -0,0 +1,131 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package integration
16 +
17 +import (
18 + "crypto/ecdh"
19 + "crypto/rand"
20 + "crypto/tls"
21 + "encoding/binary"
22 + "errors"
23 + "testing"
24 + "time"
25 +
26 + "github.com/caddyserver/caddy/v2/caddytest"
27 +)
28 +
29 +// When ECH is rejected, the server must send its current ECH configs back so
30 +// the client can retry with ECH instead of falling back to plaintext SNI.
31 +func TestECHRejectionSendsRetryConfigs(t *testing.T) {
32 + tester := caddytest.NewTester(t)
33 + tester.InitServer(`
34 + {
35 + skip_install_trust
36 + local_certs
37 + admin localhost:2999
38 + http_port 9080
39 + https_port 9443
40 + ech ech.localhost
41 + }
42 + a.localhost {
43 + respond "ok"
44 + }
45 + `, "caddyfile")
46 +
47 + // certificates are obtained in the background; the rejected handshake
48 + // is completed with the public name's certificate
49 + waitForCertificate(t, "a.localhost")
50 + waitForCertificate(t, "ech.localhost")
51 +
52 + // a well-formed ECH config with a key the server doesn't have, like a
53 + // client would hold after the server's keys were rotated
54 + stale := echConfigList(t, "ech.localhost")
55 +
56 + _, err := dialECH(stale)
57 + var rejection *tls.ECHRejectionError
58 + if !errors.As(err, &rejection) {
59 + t.Fatalf("expected ECH to be rejected, got: %v", err)
60 + }
61 + if len(rejection.RetryConfigList) == 0 {
62 + t.Fatal("ECH was rejected without retry configs")
63 + }
64 +
65 + conn, err := dialECH(rejection.RetryConfigList)
66 + if err != nil {
67 + t.Fatalf("retrying with the server's retry configs: %v", err)
68 + }
69 + defer conn.Close()
70 + if !conn.ConnectionState().ECHAccepted {
71 + t.Fatal("ECH was not accepted when retrying with the server's retry configs")
72 + }
73 +}
74 +
75 +func waitForCertificate(t *testing.T, serverName string) {
76 + t.Helper()
77 + var err error
78 + for range 50 {
79 + var conn *tls.Conn
80 + conn, err = tls.Dial("tcp", "127.0.0.1:9443", &tls.Config{
81 + ServerName: serverName,
82 + InsecureSkipVerify: true, //nolint:gosec
83 + })
84 + if err == nil {
85 + conn.Close()
86 + return
87 + }
88 + time.Sleep(100 * time.Millisecond)
89 + }
90 + t.Fatalf("no certificate for %s: %v", serverName, err)
91 +}
92 +
93 +func dialECH(configList []byte) (*tls.Conn, error) {
94 + return tls.Dial("tcp", "127.0.0.1:9443", &tls.Config{
95 + ServerName: "a.localhost",
96 + InsecureSkipVerify: true, //nolint:gosec
97 + EncryptedClientHelloConfigList: configList,
98 + // the outer handshake uses the public name's certificate from the
99 + // test's internal CA, which this client doesn't trust
100 + EncryptedClientHelloRejectionVerify: func(tls.ConnectionState) error { return nil },
101 + })
102 +}
103 +
104 +// echConfigList returns an ECHConfigList holding one version 0xfe0d config
105 +// with a fresh X25519 key, HKDF-SHA256 and AES-128-GCM.
106 +func echConfigList(t *testing.T, publicName string) []byte {
107 + key, err := ecdh.X25519().GenerateKey(rand.Reader)
108 + if err != nil {
109 + t.Fatal(err)
110 + }
111 + pub := key.PublicKey().Bytes()
112 +
113 + var contents []byte
114 + contents = append(contents, 1) // config_id
115 + contents = binary.BigEndian.AppendUint16(contents, 0x0020) // KEM: DHKEM(X25519, HKDF-SHA256)
116 + contents = binary.BigEndian.AppendUint16(contents, uint16(len(pub)))
117 + contents = append(contents, pub...)
118 + contents = binary.BigEndian.AppendUint16(contents, 4) // cipher suites length
119 + contents = binary.BigEndian.AppendUint16(contents, 0x0001) // KDF: HKDF-SHA256
120 + contents = binary.BigEndian.AppendUint16(contents, 0x0001) // AEAD: AES-128-GCM
121 + contents = append(contents, 0) // maximum_name_length
122 + contents = append(contents, byte(len(publicName)))
123 + contents = append(contents, publicName...)
124 + contents = binary.BigEndian.AppendUint16(contents, 0) // extensions
125 +
126 + config := binary.BigEndian.AppendUint16(nil, 0xfe0d) // version
127 + config = binary.BigEndian.AppendUint16(config, uint16(len(contents)))
128 + config = append(config, contents...)
129 +
130 + return append(binary.BigEndian.AppendUint16(nil, uint16(len(config))), config...)
131 +}
modules/caddytls/connpolicy.go
+11 -1
@@ -169,11 +169,21 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
169 }
170 }
171
172 - tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
172 + getECHKeys := func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
173 tlsApp.EncryptedClientHello.configsMu.RLock()
174 defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
175 return tlsApp.EncryptedClientHello.stdlibReady, nil
176 }
177 +
178 + // crypto/tls decrypts the ClientHello with the keys from this config,
179 + // but builds the retry configs it sends when ECH is rejected from the
180 + // config returned by GetConfigForClient, so both need the keys.
181 + tlsCfg.GetEncryptedClientHelloKeys = getECHKeys
182 + for _, p := range cp {
183 + if p.TLSConfig != nil && p.TLSConfig.GetEncryptedClientHelloKeys == nil {
184 + p.TLSConfig.GetEncryptedClientHelloKeys = getECHKeys
185 + }
186 + }
187 }
188 }
189