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
44
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
56 func (p TLSProbe) Name() string {
57 return p.hostname
58 }
59
60
61 func (p TLSProbe) Kind() string {
62 return "TLS"
63 }
64
65
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
92
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
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
112
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
188
189
190
191
192
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