ALL

For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.

tunnel/observer.go

SHA-256: 846b739ad636c61ad10d64caafda569b1620a7563602406e474771e55781acaf
317 lines · 9806 bytes · View on GitHub

1package main
2
3// The cleartext handshake observer (the design plan, sections 3 and 7). The node splices bytes it cannot read, but the first messages of a TLS
4// handshake are cleartext by design, and what they say is the measurement G1 needs: which groups real ChatGPT clients offer (does the hybrid
5// X25519MLKEM768 come?), and what the device's TLS server selects (version, cipher, group) so the plan's claims about strength are measured and not
6// assumed. It reads three things and nothing else:
7//
8// - the ClientHello's supported_groups (and whether it carries an ECH extension), from the bytes the node already read to learn the SNI;
9// - the ServerHello's version (supported_versions, else the legacy version), cipher suite and, in TLS 1.3, the key_share's selected group, from the
10// first records the device sends back; a HelloRetryRequest is counted and the observer waits for the real ServerHello;
11// - nothing after that: once the ServerHello is read (or the stream proves not to be TLS) the observer stops, and the rest of the stream is never
12// looked at.
13//
14// What it keeps is a count per word of a CLOSED list (the tables below): never a name, an address, a label, a stream or a time. The words are the
15// metrics' label values, so a client cannot make a new series by offering a new group: it is counted as "other". Nothing here is written to the log.
16
17import "bytes"
18
19const (
20 extSupportedGroups = 10
21 extSupportedVersions = 43
22 extKeyShare = 51
23 extECH = 0xfe0d // the RFC's code point (the draft ones are not counted)
24)
25
26// Words of the observer, in the order the metrics list them.
27var (
28 obsGroupWords = []string{
29 "x25519", "secp256r1", "secp384r1", "secp521r1", "x448", "x25519mlkem768", "secp256r1mlkem768", "secp384r1mlkem1024", "x25519kyber768draft00", "ffdhe", "other",
30 }
31 obsVersionWords = []string{"tls13", "tls12", "tls11", "tls10", "other"}
32 obsCipherWords = []string{
33 "tls_aes_128_gcm_sha256", "tls_aes_256_gcm_sha384", "tls_chacha20_poly1305_sha256",
34 "ecdhe_ecdsa_aes_128_gcm_sha256", "ecdhe_ecdsa_aes_256_gcm_sha384", "ecdhe_rsa_aes_128_gcm_sha256", "ecdhe_rsa_aes_256_gcm_sha384",
35 "ecdhe_ecdsa_chacha20_poly1305", "ecdhe_rsa_chacha20_poly1305", "other",
36 }
37 // the selected group of a handshake that has none (TLS 1.2 here, or a ServerHello without a key share) is counted under this word
38 obsNoGroup = "none"
39)
40
41func isGREASE(v uint16) bool { return v&0x0f0f == 0x0a0a && byte(v>>8) == byte(v) }
42
43// groupWord names a TLS named group.
44func groupWord(g uint16) string {
45 switch g {
46 case 0x001d:
47 return "x25519"
48 case 0x0017:
49 return "secp256r1"
50 case 0x0018:
51 return "secp384r1"
52 case 0x0019:
53 return "secp521r1"
54 case 0x001e:
55 return "x448"
56 case 0x11ec:
57 return "x25519mlkem768"
58 case 0x11eb:
59 return "secp256r1mlkem768"
60 case 0x11ed:
61 return "secp384r1mlkem1024"
62 case 0x6399:
63 return "x25519kyber768draft00"
64 }
65 if g >= 0x0100 && g <= 0x01ff {
66 return "ffdhe"
67 }
68 return "other"
69}
70
71func versionWord(v uint16) string {
72 switch v {
73 case 0x0304:
74 return "tls13"
75 case 0x0303:
76 return "tls12"
77 case 0x0302:
78 return "tls11"
79 case 0x0301:
80 return "tls10"
81 }
82 return "other"
83}
84
85func cipherWord(c uint16) string {
86 switch c {
87 case 0x1301:
88 return "tls_aes_128_gcm_sha256"
89 case 0x1302:
90 return "tls_aes_256_gcm_sha384"
91 case 0x1303:
92 return "tls_chacha20_poly1305_sha256"
93 case 0xc02b:
94 return "ecdhe_ecdsa_aes_128_gcm_sha256"
95 case 0xc02c:
96 return "ecdhe_ecdsa_aes_256_gcm_sha384"
97 case 0xc02f:
98 return "ecdhe_rsa_aes_128_gcm_sha256"
99 case 0xc030:
100 return "ecdhe_rsa_aes_256_gcm_sha384"
101 case 0xcca9:
102 return "ecdhe_ecdsa_chacha20_poly1305"
103 case 0xcca8:
104 return "ecdhe_rsa_chacha20_poly1305"
105 }
106 return "other"
107}
108
109// helloShape is what the observer takes from a ClientHello.
110type helloShape struct {
111 groups []uint16
112 ech bool
113 ok bool
114}
115
116// shapeOfClientHello reads a ClientHello body (the bytes after the 4-byte handshake header). It never reads outside body, and anything it cannot make
117// sense of is simply not observed.
118func shapeOfClientHello(body []byte) helloShape {
119 c := &cursor{b: body}
120 c.take(2) // legacy_version
121 c.take(32) // random
122 c.take(c.u8())
123 c.take(c.u16()) // cipher suites
124 c.take(c.u8()) // compression methods
125 if c.bad || len(c.b) < 2 {
126 return helloShape{}
127 }
128 ext := &cursor{b: c.take(c.u16())}
129 if c.bad {
130 return helloShape{}
131 }
132 shape := helloShape{ok: true}
133 for len(ext.b) > 0 {
134 typ := ext.u16()
135 data := &cursor{b: ext.take(ext.u16())}
136 if ext.bad {
137 return helloShape{}
138 }
139 switch typ {
140 case extSupportedGroups:
141 list := &cursor{b: data.take(data.u16())}
142 for len(list.b) >= 2 {
143 if g := uint16(list.u16()); !isGREASE(g) {
144 shape.groups = append(shape.groups, g)
145 }
146 }
147 case extECH:
148 shape.ech = true
149 }
150 }
151 return shape
152}
153
154// serverHello is what the observer takes from a ServerHello (or a HelloRetryRequest).
155type serverHello struct {
156 version, cipher uint16
157 group uint16
158 hasGroup bool
159 retry bool
160}
161
162// helloRetryRandom is the fixed "random" that makes a ServerHello a HelloRetryRequest (RFC 8446 section 4.1.3).
163var helloRetryRandom = []byte{
164 0xcf, 0x21, 0xad, 0x74, 0xe5, 0x9a, 0x61, 0x11, 0xbe, 0x1d, 0x8c, 0x02, 0x1e, 0x65, 0xb8, 0x91,
165 0xc2, 0xa2, 0x11, 0x16, 0x7a, 0xbb, 0x8c, 0x5e, 0x07, 0x9e, 0x09, 0xe2, 0xc8, 0xa8, 0x33, 0x9c,
166}
167
168// parseServerHello reads a ServerHello message body (after the 4-byte handshake header).
169func parseServerHello(body []byte) (serverHello, bool) {
170 c := &cursor{b: body}
171 legacy := uint16(c.u16())
172 random := c.take(32)
173 c.take(c.u8()) // session id echo
174 cipher := uint16(c.u16())
175 c.take(1) // compression method
176 if c.bad {
177 return serverHello{}, false
178 }
179 sh := serverHello{version: legacy, cipher: cipher, retry: bytes.Equal(random, helloRetryRandom)}
180 if len(c.b) == 0 {
181 return sh, true // TLS 1.2 without extensions
182 }
183 ext := &cursor{b: c.take(c.u16())}
184 if c.bad {
185 return serverHello{}, false
186 }
187 for len(ext.b) > 0 {
188 typ := ext.u16()
189 data := &cursor{b: ext.take(ext.u16())}
190 if ext.bad {
191 return serverHello{}, false
192 }
193 switch typ {
194 case extSupportedVersions:
195 if v := uint16(data.u16()); !data.bad {
196 sh.version = v
197 }
198 case extKeyShare:
199 // ServerHello: the group, then the key exchange. HelloRetryRequest: the group alone.
200 if g := uint16(data.u16()); !data.bad {
201 sh.group, sh.hasGroup = g, true
202 }
203 }
204 }
205 return sh, true
206}
207
208// maxObserved is the most the observer reads of a stream's first bytes from the device before it gives up: a ServerHello is a hundred bytes, and a
209// stream that has not shown one in this much is not a TLS handshake the observer understands.
210const maxObserved = 16 << 10
211
212// serverHelloWatch reads the device's first bytes of ONE client stream until the ServerHello has been seen, reporting to counters. It is used by one
213// goroutine (the pump of that direction).
214type serverHelloWatch struct {
215 m *metrics
216 buf []byte
217 seen int // hellos read: a retry request and the ServerHello after it make two
218 done bool
219}
220
221func newServerHelloWatch(m *metrics) *serverHelloWatch { return &serverHelloWatch{m: m} }
222
223// feed gives the watcher the next bytes the device sent toward the client. After the ServerHello it costs one comparison.
224func (w *serverHelloWatch) feed(p []byte) {
225 if w.done {
226 return
227 }
228 if len(w.buf)+len(p) > maxObserved {
229 w.giveUp()
230 return
231 }
232 w.buf = append(w.buf, p...)
233 for len(w.buf) > 0 {
234 // Only a handshake or a ChangeCipherSpec record (TLS 1.x) can come before the ServerHello: anything else is not one, said as soon as it shows.
235 if (w.buf[0] != 20 && w.buf[0] != 22) || (len(w.buf) >= 2 && w.buf[1] != 3) {
236 w.giveUp()
237 return
238 }
239 if len(w.buf) < 5 {
240 return
241 }
242 n := int(w.buf[3])<<8 | int(w.buf[4])
243 if len(w.buf) < 5+n {
244 return // the rest of the record is still on its way
245 }
246 typ, payload := w.buf[0], w.buf[5:5+n]
247 w.buf = w.buf[5+n:]
248 if typ == 22 && !w.handshake(payload) {
249 return
250 } // (a ChangeCipherSpec of the TLS 1.3 compatibility mode is not a hello: keep reading)
251 }
252}
253
254// handshake reads the hello a handshake record starts with. It reports whether the watcher goes on (a retry request is followed by a ServerHello).
255func (w *serverHelloWatch) handshake(payload []byte) bool {
256 if len(payload) < 4 || payload[0] != 2 {
257 w.giveUp()
258 return false
259 }
260 l := int(payload[1])<<16 | int(payload[2])<<8 | int(payload[3])
261 if len(payload) < 4+l { // a ServerHello split over records: not seen in practice, not observed
262 w.giveUp()
263 return false
264 }
265 sh, ok := parseServerHello(payload[4 : 4+l])
266 if !ok {
267 w.giveUp()
268 return false
269 }
270 w.seen++
271 if sh.retry {
272 w.m.obsRetry.Add(1)
273 if w.seen >= 2 { // two retries are not a handshake
274 w.giveUp()
275 return false
276 }
277 return true
278 }
279 w.m.obsVersion.inc(versionWord(sh.version))
280 w.m.obsCipher.inc(cipherWord(sh.cipher))
281 if sh.hasGroup {
282 w.m.obsGroup.inc(groupWord(sh.group))
283 } else {
284 w.m.obsGroup.inc(obsNoGroup)
285 }
286 w.done, w.buf = true, nil
287 return false
288}
289
290// giveUp stops watching a stream whose first bytes were not a ServerHello the observer follows.
291func (w *serverHelloWatch) giveUp() {
292 w.m.obsUnparsed.Add(1)
293 w.done, w.buf = true, nil
294}
295
296// observeClientHello counts what a ClientHello offered. peeked is the bytes the node read to find the SNI.
297func (m *metrics) observeClientHello(peeked []byte) {
298 scan := scanClientHello(peeked)
299 if !scan.complete || scan.err != nil {
300 return
301 }
302 shape := shapeOfClientHello(scan.hello)
303 if !shape.ok {
304 return
305 }
306 m.obsHellos.Add(1)
307 seen := map[string]bool{} // a group counts once per ClientHello, however many times it is listed
308 for _, g := range shape.groups {
309 if w := groupWord(g); !seen[w] {
310 seen[w] = true
311 m.obsOffered.inc(w)
312 }
313 }
314 if shape.ech {
315 m.obsECH.Add(1)
316 }
317}