diff --git a/prober/grpc.go b/prober/grpc.go index cd43c711..a73b6d19 100644 --- a/prober/grpc.go +++ b/prober/grpc.go @@ -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"}, ) ) @@ -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)) } diff --git a/prober/http.go b/prober/http.go index 5ffc029f..864bf93b 100644 --- a/prober/http.go +++ b/prober/http.go @@ -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( @@ -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 diff --git a/prober/tcp.go b/prober/tcp.go index 98262813..b10c0b5d 100644 --- a/prober/tcp.go +++ b/prober/tcp.go @@ -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, @@ -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 { @@ -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 diff --git a/prober/tls.go b/prober/tls.go index 3da17a05..0589036e 100644 --- a/prober/tls.go +++ b/prober/tls.go @@ -17,6 +17,7 @@ import ( "crypto/sha256" "crypto/tls" "encoding/hex" + "fmt" "strings" "time" ) @@ -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: diff --git a/prober/utils_test.go b/prober/utils_test.go index cbc21210..bc85d23d 100644 --- a/prober/utils_test.go +++ b/prober/utils_test.go @@ -17,6 +17,7 @@ import ( "context" "crypto/rand" "crypto/rsa" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" @@ -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()