Skip to content

Commit

Permalink
TLS Auth - Support for encrypted private key (#2488)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabrielopesantos committed Apr 21, 2022
1 parent 187d336 commit 49b0b8c
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 3 deletions.
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)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM key")
}

blockType := block.Type
if blockType == "ENCRYPTED PRIVATE KEY" {
return nil, fmt.Errorf("encrypted pkcs8 formatted key is not supported")
}
/*
Even though `DecryptPEMBlock` has been deprecated since 1.16.x it is still
being used here because it is deprecated due to it not supporting *good* crypography
ultimately though we want to support something so we will be using it for now.
*/
decryptedKey, err := x509.DecryptPEMBlock(block, []byte(password)) // nolint: staticcheck
if err != nil {
return nil, err
}
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
101 changes: 101 additions & 0 deletions lib/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,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) {
t.Parallel()
opts := Options{}.Apply(Options{NoConnectionReuse: null.BoolFrom(true)})
Expand Down

0 comments on commit 49b0b8c

Please sign in to comment.