Skip to content

Commit 89fc2f5

Browse files
committed
Decrypt RSA private key
1 parent 18a7c56 commit 89fc2f5

File tree

12 files changed

+483
-13
lines changed

12 files changed

+483
-13
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.10.0
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
44
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
55
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
66
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: 60 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,22 @@ func NewCertsBundle() *CertsBundle {
284307
if err != nil {
285308
panic(err)
286309
}
310+
serverKeyPassword := bundle.ServerKeyPassword
311+
if serverKeyPassword == "" {
312+
serverKeyPassword = DefaultKeyPassword
313+
}
314+
writeEncryptedPrivate(bundle.ServerKey.Name(), bundle.ServerKeyEncrypted, serverKeyPassword)
315+
287316
bundle.ClientTLSCert, bundle.ClientX509Cert, err = GenerateCert(bundle.CATLSCert, true, bundle.ClientCert, bundle.ClientKey)
288317
if err != nil {
289318
panic(err)
290319
}
320+
clientKeyPassword := bundle.ClientKeyPassword
321+
if clientKeyPassword == "" {
322+
clientKeyPassword = DefaultKeyPassword
323+
}
324+
writeEncryptedPrivate(bundle.ClientKey.Name(), bundle.ClientKeyEncrypted, clientKeyPassword)
325+
291326
// generate CRLs
292327
err = GenerateCRL(bundle.CAX509Cert, bundle.CATLSCert.PrivateKey, []*x509.Certificate{}, bundle.CAEmptyCRL)
293328
if err != nil {
@@ -300,6 +335,22 @@ func NewCertsBundle() *CertsBundle {
300335
return bundle
301336
}
302337

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

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/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/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

tls/keyutil/crypto.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package keyutil
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"errors"
7+
"strings"
8+
9+
"github.com/youmark/pkcs8"
10+
)
11+
12+
func DecryptPrivateKeyPEM(pemData []byte, password string) ([]byte, error) {
13+
keyBlock, _ := pem.Decode(pemData)
14+
if keyBlock == nil {
15+
return nil, errors.New("failed to parse PEM")
16+
}
17+
//nolint:staticcheck // Ignore SA1019: Support legacy encrypted PEM blocks
18+
if x509.IsEncryptedPEMBlock(keyBlock) {
19+
if password == "" {
20+
return nil, errors.New("PEM is encrypted, but password is empty")
21+
}
22+
key, err := x509.DecryptPEMBlock(keyBlock, []byte(password))
23+
if err != nil {
24+
return nil, err
25+
}
26+
block := &pem.Block{
27+
Type: "RSA PRIVATE KEY",
28+
Bytes: key,
29+
}
30+
return pem.EncodeToMemory(block), nil
31+
} else if strings.Contains(string(pemData), "ENCRYPTED PRIVATE KEY") {
32+
if password == "" {
33+
return nil, errors.New("PEM is encrypted, but password is empty")
34+
}
35+
key, err := pkcs8.ParsePKCS8PrivateKey(keyBlock.Bytes, []byte(password))
36+
if err != nil {
37+
return nil, err
38+
}
39+
return MarshalPrivateKeyToPEM(key)
40+
}
41+
return pemData, nil
42+
}
43+
44+
func EncryptPKCS8PrivateKeyPEM(pemData []byte, password string) ([]byte, error) {
45+
if password == "" {
46+
return nil, errors.New("password cannot be empty")
47+
}
48+
keyBlock, _ := pem.Decode(pemData)
49+
if keyBlock == nil {
50+
return nil, errors.New("failed to parse PEM")
51+
}
52+
53+
var (
54+
key any
55+
err error
56+
)
57+
switch keyBlock.Type {
58+
case "PRIVATE KEY":
59+
key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
60+
if err != nil {
61+
return nil, err
62+
}
63+
case "RSA PRIVATE KEY":
64+
rsaKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
65+
if err != nil {
66+
return nil, err
67+
}
68+
key, err = x509.MarshalPKCS8PrivateKey(rsaKey)
69+
if err != nil {
70+
return nil, err
71+
}
72+
// Parse back to interface{} to match the signature for pkcs8.MarshalPrivateKey
73+
key, err = x509.ParsePKCS8PrivateKey(key.([]byte))
74+
if err != nil {
75+
return nil, err
76+
}
77+
default:
78+
return nil, errors.New("unsupported key type: " + keyBlock.Type)
79+
}
80+
81+
encryptedBytes, err := pkcs8.MarshalPrivateKey(key, []byte(password), pkcs8.DefaultOpts)
82+
if err != nil {
83+
return nil, err
84+
}
85+
encryptedBlock := &pem.Block{
86+
Type: "ENCRYPTED PRIVATE KEY",
87+
Bytes: encryptedBytes,
88+
}
89+
90+
return pem.EncodeToMemory(encryptedBlock), nil
91+
}

0 commit comments

Comments
 (0)