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
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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, "/") {
|
||||||
|
|
|
||||||
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue