Skip to content

Commit f4e718e

Browse files
committed
Convert certificates without using openssl
This implements PEM-encoded X.509 certificate parsing using Go crypto, and exports the result using SSLMate's go-pkcs12 package (or rather, cert-manager's fork of go-pkcs12 which adds support for encoding with a friendly name; see SSLMate/go-pkcs12#67 for details). Signed-off-by: Stephen Kitt <skitt@redhat.com>
1 parent 5581720 commit f4e718e

File tree

4 files changed

+126
-57
lines changed

4 files changed

+126
-57
lines changed

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/projectcalico/api v0.0.0-20230602153125-fb7148692637
1515
github.com/prometheus-community/pro-bing v0.7.0
1616
github.com/prometheus/client_golang v1.23.2
17-
github.com/submariner-io/admiral v0.23.0-m0.0.20260121163245-60a10fed6460
17+
github.com/submariner-io/admiral v0.23.0-m0.0.20260127154930-464f66697354
1818
github.com/submariner-io/shipyard v0.23.0-m0.0.20260121161247-366b31d697ca
1919
github.com/tigera/operator/api v0.0.0-20250829192342-96fd517a8419
2020
github.com/vishvananda/netlink v1.3.1
@@ -30,7 +30,8 @@ require (
3030
sigs.k8s.io/controller-runtime v0.23.0
3131
sigs.k8s.io/knftables v0.0.19
3232
sigs.k8s.io/mcs-api v0.3.0
33-
sigs.k8s.io/structured-merge-diff/v6 v6.3.1
33+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
34+
software.sslmate.com/src/go-pkcs12 v0.6.0
3435
)
3536

3637
require (
@@ -101,3 +102,5 @@ require (
101102
sigs.k8s.io/randfill v1.0.0 // indirect
102103
sigs.k8s.io/yaml v1.6.0 // indirect
103104
)
105+
106+
replace software.sslmate.com/src/go-pkcs12 => github.com/cert-manager/go-pkcs12 v0.0.0-20250730101253-8f67713f0d8f

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ github.com/cenkalti/hub v1.0.1 h1:UMtjc6dHSaOQTO15SVA50MBIR9zQwvsukQupDrkIRtg=
1515
github.com/cenkalti/hub v1.0.1/go.mod h1:tcYwtS3a2d9NO/0xDXVJWx3IedurUjYCqFCmpi0lpHs=
1616
github.com/cenkalti/rpc2 v0.0.0-20210604223624-c1acbc6ec984 h1:CNwZyGS6KpfaOWbh2yLkSy3rSTUh3jub9CzpFpP6PVQ=
1717
github.com/cenkalti/rpc2 v0.0.0-20210604223624-c1acbc6ec984/go.mod h1:v2npkhrXyk5BCnkNIiPdRI23Uq6uWPUQGL2hnRcRr/M=
18+
github.com/cert-manager/go-pkcs12 v0.0.0-20250730101253-8f67713f0d8f h1:FwCDR5Jrbj6wp/SDFVQoeBIOJYhWfLzUmxl/fSNdDOk=
19+
github.com/cert-manager/go-pkcs12 v0.0.0-20250730101253-8f67713f0d8f/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
1820
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
1921
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
2022
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
185187
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
186188
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
187189
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
188-
github.com/submariner-io/admiral v0.23.0-m0.0.20260121163245-60a10fed6460 h1:rlvdzpXIatTx1+4bWuWn8GTKwe97dXxGREAP/SKLvLM=
189-
github.com/submariner-io/admiral v0.23.0-m0.0.20260121163245-60a10fed6460/go.mod h1:7OgtUvSZrwkK6uGIZRWfU57vOuz0X/MyQCyOE58olVU=
190+
github.com/submariner-io/admiral v0.23.0-m0.0.20260127154930-464f66697354 h1:ozFYPYRQlUyTiv0My5A+deAU0sKcR89V3qxncquLslc=
191+
github.com/submariner-io/admiral v0.23.0-m0.0.20260127154930-464f66697354/go.mod h1:7OgtUvSZrwkK6uGIZRWfU57vOuz0X/MyQCyOE58olVU=
190192
github.com/submariner-io/shipyard v0.23.0-m0.0.20260121161247-366b31d697ca h1:xZs0XkIh1zDP+I5Va8kuYezeBAfBRhZQ2AvUrTsGNI0=
191193
github.com/submariner-io/shipyard v0.23.0-m0.0.20260121161247-366b31d697ca/go.mod h1:RxZ0WiJQqJSdbjobbL1Q1kmKWSx24mBNZA4gD+4UxDE=
192194
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
@@ -295,7 +297,7 @@ sigs.k8s.io/mcs-api v0.3.0 h1:LjRvgzjMrvO1904GP6XBJSnIX221DJMyQlZOYt9LAnM=
295297
sigs.k8s.io/mcs-api v0.3.0/go.mod h1:zZ5CK8uS6HaLkxY4HqsmcBHfzHuNMrY2uJy8T7jffK4=
296298
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
297299
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
298-
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
299-
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
300+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
301+
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
300302
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
301303
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

pkg/cable/libreswan/certificate_handler.go

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"bytes"
2323
"context"
2424
"crypto/sha256"
25+
"crypto/x509"
26+
"encoding/pem"
2527
"fmt"
2628
"os"
2729
"os/exec"
@@ -32,6 +34,7 @@ import (
3234
"github.com/submariner-io/admiral/pkg/command"
3335
"github.com/submariner-io/admiral/pkg/log"
3436
logf "sigs.k8s.io/controller-runtime/pkg/log"
37+
"software.sslmate.com/src/go-pkcs12"
3538
)
3639

3740
var certLogger = log.Logger{Logger: logf.Log.WithName("CertHandler")}
@@ -91,53 +94,74 @@ func (c *CertificateHandler) loadCertificate(ctx context.Context, certData []byt
9194
return errors.Wrapf(err, "failed to load certificate %q", nickname)
9295
}
9396

94-
//nolint:gosec // openssl/pk12util args are from trusted config
97+
//nolint:gosec // pk12util args are from trusted config
9598
func (c *CertificateHandler) loadPrivateKey(ctx context.Context, certData, keyData []byte, nickname string) error {
96-
// Write cert and key to temporary files
97-
certFile, err := os.CreateTemp(RootDir, "submariner-cert-*.crt")
98-
if err != nil {
99-
return errors.Wrap(err, "failed to create temporary cert file")
99+
// Parse certificate data
100+
var parsedCert *x509.Certificate
101+
var err error
102+
103+
for block, rest := pem.Decode(certData); block != nil; block, rest = pem.Decode(rest) {
104+
switch block.Type {
105+
case "CERTIFICATE":
106+
parsedCert, err = x509.ParseCertificate(block.Bytes)
107+
if err != nil {
108+
return errors.Wrap(err, "error parsing certificate data")
109+
}
110+
default:
111+
return fmt.Errorf("unexpected block type %q in certificate data", block.Type)
112+
}
100113
}
101-
defer os.Remove(certFile.Name())
102114

103-
if _, err := certFile.Write(certData); err != nil {
104-
return errors.Wrap(err, "failed to write certificate to temporary file")
115+
if parsedCert == nil {
116+
return errors.New("no certificate found in certificate data")
105117
}
106118

107-
certFile.Close()
108-
109-
keyFile, err := os.CreateTemp(RootDir, "submariner-key-*.key")
110-
if err != nil {
111-
return errors.Wrap(err, "failed to create temporary key file")
119+
// Parse key data
120+
var parsedKey any
121+
122+
for block, rest := pem.Decode(keyData); block != nil; block, rest = pem.Decode(rest) {
123+
switch block.Type {
124+
case "PRIVATE KEY":
125+
parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
126+
if err != nil {
127+
return errors.Wrap(err, "error parsing key data")
128+
}
129+
case "RSA PRIVATE KEY":
130+
parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
131+
if err != nil {
132+
return errors.Wrap(err, "error parsing key data")
133+
}
134+
default:
135+
return fmt.Errorf("unexpected block type %q in key data", block.Type)
136+
}
112137
}
113-
defer os.Remove(keyFile.Name())
114138

115-
if _, err := keyFile.Write(keyData); err != nil {
116-
return errors.Wrap(err, "failed to write key to temporary file")
139+
if parsedKey == nil {
140+
return errors.New("no private key found in key data")
117141
}
118142

119-
keyFile.Close()
120-
121-
// Create PKCS#12 file with openssl
143+
// Export PKCS#12 file
122144
p12File, err := os.CreateTemp(RootDir, "submariner-client-*.p12")
123145
if err != nil {
124146
return errors.Wrap(err, "failed to create temporary pkcs12 file")
125147
}
126148

127149
defer os.Remove(p12File.Name())
128-
p12File.Close()
129150

130151
// Use empty password for PKCS#12
131152
pkcs12Password := ""
132153

133-
opensslCmd := exec.CommandContext(ctx, "openssl", "pkcs12", "-export",
134-
"-in", certFile.Name(),
135-
"-inkey", keyFile.Name(),
136-
"-out", p12File.Name(),
137-
"-name", nickname,
138-
"-passout", "pass:"+pkcs12Password)
139-
if err := execWithOutput(command.New(opensslCmd)); err != nil {
140-
return errors.Wrap(err, "failed to create PKCS#12 file")
154+
pkcsData, err := pkcs12.Modern.EncodeWithFriendlyName(nickname, parsedKey, parsedCert, []*x509.Certificate{}, pkcs12Password)
155+
if err != nil {
156+
return errors.Wrap(err, "error encoding to PKCS#12")
157+
}
158+
159+
if _, err := p12File.Write(pkcsData); err != nil {
160+
return errors.Wrap(err, "error writing PKCS#12 file")
161+
}
162+
163+
if err := p12File.Close(); err != nil {
164+
return errors.Wrap(err, "error closing PKCS#12 file")
141165
}
142166

143167
// Import PKCS#12 into NSS using pk12util

pkg/cable/libreswan/certificate_handler_test.go

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@ package libreswan_test
2020

2121
import (
2222
"context"
23-
"maps"
23+
"crypto/rand"
24+
"crypto/rsa"
25+
"crypto/x509"
26+
"crypto/x509/pkix"
27+
_ "embed"
28+
"encoding/pem"
29+
"math/big"
2430
"os"
2531
"os/exec"
2632
"path/filepath"
33+
"time"
2734

2835
. "github.com/onsi/ginkgo/v2"
2936
. "github.com/onsi/gomega"
@@ -34,17 +41,59 @@ import (
3441
)
3542

3643
var _ = Describe("CertificateHandler", func() {
37-
certData := map[string][]byte{
38-
certificate.CADataKey: []byte("-----BEGIN CERTIFICATE-----\nMOCK_CA_CERT\n-----END CERTIFICATE-----"),
39-
certificate.TLSDataKey: []byte("-----BEGIN CERTIFICATE-----\nMOCK_CLIENT_CERT\n-----END CERTIFICATE-----"),
40-
certificate.PrivateKeyDataKey: []byte("-----BEGIN PRIVATE KEY-----\nMOCK_CLIENT_KEY\n-----END PRIVATE KEY-----"),
41-
}
42-
4344
var (
44-
cmdExecutor *fakecommand.Executor
45-
handler *libreswan.CertificateHandler
45+
cmdExecutor *fakecommand.Executor
46+
handler *libreswan.CertificateHandler
47+
testCertData map[string][]byte
48+
newCertData map[string][]byte
4649
)
4750

51+
BeforeEach(func() {
52+
if testCertData == nil || newCertData == nil {
53+
// CA
54+
caKey, caCert, err := certificate.CreateCAKeyAndCertificate("CA", 24*365*10*time.Hour)
55+
Expect(err).NotTo(HaveOccurred())
56+
caDER, err := x509.CreateCertificate(rand.Reader, caCert, caCert, &caKey.PublicKey, caKey)
57+
Expect(err).NotTo(HaveOccurred())
58+
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})
59+
60+
createSignedCertificate := func(name string) map[string][]byte {
61+
privateKey, err := rsa.GenerateKey(rand.Reader, certificate.RSABitSize)
62+
Expect(err).NotTo(HaveOccurred())
63+
64+
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
65+
Expect(err).NotTo(HaveOccurred())
66+
67+
cert := &x509.Certificate{
68+
SerialNumber: serialNumber,
69+
Subject: pkix.Name{
70+
CommonName: name,
71+
Organization: []string{"submariner.io"},
72+
},
73+
NotBefore: time.Now(),
74+
NotAfter: time.Now().AddDate(10, 0, 0),
75+
KeyUsage: x509.KeyUsageDigitalSignature,
76+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
77+
}
78+
79+
certDER, err := x509.CreateCertificate(rand.Reader, cert, caCert, &privateKey.PublicKey, caKey)
80+
Expect(err).NotTo(HaveOccurred())
81+
82+
return map[string][]byte{
83+
certificate.CADataKey: caPEM,
84+
certificate.TLSDataKey: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
85+
certificate.PrivateKeyDataKey: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}),
86+
}
87+
}
88+
89+
// First test certificate
90+
testCertData = createSignedCertificate("test")
91+
92+
// New test certificate
93+
newCertData = createSignedCertificate("new")
94+
}
95+
})
96+
4897
BeforeEach(func() {
4998
setupTempDir()
5099

@@ -64,22 +113,15 @@ var _ = Describe("CertificateHandler", func() {
64113
}
65114

66115
It("should successfully load the certificates into the NSS database", func() {
67-
Expect(handler.OnSignedCallback(certData)).To(Succeed())
116+
Expect(handler.OnSignedCallback(testCertData)).To(Succeed())
68117

69118
cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-N", "-d", "sql:"+handler.NSSDatabaseDir())
70119
assertCmdStdIn(cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-A", libreswan.CACertName,
71-
"-d", "sql:"+handler.NSSDatabaseDir()), certData[certificate.CADataKey])
72-
cmdExecutor.AwaitCommand(ContainSubstring("openssl"), "pkcs12", "-export", "-name", libreswan.ClientCertName)
120+
"-d", "sql:"+handler.NSSDatabaseDir()), testCertData[certificate.CADataKey])
73121
cmdExecutor.AwaitCommand(ContainSubstring("pk12util"), "-d", "sql:"+handler.NSSDatabaseDir())
74122
cmdExecutor.Clear()
75123

76124
By("Invoking OnSignedCallback with new cert data")
77-
78-
newCertData := map[string][]byte{
79-
certificate.CADataKey: []byte("NEW_CA_CERT"),
80-
certificate.TLSDataKey: []byte("NEW_CLIENT_CERT"),
81-
certificate.PrivateKeyDataKey: []byte("NEW_CLIENT_KEY"),
82-
}
83125
Expect(handler.OnSignedCallback(newCertData)).To(Succeed())
84126

85127
cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-A", libreswan.CACertName)
@@ -103,7 +145,7 @@ var _ = Describe("CertificateHandler", func() {
103145
return fakecommand.InterceptorFuncs{}
104146
})
105147

106-
Expect(handler.OnSignedCallback(certData)).NotTo(Succeed())
148+
Expect(handler.OnSignedCallback(testCertData)).NotTo(Succeed())
107149
})
108150

109151
It("should handle certificate loading failure", func() {
@@ -117,11 +159,11 @@ var _ = Describe("CertificateHandler", func() {
117159
return fakecommand.InterceptorFuncs{}
118160
})
119161

120-
Expect(handler.OnSignedCallback(certData)).NotTo(Succeed())
162+
Expect(handler.OnSignedCallback(testCertData)).NotTo(Succeed())
121163
})
122164

123165
It("should only initialize the NSS database once", func() {
124-
Expect(handler.OnSignedCallback(certData)).To(Succeed())
166+
Expect(handler.OnSignedCallback(testCertData)).To(Succeed())
125167

126168
cmdExecutor.AwaitCommand(ContainSubstring("certutil"), "-N")
127169
cmdExecutor.Clear()
@@ -131,8 +173,6 @@ var _ = Describe("CertificateHandler", func() {
131173
_, err := os.Create(nssDBFile)
132174
Expect(err).NotTo(HaveOccurred())
133175

134-
newCertData := maps.Clone(certData)
135-
newCertData[certificate.CADataKey] = []byte("NEW_CA_CERT")
136176
Expect(handler.OnSignedCallback(newCertData)).To(Succeed())
137177

138178
cmdExecutor.EnsureNoCommand(ContainSubstring("certutil"), "-N")

0 commit comments

Comments
 (0)