Skip to content

Commit

Permalink
Reports Certificate Serial number (#1333)
Browse files Browse the repository at this point in the history
Adds `serialnumber` label to `probe_ssl_last_chain_info` metric

Output looks like

Test: `curl -s http://localhost:9115/probe\?target\=https://example.com\&module\=http_2xx`

```
probe_ssl_last_chain_info{fingerprint_sha256="efba26d8c1ce3779ac77630a90f82163a3d6892ed6afee408672cf19eba7a362",issuer="CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US",serialnumber="075bcef30689c8addf13e51af4afe187",subject="CN=www.example.org,O=Internet Corporation for Assigned Names and Numbers,L=Los Angeles,ST=California,C=US",subjectalternative="www.example.org,example.net,example.edu,example.com,example.org,www.example.com,www.example.edu,www.example.net"} 1
```

Relates to #1103
---------

Signed-off-by: Rhys Evans <[email protected]>
Co-authored-by: Rhys Evans <[email protected]>
  • Loading branch information
rhysxevans and rhys-evans authored Jan 19, 2025
1 parent 35ac89c commit 5a0541c
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 7 deletions.
4 changes: 2 additions & 2 deletions prober/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func ProbeGRPC(ctx context.Context, target string, module config.Module, registr
Name: "probe_ssl_last_chain_info",
Help: "Contains SSL leaf certificate information",
},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative"},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative", "serialnumber"},
)
)

Expand Down Expand Up @@ -204,7 +204,7 @@ func ProbeGRPC(ctx context.Context, target string, module config.Module, registr
isSSLGauge.Set(float64(1))
probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(&tlsInfo.State).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&tlsInfo.State)).Set(1)
probeSSLLastInformation.WithLabelValues(getFingerprint(&tlsInfo.State), getSubject(&tlsInfo.State), getIssuer(&tlsInfo.State), getDNSNames(&tlsInfo.State)).Set(1)
probeSSLLastInformation.WithLabelValues(getFingerprint(&tlsInfo.State), getSubject(&tlsInfo.State), getIssuer(&tlsInfo.State), getDNSNames(&tlsInfo.State), getSerialNumber(&tlsInfo.State)).Set(1)
} else {
isSSLGauge.Set(float64(0))
}
Expand Down
4 changes: 2 additions & 2 deletions prober/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
Name: "probe_ssl_last_chain_info",
Help: "Contains SSL leaf certificate information",
},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative"},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative", "serialnumber"},
)

probeTLSVersion = prometheus.NewGaugeVec(
Expand Down Expand Up @@ -647,7 +647,7 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
probeTLSVersion.WithLabelValues(getTLSVersion(resp.TLS)).Set(1)
probeTLSCipher.WithLabelValues(getTLSCipher(resp.TLS)).Set(1)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(resp.TLS).Unix()))
probeSSLLastInformation.WithLabelValues(getFingerprint(resp.TLS), getSubject(resp.TLS), getIssuer(resp.TLS), getDNSNames(resp.TLS)).Set(1)
probeSSLLastInformation.WithLabelValues(getFingerprint(resp.TLS), getSubject(resp.TLS), getIssuer(resp.TLS), getDNSNames(resp.TLS), getSerialNumber(resp.TLS)).Set(1)
if httpConfig.FailIfSSL {
logger.Error("Final request was over SSL")
success = false
Expand Down
6 changes: 3 additions & 3 deletions prober/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
Name: "probe_ssl_last_chain_info",
Help: "Contains SSL leaf certificate information",
},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative"},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative", "serialnumber"},
)
probeTLSVersion := prometheus.NewGaugeVec(
probeTLSInfoGaugeOpts,
Expand Down Expand Up @@ -147,7 +147,7 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state)).Set(1)
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state), getSerialNumber(&state)).Set(1)
}
scanner := bufio.NewScanner(conn)
for i, qr := range module.TCP.QueryResponse {
Expand Down Expand Up @@ -216,7 +216,7 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state)).Set(1)
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state), getSerialNumber(&state)).Set(1)
}
}
return true
Expand Down
9 changes: 9 additions & 0 deletions prober/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
"time"
)
Expand Down Expand Up @@ -69,6 +70,14 @@ func getLastChainExpiry(state *tls.ConnectionState) time.Time {
return lastChainExpiry
}

func getSerialNumber(state *tls.ConnectionState) string {
cert := state.PeerCertificates[0]
// Using `cert.SerialNumber.Text(16)` will drop the leading zeros when converting the SerialNumber to String, see https://github.com/mozilla/tls-observatory/pull/245.
// To avoid that, we format in lowercase the bytes with `%x` to base 16, with lower-case letters for a-f, see https://go.dev/play/p/Fylce70N2Zl.

return fmt.Sprintf("%x", cert.SerialNumber.Bytes())
}

func getTLSVersion(state *tls.ConnectionState) string {
switch state.Version {
case tls.VersionTLS10:
Expand Down
41 changes: 41 additions & 0 deletions prober/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
Expand Down Expand Up @@ -251,6 +252,46 @@ func checkMetrics(expected map[string]map[string]map[string]struct{}, mfs []*dto
}
}

func TestGetSerialNumber(t *testing.T) {
tests := []struct {
name string
serialNumber *big.Int
expected string
}{
{
name: "Serial number with leading zeros",
serialNumber: func() *big.Int {
serialNumber, _ := new(big.Int).SetString("0BFFBC11F1907D02AF719AFCD64FB253", 16)
return serialNumber
}(),
expected: "0bffbc11f1907d02af719afcd64fb253",
},
{
name: "Serial number without leading zeros",
serialNumber: func() *big.Int {
serialNumber, _ := new(big.Int).SetString("BBFFBC11F1907D02AF719AFCD64FB253", 16)
return serialNumber
}(),
expected: "bbffbc11f1907d02af719afcd64fb253",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cert := &x509.Certificate{
SerialNumber: tt.serialNumber,
}
state := &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
result := getSerialNumber(state)
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}

func checkAbsentMetrics(absent []string, mfs []*dto.MetricFamily, t *testing.T) {
for _, v := range mfs {
name := v.GetName()
Expand Down

0 comments on commit 5a0541c

Please sign in to comment.