Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS Auth - Support for encrypted private key #2488

45 changes: 42 additions & 3 deletions lib/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ package lib

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net"
"reflect"
Expand Down Expand Up @@ -130,8 +132,9 @@ func (s *TLSCipherSuites) UnmarshalJSON(data []byte) error {
// Fields for TLSAuth. Unmarshalling hack.
type TLSAuthFields struct {
// Certificate and key as a PEM-encoded string, including "-----BEGIN CERTIFICATE-----".
Cert string `json:"cert"`
Key string `json:"key"`
Cert string `json:"cert"`
Key string `json:"key"`
Password string `json:"password"`

// Domains to present the certificate to. May contain wildcards, eg. "*.example.com".
Domains []string `json:"domains"`
Expand All @@ -154,8 +157,16 @@ func (c *TLSAuth) UnmarshalJSON(data []byte) error {
}

func (c *TLSAuth) Certificate() (*tls.Certificate, error) {
key := []byte(c.Key)
var err error
if c.Password != "" {
key, err = decryptPrivateKey(c.Key, c.Password)
if err != nil {
return nil, err
}
}
if c.certificate == nil {
cert, err := tls.X509KeyPair([]byte(c.Cert), []byte(c.Key))
cert, err := tls.X509KeyPair([]byte(c.Cert), key)
if err != nil {
return nil, err
}
Expand All @@ -164,6 +175,34 @@ func (c *TLSAuth) Certificate() (*tls.Certificate, error) {
return c.certificate, nil
}

func decryptPrivateKey(privKey, password string) ([]byte, error) {
key := []byte(privKey)

block, _ := pem.Decode(key)
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
if block == nil {
return nil, fmt.Errorf("failed to decode PEM key")
}

blockType := block.Type
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
if blockType == "ENCRYPTED PRIVATE KEY" {
return nil, fmt.Errorf("encrypted pkcs8 formatted key is not supported")
}
/*
Even though `DecryptPEMBlock` has been depecrated since 1.16.x it is still
being used here because there are not alternatives in the standard library
to decrypt PEM encoded files.
Gabrielopesantos marked this conversation as resolved.
Show resolved Hide resolved
*/
decryptedKey, err := x509.DecryptPEMBlock(block, []byte(password)) // nolint: staticcheck
if err != nil {
return nil, err
}
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
key = pem.EncodeToMemory(&pem.Block{
Type: blockType,
Bytes: decryptedKey,
})
return key, nil
}

// IPNet is a wrapper around net.IPNet for JSON unmarshalling
type IPNet struct {
net.IPNet
Expand Down
102 changes: 102 additions & 0 deletions lib/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ func TestOptions(t *testing.T) {
})
})
t.Run("TLSAuth", func(t *testing.T) {
t.Parallel()
tlsAuth := []*TLSAuth{
{TLSAuthFields{
Domains: []string{"example.com", "*.example.com"},
Expand Down Expand Up @@ -286,6 +287,107 @@ func TestOptions(t *testing.T) {
assert.Error(t, json.Unmarshal([]byte(jsonStr), &opts))
})
})
t.Run("TLSAuth with", func(t *testing.T) {
t.Parallel()
domains := []string{"example.com", "*.example.com"}
cert := "-----BEGIN CERTIFICATE-----\n" +
"MIIBoTCCAUegAwIBAgIUQl0J1Gkd6U2NIMwMDnpfH8c1myEwCgYIKoZIzj0EAwIw\n" +
"EDEOMAwGA1UEAxMFTXkgQ0EwHhcNMTcwODE1MTYxODAwWhcNMTgwODE1MTYxODAw\n" +
"WjAQMQ4wDAYDVQQDEwV1c2VyMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLaf\n" +
"xEOmBHkzbqd9/0VZX/39qO2yQq2Gz5faRdvy38kuLMCV+9HYrfMx6GYCZzTUIq6h\n" +
"8QXOrlgYTixuUVfhJNWjfzB9MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggr\n" +
"BgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxmQiq5K3\n" +
"KUnVME945Byt3Ysvkh8wHwYDVR0jBBgwFoAU3qEhcpRgpsqo9V+LFns9a+oZIYww\n" +
"CgYIKoZIzj0EAwIDSAAwRQIgSGxnJ+/cLUNTzt7fhr/mjJn7ShsTW33dAdfLM7H2\n" +
"z/gCIQDyVf8DePtxlkMBScTxZmIlMQdNc6+6VGZQ4QscruVLmg==\n" +
"-----END CERTIFICATE-----"
tests := []struct {
name string
privateKey string
password string
hasError bool
errorMessage string
}{
{
name: "encrypted key and invalid password",
privateKey: "-----BEGIN EC PRIVATE KEY-----\n" +
"Proc-Type: 4,ENCRYPTED\n" +
"DEK-Info: AES-256-CBC,DF2445CBFE2E5B112FB2B721063757E5\n" +
"o/VKNZjQcRM2hatqUkQ0dTolL7i2i5hJX9XYsl+TMsq8ZkC83uY/JdR986QS+W2c\n" +
"EoQGtVGVeL0KGvGpzjTX3YAKXM7Lg5btAeS8GvJ9S7YFd8s0q1pqDdffl2RyjJav\n" +
"t1jx6XvLu2nBrOUARvHqjkkJQCTdRf2a34GJdbZqE+4=\n" +
"-----END EC PRIVATE KEY-----",
password: "iZfYGcrgFHOg4nweEo7ufT",
hasError: true,
errorMessage: "x509: decryption password incorrect",
},
{
name: "encrypted key and valid password",
privateKey: "-----BEGIN EC PRIVATE KEY-----\n" +
"Proc-Type: 4,ENCRYPTED\n" +
"DEK-Info: AES-256-CBC,DF2445CBFE2E5B112FB2B721063757E5\n" +
"o/VKNZjQcRM2hatqUkQ0dTolL7i2i5hJX9XYsl+TMsq8ZkC83uY/JdR986QS+W2c\n" +
"EoQGtVGVeL0KGvGpzjTX3YAKXM7Lg5btAeS8GvJ9S7YFd8s0q1pqDdffl2RyjJav\n" +
"t1jx6XvLu2nBrOUARvHqjkkJQCTdRf2a34GJdbZqE+4=\n" +
"-----END EC PRIVATE KEY-----",
password: "12345",
hasError: false,
errorMessage: "",
},
{
name: "encrypted pks8 format key and valid password",
privateKey: "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" +
"MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAjcfarGfrRgUgICCAAw\n" +
"DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEFmtmKEFmThbkbpxmC6iBvoEgZCE\n" +
"pDCpH/yCLmSpjdi/PC74I794nzHyCWf/oS0JhM0Q7J+abZP+p5pnreKft1f15Dbw\n" +
"QG9alfoM6EffJcVo3gf1tgQrpGGFMwczc4VhQgSGDy0XjZSbd2K0QCFGSmD2ZIR1\n" +
"qPG3WepWjKmIsYffGeKZx+FjXHSFeGk7RnssNAyKcPruDQIdWWyXxX1+ugBKuBw=\n" +
"-----END ENCRYPTED PRIVATE KEY-----\n",
password: "12345",
hasError: true,
errorMessage: "encrypted pkcs8 formatted key is not supported",
},
{
name: "non encrypted key and password",
privateKey: "-----BEGIN EC PRIVATE KEY-----\n" +
"MHcCAQEEINVilD5qOBkSy+AYfd41X0QPB5N3Z6OzgoBj8FZmSJOFoAoGCCqGSM49\n" +
"AwEHoUQDQgAEF8XzmC7x8Ns0Y2Wyu2c77ge+6I/ghcDTjWOMZzMPmRRDxqKFLuGD\n" +
"zW1Kss13WODGSS8+j7dNCPOeLKyK6cbeIg==\n" +
"-----END EC PRIVATE KEY-----",
password: "12345",
hasError: true,
errorMessage: "x509: no DEK-Info header in block",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tlsAuth := []*TLSAuth{
{TLSAuthFields{
Domains: domains,
Cert: cert,
Key: tc.privateKey,
Password: tc.password,
}, nil},
}
opts := Options{}.Apply(Options{TLSAuth: tlsAuth})
assert.Equal(t, tlsAuth, opts.TLSAuth)

t.Run("Roundtrip", func(t *testing.T) {
optsData, err := json.Marshal(opts)
assert.NoError(t, err)

var opts2 Options
err = json.Unmarshal(optsData, &opts2)
if tc.hasError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.errorMessage)
} else {
assert.NoError(t, err)
}
})
})
}
})
t.Run("NoConnectionReuse", func(t *testing.T) {
opts := Options{}.Apply(Options{NoConnectionReuse: null.BoolFrom(true)})
assert.True(t, opts.NoConnectionReuse.Valid)
Expand Down