feat(inputs.x509_cert): Add support for JKS and PKCS#12 keystores (#16508)
Signed-off-by: Paulo Dias <paulodias.gm@gmail.com>
This commit is contained in:
parent
f7bd90f194
commit
d231442040
|
|
@ -317,6 +317,7 @@ following works:
|
|||
- github.com/opentracing/opentracing-go [Apache License 2.0](https://github.com/opentracing/opentracing-go/blob/master/LICENSE)
|
||||
- github.com/p4lang/p4runtime [Apache License 2.0](https://github.com/p4lang/p4runtime/blob/main/LICENSE)
|
||||
- github.com/paulmach/orb [MIT License](https://github.com/paulmach/orb/blob/master/LICENSE.md)
|
||||
- github.com/pavlo-v-chernykh/keystore-go [MIT License](https://github.com/pavlo-v-chernykh/keystore-go/blob/main/LICENSE)
|
||||
- github.com/pborman/ansi [BSD 3-Clause "New" or "Revised" License](https://github.com/pborman/ansi/blob/master/LICENSE)
|
||||
- github.com/pcolladosoto/goslurm [MIT License](https://github.com/pcolladosoto/goslurm/blob/main/LICENSE)
|
||||
- github.com/peterbourgon/unixtransport [Apache License 2.0](https://github.com/peterbourgon/unixtransport/blob/main/LICENSE)
|
||||
|
|
@ -471,6 +472,7 @@ following works:
|
|||
- sigs.k8s.io/json [Apache License 2.0](https://github.com/kubernetes/client-go/blob/master/LICENSE)
|
||||
- sigs.k8s.io/structured-merge-diff [Apache License 2.0](https://github.com/kubernetes/client-go/blob/master/LICENSE)
|
||||
- sigs.k8s.io/yaml [Apache License 2.0](https://github.com/kubernetes/client-go/blob/master/LICENSE)
|
||||
- software.sslmate.com/src/go-pkcs12 [BSD 3-Clause "New" or "Revised" License](https://github.com/SSLMate/go-pkcs12/blob/master/LICENSE)
|
||||
|
||||
## Telegraf used and modified code from these projects
|
||||
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -161,6 +161,7 @@ require (
|
|||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0
|
||||
github.com/openzipkin/zipkin-go v0.4.3
|
||||
github.com/p4lang/p4runtime v1.4.0
|
||||
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
|
||||
github.com/pborman/ansi v1.0.0
|
||||
github.com/pcolladosoto/goslurm v0.1.0
|
||||
github.com/peterbourgon/unixtransport v0.0.4
|
||||
|
|
@ -240,6 +241,7 @@ require (
|
|||
k8s.io/client-go v0.32.1
|
||||
layeh.com/radius v0.0.0-20221205141417-e7fbddd11d68
|
||||
modernc.org/sqlite v1.34.1
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -2035,6 +2035,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK
|
|||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ=
|
||||
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ=
|
||||
github.com/pborman/ansi v1.0.0 h1:OqjHMhvlSuCCV5JT07yqPuJPQzQl+WXsiZ14gZsqOrQ=
|
||||
github.com/pborman/ansi v1.0.0/go.mod h1:SgWzwMAx1X/Ez7i90VqF8LRiQtx52pWDiQP+x3iGnzw=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
|
|
@ -3473,6 +3475,8 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aN
|
|||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
storj.io/common v0.0.0-20240812101423-26b53789c348 h1:Urs3fX+1Fyb+CFKGw0mCJV3MPR499WM+Vs6osw4Rqtk=
|
||||
storj.io/common v0.0.0-20240812101423-26b53789c348/go.mod h1:XMpwKxc04HCBl4H5IFCGv1ca5Dm0tvH4NL7Jx+JhxuA=
|
||||
storj.io/drpc v0.0.35-0.20240709171858-0075ac871661 h1:hLvEV2RMTscX3JHPd+LSQCeTt8i1Q0Yt7U2EdfyMnaQ=
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
|||
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"]
|
||||
"/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"
|
||||
|
|
@ -42,6 +44,9 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
|||
## 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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
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"]
|
||||
"/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"
|
||||
|
|
@ -22,6 +24,9 @@
|
|||
## 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"
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ 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:"-"`
|
||||
|
|
@ -446,6 +447,12 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica
|
|||
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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue