...

Source file src/github.com/letsencrypt/boulder/observer/probers/tls/tls.go

Documentation: github.com/letsencrypt/boulder/observer/probers/tls

     1  package probers
     2  
     3  import (
     4  	"crypto/tls"
     5  	"crypto/x509"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"time"
    12  
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	"golang.org/x/crypto/ocsp"
    15  )
    16  
    17  type reason int
    18  
    19  const (
    20  	none reason = iota
    21  	internalError
    22  	ocspError
    23  	rootDidNotMatch
    24  	responseDidNotMatch
    25  )
    26  
    27  var reasonToString = map[reason]string{
    28  	none:                "nil",
    29  	internalError:       "internalError",
    30  	ocspError:           "ocspError",
    31  	rootDidNotMatch:     "rootDidNotMatch",
    32  	responseDidNotMatch: "responseDidNotMatch",
    33  }
    34  
    35  func getReasons() []string {
    36  	var allReasons []string
    37  	for _, v := range reasonToString {
    38  		allReasons = append(allReasons, v)
    39  	}
    40  	return allReasons
    41  }
    42  
    43  // TLSProbe is the exported `Prober` object for monitors configured to perform
    44  // TLS protocols.
    45  type TLSProbe struct {
    46  	hostname  string
    47  	rootOrg   string
    48  	rootCN    string
    49  	response  string
    50  	notAfter  *prometheus.GaugeVec
    51  	notBefore *prometheus.GaugeVec
    52  	reason    *prometheus.CounterVec
    53  }
    54  
    55  // Name returns a string that uniquely identifies the monitor.
    56  func (p TLSProbe) Name() string {
    57  	return p.hostname
    58  }
    59  
    60  // Kind returns a name that uniquely identifies the `Kind` of `Prober`.
    61  func (p TLSProbe) Kind() string {
    62  	return "TLS"
    63  }
    64  
    65  // Get OCSP status (good, revoked or unknown) of certificate
    66  func checkOCSP(cert, issuer *x509.Certificate, want int) (bool, error) {
    67  	req, err := ocsp.CreateRequest(cert, issuer, nil)
    68  	if err != nil {
    69  		return false, err
    70  	}
    71  
    72  	url := fmt.Sprintf("%s/%s", cert.OCSPServer[0], base64.StdEncoding.EncodeToString(req))
    73  	res, err := http.Get(url)
    74  	if err != nil {
    75  		return false, err
    76  	}
    77  
    78  	output, err := io.ReadAll(res.Body)
    79  	if err != nil {
    80  		return false, err
    81  	}
    82  
    83  	ocspRes, err := ocsp.ParseResponseForCert(output, cert, issuer)
    84  	if err != nil {
    85  		return false, err
    86  	}
    87  
    88  	return ocspRes.Status == want, nil
    89  }
    90  
    91  // Return an error if the root settings are nonempty and do not match the
    92  // expected root.
    93  func (p TLSProbe) checkRoot(rootOrg, rootCN string) error {
    94  	if (p.rootCN == "" && p.rootOrg == "") || (rootOrg == p.rootOrg && rootCN == p.rootCN) {
    95  		return nil
    96  	}
    97  	return fmt.Errorf("Expected root does not match.")
    98  }
    99  
   100  // Export expiration timestamp and reason to Prometheus.
   101  func (p TLSProbe) exportMetrics(cert *x509.Certificate, reason reason) {
   102  	if cert != nil {
   103  		p.notAfter.WithLabelValues(p.hostname).Set(float64(cert.NotAfter.Unix()))
   104  		p.notBefore.WithLabelValues(p.hostname).Set(float64(cert.NotBefore.Unix()))
   105  	}
   106  	p.reason.WithLabelValues(p.hostname, reasonToString[reason]).Inc()
   107  }
   108  
   109  func (p TLSProbe) probeExpired(timeout time.Duration) bool {
   110  	config := &tls.Config{
   111  		// Set InsecureSkipVerify to skip the default validation we are
   112  		// replacing. This will not disable VerifyConnection.
   113  		InsecureSkipVerify: true,
   114  		VerifyConnection: func(cs tls.ConnectionState) error {
   115  			opts := x509.VerifyOptions{
   116  				CurrentTime:   cs.PeerCertificates[0].NotAfter,
   117  				Intermediates: x509.NewCertPool(),
   118  			}
   119  			for _, cert := range cs.PeerCertificates[1:] {
   120  				opts.Intermediates.AddCert(cert)
   121  			}
   122  			_, err := cs.PeerCertificates[0].Verify(opts)
   123  			return err
   124  		},
   125  	}
   126  	conn, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", p.hostname+":443", config)
   127  	if err != nil {
   128  		p.exportMetrics(nil, internalError)
   129  		return false
   130  	}
   131  
   132  	defer conn.Close()
   133  	peers := conn.ConnectionState().PeerCertificates
   134  	if time.Until(peers[0].NotAfter) > 0 {
   135  		p.exportMetrics(peers[0], responseDidNotMatch)
   136  		return false
   137  	}
   138  
   139  	root := peers[len(peers)-1].Issuer
   140  	err = p.checkRoot(root.Organization[0], root.CommonName)
   141  	if err != nil {
   142  		p.exportMetrics(peers[0], rootDidNotMatch)
   143  		return false
   144  	}
   145  
   146  	p.exportMetrics(peers[0], none)
   147  	return true
   148  }
   149  
   150  func (p TLSProbe) probeUnexpired(timeout time.Duration) bool {
   151  	conn, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", p.hostname+":443", &tls.Config{})
   152  	if err != nil {
   153  		p.exportMetrics(nil, internalError)
   154  		return false
   155  	}
   156  
   157  	defer conn.Close()
   158  	peers := conn.ConnectionState().PeerCertificates
   159  	root := peers[len(peers)-1].Issuer
   160  	err = p.checkRoot(root.Organization[0], root.CommonName)
   161  	if err != nil {
   162  		p.exportMetrics(peers[0], rootDidNotMatch)
   163  		return false
   164  	}
   165  
   166  	var ocspStatus bool
   167  	switch p.response {
   168  	case "valid":
   169  		ocspStatus, err = checkOCSP(peers[0], peers[1], ocsp.Good)
   170  	case "revoked":
   171  		ocspStatus, err = checkOCSP(peers[0], peers[1], ocsp.Revoked)
   172  	}
   173  	if err != nil {
   174  		p.exportMetrics(peers[0], ocspError)
   175  		return false
   176  	}
   177  
   178  	if !ocspStatus {
   179  		p.exportMetrics(peers[0], responseDidNotMatch)
   180  		return false
   181  	}
   182  
   183  	p.exportMetrics(peers[0], none)
   184  	return true
   185  }
   186  
   187  // Probe performs the configured TLS probe. Return true if the root has the
   188  // expected Subject (or if no root is provided for comparison in settings), and
   189  // the end entity certificate has the correct expiration status (either expired
   190  // or unexpired, depending on what is configured). Exports metrics for the
   191  // NotAfter timestamp of the end entity certificate and the reason for the Probe
   192  // returning false ("none" if returns true).
   193  func (p TLSProbe) Probe(timeout time.Duration) (bool, time.Duration) {
   194  	start := time.Now()
   195  	var success bool
   196  	if p.response == "expired" {
   197  		success = p.probeExpired(timeout)
   198  	} else {
   199  		success = p.probeUnexpired(timeout)
   200  	}
   201  
   202  	return success, time.Since(start)
   203  }
   204  

View as plain text