diff --git a/lib/options.go b/lib/options.go index c4bbf8e24d7..b247a5aba0a 100644 --- a/lib/options.go +++ b/lib/options.go @@ -22,7 +22,9 @@ package lib import ( "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "net" "reflect" @@ -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"` @@ -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 } @@ -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 diff --git a/lib/options_test.go b/lib/options_test.go index f46801d8b1b..9aee1c993dc 100644 --- a/lib/options_test.go +++ b/lib/options_test.go @@ -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"}, @@ -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)