1
0
Fork 0
telegraf/plugins/inputs/x509_cert/x509_cert_test.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

682 lines
19 KiB
Go

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...)
}