Skip to content

Commit

Permalink
Decrypt RSA private key
Browse files Browse the repository at this point in the history
  • Loading branch information
everesio committed Feb 4, 2025
1 parent 18a7c56 commit 2691c33
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 4 deletions.
8 changes: 5 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
)

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

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

type TLSClientFiles struct {
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ module github.com/grepplabs/cert-source

go 1.21

require github.com/stretchr/testify v1.10.0
require (
github.com/stretchr/testify v1.10.0
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
1 change: 1 addition & 0 deletions tls/client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func GetTLSClientConfigFunc(logger *slog.Logger, conf *config.TLSClientConfig, o
filesource.WithInsecureSkipVerify(conf.InsecureSkipVerify),
filesource.WithClientCert(conf.File.Cert, conf.File.Key),
filesource.WithClientRootCAs(conf.File.RootCAs),
filesource.WithKeyPassword(conf.KeyPassword),
)
if err != nil {
return nil, fmt.Errorf("setup client cert file source: %w", err)
Expand Down
5 changes: 5 additions & 0 deletions tls/client/filesource/filesource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (
"time"

tlscert "github.com/grepplabs/cert-source/tls/client/source"
"github.com/grepplabs/cert-source/tls/keyutil"
"github.com/grepplabs/cert-source/tls/watcher"
)

type fileSource struct {
insecureSkipVerify bool
certFile string
keyFile string
keyPassword string
rootCAsFile string
refresh time.Duration
logger *slog.Logger
Expand Down Expand Up @@ -105,6 +107,9 @@ func (s *fileSource) Load() (pemBlocks *tlscert.ClientPEMs, err error) {
if pemBlocks.KeyPEMBlock, err = s.readFile(s.keyFile); err != nil {
return nil, err
}
if pemBlocks.KeyPEMBlock, err = keyutil.DecryptPrivateKeyPEM(pemBlocks.KeyPEMBlock, s.keyPassword); err != nil {
return nil, err
}
}
if pemBlocks.RootCAsPEMBlock, err = s.readFile(s.rootCAsFile); err != nil {
return nil, err
Expand Down
6 changes: 6 additions & 0 deletions tls/client/filesource/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func WithClientCert(certFile, keyFile string) Option {
}
}

func WithKeyPassword(keyPassword string) Option {
return func(c *fileSource) {
c.keyPassword = keyPassword
}
}

func WithClientRootCAs(rootCAsFile string) Option {
return func(c *fileSource) {
c.rootCAsFile = rootCAsFile
Expand Down
91 changes: 91 additions & 0 deletions tls/keyutil/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package keyutil

import (
"crypto/x509"
"encoding/pem"
"errors"
"strings"

"github.com/youmark/pkcs8"
)

func DecryptPrivateKeyPEM(pemData []byte, password string) ([]byte, error) {
keyBlock, _ := pem.Decode(pemData)
if keyBlock == nil {
return nil, errors.New("failed to parse PEM")
}
//nolint:staticcheck // Ignore SA1019: Support legacy encrypted PEM blocks
if x509.IsEncryptedPEMBlock(keyBlock) {
if password == "" {
return nil, errors.New("PEM is encrypted, but password is empty")
}
key, err := x509.DecryptPEMBlock(keyBlock, []byte(password))
if err != nil {
return nil, err
}
block := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: key,
}
return pem.EncodeToMemory(block), nil
} else if strings.Contains(string(pemData), "ENCRYPTED PRIVATE KEY") {
if password == "" {
return nil, errors.New("PEM is encrypted, but password is empty")
}
key, err := pkcs8.ParsePKCS8PrivateKey(keyBlock.Bytes, []byte(password))
if err != nil {
return nil, err
}
return MarshalPrivateKeyToPEM(key)
}
return pemData, nil
}

func EncryptPKCS8PrivateKeyPEM(pemData []byte, password string) ([]byte, error) {
if password == "" {
return nil, errors.New("password cannot be empty")
}
keyBlock, _ := pem.Decode(pemData)
if keyBlock == nil {
return nil, errors.New("failed to parse PEM")
}

var (
key any
err error
)
switch keyBlock.Type {
case "PRIVATE KEY":
key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, err
}
case "RSA PRIVATE KEY":
rsaKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
if err != nil {
return nil, err
}
key, err = x509.MarshalPKCS8PrivateKey(rsaKey)
if err != nil {
return nil, err
}
// Parse back to interface{} to match the signature for pkcs8.MarshalPrivateKey
key, err = x509.ParsePKCS8PrivateKey(key.([]byte))
if err != nil {
return nil, err
}
default:
return nil, errors.New("unsupported key type: " + keyBlock.Type)
}

encryptedBytes, err := pkcs8.MarshalPrivateKey(key, []byte(password), pkcs8.DefaultOpts)
if err != nil {
return nil, err
}
encryptedBlock := &pem.Block{
Type: "ENCRYPTED PRIVATE KEY",
Bytes: encryptedBytes,
}

return pem.EncodeToMemory(encryptedBlock), nil
}
Loading

0 comments on commit 2691c33

Please sign in to comment.