Skip to content

Commit 3174af9

Browse files
committed
Use system cert pool
Add WithTLSServerCurvePreferences, WithTLSServerCipherSuites and WithTLSServerVerifyPeerCertificate TLSServerConfigOption Decrypt RSA private key GetClientCertificate cannot return nil cert in TLS 1.3
1 parent 1220d02 commit 3174af9

File tree

19 files changed

+812
-24
lines changed

19 files changed

+812
-24
lines changed

config/config.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import (
55
)
66

77
type TLSServerConfig struct {
8-
Enable bool `help:"Enable server-side TLS."`
9-
Refresh time.Duration `default:"0s" help:"Interval for refreshing server TLS certificates."`
10-
File TLSServerFiles `embed:"" prefix:"file."`
8+
Enable bool `help:"Enable server-side TLS."`
9+
Refresh time.Duration `default:"0s" help:"Interval for refreshing server TLS certificates."`
10+
File TLSServerFiles `embed:"" prefix:"file."`
11+
KeyPassword string `help:"Optional password to decrypt RSA private key."`
1112
}
1213

1314
type TLSServerFiles struct {
@@ -22,6 +23,7 @@ type TLSClientConfig struct {
2223
Refresh time.Duration `default:"0s" help:"Interval for refreshing client TLS certificates."`
2324
InsecureSkipVerify bool `help:"Skip TLS verification on client side."`
2425
File TLSClientFiles `embed:"" prefix:"file."`
26+
KeyPassword string `help:"Optional password to decrypt RSA private key."`
2527
}
2628

2729
type TLSClientFiles struct {

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ module github.com/grepplabs/cert-source
22

33
go 1.21
44

5-
require github.com/stretchr/testify v1.8.4
5+
require (
6+
github.com/stretchr/testify v1.10.0
7+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
8+
)
69

710
require (
811
github.com/davecgh/go-spew v1.1.1 // indirect
912
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
golang.org/x/crypto v0.32.0 // indirect
1014
gopkg.in/yaml.v3 v3.0.1 // indirect
1115
)

go.sum

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
6-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
5+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
8+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
9+
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
10+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
711
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
812
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
913
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/testutil/certs.go

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package testutil
22

33
import (
4+
"bytes"
45
"crypto"
56
"crypto/rand"
67
"crypto/rsa"
@@ -9,6 +10,7 @@ import (
910
"crypto/x509/pkix"
1011
"encoding/pem"
1112
"fmt"
13+
"io"
1214
"math/big"
1315
mathrand "math/rand"
1416
"net"
@@ -18,8 +20,11 @@ import (
1820
"time"
1921

2022
tlsclient "github.com/grepplabs/cert-source/tls/client"
23+
"github.com/grepplabs/cert-source/tls/keyutil"
2124
)
2225

26+
const DefaultKeyPassword = "test123"
27+
2328
func GenerateCRL(caX509Cert *x509.Certificate, caPrivateKey crypto.PrivateKey, certs []*x509.Certificate, crlFile *os.File) error {
2429
revoked := make([]x509.RevocationListEntry, 0)
2530
for _, cert := range certs {
@@ -193,25 +198,31 @@ type CertsBundle struct {
193198
CATLSCert *tls.Certificate
194199
CAX509Cert *x509.Certificate
195200

196-
ServerCert *os.File
197-
ServerKey *os.File
198-
ServerTLSCert *tls.Certificate
199-
ServerX509Cert *x509.Certificate
201+
ServerCert *os.File
202+
ServerKey *os.File
203+
ServerKeyPassword string
204+
ServerKeyEncrypted *os.File
205+
ServerTLSCert *tls.Certificate
206+
ServerX509Cert *x509.Certificate
200207

201-
ClientCert *os.File
202-
ClientKey *os.File
203-
ClientCRL *os.File
204-
ClientTLSCert *tls.Certificate
205-
ClientX509Cert *x509.Certificate
208+
ClientCert *os.File
209+
ClientKey *os.File
210+
ClientKeyPassword string
211+
ClientKeyEncrypted *os.File
212+
ClientCRL *os.File
213+
ClientTLSCert *tls.Certificate
214+
ClientX509Cert *x509.Certificate
206215
}
207216

208217
func (bundle *CertsBundle) Close() {
209218
_ = os.Remove(bundle.CACert.Name())
210219
_ = os.Remove(bundle.CAKey.Name())
211220
_ = os.Remove(bundle.ServerCert.Name())
212221
_ = os.Remove(bundle.ServerKey.Name())
222+
_ = os.Remove(bundle.ServerKeyEncrypted.Name())
213223
_ = os.Remove(bundle.ClientCert.Name())
214224
_ = os.Remove(bundle.ClientKey.Name())
225+
_ = os.Remove(bundle.ClientKeyEncrypted.Name())
215226
_ = os.Remove(bundle.dirName)
216227
}
217228

@@ -257,6 +268,12 @@ func NewCertsBundle() *CertsBundle {
257268
}
258269
defer closeFile(bundle.ServerKey)
259270

271+
bundle.ServerKeyEncrypted, err = os.CreateTemp(dirName, "server-key-encrypted-")
272+
if err != nil {
273+
panic(err)
274+
}
275+
defer closeFile(bundle.ServerKeyEncrypted)
276+
260277
bundle.ClientCert, err = os.CreateTemp(dirName, "client-cert-")
261278
if err != nil {
262279
panic(err)
@@ -269,6 +286,12 @@ func NewCertsBundle() *CertsBundle {
269286
}
270287
defer closeFile(bundle.ClientKey)
271288

289+
bundle.ClientKeyEncrypted, err = os.CreateTemp("", "client-key-encrypted-")
290+
if err != nil {
291+
panic(err)
292+
}
293+
defer closeFile(bundle.ClientKeyEncrypted)
294+
272295
bundle.ClientCRL, err = os.CreateTemp("", "client-crl-")
273296
if err != nil {
274297
panic(err)
@@ -284,10 +307,20 @@ func NewCertsBundle() *CertsBundle {
284307
if err != nil {
285308
panic(err)
286309
}
310+
if bundle.ServerKeyPassword == "" {
311+
bundle.ServerKeyPassword = DefaultKeyPassword
312+
}
313+
writeEncryptedPrivate(bundle.ServerKey.Name(), bundle.ServerKeyEncrypted, bundle.ServerKeyPassword)
314+
287315
bundle.ClientTLSCert, bundle.ClientX509Cert, err = GenerateCert(bundle.CATLSCert, true, bundle.ClientCert, bundle.ClientKey)
288316
if err != nil {
289317
panic(err)
290318
}
319+
if bundle.ClientKeyPassword == "" {
320+
bundle.ClientKeyPassword = DefaultKeyPassword
321+
}
322+
writeEncryptedPrivate(bundle.ClientKey.Name(), bundle.ClientKeyEncrypted, bundle.ClientKeyPassword)
323+
291324
// generate CRLs
292325
err = GenerateCRL(bundle.CAX509Cert, bundle.CATLSCert.PrivateKey, []*x509.Certificate{}, bundle.CAEmptyCRL)
293326
if err != nil {
@@ -300,6 +333,22 @@ func NewCertsBundle() *CertsBundle {
300333
return bundle
301334
}
302335

336+
func writeEncryptedPrivate(keyFilename string, encryptedFile *os.File, password string) {
337+
keyData, err := os.ReadFile(keyFilename)
338+
if err != nil {
339+
panic(err)
340+
}
341+
encryptedKey, err := keyutil.EncryptPKCS8PrivateKeyPEM(keyData, password)
342+
if err != nil {
343+
panic(err)
344+
}
345+
346+
_, err = io.Copy(encryptedFile, bytes.NewReader(encryptedKey))
347+
if err != nil {
348+
panic(err)
349+
}
350+
}
351+
303352
func closeFile(file *os.File) {
304353
err := file.Close()
305354
if err != nil {

tls/client/client.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@ func NewTLSClientConfigFunc(logger *slog.Logger, src source.ClientCertsSource, o
2020
if err != nil {
2121
return nil, err
2222
}
23+
var getClientCertificateFunc func(info *tls.CertificateRequestInfo) (*tls.Certificate, error)
24+
if store.LoadClientCerts().Certificate != nil {
25+
// Set function only when client certificate is available.
26+
// TLS 1.3 checks if GetClientCertificate function is nil, if it is not nil,
27+
// it assumes client certificate is available which call cause the panic if nil is returned.
28+
// nolint:unparam
29+
getClientCertificateFunc = func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
30+
return store.LoadClientCerts().Certificate, nil
31+
}
32+
}
2333
return func() *tls.Config {
2434
cs := store.LoadClientCerts()
2535
x := &tls.Config{
26-
RootCAs: cs.RootCAs,
27-
InsecureSkipVerify: cs.InsecureSkipVerify,
28-
GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
29-
return store.LoadClientCerts().Certificate, nil
30-
},
36+
RootCAs: cs.RootCAs,
37+
InsecureSkipVerify: cs.InsecureSkipVerify,
38+
GetClientCertificate: getClientCertificateFunc,
3139
}
3240
for _, opt := range opts {
3341
opt(x)

tls/client/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func GetTLSClientConfigFunc(logger *slog.Logger, conf *config.TLSClientConfig, o
1919
filesource.WithInsecureSkipVerify(conf.InsecureSkipVerify),
2020
filesource.WithClientCert(conf.File.Cert, conf.File.Key),
2121
filesource.WithClientRootCAs(conf.File.RootCAs),
22+
filesource.WithKeyPassword(conf.KeyPassword),
2223
)
2324
if err != nil {
2425
return nil, fmt.Errorf("setup client cert file source: %w", err)

tls/client/config/config_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,37 @@ func TestGetClientTLSConfig(t *testing.T) {
3131
require.NoError(t, err)
3232
require.NotNil(t, clientCert)
3333
}
34+
35+
func TestGetClientTLSConfigNoConfig(t *testing.T) {
36+
bundle := testutil.NewCertsBundle()
37+
defer bundle.Close()
38+
tlsConfigFunc, err := GetTLSClientConfigFunc(slog.Default(), &config.TLSClientConfig{
39+
Enable: true,
40+
Refresh: 0,
41+
File: config.TLSClientFiles{},
42+
})
43+
require.NoError(t, err)
44+
tlsConfig := tlsConfigFunc()
45+
require.Nil(t, tlsConfig.RootCAs)
46+
require.Nil(t, tlsConfig.GetClientCertificate)
47+
}
48+
49+
func TestGetClientTLSConfigSkipVerify(t *testing.T) {
50+
bundle := testutil.NewCertsBundle()
51+
defer bundle.Close()
52+
tlsConfigFunc, err := GetTLSClientConfigFunc(slog.Default(), &config.TLSClientConfig{
53+
Enable: true,
54+
Refresh: 0,
55+
File: config.TLSClientFiles{
56+
Key: bundle.ClientKey.Name(),
57+
Cert: bundle.ClientCert.Name(),
58+
},
59+
})
60+
require.NoError(t, err)
61+
tlsConfig := tlsConfigFunc()
62+
require.Nil(t, tlsConfig.RootCAs)
63+
64+
clientCert, err := tlsConfig.GetClientCertificate(nil)
65+
require.NoError(t, err)
66+
require.NotNil(t, clientCert)
67+
}

tls/client/filesource/filesource.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import (
88
"time"
99

1010
tlscert "github.com/grepplabs/cert-source/tls/client/source"
11+
"github.com/grepplabs/cert-source/tls/keyutil"
1112
"github.com/grepplabs/cert-source/tls/watcher"
1213
)
1314

1415
type fileSource struct {
1516
insecureSkipVerify bool
1617
certFile string
1718
keyFile string
19+
keyPassword string
1820
rootCAsFile string
1921
refresh time.Duration
2022
logger *slog.Logger
@@ -105,6 +107,9 @@ func (s *fileSource) Load() (pemBlocks *tlscert.ClientPEMs, err error) {
105107
if pemBlocks.KeyPEMBlock, err = s.readFile(s.keyFile); err != nil {
106108
return nil, err
107109
}
110+
if pemBlocks.KeyPEMBlock, err = keyutil.DecryptPrivateKeyPEM(pemBlocks.KeyPEMBlock, s.keyPassword); err != nil {
111+
return nil, err
112+
}
108113
}
109114
if pemBlocks.RootCAsPEMBlock, err = s.readFile(s.rootCAsFile); err != nil {
110115
return nil, err

tls/client/filesource/filesource_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,44 @@ func TestCertRotation(t *testing.T) {
8282
require.Contains(t, err.Error(), "unknown certificate authority")
8383

8484
}
85+
86+
func TestKeyEncryption(t *testing.T) {
87+
bundle := testutil.NewCertsBundle()
88+
defer bundle.Close()
89+
90+
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
91+
w.WriteHeader(http.StatusOK)
92+
}))
93+
defer ts.Close()
94+
95+
clientSource := MustNew(
96+
WithClientRootCAs(bundle.CACert.Name()),
97+
WithClientCert(bundle.ClientCert.Name(), bundle.ClientKeyEncrypted.Name()),
98+
WithKeyPassword(bundle.ClientKeyPassword),
99+
WithRefresh(1*time.Second),
100+
).(*fileSource)
101+
102+
clientCertsStore, err := tlsclient.NewTLSClientCertsStore(slog.Default(), clientSource)
103+
require.NoError(t, err)
104+
105+
serverSource := serverfilesource.MustNew(
106+
serverfilesource.WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKeyEncrypted.Name()),
107+
serverfilesource.WithKeyPassword(bundle.ServerKeyPassword),
108+
serverfilesource.WithClientAuthFile(bundle.CACert.Name()),
109+
serverfilesource.WithClientCRLFile(bundle.CAEmptyCRL.Name()),
110+
serverfilesource.WithRefresh(1*time.Second),
111+
)
112+
ts.TLS = servertls.MustNewServerConfig(slog.Default(), serverSource)
113+
ts.StartTLS()
114+
115+
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
116+
require.NoError(t, err)
117+
118+
// when
119+
client := &http.Client{
120+
Transport: tlsclient.NewDefaultRoundTripper(tlsclient.WithClientCertsStore(clientCertsStore)),
121+
}
122+
_, err = client.Do(req)
123+
require.NoError(t, err)
124+
125+
}

tls/client/filesource/option.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ func WithClientCert(certFile, keyFile string) Option {
2020
}
2121
}
2222

23+
func WithKeyPassword(keyPassword string) Option {
24+
return func(c *fileSource) {
25+
c.keyPassword = keyPassword
26+
}
27+
}
28+
2329
func WithClientRootCAs(rootCAsFile string) Option {
2430
return func(c *fileSource) {
2531
c.rootCAsFile = rootCAsFile

0 commit comments

Comments
 (0)