github.com/letsencrypt/boulder@v0.20251208.0/observer/probers/tls/tls.go (about)

     1  package probers
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"time"
    14  
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"golang.org/x/crypto/ocsp"
    17  
    18  	"github.com/letsencrypt/boulder/observer/obsdialer"
    19  )
    20  
    21  type reason int
    22  
    23  const (
    24  	none reason = iota
    25  	internalError
    26  	revocationStatusError
    27  	rootDidNotMatch
    28  	statusDidNotMatch
    29  )
    30  
    31  var reasonToString = map[reason]string{
    32  	none:                  "nil",
    33  	internalError:         "internalError",
    34  	revocationStatusError: "revocationStatusError",
    35  	rootDidNotMatch:       "rootDidNotMatch",
    36  	statusDidNotMatch:     "statusDidNotMatch",
    37  }
    38  
    39  func getReasons() []string {
    40  	var allReasons []string
    41  	for _, v := range reasonToString {
    42  		allReasons = append(allReasons, v)
    43  	}
    44  	return allReasons
    45  }
    46  
    47  // TLSProbe is the exported `Prober` object for monitors configured to perform
    48  // TLS protocols.
    49  type TLSProbe struct {
    50  	hostname  string
    51  	rootOrg   string
    52  	rootCN    string
    53  	response  string
    54  	notAfter  *prometheus.GaugeVec
    55  	notBefore *prometheus.GaugeVec
    56  	reason    *prometheus.CounterVec
    57  }
    58  
    59  // Name returns a string that uniquely identifies the monitor.
    60  func (p TLSProbe) Name() string {
    61  	return p.hostname
    62  }
    63  
    64  // Kind returns a name that uniquely identifies the `Kind` of `Prober`.
    65  func (p TLSProbe) Kind() string {
    66  	return "TLS"
    67  }
    68  
    69  // Get OCSP status (good, revoked or unknown) of certificate
    70  func checkOCSP(ctx context.Context, cert, issuer *x509.Certificate, want int) (bool, error) {
    71  	req, err := ocsp.CreateRequest(cert, issuer, nil)
    72  	if err != nil {
    73  		return false, err
    74  	}
    75  
    76  	url := fmt.Sprintf("%s/%s", cert.OCSPServer[0], base64.StdEncoding.EncodeToString(req))
    77  	r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    78  	if err != nil {
    79  		return false, err
    80  	}
    81  
    82  	res, err := http.DefaultClient.Do(r)
    83  	if err != nil {
    84  		return false, err
    85  	}
    86  
    87  	output, err := io.ReadAll(res.Body)
    88  	if err != nil {
    89  		return false, err
    90  	}
    91  
    92  	ocspRes, err := ocsp.ParseResponseForCert(output, cert, issuer)
    93  	if err != nil {
    94  		return false, err
    95  	}
    96  
    97  	return ocspRes.Status == want, nil
    98  }
    99  
   100  func checkCRL(ctx context.Context, cert, issuer *x509.Certificate, want int) (bool, error) {
   101  	if len(cert.CRLDistributionPoints) != 1 {
   102  		return false, errors.New("cert does not contain CRLDP URI")
   103  	}
   104  
   105  	req, err := http.NewRequestWithContext(ctx, "GET", cert.CRLDistributionPoints[0], nil)
   106  	if err != nil {
   107  		return false, fmt.Errorf("creating HTTP request: %w", err)
   108  	}
   109  
   110  	resp, err := http.DefaultClient.Do(req)
   111  	if err != nil {
   112  		return false, fmt.Errorf("downloading CRL: %w", err)
   113  	}
   114  	defer resp.Body.Close()
   115  
   116  	der, err := io.ReadAll(resp.Body)
   117  	if err != nil {
   118  		return false, fmt.Errorf("reading CRL: %w", err)
   119  	}
   120  
   121  	crl, err := x509.ParseRevocationList(der)
   122  	if err != nil {
   123  		return false, fmt.Errorf("parsing CRL: %w", err)
   124  	}
   125  
   126  	err = crl.CheckSignatureFrom(issuer)
   127  	if err != nil {
   128  		return false, fmt.Errorf("validating CRL: %w", err)
   129  	}
   130  
   131  	for _, entry := range crl.RevokedCertificateEntries {
   132  		if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
   133  			return want == ocsp.Revoked, nil
   134  		}
   135  	}
   136  	return want == ocsp.Good, nil
   137  }
   138  
   139  // Return an error if the root settings are nonempty and do not match the
   140  // expected root.
   141  func (p TLSProbe) checkRoot(rootOrg, rootCN string) error {
   142  	if (p.rootCN == "" && p.rootOrg == "") || (rootOrg == p.rootOrg && rootCN == p.rootCN) {
   143  		return nil
   144  	}
   145  	return fmt.Errorf("Expected root does not match.")
   146  }
   147  
   148  // Export expiration timestamp and reason to Prometheus.
   149  func (p TLSProbe) exportMetrics(cert *x509.Certificate, reason reason) {
   150  	if cert != nil {
   151  		p.notAfter.WithLabelValues(p.hostname).Set(float64(cert.NotAfter.Unix()))
   152  		p.notBefore.WithLabelValues(p.hostname).Set(float64(cert.NotBefore.Unix()))
   153  	}
   154  	p.reason.WithLabelValues(p.hostname, reasonToString[reason]).Inc()
   155  }
   156  
   157  func (p TLSProbe) probeExpired(timeout time.Duration) bool {
   158  	addr := p.hostname
   159  	_, _, err := net.SplitHostPort(addr)
   160  	if err != nil {
   161  		addr = net.JoinHostPort(addr, "443")
   162  	}
   163  
   164  	tlsDialer := tls.Dialer{
   165  		NetDialer: &obsdialer.Dialer,
   166  		Config: &tls.Config{
   167  			// Set InsecureSkipVerify to skip the default validation we are
   168  			// replacing. This will not disable VerifyConnection.
   169  			InsecureSkipVerify: true,
   170  			VerifyConnection: func(cs tls.ConnectionState) error {
   171  				issuers := x509.NewCertPool()
   172  				for _, cert := range cs.PeerCertificates[1:] {
   173  					issuers.AddCert(cert)
   174  				}
   175  				opts := x509.VerifyOptions{
   176  					// We set the current time to be the cert's expiration date so that
   177  					// the validation routine doesn't complain that the cert is expired.
   178  					CurrentTime: cs.PeerCertificates[0].NotAfter,
   179  					// By settings roots and intermediates to be whatever was presented
   180  					// in the handshake, we're saying that we don't care about the cert
   181  					// chaining up to the system trust store. This is safe because we
   182  					// check the root ourselves in checkRoot().
   183  					Intermediates: issuers,
   184  					Roots:         issuers,
   185  				}
   186  				_, err := cs.PeerCertificates[0].Verify(opts)
   187  				return err
   188  			},
   189  		},
   190  	}
   191  
   192  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   193  	defer cancel()
   194  
   195  	conn, err := tlsDialer.DialContext(ctx, "tcp", addr)
   196  	if err != nil {
   197  		p.exportMetrics(nil, internalError)
   198  		return false
   199  	}
   200  	defer conn.Close()
   201  
   202  	// tls.Dialer.DialContext is documented to always return *tls.Conn
   203  	peers := conn.(*tls.Conn).ConnectionState().PeerCertificates
   204  	if time.Until(peers[0].NotAfter) > 0 {
   205  		p.exportMetrics(peers[0], statusDidNotMatch)
   206  		return false
   207  	}
   208  
   209  	root := peers[len(peers)-1].Issuer
   210  	err = p.checkRoot(root.Organization[0], root.CommonName)
   211  	if err != nil {
   212  		p.exportMetrics(peers[0], rootDidNotMatch)
   213  		return false
   214  	}
   215  
   216  	p.exportMetrics(peers[0], none)
   217  	return true
   218  }
   219  
   220  func (p TLSProbe) probeUnexpired(timeout time.Duration) bool {
   221  	addr := p.hostname
   222  	_, _, err := net.SplitHostPort(addr)
   223  	if err != nil {
   224  		addr = net.JoinHostPort(addr, "443")
   225  	}
   226  
   227  	tlsDialer := tls.Dialer{
   228  		NetDialer: &obsdialer.Dialer,
   229  		Config: &tls.Config{
   230  			// Set InsecureSkipVerify to skip the default validation we are
   231  			// replacing. This will not disable VerifyConnection.
   232  			InsecureSkipVerify: true,
   233  			VerifyConnection: func(cs tls.ConnectionState) error {
   234  				issuers := x509.NewCertPool()
   235  				for _, cert := range cs.PeerCertificates[1:] {
   236  					issuers.AddCert(cert)
   237  				}
   238  				opts := x509.VerifyOptions{
   239  					// By settings roots and intermediates to be whatever was presented
   240  					// in the handshake, we're saying that we don't care about the cert
   241  					// chaining up to the system trust store. This is safe because we
   242  					// check the root ourselves in checkRoot().
   243  					Intermediates: issuers,
   244  					Roots:         issuers,
   245  				}
   246  				_, err := cs.PeerCertificates[0].Verify(opts)
   247  				return err
   248  			},
   249  		},
   250  	}
   251  
   252  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   253  	defer cancel()
   254  
   255  	conn, err := tlsDialer.DialContext(ctx, "tcp", addr)
   256  	if err != nil {
   257  		p.exportMetrics(nil, internalError)
   258  		return false
   259  	}
   260  	defer conn.Close()
   261  
   262  	// tls.Dialer.DialContext is documented to always return *tls.Conn
   263  	peers := conn.(*tls.Conn).ConnectionState().PeerCertificates
   264  	root := peers[len(peers)-1].Issuer
   265  	err = p.checkRoot(root.Organization[0], root.CommonName)
   266  	if err != nil {
   267  		p.exportMetrics(peers[0], rootDidNotMatch)
   268  		return false
   269  	}
   270  
   271  	var wantStatus int
   272  	switch p.response {
   273  	case "valid":
   274  		wantStatus = ocsp.Good
   275  	case "revoked":
   276  		wantStatus = ocsp.Revoked
   277  	}
   278  
   279  	var statusMatch bool
   280  	if len(peers[0].OCSPServer) != 0 {
   281  		statusMatch, err = checkOCSP(ctx, peers[0], peers[1], wantStatus)
   282  	} else {
   283  		statusMatch, err = checkCRL(ctx, peers[0], peers[1], wantStatus)
   284  	}
   285  	if err != nil {
   286  		p.exportMetrics(peers[0], revocationStatusError)
   287  		return false
   288  	}
   289  
   290  	if !statusMatch {
   291  		p.exportMetrics(peers[0], statusDidNotMatch)
   292  		return false
   293  	}
   294  
   295  	p.exportMetrics(peers[0], none)
   296  	return true
   297  }
   298  
   299  // Probe performs the configured TLS probe. Return true if the root has the
   300  // expected Subject (or if no root is provided for comparison in settings), and
   301  // the end entity certificate has the correct expiration status (either expired
   302  // or unexpired, depending on what is configured). Exports metrics for the
   303  // NotAfter timestamp of the end entity certificate and the reason for the Probe
   304  // returning false ("none" if returns true).
   305  func (p TLSProbe) Probe(timeout time.Duration) (bool, time.Duration) {
   306  	start := time.Now()
   307  	var success bool
   308  	if p.response == "expired" {
   309  		success = p.probeExpired(timeout)
   310  	} else {
   311  		success = p.probeUnexpired(timeout)
   312  	}
   313  
   314  	return success, time.Since(start)
   315  }