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
- tags:
- type - "leaf", "intermediate" or "root" classification of certificate
- source - source of the certificate
- organization
- organizational_unit

View File

@ -8,6 +8,7 @@ import (
"crypto/tls"
"crypto/x509"
_ "embed"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
@ -50,6 +51,8 @@ type X509Cert struct {
tlsCfg *tls.Config
locations []*url.URL
globpaths []*globpath.GlobPath
classification map[string]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()))
}
dnsName := c.serverName(location)
for i, cert := range certs {
fields := getFields(cert, now)
tags := getTags(cert, location.String())
// Add all returned certs to the pool of intermediates except for
// the leaf node which has to come first
intermediates := x509.NewCertPool()
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
// needs DNS name validation against the URL hostname.
opts := x509.VerifyOptions{
Intermediates: x509.NewCertPool(),
Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
Roots: c.tlsCfg.RootCAs,
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
dnsName = ""
// Add all returned certs to the pool if intermediates except for
// the leaf node and ourself
for j, c := range certs[1:] {
if i+1 != j {
opts.Intermediates.AddCert(c)
}
}
// Do the processing
results = append(results, c.processCertificate(cert, opts))
}
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"
fields["verification_code"] = 0
} 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"
fields["verification_code"] = 1
fields["verification_error"] = err.Error()
@ -192,6 +194,14 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
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)
if c.ExcludeRootCerts {
break
@ -202,6 +212,66 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
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 {
for _, source := range c.Sources {
if strings.HasPrefix(source, "file://") || strings.HasPrefix(source, "/") {

View File

@ -1,8 +1,13 @@
package x509_cert
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"net"
@ -15,11 +20,13 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/pion/dtls/v2"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
_tls "github.com/influxdata/telegraf/plugins/common/tls"
"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")
}
// 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.
func MetricEqual(expected, actual telegraf.Metric, opts ...cmp.Option) bool {
var lhs, rhs *metricDiff