feat(inputs.x509_cert): Add tag for certificate type-classification (#12656)
This commit is contained in:
parent
39d6b1d5cb
commit
d1d9737da6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, "/") {
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue