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