diff --git a/go.mod b/go.mod index b5c5091de..f86dee197 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/cenkalti/backoff/v4 v4.3.0 + github.com/cert-manager/go-pkcs12 v0.0.0-20251218073410-44b982790b7c github.com/coreos/go-iptables v0.8.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/onsi/ginkgo/v2 v2.27.5 @@ -14,7 +15,7 @@ require ( github.com/projectcalico/api v0.0.0-20230602153125-fb7148692637 github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus/client_golang v1.23.2 - github.com/submariner-io/admiral v0.23.0-m0.0.20260121163245-60a10fed6460 + github.com/submariner-io/admiral v0.23.0-m0.0.20260127154930-464f66697354 github.com/submariner-io/shipyard v0.23.0-m0.0.20260121161247-366b31d697ca github.com/tigera/operator/api v0.0.0-20250829192342-96fd517a8419 github.com/vishvananda/netlink v1.3.1 diff --git a/go.sum b/go.sum index 5f7f54a68..5289cedc9 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/cenkalti/hub v1.0.1 h1:UMtjc6dHSaOQTO15SVA50MBIR9zQwvsukQupDrkIRtg= github.com/cenkalti/hub v1.0.1/go.mod h1:tcYwtS3a2d9NO/0xDXVJWx3IedurUjYCqFCmpi0lpHs= github.com/cenkalti/rpc2 v0.0.0-20210604223624-c1acbc6ec984 h1:CNwZyGS6KpfaOWbh2yLkSy3rSTUh3jub9CzpFpP6PVQ= github.com/cenkalti/rpc2 v0.0.0-20210604223624-c1acbc6ec984/go.mod h1:v2npkhrXyk5BCnkNIiPdRI23Uq6uWPUQGL2hnRcRr/M= +github.com/cert-manager/go-pkcs12 v0.0.0-20251218073410-44b982790b7c h1:19zT0oEfVkTB3COjNmwcwS0q+rxD1/kClDp9Me3nggY= +github.com/cert-manager/go-pkcs12 v0.0.0-20251218073410-44b982790b7c/go.mod h1:Y/slpytWkfXJRipZINyf9Ub2pxh2goBeEpsPtI4dHKk= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containernetworking/cni v0.8.1 h1:7zpDnQ3T3s4ucOuJ/ZCLrYBxzkg0AELFfII3Epo9TmI= @@ -185,8 +187,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/submariner-io/admiral v0.23.0-m0.0.20260121163245-60a10fed6460 h1:rlvdzpXIatTx1+4bWuWn8GTKwe97dXxGREAP/SKLvLM= -github.com/submariner-io/admiral v0.23.0-m0.0.20260121163245-60a10fed6460/go.mod h1:7OgtUvSZrwkK6uGIZRWfU57vOuz0X/MyQCyOE58olVU= +github.com/submariner-io/admiral v0.23.0-m0.0.20260127154930-464f66697354 h1:ozFYPYRQlUyTiv0My5A+deAU0sKcR89V3qxncquLslc= +github.com/submariner-io/admiral v0.23.0-m0.0.20260127154930-464f66697354/go.mod h1:7OgtUvSZrwkK6uGIZRWfU57vOuz0X/MyQCyOE58olVU= github.com/submariner-io/shipyard v0.23.0-m0.0.20260121161247-366b31d697ca h1:xZs0XkIh1zDP+I5Va8kuYezeBAfBRhZQ2AvUrTsGNI0= github.com/submariner-io/shipyard v0.23.0-m0.0.20260121161247-366b31d697ca/go.mod h1:RxZ0WiJQqJSdbjobbL1Q1kmKWSx24mBNZA4gD+4UxDE= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= diff --git a/pkg/cable/libreswan/certificate_handler.go b/pkg/cable/libreswan/certificate_handler.go index bf216e2f1..5b825430c 100644 --- a/pkg/cable/libreswan/certificate_handler.go +++ b/pkg/cable/libreswan/certificate_handler.go @@ -22,11 +22,14 @@ import ( "bytes" "context" "crypto/sha256" + "crypto/x509" + "encoding/pem" "fmt" "os" "os/exec" "time" + "github.com/cert-manager/go-pkcs12" "github.com/pkg/errors" "github.com/submariner-io/admiral/pkg/certificate" "github.com/submariner-io/admiral/pkg/command" @@ -91,53 +94,74 @@ func (c *CertificateHandler) loadCertificate(ctx context.Context, certData []byt return errors.Wrapf(err, "failed to load certificate %q", nickname) } -//nolint:gosec // openssl/pk12util args are from trusted config +//nolint:gosec // pk12util args are from trusted config func (c *CertificateHandler) loadPrivateKey(ctx context.Context, certData, keyData []byte, nickname string) error { - // Write cert and key to temporary files - certFile, err := os.CreateTemp(RootDir, "submariner-cert-*.crt") - if err != nil { - return errors.Wrap(err, "failed to create temporary cert file") + // Parse certificate data + var parsedCert *x509.Certificate + var err error + + for block, rest := pem.Decode(certData); block != nil; block, rest = pem.Decode(rest) { + switch block.Type { + case "CERTIFICATE": + parsedCert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.Wrap(err, "error parsing certificate data") + } + default: + return fmt.Errorf("unexpected block type %q in certificate data", block.Type) + } } - defer os.Remove(certFile.Name()) - if _, err := certFile.Write(certData); err != nil { - return errors.Wrap(err, "failed to write certificate to temporary file") + if parsedCert == nil { + return errors.New("no certificate found in certificate data") } - certFile.Close() - - keyFile, err := os.CreateTemp(RootDir, "submariner-key-*.key") - if err != nil { - return errors.Wrap(err, "failed to create temporary key file") + // Parse key data + var parsedKey any + + for block, rest := pem.Decode(keyData); block != nil; block, rest = pem.Decode(rest) { + switch block.Type { + case "PRIVATE KEY": + parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return errors.Wrap(err, "error parsing key data") + } + case "RSA PRIVATE KEY": + parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return errors.Wrap(err, "error parsing key data") + } + default: + return fmt.Errorf("unexpected block type %q in key data", block.Type) + } } - defer os.Remove(keyFile.Name()) - if _, err := keyFile.Write(keyData); err != nil { - return errors.Wrap(err, "failed to write key to temporary file") + if parsedKey == nil { + return errors.New("no private key found in key data") } - keyFile.Close() - - // Create PKCS#12 file with openssl + // Export PKCS#12 file p12File, err := os.CreateTemp(RootDir, "submariner-client-*.p12") if err != nil { return errors.Wrap(err, "failed to create temporary pkcs12 file") } defer os.Remove(p12File.Name()) - p12File.Close() // Use empty password for PKCS#12 pkcs12Password := "" - opensslCmd := exec.CommandContext(ctx, "openssl", "pkcs12", "-export", - "-in", certFile.Name(), - "-inkey", keyFile.Name(), - "-out", p12File.Name(), - "-name", nickname, - "-passout", "pass:"+pkcs12Password) - if err := execWithOutput(command.New(opensslCmd)); err != nil { - return errors.Wrap(err, "failed to create PKCS#12 file") + pkcsData, err := pkcs12.Modern.EncodeWithFriendlyName(nickname, parsedKey, parsedCert, []*x509.Certificate{}, pkcs12Password) + if err != nil { + return errors.Wrap(err, "error encoding to PKCS#12") + } + + if _, err := p12File.Write(pkcsData); err != nil { + return errors.Wrap(err, "error writing PKCS#12 file") + } + + if err := p12File.Close(); err != nil { + return errors.Wrap(err, "error closing PKCS#12 file") } // Import PKCS#12 into NSS using pk12util diff --git a/pkg/cable/libreswan/certificate_handler_test.go b/pkg/cable/libreswan/certificate_handler_test.go index f3ca9bf57..1903f1dde 100644 --- a/pkg/cable/libreswan/certificate_handler_test.go +++ b/pkg/cable/libreswan/certificate_handler_test.go @@ -20,10 +20,17 @@ package libreswan_test import ( "context" - "maps" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + _ "embed" + "encoding/pem" + "math/big" "os" "os/exec" "path/filepath" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -34,17 +41,59 @@ import ( ) var _ = Describe("CertificateHandler", func() { - certData := map[string][]byte{ - certificate.CADataKey: []byte("-----BEGIN CERTIFICATE-----\nMOCK_CA_CERT\n-----END CERTIFICATE-----"), - certificate.TLSDataKey: []byte("-----BEGIN CERTIFICATE-----\nMOCK_CLIENT_CERT\n-----END CERTIFICATE-----"), - certificate.PrivateKeyDataKey: []byte("-----BEGIN PRIVATE KEY-----\nMOCK_CLIENT_KEY\n-----END PRIVATE KEY-----"), - } - var ( - cmdExecutor *fakecommand.Executor - handler *libreswan.CertificateHandler + cmdExecutor *fakecommand.Executor + handler *libreswan.CertificateHandler + testCertData map[string][]byte + newCertData map[string][]byte ) + BeforeEach(func() { + if testCertData == nil || newCertData == nil { + // CA + caKey, caCert, err := certificate.CreateCAKeyAndCertificate("CA", 24*365*10*time.Hour) + Expect(err).NotTo(HaveOccurred()) + caDER, err := x509.CreateCertificate(rand.Reader, caCert, caCert, &caKey.PublicKey, caKey) + Expect(err).NotTo(HaveOccurred()) + caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}) + + createSignedCertificate := func(name string) map[string][]byte { + privateKey, err := rsa.GenerateKey(rand.Reader, certificate.RSABitSize) + Expect(err).NotTo(HaveOccurred()) + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + Expect(err).NotTo(HaveOccurred()) + + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: name, + Organization: []string{"submariner.io"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, cert, caCert, &privateKey.PublicKey, caKey) + Expect(err).NotTo(HaveOccurred()) + + return map[string][]byte{ + certificate.CADataKey: caPEM, + certificate.TLSDataKey: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}), + certificate.PrivateKeyDataKey: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}), + } + } + + // First test certificate + testCertData = createSignedCertificate("test") + + // New test certificate + newCertData = createSignedCertificate("new") + } + }) + BeforeEach(func() { setupTempDir() @@ -64,22 +113,15 @@ var _ = Describe("CertificateHandler", func() { } It("should successfully load the certificates into the NSS database", func() { - Expect(handler.OnSignedCallback(certData)).To(Succeed()) + Expect(handler.OnSignedCallback(testCertData)).To(Succeed()) cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-N", "-d", "sql:"+handler.NSSDatabaseDir()) assertCmdStdIn(cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-A", libreswan.CACertName, - "-d", "sql:"+handler.NSSDatabaseDir()), certData[certificate.CADataKey]) - cmdExecutor.AwaitCommand(ContainSubstring("openssl"), "pkcs12", "-export", "-name", libreswan.ClientCertName) + "-d", "sql:"+handler.NSSDatabaseDir()), testCertData[certificate.CADataKey]) cmdExecutor.AwaitCommand(ContainSubstring("pk12util"), "-d", "sql:"+handler.NSSDatabaseDir()) cmdExecutor.Clear() By("Invoking OnSignedCallback with new cert data") - - newCertData := map[string][]byte{ - certificate.CADataKey: []byte("NEW_CA_CERT"), - certificate.TLSDataKey: []byte("NEW_CLIENT_CERT"), - certificate.PrivateKeyDataKey: []byte("NEW_CLIENT_KEY"), - } Expect(handler.OnSignedCallback(newCertData)).To(Succeed()) cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-A", libreswan.CACertName) @@ -103,7 +145,7 @@ var _ = Describe("CertificateHandler", func() { return fakecommand.InterceptorFuncs{} }) - Expect(handler.OnSignedCallback(certData)).NotTo(Succeed()) + Expect(handler.OnSignedCallback(testCertData)).NotTo(Succeed()) }) It("should handle certificate loading failure", func() { @@ -117,11 +159,11 @@ var _ = Describe("CertificateHandler", func() { return fakecommand.InterceptorFuncs{} }) - Expect(handler.OnSignedCallback(certData)).NotTo(Succeed()) + Expect(handler.OnSignedCallback(testCertData)).NotTo(Succeed()) }) It("should only initialize the NSS database once", func() { - Expect(handler.OnSignedCallback(certData)).To(Succeed()) + Expect(handler.OnSignedCallback(testCertData)).To(Succeed()) cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-N") cmdExecutor.Clear() @@ -131,8 +173,6 @@ var _ = Describe("CertificateHandler", func() { _, err := os.Create(nssDBFile) Expect(err).NotTo(HaveOccurred()) - newCertData := maps.Clone(certData) - newCertData[certificate.CADataKey] = []byte("NEW_CA_CERT") Expect(handler.OnSignedCallback(newCertData)).To(Succeed()) cmdExecutor.EnsureNoCommand(ContainSubstring("certutil"), "-N")