Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics for revocation information provided via Certificate Revocation Lists #189

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ Flags:
| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | | tcp, https |
| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | | tcp, https |
| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https |
| ssl_crl_status | The status of the CRL check 0=Good 1=Revoked 2=Unknown | | tcp, https |
| ssl_crl_revoke_reason | The reason code for revocation in the CRL as specified in RFC 5280 Section 5.3.1 | | tcp, https |
| ssl_crl_revoked_at | The revocationTime value in the CRL, expressed as a Unix Epoch Time | | tcp, https |
| ssl_crl_number | The value of the X.509 v2 cRLNumber extension in the CRL | | tcp, https |
| ssl_crl_this_update | The thisUpdate value in the CRL, expressed as a Unix Epoch Time | | tcp, https |
| ssl_crl_next_update | The nextUpdate value in the CRL, expressed as a Unix Epoch Time | | tcp, https |
| ssl_probe_success | Was the probe successful? Boolean. | | all |
| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober | all |
| ssl_tls_version_info | The TLS version used. Always 1. | version | tcp, https |
Expand Down
8 changes: 8 additions & 0 deletions prober/https_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestProbeHTTPS(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -164,6 +165,7 @@ func TestProbeHTTPSNoScheme(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -207,6 +209,7 @@ func TestProbeHTTPSServerName(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -285,6 +288,7 @@ func TestProbeHTTPSClientAuth(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -422,6 +426,7 @@ func TestProbeHTTPSExpiredInsecure(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -486,6 +491,7 @@ func TestProbeHTTPSProxy(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -532,6 +538,7 @@ func TestProbeHTTPSOCSP(t *testing.T) {

checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics(resp, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -613,6 +620,7 @@ func TestProbeHTTPSVerifiedChains(t *testing.T) {

checkCertificateMetrics(serverCert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkVerifiedChainMetrics(verifiedChains, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
139 changes: 135 additions & 4 deletions prober/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"io"
"math/big"
"net/http"
"os"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -231,6 +234,90 @@ func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) erro
return nil
}

func collectCRLMetrics(verifiedChains [][]*x509.Certificate, registry *prometheus.Registry) error {
var (
crlStatus = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_status"),
Help: "The status of the CRL check 0=Good 1=Revoked 2=Unknown",
},
)
crlRevokeReason = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_revoke_reason"),
Help: "The reason code for revocation in the CRL as specified in RFC 5280 Section 5.3.1",
},
)
crlRevokedAt = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_revoked_at"),
Help: "The revocationTime value in the CRL, expressed as a Unix Epoch Time",
},
)
crlNumber = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_number"),
Help: "The value of the X.509 v2 cRLNumber extension in the CRL",
},
)
crlThisUpdate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_this_update"),
Help: "The thisUpdate value in the CRL, expressed as a Unix Epoch Time",
},
)
crlNextUpdate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_next_update"),
Help: "The nextUpdate value in the CRL, expressed as a Unix Epoch Time",
},
)
)
registry.MustRegister(
crlStatus,
crlRevokeReason,
crlRevokedAt,
crlNumber,
crlThisUpdate,
crlNextUpdate,
)

if len(verifiedChains) == 0 {
crlStatus.Set(2)
return nil
}
issuerIndex := 1
if len(verifiedChains[0]) < 2 {
issuerIndex = 0
}

cert := verifiedChains[0][0]
issuer := verifiedChains[0][issuerIndex]

crl, err := fetchCRL(cert, issuer)
if err != nil {
crlStatus.Set(2)
return err
}
if crl == nil {
crlStatus.Set(2)
return nil
}
num, _ := new(big.Float).SetInt(crl.Number).Float64()
crlNumber.Set(num)
crlThisUpdate.Set(float64(crl.ThisUpdate.Unix()))
crlNextUpdate.Set(float64(crl.NextUpdate.Unix()))
for _, revokedCert := range crl.RevokedCertificateEntries {
if revokedCert.SerialNumber.Cmp(cert.SerialNumber) == 0 {
crlStatus.Set(1)
crlRevokeReason.Set(float64(revokedCert.ReasonCode))
crlRevokedAt.Set(float64(revokedCert.RevocationTime.Unix()))
break
}
}
return nil
}

func collectFileMetrics(logger log.Logger, files []string, registry *prometheus.Registry) error {
var (
totalCerts []*x509.Certificate
Expand All @@ -252,7 +339,7 @@ func collectFileMetrics(logger log.Logger, files []string, registry *prometheus.
registry.MustRegister(fileNotAfter, fileNotBefore)

for _, f := range files {
data, err := ioutil.ReadFile(f)
data, err := os.ReadFile(f)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", f, err))
continue
Expand Down Expand Up @@ -363,7 +450,7 @@ func collectKubeconfigMetrics(logger log.Logger, kubeconfig KubeConfig, registry
return err
}
} else if c.Cluster.CertificateAuthority != "" {
data, err = ioutil.ReadFile(c.Cluster.CertificateAuthority)
data, err = os.ReadFile(c.Cluster.CertificateAuthority)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", c.Cluster.CertificateAuthority, err))
return err
Expand Down Expand Up @@ -399,7 +486,7 @@ func collectKubeconfigMetrics(logger log.Logger, kubeconfig KubeConfig, registry
return err
}
} else if u.User.ClientCertificate != "" {
data, err = ioutil.ReadFile(u.User.ClientCertificate)
data, err = os.ReadFile(u.User.ClientCertificate)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", u.User.ClientCertificate, err))
return err
Expand Down Expand Up @@ -480,3 +567,47 @@ func organizationalUnits(cert *x509.Certificate) string {

return ""
}

func fetchCRLDistributionPointFromCert(cert *x509.Certificate) string {
for _, url := range cert.CRLDistributionPoints {
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
return url
}
}
return ""
}

func fetchCRL(cert, issuer *x509.Certificate) (*x509.RevocationList, error) {
crlURL := fetchCRLDistributionPointFromCert(cert)
// the leaf certificate may not always contain a CRL distribution point, but its issuer should
if crlURL == "" {
crlURL = fetchCRLDistributionPointFromCert(issuer)
}
if crlURL == "" {
// CA/B Forum Ballot SC-063 v4 requires a CRL distribution point, but that only applies for publicly trusted CAs
return nil, nil
}

resp, err := http.Get(crlURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

crl, err := x509.ParseRevocationList(data)
if err != nil {
return nil, err
}
if err := crl.CheckSignatureFrom(issuer); err != nil {
return nil, err
}
if crl.NextUpdate.Before(time.Now()) {
return nil, fmt.Errorf("CRL has expired")
}
return crl, nil
}
64 changes: 64 additions & 0 deletions prober/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,70 @@ func checkOCSPMetrics(resp []byte, registry *prometheus.Registry, t *testing.T)
checkRegistryResults(expectedResults, mfs, t)
}

func checkCRLMetrics(crlRaw []byte, registry *prometheus.Registry, t *testing.T) {
var (
status float64
reason float64
revokedAt float64
number float64
thisUpdate float64
nextUpdate float64
)
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
if len(crlRaw) == 0 {
expectedResults := []*registryResult{
{
Name: "ssl_crl_status",
Value: 2,
},
}
checkRegistryResults(expectedResults, mfs, t)
return
}
crl, err := x509.ParseRevocationList(crlRaw)
if err != nil {
t.Fatal(err)
}
number = float64(crl.Number.Int64())
thisUpdate = float64(crl.ThisUpdate.Unix())
nextUpdate = float64(crl.NextUpdate.Unix())
if len(crl.RevokedCertificateEntries) > 0 {
status = 1
reason = float64(crl.RevokedCertificateEntries[0].ReasonCode)
revokedAt = float64(crl.RevokedCertificateEntries[0].RevocationTime.Unix())
}
expectedResults := []*registryResult{
{
Name: "ssl_crl_status",
Value: status,
},
{
Name: "ssl_crl_revoke_reason",
Value: reason,
},
{
Name: "ssl_crl_revoked_at",
Value: revokedAt,
},
{
Name: "ssl_crl_number",
Value: number,
},
{
Name: "ssl_crl_this_update",
Value: thisUpdate,
},
{
Name: "ssl_crl_next_update",
Value: nextUpdate,
},
}
checkRegistryResults(expectedResults, mfs, t)
}

func checkTLSVersionMetrics(version string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
Expand Down
Loading