Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
102
plugins/inputs/x509_cert/README.md
Normal file
102
plugins/inputs/x509_cert/README.md
Normal 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
|
||||
```
|
101
plugins/inputs/x509_cert/java_key_store.go
Normal file
101
plugins/inputs/x509_cert/java_key_store.go
Normal 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
|
||||
}
|
201
plugins/inputs/x509_cert/java_key_store_test.go
Normal file
201
plugins/inputs/x509_cert/java_key_store_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
38
plugins/inputs/x509_cert/sample.conf
Normal file
38
plugins/inputs/x509_cert/sample.conf
Normal 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"
|
546
plugins/inputs/x509_cert/x509_cert.go
Normal file
546
plugins/inputs/x509_cert/x509_cert.go
Normal 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),
|
||||
}
|
||||
})
|
||||
}
|
682
plugins/inputs/x509_cert/x509_cert_test.go
Normal file
682
plugins/inputs/x509_cert/x509_cert_test.go
Normal 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...)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue