tls.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package prober
  4. import (
  5. "context"
  6. "crypto/tls"
  7. "crypto/x509"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/netip"
  13. "slices"
  14. "time"
  15. )
  16. const expiresSoon = 7 * 24 * time.Hour // 7 days from now
  17. // Let’s Encrypt promises to issue certificates with CRL servers after 2025-05-07:
  18. // https://letsencrypt.org/2024/12/05/ending-ocsp/
  19. // https://github.com/tailscale/tailscale/issues/15912
  20. const letsEncryptStartedStaplingCRL int64 = 1746576000 // 2025-05-07 00:00:00 UTC
  21. // TLS returns a Probe that healthchecks a TLS endpoint.
  22. //
  23. // The ProbeFunc connects to a hostPort (host:port string), does a TLS
  24. // handshake, verifies that the hostname matches the presented certificate,
  25. // checks certificate validity time and OCSP revocation status.
  26. //
  27. // The TLS config is optional and may be nil.
  28. func TLS(hostPort string, config *tls.Config) ProbeClass {
  29. return ProbeClass{
  30. Probe: func(ctx context.Context) error {
  31. return probeTLS(ctx, config, hostPort)
  32. },
  33. Class: "tls",
  34. }
  35. }
  36. // TLSWithIP is like TLS, but dials the provided dialAddr instead of using DNS
  37. // resolution. Use config.ServerName to send SNI and validate the name in the
  38. // cert.
  39. func TLSWithIP(dialAddr netip.AddrPort, config *tls.Config) ProbeClass {
  40. return ProbeClass{
  41. Probe: func(ctx context.Context) error {
  42. return probeTLS(ctx, config, dialAddr.String())
  43. },
  44. Class: "tls",
  45. }
  46. }
  47. func probeTLS(ctx context.Context, config *tls.Config, dialHostPort string) error {
  48. dialer := &tls.Dialer{Config: config}
  49. conn, err := dialer.DialContext(ctx, "tcp", dialHostPort)
  50. if err != nil {
  51. return fmt.Errorf("connecting to %q: %w", dialHostPort, err)
  52. }
  53. defer conn.Close()
  54. tlsConnState := conn.(*tls.Conn).ConnectionState()
  55. return validateConnState(ctx, &tlsConnState)
  56. }
  57. // validateConnState verifies certificate validity time in all certificates
  58. // returned by the TLS server and checks OCSP revocation status for the
  59. // leaf cert.
  60. func validateConnState(ctx context.Context, cs *tls.ConnectionState) (returnerr error) {
  61. var errs []error
  62. defer func() {
  63. returnerr = errors.Join(errs...)
  64. }()
  65. latestAllowedExpiration := time.Now().Add(expiresSoon)
  66. var leafCert *x509.Certificate
  67. var issuerCert *x509.Certificate
  68. var leafAuthorityKeyID string
  69. // PeerCertificates will never be len == 0 on the client side
  70. for i, cert := range cs.PeerCertificates {
  71. if i == 0 {
  72. leafCert = cert
  73. leafAuthorityKeyID = string(cert.AuthorityKeyId)
  74. }
  75. if i > 0 {
  76. if leafAuthorityKeyID == string(cert.SubjectKeyId) {
  77. issuerCert = cert
  78. }
  79. }
  80. // Do not check certificate validity period for self-signed certs.
  81. // The practical reason is to avoid raising alerts for expiring
  82. // DERP metaCert certificates that are returned as part of regular
  83. // TLS handshake.
  84. if string(cert.SubjectKeyId) == string(cert.AuthorityKeyId) {
  85. continue
  86. }
  87. if time.Now().Before(cert.NotBefore) {
  88. errs = append(errs, fmt.Errorf("one of the certs has NotBefore in the future (%v): %v", cert.NotBefore, cert.Subject))
  89. }
  90. if latestAllowedExpiration.After(cert.NotAfter) {
  91. left := cert.NotAfter.Sub(time.Now())
  92. errs = append(errs, fmt.Errorf("one of the certs expires in %v: %v", left, cert.Subject))
  93. }
  94. }
  95. if len(leafCert.CRLDistributionPoints) == 0 {
  96. if !slices.Contains(leafCert.Issuer.Organization, "Let's Encrypt") {
  97. // LE certs contain a CRL, but certs from other CAs might not.
  98. return
  99. }
  100. if leafCert.NotBefore.Before(time.Unix(letsEncryptStartedStaplingCRL, 0)) {
  101. // Certificate might not have a CRL.
  102. return
  103. }
  104. errs = append(errs, fmt.Errorf("no CRL server presented in leaf cert for %v", leafCert.Subject))
  105. return
  106. }
  107. err := checkCertCRL(ctx, leafCert.CRLDistributionPoints[0], leafCert, issuerCert)
  108. if err != nil {
  109. errs = append(errs, fmt.Errorf("CRL verification failed for %v: %w", leafCert.Subject, err))
  110. }
  111. return
  112. }
  113. func checkCertCRL(ctx context.Context, crlURL string, leafCert, issuerCert *x509.Certificate) error {
  114. hreq, err := http.NewRequestWithContext(ctx, "GET", crlURL, nil)
  115. if err != nil {
  116. return fmt.Errorf("could not create CRL GET request: %w", err)
  117. }
  118. hresp, err := http.DefaultClient.Do(hreq)
  119. if err != nil {
  120. return fmt.Errorf("CRL request failed: %w", err)
  121. }
  122. defer hresp.Body.Close()
  123. if hresp.StatusCode != http.StatusOK {
  124. return fmt.Errorf("crl: non-200 status code from CRL server: %s", hresp.Status)
  125. }
  126. lr := io.LimitReader(hresp.Body, 10<<20) // 10MB
  127. crlB, err := io.ReadAll(lr)
  128. if err != nil {
  129. return err
  130. }
  131. crl, err := x509.ParseRevocationList(crlB)
  132. if err != nil {
  133. return fmt.Errorf("could not parse CRL: %w", err)
  134. }
  135. if err := crl.CheckSignatureFrom(issuerCert); err != nil {
  136. return fmt.Errorf("could not verify CRL signature: %w", err)
  137. }
  138. for _, revoked := range crl.RevokedCertificateEntries {
  139. if revoked.SerialNumber.Cmp(leafCert.SerialNumber) == 0 {
  140. return fmt.Errorf("cert for %v has been revoked on %v, reason: %v", leafCert.Subject, revoked.RevocationTime, revoked.ReasonCode)
  141. }
  142. }
  143. return nil
  144. }