feat(inputs.x509_cert): Add tag for certificate type-classification (#12656)

This commit is contained in:
Sven Rebhan 2023-02-22 13:39:15 +01:00 committed by GitHub
parent 39d6b1d5cb
commit d1d9737da6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 311 additions and 24 deletions

View File

@ -54,6 +54,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
- x509_cert - x509_cert
- tags: - tags:
- type - "leaf", "intermediate" or "root" classification of certificate
- source - source of the certificate - source - source of the certificate
- organization - organization
- organizational_unit - organizational_unit

View File

@ -8,6 +8,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
_ "embed" _ "embed"
"encoding/hex"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
@ -50,6 +51,8 @@ type X509Cert struct {
tlsCfg *tls.Config tlsCfg *tls.Config
locations []*url.URL locations []*url.URL
globpaths []*globpath.GlobPath globpaths []*globpath.GlobPath
classification map[string]string
} }
func (*X509Cert) SampleConfig() string { func (*X509Cert) SampleConfig() string {
@ -99,15 +102,23 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
acc.AddError(fmt.Errorf("cannot get SSL cert '%s': %s", location, err.Error())) acc.AddError(fmt.Errorf("cannot get SSL cert '%s': %s", location, err.Error()))
} }
dnsName := c.serverName(location) // Add all returned certs to the pool of intermediates except for
for i, cert := range certs { // the leaf node which has to come first
fields := getFields(cert, now) intermediates := x509.NewCertPool()
tags := getTags(cert, location.String()) if len(certs) > 1 {
for _, c := range certs[1:] {
intermediates.AddCert(c)
}
}
dnsName := c.serverName(location)
results := make([]error, 0, len(certs))
c.classification = make(map[string]string)
for _, cert := range certs {
// The first certificate is the leaf/end-entity certificate which // The first certificate is the leaf/end-entity certificate which
// needs DNS name validation against the URL hostname. // needs DNS name validation against the URL hostname.
opts := x509.VerifyOptions{ opts := x509.VerifyOptions{
Intermediates: x509.NewCertPool(), Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
Roots: c.tlsCfg.RootCAs, Roots: c.tlsCfg.RootCAs,
DNSName: dnsName, DNSName: dnsName,
@ -115,29 +126,20 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
// Reset DNS name to only use it for the leaf node // Reset DNS name to only use it for the leaf node
dnsName = "" dnsName = ""
// Add all returned certs to the pool if intermediates except for // Do the processing
// the leaf node and ourself results = append(results, c.processCertificate(cert, opts))
for j, c := range certs[1:] { }
if i+1 != j {
opts.Intermediates.AddCert(c)
}
}
if _, err := cert.Verify(opts); err == nil { for i, cert := range certs {
fields := getFields(cert, now)
tags := getTags(cert, location.String())
// Extract the verification result
err := results[i]
if err == nil {
tags["verification"] = "valid" tags["verification"] = "valid"
fields["verification_code"] = 0 fields["verification_code"] = 0
} else { } else {
c.Log.Debugf("Invalid certificate at index %2d!", i)
c.Log.Debugf(" cert DNS names: %v", cert.DNSNames)
c.Log.Debugf(" cert IP addresses: %v", cert.IPAddresses)
c.Log.Debugf(" cert subject: %v", cert.Subject)
c.Log.Debugf(" cert issuer: %v", cert.Issuer)
c.Log.Debugf(" opts.DNSName: %v", opts.DNSName)
c.Log.Debugf(" verify options: %v", opts)
c.Log.Debugf(" verify error: %v", err)
c.Log.Debugf(" location: %v", location)
c.Log.Debugf(" tlsCfg.ServerName: %v", c.tlsCfg.ServerName)
c.Log.Debugf(" ServerName: %v", c.ServerName)
tags["verification"] = "invalid" tags["verification"] = "invalid"
fields["verification_code"] = 1 fields["verification_code"] = 1
fields["verification_error"] = err.Error() fields["verification_error"] = err.Error()
@ -192,6 +194,14 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
tags["ocsp_stapled"] = "no" tags["ocsp_stapled"] = "no"
} }
// Determine the classification
sig := hex.EncodeToString(cert.Signature)
if class, found := c.classification[sig]; found {
tags["type"] = class
} else {
tags["type"] = "leaf"
}
acc.AddFields("x509_cert", fields, tags) acc.AddFields("x509_cert", fields, tags)
if c.ExcludeRootCerts { if c.ExcludeRootCerts {
break break
@ -202,6 +212,66 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
return nil return nil
} }
func (c *X509Cert) processCertificate(certificate *x509.Certificate, opts x509.VerifyOptions) error {
chains, err := certificate.Verify(opts)
if err != nil {
c.Log.Debugf("Invalid certificate %v", certificate.SerialNumber.Text(16))
c.Log.Debugf(" cert DNS names: %v", certificate.DNSNames)
c.Log.Debugf(" cert IP addresses: %v", certificate.IPAddresses)
c.Log.Debugf(" cert subject: %v", certificate.Subject)
c.Log.Debugf(" cert issuer: %v", certificate.Issuer)
c.Log.Debugf(" opts.DNSName: %v", opts.DNSName)
c.Log.Debugf(" verify options: %v", opts)
c.Log.Debugf(" verify error: %v", err)
c.Log.Debugf(" tlsCfg.ServerName: %v", c.tlsCfg.ServerName)
c.Log.Debugf(" ServerName: %v", c.ServerName)
}
// Check if the certificate is a root-certificate.
// The only reliable way to distinguish root certificates from
// intermediates is the fact that root certificates are self-signed,
// i.e. you can verify the certificate with its own public key.
rootErr := certificate.CheckSignature(certificate.SignatureAlgorithm, certificate.RawTBSCertificate, certificate.Signature)
if rootErr == nil {
sig := hex.EncodeToString(certificate.Signature)
c.classification[sig] = "root"
}
// Identify intermediate certificates
for _, chain := range chains {
// All nodes except the first one are of intermediate or CA type.
// Mark them as such. We never add leaf nodes to the classification
// so in the end if a cert is NOT in the classification it is a true
// leaf node.
for _, cert := range chain[1:] {
// Never change a classification if we already have one
sig := hex.EncodeToString(cert.Signature)
if _, found := c.classification[sig]; found {
continue
}
// We found an intermediate certificate which is not a CA. This
// should never happen actually.
if !cert.IsCA {
c.classification[sig] = "unknown"
continue
}
// The only reliable way to distinguish root certificates from
// intermediates is the fact that root certificates are self-signed,
// i.e. you can verify the certificate with its own public key.
rootErr := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
if rootErr != nil {
c.classification[sig] = "intermediate"
} else {
c.classification[sig] = "root"
}
}
}
return err
}
func (c *X509Cert) sourcesToURLs() error { func (c *X509Cert) sourcesToURLs() error {
for _, source := range c.Sources { for _, source := range c.Sources {
if strings.HasPrefix(source, "file://") || strings.HasPrefix(source, "/") { if strings.HasPrefix(source, "file://") || strings.HasPrefix(source, "/") {

View File

@ -1,8 +1,13 @@
package x509_cert package x509_cert
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64" "encoding/base64"
"encoding/pem"
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
@ -15,11 +20,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp"
"github.com/pion/dtls/v2" "github.com/pion/dtls/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
_tls "github.com/influxdata/telegraf/plugins/common/tls" _tls "github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/testutil" "github.com/influxdata/telegraf/testutil"
) )
@ -466,3 +473,197 @@ func TestServerName(t *testing.T) {
}) })
} }
} }
// Bases on code from
// https://medium.com/@shaneutt/create-sign-x509-certificates-in-golang-8ac4ae49f903
func TestClassification(t *testing.T) {
start := time.Now()
end := time.Now().AddDate(0, 0, 1)
tmpDir, err := os.MkdirTemp("", "telegraf-x509-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create the CA certificate
caPriv, err := rsa.GenerateKey(rand.Reader, 4096)
require.NoError(t, err)
ca := &x509.Certificate{
SerialNumber: big.NewInt(342350),
Subject: pkix.Name{
Organization: []string{"Testing Inc."},
Country: []string{"US"},
CommonName: "Root CA",
},
NotBefore: start,
NotAfter: end,
IsCA: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPriv.PublicKey, caPriv)
require.NoError(t, err)
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caBytes})
// Write CA cert
f, err := os.Create(filepath.Join(tmpDir, "ca.pem"))
require.NoError(t, err)
_, err = f.Write(caPEM)
require.NoError(t, err)
require.NoError(t, f.Close())
// Create an intermediate certificate
intermediatePriv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
intermediate := &x509.Certificate{
SerialNumber: big.NewInt(342351),
Subject: pkix.Name{
Organization: []string{"Testing Inc."},
Country: []string{"US"},
CommonName: "Intermediate CA",
},
NotBefore: start,
NotAfter: end,
IsCA: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
intermediateBytes, err := x509.CreateCertificate(rand.Reader, intermediate, ca, &intermediatePriv.PublicKey, caPriv)
require.NoError(t, err)
intermediatePEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: intermediateBytes})
// Create a leaf certificate
leafPriv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
leaf := &x509.Certificate{
SerialNumber: big.NewInt(342352),
Subject: pkix.Name{
Organization: []string{"Testing Inc."},
Country: []string{"US"},
CommonName: "My server",
},
NotBefore: start,
NotAfter: end,
IsCA: false,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
leafBytes, err := x509.CreateCertificate(rand.Reader, leaf, intermediate, &leafPriv.PublicKey, intermediatePriv)
require.NoError(t, err)
leafPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafBytes})
// Write the chain
out := append(leafPEM, intermediatePEM...)
out = append(out, caPEM...)
f, err = os.Create(filepath.Join(tmpDir, "cert.pem"))
require.NoError(t, err)
_, err = f.Write(out)
require.NoError(t, err)
require.NoError(t, f.Close())
// Create the actual test
certURI := "file://" + filepath.Join(tmpDir, "cert.pem")
plugin := &X509Cert{
Sources: []string{certURI},
ClientConfig: _tls.ClientConfig{
TLSCA: filepath.Join(tmpDir, "ca.pem"),
},
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
require.Empty(t, acc.Errors)
expected := []telegraf.Metric{
metric.New(
"x509_cert",
map[string]string{
"common_name": "My server",
"country": "US",
"issuer_common_name": "Intermediate CA",
"issuer_serial_number": "",
"organization": "Testing Inc.",
"public_key_algorithm": "RSA",
"san": "127.0.0.1",
"serial_number": "53950",
"signature_algorithm": "SHA256-RSA",
"source": filepath.ToSlash(certURI),
"type": "leaf",
"verification": "valid",
},
map[string]interface{}{
"age": int64(0),
"expiry": int64(86399),
"startdate": start.Unix(),
"enddate": end.Unix(),
"verification_code": int64(0),
},
time.Unix(0, 0),
),
metric.New(
"x509_cert",
map[string]string{
"common_name": "Intermediate CA",
"country": "US",
"issuer_common_name": "Root CA",
"issuer_serial_number": "",
"organization": "Testing Inc.",
"public_key_algorithm": "RSA",
"san": "",
"serial_number": "5394f",
"signature_algorithm": "SHA256-RSA",
"source": filepath.ToSlash(certURI),
"type": "intermediate",
"verification": "valid",
},
map[string]interface{}{
"age": int64(0),
"expiry": int64(86399),
"startdate": start.Unix(),
"enddate": end.Unix(),
"verification_code": int64(0),
},
time.Unix(0, 0),
),
metric.New(
"x509_cert",
map[string]string{
"common_name": "Root CA",
"country": "US",
"issuer_common_name": "Root CA",
"issuer_serial_number": "",
"organization": "Testing Inc.",
"public_key_algorithm": "RSA",
"san": "",
"serial_number": "5394e",
"signature_algorithm": "SHA256-RSA",
"source": filepath.ToSlash(certURI),
"type": "root",
"verification": "valid",
},
map[string]interface{}{
"age": int64(0),
"expiry": int64(86399),
"startdate": start.Unix(),
"enddate": end.Unix(),
"verification_code": int64(0),
},
time.Unix(0, 0),
),
}
opts := []cmp.Option{
testutil.SortMetrics(),
testutil.IgnoreTime(),
// We need to ignore those fields as they are timing sensitive.
testutil.IgnoreFields("age", "expiry"),
}
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, opts...)
}

View File

@ -128,6 +128,21 @@ func IgnoreTime() cmp.Option {
return cmpopts.IgnoreFields(metricDiff{}, "Time") return cmpopts.IgnoreFields(metricDiff{}, "Time")
} }
// IgnoreFields disables comparison of the fields with the given names.
// The field-names are case-sensitive!
func IgnoreFields(names ...string) cmp.Option {
return cmpopts.IgnoreSliceElements(
func(f *telegraf.Field) bool {
for _, n := range names {
if f.Key == n {
return true
}
}
return false
},
)
}
// MetricEqual returns true if the metrics are equal. // MetricEqual returns true if the metrics are equal.
func MetricEqual(expected, actual telegraf.Metric, opts ...cmp.Option) bool { func MetricEqual(expected, actual telegraf.Metric, opts ...cmp.Option) bool {
var lhs, rhs *metricDiff var lhs, rhs *metricDiff