1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,102 @@
# x509 Certificate Input Plugin
This plugin provides information about X509 certificate accessible via local
file, tcp, udp, https or smtp protocol.
When using a UDP address as a certificate source, the server must support
[DTLS](https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security).
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Reads metrics from a SSL certificate
[[inputs.x509_cert]]
## List certificate sources, support wildcard expands for files
## Prefix your entry with 'file://' if you intend to use relative paths
sources = ["tcp://example.org:443", "https://influxdata.com:443",
"smtp://mail.localhost:25", "udp://127.0.0.1:4433",
"/etc/ssl/certs/ssl-cert-snakeoil.pem",
"/etc/mycerts/*.mydomain.org.pem", "file:///path/to/*.pem",
"jks:///etc/mycerts/keystore.jks",
"pkcs12:///etc/mycerts/keystore.p12"]
## Timeout for SSL connection
# timeout = "5s"
## Pass a different name into the TLS request (Server Name Indication).
## This is synonymous with tls_server_name, and only one of the two
## options may be specified at one time.
## example: server_name = "myhost.example.org"
# server_name = "myhost.example.org"
## Only output the leaf certificates and omit the root ones.
# exclude_root_certs = false
## Pad certificate serial number with zeroes to 128-bits.
# pad_serial_with_zeroes = false
## Password to be used with PKCS#12 or JKS files
# password = ""
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
# tls_server_name = "myhost.example.org"
## Set the proxy URL
# use_proxy = true
# proxy_url = "http://localhost:8888"
```
## Metrics
- x509_cert
- tags:
- type - "leaf", "intermediate" or "root" classification of certificate
- source - source of the certificate
- organization
- organizational_unit
- country
- province
- locality
- verification
- serial_number
- signature_algorithm
- public_key_algorithm
- issuer_common_name
- issuer_serial_number
- san
- ocsp_stapled
- ocsp_status (when ocsp_stapled=yes)
- ocsp_verified (when ocsp_stapled=yes)
- fields:
- verification_code (int)
- verification_error (string)
- expiry (int, seconds) - Time when the certificate will expire, in seconds
since the Unix epoch. `SELECT (expiry / 60 / 60 / 24) as "expiry_in_days"`
- age (int, seconds)
- startdate (int, seconds)
- enddate (int, seconds)
- ocsp_status_code (int)
- ocsp_next_update (int, seconds)
- ocsp_produced_at (int, seconds)
- ocsp_this_update (int, seconds)
## Example Output
```text
x509_cert,common_name=ubuntu,ocsp_stapled=no,source=/etc/ssl/certs/ssl-cert-snakeoil.pem,verification=valid age=7693222i,enddate=1871249033i,expiry=307666777i,startdate=1555889033i,verification_code=0i 1563582256000000000
x509_cert,common_name=www.example.org,country=US,locality=Los\ Angeles,organization=Internet\ Corporation\ for\ Assigned\ Names\ and\ Numbers,organizational_unit=Technology,province=California,ocsp_stapled=no,source=https://example.org:443,verification=invalid age=20219055i,enddate=1606910400i,expiry=43328144i,startdate=1543363200i,verification_code=1i,verification_error="x509: certificate signed by unknown authority" 1563582256000000000
x509_cert,common_name=DigiCert\ SHA2\ Secure\ Server\ CA,country=US,organization=DigiCert\ Inc,ocsp_stapled=no,source=https://example.org:443,verification=valid age=200838255i,enddate=1678276800i,expiry=114694544i,startdate=1362744000i,verification_code=0i 1563582256000000000
x509_cert,common_name=DigiCert\ Global\ Root\ CA,country=US,organization=DigiCert\ Inc,organizational_unit=www.digicert.com,ocsp_stapled=yes,ocsp_status=good,ocsp_verified=yes,source=https://example.org:443,verification=valid age=400465455i,enddate=1952035200i,expiry=388452944i,ocsp_next_update=1676714398i,ocsp_produced_at=1676112480i,ocsp_status_code=0i,ocsp_this_update=1676109600i,startdate=1163116800i,verification_code=0i 1563582256000000000
```

View file

@ -0,0 +1,101 @@
package x509_cert
import (
"crypto/x509"
"fmt"
"os"
"path/filepath"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"software.sslmate.com/src/go-pkcs12"
)
func normalizePath(path string) string {
normalized := filepath.ToSlash(path)
// Removing leading slash in Windows path containing a drive-letter
// like "file:///C:/Windows/..."
normalized = reDriveLetter.ReplaceAllString(normalized, "$1")
return filepath.FromSlash(normalized)
}
func (c *X509Cert) processPKCS12(path string) ([]*x509.Certificate, error) {
data, err := os.ReadFile(normalizePath(path))
if err != nil {
return nil, fmt.Errorf("failed to read PKCS#12 file: %w", err)
}
// Get the password string from config.Secret
password, err := c.Password.Get()
if err != nil {
return nil, fmt.Errorf("failed to get password: %w", err)
}
passwordStr := password.String()
password.Destroy()
_, cert, caCerts, err := pkcs12.DecodeChain(data, passwordStr)
if err != nil {
return nil, fmt.Errorf("failed to decode PKCS#12 keystore: %w", err)
}
// Ensure Root CA pool exists
if c.tlsCfg.RootCAs == nil {
c.tlsCfg.RootCAs = x509.NewCertPool()
}
// Add CA certificates to RootCAs
for _, caCert := range caCerts {
c.tlsCfg.RootCAs.AddCert(caCert)
}
return append([]*x509.Certificate{cert}, caCerts...), nil
}
func (c *X509Cert) processJKS(path string) ([]*x509.Certificate, error) {
file, err := os.Open(normalizePath(path))
if err != nil {
return nil, fmt.Errorf("failed to open JKS file: %w", err)
}
defer file.Close()
// Get the password string from config.Secret
password, err := c.Password.Get()
if err != nil {
return nil, fmt.Errorf("failed to get password: %w", err)
}
defer password.Destroy()
ks := keystore.New()
if err := ks.Load(file, password.Bytes()); err != nil {
return nil, fmt.Errorf("failed to decode JKS: %w", err)
}
// Ensure Root CA pool exists
if c.tlsCfg.RootCAs == nil {
c.tlsCfg.RootCAs = x509.NewCertPool()
}
certs := make([]*x509.Certificate, 0, len(ks.Aliases()))
for _, alias := range ks.Aliases() {
// Check for both trusted certificates and private key entries
if entry, err := ks.GetTrustedCertificateEntry(alias); err == nil {
cert, err := x509.ParseCertificate(entry.Certificate.Content)
if err == nil {
c.tlsCfg.RootCAs.AddCert(cert)
certs = append(certs, cert)
}
} else if entry, err := ks.GetPrivateKeyEntry(alias, password.Bytes()); err == nil {
for _, certData := range entry.CertificateChain {
cert, err := x509.ParseCertificate(certData.Content)
if err == nil {
c.tlsCfg.RootCAs.AddCert(cert)
certs = append(certs, cert)
}
}
}
}
return certs, nil
}

View file

@ -0,0 +1,201 @@
package x509_cert
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"github.com/stretchr/testify/require"
"software.sslmate.com/src/go-pkcs12"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/testutil"
)
type selfSignedCert struct {
certPEM []byte
keyPEM []byte
certDER []byte
}
// generateTestKeystores creates temporary JKS & PKCS#12 keystores for testing
func generateTestKeystores(t *testing.T) (pkcs12Path, jksPath string) {
t.Helper()
// Generate a test certificate
selfSigned := generateselfSignedCert(t)
pkcs12Path = createTestPKCS12(t, selfSigned.certPEM, selfSigned.keyPEM)
jksPath = createTestJKS(t, selfSigned.certDER)
return pkcs12Path, jksPath
}
// generateselfSignedCert generates a dummy self-signed certificate
func generateselfSignedCert(t *testing.T) selfSignedCert {
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test Certificate",
Organization: []string{"Test Org"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
require.NoError(t, err)
return selfSignedCert{
certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
keyPEM: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privKey)}),
certDER: certDER,
}
}
// createTestPKCS12 creates a temporary PKCS#12 keystore
func createTestPKCS12(t *testing.T, certPEM, keyPEM []byte) string {
t.Helper()
// Decode certificate
block, _ := pem.Decode(certPEM)
require.NotNil(t, block, "failed to parse certificate PEM")
cert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
// Decode private key
block, _ = pem.Decode(keyPEM)
require.NotNil(t, block, "failed to parse private key PEM")
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
require.NoError(t, err)
// Encode PKCS#12 keystore
pfxData, err := pkcs12.Modern.Encode(privKey, cert, nil, "test-password")
require.NoError(t, err)
// Use `t.TempDir()` to ensure cleanup
tempDir := t.TempDir()
pkcs12Path := filepath.Join(tempDir, "test-keystore.p12")
err = os.WriteFile(pkcs12Path, pfxData, 0600)
require.NoError(t, err)
pkcs12Path = filepath.ToSlash(pkcs12Path)
if !strings.HasPrefix(pkcs12Path, "/") {
pkcs12Path = "/" + pkcs12Path
}
return "pkcs12://" + pkcs12Path
}
// createTestJKS creates a temporary JKS keystore
func createTestJKS(t *testing.T, certDER []byte) string {
t.Helper()
// Use `t.TempDir()` to ensure cleanup
tempDir := t.TempDir()
jksPath := filepath.Join(tempDir, "test-keystore.jks")
// Create JKS keystore and add a trusted certificate
jks := keystore.New()
err := jks.SetTrustedCertificateEntry("test-alias", keystore.TrustedCertificateEntry{
Certificate: keystore.Certificate{
Type: "X.509",
Content: certDER,
},
})
require.NoError(t, err)
// Write keystore to file
output, err := os.Create(jksPath)
require.NoError(t, err)
defer output.Close()
require.NoError(t, jks.Store(output, []byte("test-password")))
jksPath = filepath.ToSlash(jksPath)
if !strings.HasPrefix(jksPath, "/") {
jksPath = "/" + jksPath
}
return "jks://" + jksPath
}
func TestGatherKeystores(t *testing.T) {
pkcs12Path, jksPath := generateTestKeystores(t)
tests := []struct {
name string
content string
password string
}{
{name: "valid PKCS12 keystore", content: pkcs12Path, password: "test-password"},
{name: "valid JKS keystore", content: jksPath, password: "test-password"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
plugin := X509Cert{
Sources: []string{test.content},
Password: config.NewSecret([]byte(test.password)),
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
})
}
}
func TestGatherKeystoresFail(t *testing.T) {
pkcs12Path, jksPath := generateTestKeystores(t)
tests := []struct {
name string
content string
password string
expected string
}{
{name: "missing password PKCS12", content: pkcs12Path, expected: "decryption password incorrect"},
{name: "missing password JKS", content: jksPath, expected: "got invalid digest"},
{name: "wrong password PKCS12", content: pkcs12Path, password: "wrong-password", expected: "decryption password incorrect"},
{name: "wrong password JKS", content: jksPath, password: "wrong-password", expected: "got invalid digest"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
plugin := X509Cert{
Sources: []string{test.content},
Log: testutil.Logger{},
}
if test.password != "" {
plugin.Password = config.NewSecret([]byte(test.password))
} else {
plugin.Password = config.NewSecret(nil)
}
require.NoError(t, plugin.Init())
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
require.NotEmpty(t, acc.Errors)
require.ErrorContains(t, acc.Errors[0], test.expected)
})
}
}

View file

@ -0,0 +1,38 @@
# Reads metrics from a SSL certificate
[[inputs.x509_cert]]
## List certificate sources, support wildcard expands for files
## Prefix your entry with 'file://' if you intend to use relative paths
sources = ["tcp://example.org:443", "https://influxdata.com:443",
"smtp://mail.localhost:25", "udp://127.0.0.1:4433",
"/etc/ssl/certs/ssl-cert-snakeoil.pem",
"/etc/mycerts/*.mydomain.org.pem", "file:///path/to/*.pem",
"jks:///etc/mycerts/keystore.jks",
"pkcs12:///etc/mycerts/keystore.p12"]
## Timeout for SSL connection
# timeout = "5s"
## Pass a different name into the TLS request (Server Name Indication).
## This is synonymous with tls_server_name, and only one of the two
## options may be specified at one time.
## example: server_name = "myhost.example.org"
# server_name = "myhost.example.org"
## Only output the leaf certificates and omit the root ones.
# exclude_root_certs = false
## Pad certificate serial number with zeroes to 128-bits.
# pad_serial_with_zeroes = false
## Password to be used with PKCS#12 or JKS files
# password = ""
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
# tls_server_name = "myhost.example.org"
## Set the proxy URL
# use_proxy = true
# proxy_url = "http://localhost:8888"

View file

@ -0,0 +1,546 @@
// Package x509_cert reports metrics from an SSL certificate.
//
//go:generate ../../../tools/readme_config_includer/generator
package x509_cert
import (
"bytes"
"crypto/tls"
"crypto/x509"
_ "embed"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"net"
"net/smtp"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/pion/dtls/v2"
"golang.org/x/crypto/ocsp"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/globpath"
"github.com/influxdata/telegraf/plugins/common/proxy"
common_tls "github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
// Regexp for handling file URIs containing a drive letter and leading slash
var reDriveLetter = regexp.MustCompile(`^/([a-zA-Z]:/)`)
type X509Cert struct {
Sources []string `toml:"sources"`
Timeout config.Duration `toml:"timeout"`
ServerName string `toml:"server_name"`
Password config.Secret `toml:"password"`
ExcludeRootCerts bool `toml:"exclude_root_certs"`
PadSerial bool `toml:"pad_serial_with_zeroes"`
Log telegraf.Logger `toml:"-"`
common_tls.ClientConfig
proxy.TCPProxy
tlsCfg *tls.Config
locations []*url.URL
globpaths []*globpath.GlobPath
classification map[string]string
}
func (*X509Cert) SampleConfig() string {
return sampleConfig
}
func (c *X509Cert) Init() error {
// Check if we do have at least one source
if len(c.Sources) == 0 {
return errors.New("no source configured")
}
// Check the server name and transfer it if necessary
if c.ClientConfig.ServerName != "" && c.ServerName != "" {
return fmt.Errorf("both server_name (%q) and tls_server_name (%q) are set, but they are mutually exclusive", c.ServerName, c.ClientConfig.ServerName)
} else if c.ServerName != "" {
// Store the user-provided server-name in the TLS configuration
c.ClientConfig.ServerName = c.ServerName
}
// Normalize the sources, handle files and file-globbing
if err := c.sourcesToURLs(); err != nil {
return err
}
// Create the TLS configuration
tlsCfg, err := c.ClientConfig.TLSConfig()
if err != nil {
return err
}
if tlsCfg == nil {
tlsCfg = &tls.Config{}
}
c.tlsCfg = tlsCfg
return nil
}
func (c *X509Cert) Gather(acc telegraf.Accumulator) error {
now := time.Now()
collectedUrls := append(c.locations, c.collectCertURLs()...)
for _, location := range collectedUrls {
certs, ocspresp, err := c.getCert(location, time.Duration(c.Timeout))
if err != nil {
acc.AddError(fmt.Errorf("cannot get SSL cert %q: %w", location, err))
}
// 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: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
Roots: c.tlsCfg.RootCAs,
DNSName: dnsName,
}
// Reset DNS name to only use it for the leaf node
dnsName = ""
// Do the processing
results = append(results, c.processCertificate(cert, opts))
}
for i, cert := range certs {
fields := getFields(cert, now)
tags := c.getTags(cert, location.String())
// Extract the verification result
err := results[i]
if err == nil {
tags["verification"] = "valid"
fields["verification_code"] = 0
} else {
tags["verification"] = "invalid"
fields["verification_code"] = 1
fields["verification_error"] = err.Error()
}
// OCSPResponse only for leaf cert
if i == 0 && ocspresp != nil && len(*ocspresp) > 0 {
var ocspissuer *x509.Certificate
for _, chaincert := range certs[1:] {
if cert.Issuer.CommonName == chaincert.Subject.CommonName &&
cert.Issuer.SerialNumber == chaincert.Subject.SerialNumber {
ocspissuer = chaincert
break
}
}
resp, err := ocsp.ParseResponse(*ocspresp, ocspissuer)
if err != nil {
if ocspissuer == nil {
tags["ocsp_stapled"] = "no"
fields["ocsp_error"] = err.Error()
} else {
ocspissuer = nil // retry parsing w/out issuer cert
resp, err = ocsp.ParseResponse(*ocspresp, ocspissuer)
}
}
if err != nil {
tags["ocsp_stapled"] = "no"
fields["ocsp_error"] = err.Error()
} else {
tags["ocsp_stapled"] = "yes"
if ocspissuer != nil {
tags["ocsp_verified"] = "yes"
} else {
tags["ocsp_verified"] = "no"
}
// resp.Status: 0=Good 1=Revoked 2=Unknown
fields["ocsp_status_code"] = resp.Status
switch resp.Status {
case 0:
tags["ocsp_status"] = "good"
case 1:
tags["ocsp_status"] = "revoked"
// Status=Good: revoked_at always = -62135596800
fields["ocsp_revoked_at"] = resp.RevokedAt.Unix()
default:
tags["ocsp_status"] = "unknown"
}
fields["ocsp_produced_at"] = resp.ProducedAt.Unix()
fields["ocsp_this_update"] = resp.ThisUpdate.Unix()
fields["ocsp_next_update"] = resp.NextUpdate.Unix()
}
} else {
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
}
}
}
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", c.getSerialNumberString(certificate))
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, "/") {
source = filepath.ToSlash(strings.TrimPrefix(source, "file://"))
// Removing leading slash in Windows path containing a drive-letter
// like "file:///C:/Windows/..."
source = reDriveLetter.ReplaceAllString(source, "$1")
g, err := globpath.Compile(source)
if err != nil {
return fmt.Errorf("could not compile glob %q: %w", source, err)
}
c.globpaths = append(c.globpaths, g)
} else {
if strings.Index(source, ":\\") == 1 {
source = "file://" + filepath.ToSlash(source)
}
u, err := url.Parse(source)
if err != nil {
return fmt.Errorf("failed to parse cert location: %w", err)
}
c.locations = append(c.locations, u)
}
}
return nil
}
func (c *X509Cert) serverName(u *url.URL) string {
if c.tlsCfg.ServerName != "" {
return c.tlsCfg.ServerName
}
return u.Hostname()
}
func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certificate, *[]byte, error) {
protocol := u.Scheme
switch u.Scheme {
case "udp", "udp4", "udp6":
ipConn, err := net.DialTimeout(u.Scheme, u.Host, timeout)
if err != nil {
return nil, nil, err
}
defer ipConn.Close()
dtlsCfg := &dtls.Config{
InsecureSkipVerify: true,
Certificates: c.tlsCfg.Certificates,
RootCAs: c.tlsCfg.RootCAs,
ServerName: c.serverName(u),
}
conn, err := dtls.Client(ipConn, dtlsCfg)
if err != nil {
return nil, nil, err
}
defer conn.Close()
rawCerts := conn.ConnectionState().PeerCertificates
var certs []*x509.Certificate
for _, rawCert := range rawCerts {
parsed, err := x509.ParseCertificate(rawCert)
if err != nil {
return nil, nil, err
}
if parsed != nil {
certs = append(certs, parsed)
}
}
return certs, nil, nil
case "https":
protocol = "tcp"
if u.Port() == "" {
u.Host += ":443"
}
fallthrough
case "tcp", "tcp4", "tcp6":
dialer, err := c.Proxy()
if err != nil {
return nil, nil, err
}
ipConn, err := dialer.DialTimeout(protocol, u.Host, timeout)
if err != nil {
return nil, nil, err
}
defer ipConn.Close()
downloadTLSCfg := c.tlsCfg.Clone()
downloadTLSCfg.ServerName = c.serverName(u)
downloadTLSCfg.InsecureSkipVerify = true
conn := tls.Client(ipConn, downloadTLSCfg)
defer conn.Close()
hsErr := conn.Handshake()
if hsErr != nil {
return nil, nil, hsErr
}
certs := conn.ConnectionState().PeerCertificates
ocspresp := conn.ConnectionState().OCSPResponse
return certs, &ocspresp, nil
case "file":
content, err := os.ReadFile(u.Path)
if err != nil {
return nil, nil, err
}
var certs []*x509.Certificate
for {
block, rest := pem.Decode(bytes.TrimSpace(content))
if block == nil {
return nil, nil, errors.New("failed to parse certificate PEM")
}
if block.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
certs = append(certs, cert)
}
if len(rest) == 0 {
break
}
content = rest
}
return certs, nil, nil
case "smtp":
ipConn, err := net.DialTimeout("tcp", u.Host, timeout)
if err != nil {
return nil, nil, err
}
defer ipConn.Close()
downloadTLSCfg := c.tlsCfg.Clone()
downloadTLSCfg.ServerName = c.serverName(u)
downloadTLSCfg.InsecureSkipVerify = true
smtpConn, err := smtp.NewClient(ipConn, u.Host)
if err != nil {
return nil, nil, err
}
err = smtpConn.Hello(downloadTLSCfg.ServerName)
if err != nil {
return nil, nil, err
}
id, err := smtpConn.Text.Cmd("STARTTLS")
if err != nil {
return nil, nil, err
}
smtpConn.Text.StartResponse(id)
defer smtpConn.Text.EndResponse(id)
_, _, err = smtpConn.Text.ReadResponse(220)
if err != nil {
return nil, nil, fmt.Errorf("did not get 220 after STARTTLS: %w", err)
}
tlsConn := tls.Client(ipConn, downloadTLSCfg)
defer tlsConn.Close()
hsErr := tlsConn.Handshake()
if hsErr != nil {
return nil, nil, hsErr
}
certs := tlsConn.ConnectionState().PeerCertificates
ocspresp := tlsConn.ConnectionState().OCSPResponse
return certs, &ocspresp, nil
case "jks":
certs, err := c.processJKS(u.Path)
return certs, nil, err
case "pkcs12":
certs, err := c.processPKCS12(u.Path)
return certs, nil, err
default:
return nil, nil, fmt.Errorf("unsupported scheme %q in location %s", u.Scheme, u.String())
}
}
func getFields(cert *x509.Certificate, now time.Time) map[string]interface{} {
age := int(now.Sub(cert.NotBefore).Seconds())
expiry := int(cert.NotAfter.Sub(now).Seconds())
startdate := cert.NotBefore.Unix()
enddate := cert.NotAfter.Unix()
fields := map[string]interface{}{
"age": age,
"expiry": expiry,
"startdate": startdate,
"enddate": enddate,
}
return fields
}
func (c *X509Cert) getTags(cert *x509.Certificate, location string) map[string]string {
tags := map[string]string{
"source": location,
"common_name": cert.Subject.CommonName,
"serial_number": c.getSerialNumberString(cert),
"signature_algorithm": cert.SignatureAlgorithm.String(),
"public_key_algorithm": cert.PublicKeyAlgorithm.String(),
}
if len(cert.Subject.Organization) > 0 {
tags["organization"] = cert.Subject.Organization[0]
}
if len(cert.Subject.OrganizationalUnit) > 0 {
tags["organizational_unit"] = cert.Subject.OrganizationalUnit[0]
}
if len(cert.Subject.Country) > 0 {
tags["country"] = cert.Subject.Country[0]
}
if len(cert.Subject.Province) > 0 {
tags["province"] = cert.Subject.Province[0]
}
if len(cert.Subject.Locality) > 0 {
tags["locality"] = cert.Subject.Locality[0]
}
tags["issuer_common_name"] = cert.Issuer.CommonName
tags["issuer_serial_number"] = cert.Issuer.SerialNumber
san := append(cert.DNSNames, cert.EmailAddresses...)
for _, ip := range cert.IPAddresses {
san = append(san, ip.String())
}
for _, uri := range cert.URIs {
san = append(san, uri.String())
}
tags["san"] = strings.Join(san, ",")
return tags
}
func (c *X509Cert) collectCertURLs() []*url.URL {
var urls []*url.URL
for _, path := range c.globpaths {
files := path.Match()
if len(files) == 0 {
c.Log.Errorf("could not find file: %v", path.GetRoots())
continue
}
for _, file := range files {
fn := filepath.ToSlash(file)
urls = append(urls, &url.URL{Scheme: "file", Path: fn})
}
}
return urls
}
func (c *X509Cert) getSerialNumberString(cert *x509.Certificate) string {
if c.PadSerial {
return fmt.Sprintf("%016x", cert.SerialNumber)
}
return cert.SerialNumber.Text(16)
}
func init() {
inputs.Add("x509_cert", func() telegraf.Input {
return &X509Cert{
Timeout: config.Duration(5 * time.Second),
}
})
}

View file

@ -0,0 +1,682 @@
package x509_cert
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"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"
common_tls "github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/testutil"
)
var pki = testutil.NewPKI("../../../testutil/pki")
// Make sure X509Cert implements telegraf.Input
var _ telegraf.Input = &X509Cert{}
func TestGatherRemoteIntegration(t *testing.T) {
t.Skip("Skipping network-dependent test due to race condition when test-all")
tmpfile, err := os.CreateTemp(t.TempDir(), "example")
require.NoError(t, err)
_, err = tmpfile.WriteString(pki.ReadServerCert())
require.NoError(t, err)
tests := []struct {
name string
server string
timeout time.Duration
close bool
unset bool
noshake bool
error bool
}{
{name: "wrong port", server: ":99999", error: true},
{name: "no server", timeout: 5},
{name: "successful https", server: "https://example.org:443", timeout: 5},
{name: "successful file", server: "file://" + filepath.ToSlash(tmpfile.Name()), timeout: 5},
{name: "unsupported scheme", server: "foo://", timeout: 5, error: true},
{name: "no certificate", timeout: 5, unset: true, error: true},
{name: "closed connection", close: true, error: true},
{name: "no handshake", timeout: 5, noshake: true, error: true},
}
pair, err := tls.X509KeyPair([]byte(pki.ReadServerCert()), []byte(pki.ReadServerKey()))
require.NoError(t, err)
cfg := &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{pair},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.unset {
cfg.Certificates = nil
cfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, nil
}
}
ln, err := tls.Listen("tcp", "127.0.0.1:0", cfg)
require.NoError(t, err)
defer ln.Close()
go func() {
sconn, err := ln.Accept()
if err != nil {
t.Error(err)
return
}
if test.close {
sconn.Close()
}
serverConfig := cfg.Clone()
srv := tls.Server(sconn, serverConfig)
if test.noshake {
srv.Close()
}
if err = srv.Handshake(); err != nil {
t.Error(err)
return
}
}()
if test.server == "" {
test.server = "tcp://" + ln.Addr().String()
}
sc := X509Cert{
Sources: []string{test.server},
Timeout: config.Duration(test.timeout),
Log: testutil.Logger{},
}
require.NoError(t, sc.Init())
sc.InsecureSkipVerify = true
testErr := false
acc := testutil.Accumulator{}
err = sc.Gather(&acc)
if len(acc.Errors) > 0 {
testErr = true
}
if testErr != test.error {
t.Errorf("%s", err)
}
})
}
}
func TestGatherLocal(t *testing.T) {
wrongCert := fmt.Sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", base64.StdEncoding.EncodeToString([]byte("test")))
tests := []struct {
name string
mode os.FileMode
content string
error bool
}{
{name: "permission denied", mode: 0001, error: true},
{name: "not a certificate", mode: 0640, content: "test", error: true},
{name: "wrong certificate", mode: 0640, content: wrongCert, error: true},
{name: "correct certificate", mode: 0640, content: pki.ReadServerCert()},
{name: "correct client certificate", mode: 0640, content: pki.ReadClientCert()},
{name: "correct certificate and extra trailing space", mode: 0640, content: pki.ReadServerCert() + " "},
{name: "correct certificate and extra leading space", mode: 0640, content: " " + pki.ReadServerCert()},
{name: "correct multiple certificates", mode: 0640, content: pki.ReadServerCert() + pki.ReadCACert()},
{name: "correct multiple certificates and key", mode: 0640, content: pki.ReadServerCert() + pki.ReadCACert() + pki.ReadServerKey()},
{name: "correct certificate and wrong certificate", mode: 0640, content: pki.ReadServerCert() + "\n" + wrongCert, error: true},
{name: "correct certificate and not a certificate", mode: 0640, content: pki.ReadServerCert() + "\ntest", error: true},
{name: "correct multiple certificates and extra trailing space", mode: 0640, content: pki.ReadServerCert() + pki.ReadServerCert() + " "},
{name: "correct multiple certificates and extra leading space", mode: 0640, content: " " + pki.ReadServerCert() + pki.ReadServerCert()},
{name: "correct multiple certificates and extra middle space", mode: 0640, content: pki.ReadServerCert() + " " + pki.ReadServerCert()},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "x509_cert")
require.NoError(t, err)
_, err = f.WriteString(test.content)
require.NoError(t, err)
if runtime.GOOS != "windows" {
require.NoError(t, f.Chmod(test.mode))
}
require.NoError(t, f.Close())
sc := X509Cert{
Sources: []string{f.Name()},
Log: testutil.Logger{},
}
require.NoError(t, sc.Init())
acc := testutil.Accumulator{}
err = sc.Gather(&acc)
if (len(acc.Errors) > 0) != test.error {
t.Errorf("%s", err)
}
})
}
}
func TestTags(t *testing.T) {
cert := fmt.Sprintf("%s\n%s", pki.ReadServerCert(), pki.ReadCACert())
f, err := os.CreateTemp(t.TempDir(), "x509_cert")
require.NoError(t, err)
_, err = f.WriteString(cert)
require.NoError(t, err)
require.NoError(t, f.Close())
defer os.Remove(f.Name())
sc := X509Cert{
Sources: []string{f.Name()},
Log: testutil.Logger{},
}
require.NoError(t, sc.Init())
acc := testutil.Accumulator{}
require.NoError(t, sc.Gather(&acc))
require.True(t, acc.HasMeasurement("x509_cert"))
require.True(t, acc.HasTag("x509_cert", "common_name"))
require.Equal(t, "localhost", acc.TagValue("x509_cert", "common_name"))
require.True(t, acc.HasTag("x509_cert", "signature_algorithm"))
require.Equal(t, "SHA256-RSA", acc.TagValue("x509_cert", "signature_algorithm"))
require.True(t, acc.HasTag("x509_cert", "public_key_algorithm"))
require.Equal(t, "RSA", acc.TagValue("x509_cert", "public_key_algorithm"))
require.True(t, acc.HasTag("x509_cert", "issuer_common_name"))
require.Equal(t, "Telegraf Test CA", acc.TagValue("x509_cert", "issuer_common_name"))
require.True(t, acc.HasTag("x509_cert", "san"))
require.Equal(t, "localhost,127.0.0.1", acc.TagValue("x509_cert", "san"))
require.True(t, acc.HasTag("x509_cert", "serial_number"))
serialNumber := new(big.Int)
_, validSerialNumber := serialNumber.SetString(acc.TagValue("x509_cert", "serial_number"), 16)
require.Truef(t, validSerialNumber, "Expected a valid Hex serial number but got %s", acc.TagValue("x509_cert", "serial_number"))
require.Equal(t, big.NewInt(1), serialNumber)
// expect root/intermediate certs (more than one cert)
require.Greater(t, acc.NMetrics(), uint64(1))
}
func TestGatherExcludeRootCerts(t *testing.T) {
cert := fmt.Sprintf("%s\n%s", pki.ReadServerCert(), pki.ReadCACert())
f, err := os.CreateTemp(t.TempDir(), "x509_cert")
require.NoError(t, err)
_, err = f.WriteString(cert)
require.NoError(t, err)
require.NoError(t, f.Close())
sc := X509Cert{
Sources: []string{f.Name()},
ExcludeRootCerts: true,
Log: testutil.Logger{},
}
require.NoError(t, sc.Init())
acc := testutil.Accumulator{}
require.NoError(t, sc.Gather(&acc))
require.True(t, acc.HasMeasurement("x509_cert"))
require.Equal(t, uint64(1), acc.NMetrics())
}
func TestGatherChain(t *testing.T) {
cert := fmt.Sprintf("%s\n%s", pki.ReadServerCert(), pki.ReadCACert())
tests := []struct {
name string
content string
error bool
}{
{name: "chain certificate", content: cert},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "x509_cert")
require.NoError(t, err)
_, err = f.WriteString(test.content)
require.NoError(t, err)
require.NoError(t, f.Close())
sc := X509Cert{
Sources: []string{f.Name()},
Log: testutil.Logger{},
}
require.NoError(t, sc.Init())
acc := testutil.Accumulator{}
err = sc.Gather(&acc)
if (err != nil) != test.error {
t.Errorf("%s", err)
}
})
}
}
func TestGatherUDPCertIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
pair, err := tls.X509KeyPair([]byte(pki.ReadServerCert()), []byte(pki.ReadServerKey()))
require.NoError(t, err)
cfg := &dtls.Config{
Certificates: []tls.Certificate{pair},
}
addr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}
listener, err := dtls.Listen("udp", addr, cfg)
require.NoError(t, err)
defer listener.Close()
go func() {
if _, err := listener.Accept(); err != nil {
t.Error(err)
}
}()
m := &X509Cert{
Sources: []string{"udp://" + listener.Addr().String()},
Log: testutil.Logger{},
}
require.NoError(t, m.Init())
var acc testutil.Accumulator
require.NoError(t, m.Gather(&acc))
require.Empty(t, acc.Errors)
require.True(t, acc.HasMeasurement("x509_cert"))
require.True(t, acc.HasTag("x509_cert", "ocsp_stapled"))
}
func TestGatherTCPCert(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
m := &X509Cert{
Sources: []string{ts.URL},
Log: testutil.Logger{},
}
require.NoError(t, m.Init())
var acc testutil.Accumulator
require.NoError(t, m.Gather(&acc))
require.Empty(t, acc.Errors)
require.True(t, acc.HasMeasurement("x509_cert"))
}
func TestGatherCertIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
m := &X509Cert{
Sources: []string{"https://www.influxdata.com:443"},
Log: testutil.Logger{},
}
require.NoError(t, m.Init())
var acc testutil.Accumulator
require.NoError(t, m.Gather(&acc))
require.True(t, acc.HasMeasurement("x509_cert"))
require.True(t, acc.HasTag("x509_cert", "ocsp_stapled"))
}
func TestGatherCertMustNotTimeoutIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
duration := time.Duration(15) * time.Second
m := &X509Cert{
Sources: []string{"https://www.influxdata.com:443"},
Timeout: config.Duration(duration),
Log: testutil.Logger{},
}
require.NoError(t, m.Init())
var acc testutil.Accumulator
require.NoError(t, m.Gather(&acc))
require.Empty(t, acc.Errors)
require.True(t, acc.HasMeasurement("x509_cert"))
require.True(t, acc.HasTag("x509_cert", "ocsp_stapled"))
}
func TestSourcesToURLs(t *testing.T) {
m := &X509Cert{
Sources: []string{
"https://www.influxdata.com:443",
"tcp://influxdata.com:443",
"smtp://influxdata.com:25",
"file:///dummy_test_path_file.pem",
"file:///windows/temp/test.pem",
`file://C:\windows\temp\test.pem`,
`file:///C:/windows/temp/test.pem`,
"/tmp/dummy_test_path_glob*.pem",
},
Log: testutil.Logger{},
}
require.NoError(t, m.Init())
expected := []string{
"https://www.influxdata.com:443",
"tcp://influxdata.com:443",
"smtp://influxdata.com:25",
}
expectedPaths := []string{
"/dummy_test_path_file.pem",
"/windows/temp/test.pem",
"C:\\windows\\temp\\test.pem",
"C:/windows/temp/test.pem",
}
for _, p := range expectedPaths {
expected = append(expected, filepath.FromSlash(p))
}
actual := make([]string, 0, len(m.globpaths)+len(m.locations))
for _, p := range m.globpaths {
actual = append(actual, p.GetRoots()...)
}
for _, p := range m.locations {
actual = append(actual, p.String())
}
require.Len(t, m.globpaths, 5)
require.Len(t, m.locations, 3)
require.ElementsMatch(t, expected, actual)
}
func TestServerName(t *testing.T) {
tests := []struct {
name string
fromTLS string
fromCfg string
url string
expected string
err bool
}{
{name: "in cfg", fromCfg: "example.com", url: "https://other.example.com", expected: "example.com"},
{name: "in tls", fromTLS: "example.com", url: "https://other.example.com", expected: "example.com"},
{name: "from URL", url: "https://other.example.com", expected: "other.example.com"},
{name: "errors", fromCfg: "otherex.com", fromTLS: "example.com", url: "https://other.example.com", err: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sc := &X509Cert{
Sources: []string{test.url},
ServerName: test.fromCfg,
ClientConfig: common_tls.ClientConfig{ServerName: test.fromTLS},
Log: testutil.Logger{},
}
err := sc.Init()
if test.err {
require.Error(t, err)
return
}
require.NoError(t, err)
u, err := url.Parse(test.url)
require.NoError(t, err)
require.Equal(t, test.expected, sc.serverName(u))
})
}
}
func TestCertificateSerialNumberRetainsLeadingZeroes(t *testing.T) {
bi := &big.Int{}
bi.SetString("123456789abcdef", 16)
plugin := &X509Cert{}
certificate := &x509.Certificate{
SerialNumber: bi,
}
require.Equal(t, "123456789abcdef", plugin.getSerialNumberString(certificate))
plugin.PadSerial = true
require.Equal(t, "0123456789abcdef", plugin.getSerialNumberString(certificate))
}
// 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 := t.TempDir()
// 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: common_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": "",
"ocsp_stapled": "no",
"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": "",
"ocsp_stapled": "no",
"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": "",
"ocsp_stapled": "no",
"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...)
}