1 package va
2
3 import (
4 "context"
5 "crypto/tls"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "net/http"
11 "net/url"
12 "strconv"
13 "strings"
14 "time"
15 "unicode"
16
17 "github.com/letsencrypt/boulder/core"
18 berrors "github.com/letsencrypt/boulder/errors"
19 "github.com/letsencrypt/boulder/iana"
20 "github.com/letsencrypt/boulder/identifier"
21 "github.com/letsencrypt/boulder/probs"
22 )
23
24 const (
25
26
27 maxRedirect = 10
28
29
30
31
32
33 maxResponseSize = 128
34
35
36 maxPathSize = 2000
37 )
38
39
40
41
42
43 type preresolvedDialer struct {
44 ip net.IP
45 port int
46 hostname string
47 timeout time.Duration
48 }
49
50
51
52 type dialerMismatchError struct {
53
54 dialerHost string
55 dialerIP string
56 dialerPort int
57
58 host string
59 }
60
61 func (e *dialerMismatchError) Error() string {
62 return fmt.Sprintf(
63 "preresolvedDialer mismatch: dialer is for %q (ip: %q port: %d) not %q",
64 e.dialerHost, e.dialerIP, e.dialerPort, e.host)
65 }
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 func (d *preresolvedDialer) DialContext(
81 ctx context.Context,
82 network,
83 origAddr string) (net.Conn, error) {
84 deadline, ok := ctx.Deadline()
85 if !ok {
86
87 deadline = time.Now().Add(100 * time.Second)
88 } else {
89
90
91
92 deadline = deadline.Add(-10 * time.Millisecond)
93 }
94 ctx, cancel := context.WithDeadline(ctx, deadline)
95 defer cancel()
96
97
98
99
100
101
102
103
104
105
106
107 origHost, _, err := net.SplitHostPort(origAddr)
108 if err != nil {
109 return nil, err
110 }
111
112
113
114 if origHost != d.hostname {
115 return nil, &dialerMismatchError{
116 dialerHost: d.hostname,
117 dialerIP: d.ip.String(),
118 dialerPort: d.port,
119 host: origHost,
120 }
121 }
122
123
124 targetAddr := net.JoinHostPort(d.ip.String(), strconv.Itoa(d.port))
125
126
127
128 throwAwayDialer := &net.Dialer{
129 Timeout: d.timeout,
130
131 KeepAlive: 30 * time.Second,
132 }
133 return throwAwayDialer.DialContext(ctx, network, targetAddr)
134 }
135
136
137
138 type dialerFunc func(ctx context.Context, network, addr string) (net.Conn, error)
139
140
141
142
143 func httpTransport(df dialerFunc) *http.Transport {
144 return &http.Transport{
145 DialContext: df,
146
147
148 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
149
150
151 DisableKeepAlives: true,
152
153 MaxIdleConns: 1,
154 IdleConnTimeout: time.Second,
155 TLSHandshakeTimeout: 10 * time.Second,
156 }
157 }
158
159
160
161 type httpValidationTarget struct {
162
163 host string
164
165 port int
166
167 path string
168
169
170 query string
171
172 available []net.IP
173
174
175 tried []net.IP
176
177 next []net.IP
178
179 cur net.IP
180 }
181
182
183
184
185
186 func (vt *httpValidationTarget) nextIP() error {
187 if len(vt.next) == 0 {
188 return fmt.Errorf(
189 "host %q has no IP addresses remaining to use",
190 vt.host)
191 }
192 vt.tried = append(vt.tried, vt.cur)
193 vt.cur = vt.next[0]
194 vt.next = vt.next[1:]
195 return nil
196 }
197
198
199
200 func (vt *httpValidationTarget) ip() net.IP {
201 return vt.cur
202 }
203
204
205
206
207
208 func (va *ValidationAuthorityImpl) newHTTPValidationTarget(
209 ctx context.Context,
210 host string,
211 port int,
212 path string,
213 query string) (*httpValidationTarget, error) {
214
215 addrs, err := va.getAddrs(ctx, host)
216 if err != nil {
217 return nil, err
218 }
219
220 target := &httpValidationTarget{
221 host: host,
222 port: port,
223 path: path,
224 query: query,
225 available: addrs,
226 }
227
228
229 v4Addrs, v6Addrs := availableAddresses(addrs)
230 hasV6Addrs := len(v6Addrs) > 0
231 hasV4Addrs := len(v4Addrs) > 0
232
233 if !hasV6Addrs && !hasV4Addrs {
234
235
236 return nil, fmt.Errorf("host %q has no IPv4 or IPv6 addresses", host)
237 } else if !hasV6Addrs && hasV4Addrs {
238
239
240 target.next = []net.IP{v4Addrs[0]}
241 } else if hasV6Addrs && hasV4Addrs {
242
243
244 target.next = []net.IP{v6Addrs[0], v4Addrs[0]}
245 } else if hasV6Addrs && !hasV4Addrs {
246
247
248 target.next = []net.IP{v6Addrs[0]}
249 }
250
251
252 _ = target.nextIP()
253 return target, nil
254 }
255
256
257
258
259
260
261
262 func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (string, int, error) {
263
264 if req == nil {
265 return "", 0, fmt.Errorf("redirect HTTP request was nil")
266 }
267
268 reqScheme := req.URL.Scheme
269
270
271 if reqScheme != "http" && reqScheme != "https" {
272 return "", 0, berrors.ConnectionFailureError(
273 "Invalid protocol scheme in redirect target. "+
274 `Only "http" and "https" protocol schemes are supported, not %q`, reqScheme)
275 }
276
277
278
279
280 reqHost := req.URL.Host
281 var reqPort int
282 if h, p, err := net.SplitHostPort(reqHost); err == nil {
283 reqHost = h
284 reqPort, err = strconv.Atoi(p)
285 if err != nil {
286 return "", 0, err
287 }
288
289
290 if reqPort != va.httpPort && reqPort != va.httpsPort {
291 return "", 0, berrors.ConnectionFailureError(
292 "Invalid port in redirect target. Only ports %d and %d are supported, not %d",
293 va.httpPort, va.httpsPort, reqPort)
294 }
295 } else if reqScheme == "http" {
296 reqPort = va.httpPort
297 } else if reqScheme == "https" {
298 reqPort = va.httpsPort
299 } else {
300
301
302 return "", 0, fmt.Errorf("unable to determine redirect HTTP request port")
303 }
304
305 if reqHost == "" {
306 return "", 0, berrors.ConnectionFailureError("Invalid empty hostname in redirect target")
307 }
308
309
310
311 if net.ParseIP(reqHost) != nil {
312 return "", 0, berrors.ConnectionFailureError("Invalid host in redirect target %q. Only domain names are supported, not IP addresses", reqHost)
313 }
314
315
316
317
318
319
320
321
322
323
324 if strings.HasSuffix(reqHost, ".well-known") {
325 return "", 0, berrors.ConnectionFailureError(
326 "Invalid host in redirect target %q. Check webserver config for missing '/' in redirect target.",
327 reqHost,
328 )
329 }
330
331 if _, err := iana.ExtractSuffix(reqHost); err != nil {
332 return "", 0, berrors.ConnectionFailureError("Invalid hostname in redirect target, must end in IANA registered TLD")
333 }
334
335 return reqHost, reqPort, nil
336 }
337
338
339
340
341
342 func (va *ValidationAuthorityImpl) setupHTTPValidation(
343 reqURL string,
344 target *httpValidationTarget) (*preresolvedDialer, core.ValidationRecord, error) {
345 if reqURL == "" {
346 return nil,
347 core.ValidationRecord{},
348 fmt.Errorf("reqURL can not be nil")
349 }
350 if target == nil {
351
352
353 return nil,
354 core.ValidationRecord{},
355 fmt.Errorf("httpValidationTarget can not be nil")
356 }
357
358
359
360 record := core.ValidationRecord{
361 Hostname: target.host,
362 Port: strconv.Itoa(target.port),
363 AddressesResolved: target.available,
364 URL: reqURL,
365 }
366
367
368 targetIP := target.ip()
369 if targetIP == nil {
370 return nil,
371 record,
372 fmt.Errorf(
373 "host %q has no IP addresses remaining to use",
374 target.host)
375 }
376 record.AddressUsed = targetIP
377
378 dialer := &preresolvedDialer{
379 ip: targetIP,
380 port: target.port,
381 hostname: target.host,
382 timeout: va.singleDialTimeout,
383 }
384 return dialer, record, nil
385 }
386
387
388
389
390 func (va *ValidationAuthorityImpl) fetchHTTP(
391 ctx context.Context,
392 host string,
393 path string) ([]byte, []core.ValidationRecord, *probs.ProblemDetails) {
394 body, records, err := va.processHTTPValidation(ctx, host, path)
395 if err != nil {
396
397 return body, records, detailedError(err)
398 }
399 return body, records, nil
400 }
401
402
403
404
405
406 func fallbackErr(err error) bool {
407
408 if err == nil {
409 return false
410 }
411
412
413 var netOpError *net.OpError
414 return errors.As(err, &netOpError) && netOpError.Op == "dial"
415 }
416
417
418
419
420
421 func (va *ValidationAuthorityImpl) processHTTPValidation(
422 ctx context.Context,
423 host string,
424 path string) ([]byte, []core.ValidationRecord, error) {
425
426
427 target, err := va.newHTTPValidationTarget(ctx, host, va.httpPort, path, "")
428 if err != nil {
429 return nil, nil, err
430 }
431
432
433
434
435 newIPError := func(target *httpValidationTarget, err error) error {
436 return ipError{ip: target.cur, err: err}
437 }
438
439
440 initialURL := url.URL{
441 Scheme: "http",
442 Host: host,
443 Path: path,
444 }
445 initialReq, err := http.NewRequest("GET", initialURL.String(), nil)
446 if err != nil {
447 return nil, nil, newIPError(target, err)
448 }
449
450
451
452
453
454
455
456
457
458 deadline, ok := ctx.Deadline()
459 if !ok {
460 return nil, nil, fmt.Errorf("processHTTPValidation had no deadline")
461 } else {
462 deadline = deadline.Add(-200 * time.Millisecond)
463 }
464 ctx, cancel := context.WithDeadline(ctx, deadline)
465 defer cancel()
466 initialReq = initialReq.WithContext(ctx)
467 if va.userAgent != "" {
468 initialReq.Header.Set("User-Agent", va.userAgent)
469 }
470
471
472
473
474
475
476
477
478
479 initialReq.Header.Set("Accept", "*/*")
480
481
482 dialer, baseRecord, err := va.setupHTTPValidation(initialReq.URL.String(), target)
483 if err != nil {
484 return nil, []core.ValidationRecord{}, newIPError(target, err)
485 }
486
487
488
489 transport := httpTransport(dialer.DialContext)
490
491 va.log.AuditInfof("Attempting to validate HTTP-01 for %q with GET to %q",
492 initialReq.Host, initialReq.URL.String())
493
494
495
496
497 records := []core.ValidationRecord{baseRecord}
498 numRedirects := 0
499 processRedirect := func(req *http.Request, via []*http.Request) error {
500 va.log.Debugf("processing a HTTP redirect from the server to %q", req.URL.String())
501
502 if numRedirects > maxRedirect {
503 return berrors.ConnectionFailureError("Too many redirects")
504 }
505 numRedirects++
506 va.metrics.http01Redirects.Inc()
507
508 if req.Response.TLS != nil && req.Response.TLS.Version < tls.VersionTLS12 {
509 return berrors.ConnectionFailureError(
510 "validation attempt was redirected to an HTTPS server that doesn't " +
511 "support TLSv1.2 or better. See " +
512 "https://community.letsencrypt.org/t/rejecting-sha-1-csrs-and-validation-using-tls-1-0-1-1-urls/175144")
513 }
514
515
516
517
518
519
520
521 acceptableRedirects := map[int]struct{}{
522 301: {}, 302: {}, 307: {}, 308: {},
523 }
524 if _, present := acceptableRedirects[req.Response.StatusCode]; !present {
525 return berrors.ConnectionFailureError("received disallowed redirect status code")
526 }
527
528
529
530 req.URL.Host = strings.ToLower(req.URL.Host)
531
532
533
534 redirHost, redirPort, err := va.extractRequestTarget(req)
535 if err != nil {
536 return err
537 }
538
539 redirPath := req.URL.Path
540 if len(redirPath) > maxPathSize {
541 return berrors.ConnectionFailureError("Redirect target too long")
542 }
543
544
545
546 redirQuery := ""
547 if req.URL.RawQuery != "" {
548 redirQuery = req.URL.RawQuery
549 }
550
551
552
553 for _, record := range records {
554 if req.URL.String() == record.URL {
555 return berrors.ConnectionFailureError("Redirect loop detected")
556 }
557 }
558
559
560
561 redirTarget, err := va.newHTTPValidationTarget(ctx, redirHost, redirPort, redirPath, redirQuery)
562 if err != nil {
563 return err
564 }
565
566
567
568
569 redirDialer, redirRecord, err := va.setupHTTPValidation(req.URL.String(), redirTarget)
570 records = append(records, redirRecord)
571 if err != nil {
572 return err
573 }
574
575 va.log.Debugf("following redirect to host %q url %q", req.Host, req.URL.String())
576
577
578 transport.DialContext = redirDialer.DialContext
579 return nil
580 }
581
582
583
584 client := http.Client{
585 Transport: transport,
586 CheckRedirect: processRedirect,
587 }
588
589
590
591 httpResponse, err := client.Do(initialReq)
592
593
594 if err != nil && fallbackErr(err) {
595
596
597 advanceTargetIPErr := target.nextIP()
598 if advanceTargetIPErr != nil {
599 return nil, records, newIPError(target, err)
600 }
601
602
603
604 retryDialer, retryRecord, err := va.setupHTTPValidation(initialReq.URL.String(), target)
605 records = append(records, retryRecord)
606 if err != nil {
607 return nil, records, newIPError(target, err)
608 }
609 va.metrics.http01Fallbacks.Inc()
610
611
612 transport.DialContext = retryDialer.DialContext
613
614
615 httpResponse, err = client.Do(initialReq)
616
617
618 if err != nil {
619 return nil, records, newIPError(target, err)
620 }
621 } else if err != nil {
622
623 return nil, records, newIPError(target, err)
624 }
625
626 if httpResponse.StatusCode != 200 {
627 return nil, records, newIPError(target, berrors.UnauthorizedError("Invalid response from %s: %d",
628 records[len(records)-1].URL, httpResponse.StatusCode))
629 }
630
631
632
633 body, err := io.ReadAll(&io.LimitedReader{R: httpResponse.Body, N: maxResponseSize})
634 closeErr := httpResponse.Body.Close()
635 if err == nil {
636 err = closeErr
637 }
638 if err != nil {
639 return nil, records, newIPError(target, berrors.UnauthorizedError("Error reading HTTP response body: %v", err))
640 }
641
642
643
644 if len(body) >= maxResponseSize {
645 return nil, records, newIPError(target, berrors.UnauthorizedError("Invalid response from %s: %q",
646 records[len(records)-1].URL, body))
647 }
648 return body, records, nil
649 }
650
651 func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident identifier.ACMEIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) {
652 if ident.Type != identifier.DNS {
653 va.log.Infof("Got non-DNS identifier for HTTP validation: %s", ident)
654 return nil, probs.Malformed("Identifier type for HTTP validation was not DNS")
655 }
656
657
658 path := fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token)
659 body, validationRecords, prob := va.fetchHTTP(ctx, ident.Value, "/"+path)
660 if prob != nil {
661 return validationRecords, prob
662 }
663
664 payload := strings.TrimRightFunc(string(body), unicode.IsSpace)
665
666 if payload != challenge.ProvidedKeyAuthorization {
667 problem := probs.Unauthorized(fmt.Sprintf("The key authorization file from the server did not match this challenge. Expected %q (got %q)",
668 challenge.ProvidedKeyAuthorization, payload))
669 va.log.Infof("%s for %s", problem.Detail, ident)
670 return validationRecords, problem
671 }
672
673 return validationRecords, nil
674 }
675
View as plain text