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

Support for per-vu TLS configuration (grpc.Client.connect only) #3159

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
abbd1a8
### Support for per-vu TLS configuration (grpc.Client.connect only) a…
Jul 2, 2023
2c3e1c8
Fixed cast when cacerts is passed as a string[]
Jul 4, 2023
2da60cb
Remove CACerts change from TLSAuthFields per PR request
Jul 4, 2023
748bbf3
Fix execution_test after removing CACerts from tlsAuth option
Jul 4, 2023
044e176
Added tests for feature/per-vu-grpc-tls-config-with-rootcas
Jul 4, 2023
9a2f3ba
Added tests for grpc.Client connectParams tls option
Jul 4, 2023
6850c71
Added missed password field for encrypted certificate key on tls option
Jul 4, 2023
4cbf416
Address security scan for "RSA Private Key exposed on GitHub" for loc…
Jul 5, 2023
7bec30a
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 6, 2023
6fe2efa
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 6, 2023
5d7cfd9
PR Feedback
Jul 6, 2023
fd512e6
Cleaned (removed) prior changes to httpmultibin.go
Jul 7, 2023
a5df2fe
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 7, 2023
135050c
TestClient_Connect_TlsParameters renamed for clarity
Jul 7, 2023
36bdf7d
Merge remote-tracking branch 'origin/feature/per-vu-grpc-tls-config-w…
Jul 7, 2023
514ea16
Give CI some more time to verify
Jul 7, 2023
a1bf3d9
client_test housekeeping
Jul 8, 2023
0fd36f5
Merge branch 'grafana:master' into feature/per-vu-grpc-tls-config-wit…
chrismoran-mica Jul 9, 2023
2d4ce15
Remove timeout from failing test
Jul 10, 2023
f71d3e0
Removed timeout on failing CI test
Jul 11, 2023
c35a3b1
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 12, 2023
954e982
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 12, 2023
05132c3
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 12, 2023
e0a13c7
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 12, 2023
692387d
Update js/modules/k6/grpc/client.go
chrismoran-mica Jul 12, 2023
af7770c
Fixed the test cased tied to the PR requested change that was committed
Jul 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 140 additions & 1 deletion js/modules/k6/grpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package grpc

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -108,6 +111,101 @@ func (c *Client) LoadProtoset(protosetPath string) ([]MethodInfo, error) {
return c.convertToMethodInfo(fdset)
}

// Note: this function was lifted from `lib/options.go`
func decryptPrivateKey(key, password []byte) ([]byte, error) {
block, _ := pem.Decode(key)
if block == nil {
return nil, errors.New("failed to decode PEM key")
}

blockType := block.Type
if blockType == "ENCRYPTED PRIVATE KEY" {
return nil, errors.New("encrypted pkcs8 formatted key is not supported")
}
/*
chrismoran-mica marked this conversation as resolved.
Show resolved Hide resolved
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* cryptography
ultimately though we want to support something so we will be using it for now.
*/
chrismoran-mica marked this conversation as resolved.
Show resolved Hide resolved
decryptedKey, err := x509.DecryptPEMBlock(block, password) //nolint:staticcheck
if err != nil {
return nil, err
}
key = pem.EncodeToMemory(&pem.Block{
Type: blockType,
Bytes: decryptedKey,
})
return key, nil
}

func buildTLSConfig(parentConfig *tls.Config, certificate, key []byte, caCertificates [][]byte) (*tls.Config, error) {
var cp *x509.CertPool
if len(caCertificates) > 0 {
chrismoran-mica marked this conversation as resolved.
Show resolved Hide resolved
cp, _ = x509.SystemCertPool()
for i, caCert := range caCertificates {
if ok := cp.AppendCertsFromPEM(caCert); !ok {
return nil, fmt.Errorf("failed to append ca certificate [%d] from PEM", i)
}
}
}

// Ignoring 'TLS MinVersion is too low' because this tls.Config will inherit MinValue and MaxValue
// from the vu state tls.Config

//nolint:golint,gosec
tlsCfg := &tls.Config{
CipherSuites: parentConfig.CipherSuites,
InsecureSkipVerify: parentConfig.InsecureSkipVerify,
MinVersion: parentConfig.MinVersion,
MaxVersion: parentConfig.MaxVersion,
Renegotiation: parentConfig.Renegotiation,
RootCAs: cp,
}
if len(certificate) > 0 && len(key) > 0 {
cert, err := tls.X509KeyPair(certificate, key)
if err != nil {
return nil, fmt.Errorf("failed to append certificate from PEM: %w", err)
}
tlsCfg.Certificates = []tls.Certificate{cert}
}
return tlsCfg, nil
}

func buildTLSConfigFromMap(parentConfig *tls.Config, tlsConfigMap map[string]interface{}) (*tls.Config, error) {
var cert, key, pass []byte
var ca [][]byte
var err error
if certstr, ok := tlsConfigMap["cert"].(string); ok {
cert = []byte(certstr)
}
if keystr, ok := tlsConfigMap["key"].(string); ok {
key = []byte(keystr)
}
if passwordStr, ok := tlsConfigMap["password"].(string); ok {
pass = []byte(passwordStr)
if len(pass) > 0 {
if key, err = decryptPrivateKey(key, pass); err != nil {
return nil, err
}
}
}
if cas, ok := tlsConfigMap["cacerts"]; ok {
var caCertsArray []interface{}
if caCertsArray, ok = cas.([]interface{}); ok {
ca = make([][]byte, len(caCertsArray))
for i, entry := range caCertsArray {
var entryStr string
if entryStr, ok = entry.(string); ok {
ca[i] = []byte(entryStr)
}
}
} else if caCertStr, caCertStrOk := cas.(string); caCertStrOk {
ca = [][]byte{[]byte(caCertStr)}
}
}
return buildTLSConfig(parentConfig, cert, key, ca)
}

// Connect is a block dial to the gRPC server at the given address (host:port)
func (c *Client) Connect(addr string, params map[string]interface{}) (bool, error) {
state := c.vu.State()
Expand All @@ -125,9 +223,13 @@ func (c *Client) Connect(addr string, params map[string]interface{}) (bool, erro
var tcred credentials.TransportCredentials
if !p.IsPlaintext {
tlsCfg := state.TLSConfig.Clone()
if len(p.TLS) > 0 {
if tlsCfg, err = buildTLSConfigFromMap(tlsCfg, p.TLS); err != nil {
return false, err
}
}
tlsCfg.NextProtos = []string{"h2"}

// TODO(rogchap): Would be good to add support for custom RootCAs (self signed)
tcred = credentials.NewTLS(tlsCfg)
} else {
tcred = insecure.NewCredentials()
Expand Down Expand Up @@ -373,6 +475,7 @@ type connectParams struct {
Timeout time.Duration
MaxReceiveSize int64
MaxSendSize int64
TLS map[string]interface{}
}

func (c *Client) parseConnectParams(raw map[string]interface{}) (connectParams, error) {
Expand Down Expand Up @@ -421,7 +524,43 @@ func (c *Client) parseConnectParams(raw map[string]interface{}) (connectParams,
if params.MaxSendSize < 0 {
return params, fmt.Errorf("invalid maxSendSize value: '%#v, it needs to be a positive integer", v)
}
case "tls":
var ok bool
params.TLS, ok = v.(map[string]interface{})

if !ok {
return params, fmt.Errorf("invalid tls value: '%#v', expected (optional) keys: cert, key, password, and cacerts", v)
}
// optional map keys below
if cert, certok := params.TLS["cert"]; certok {
if _, ok = cert.(string); !ok {
return params, fmt.Errorf("invalid tls cert value: '%#v', it needs to be a PEM formatted string", v)
}
}
if key, keyok := params.TLS["key"]; keyok {
if _, ok = key.(string); !ok {
return params, fmt.Errorf("invalid tls key value: '%#v', it needs to be a PEM formatted string", v)
}
}
if pass, passok := params.TLS["password"]; passok {
if _, ok = pass.(string); !ok {
return params, fmt.Errorf("invalid tls password value: '%#v', it needs to be a string", v)
}
}
if cacerts, cacertsok := params.TLS["cacerts"]; cacertsok {
var cacertsArray []interface{}
if cacertsArray, ok = cacerts.([]interface{}); ok {
for _, cacertsArrayEntry := range cacertsArray {
if _, ok = cacertsArrayEntry.(string); !ok {
return params, fmt.Errorf("invalid tls cacerts value: '%#v',"+
" it needs to be a string or an array of PEM formatted strings", v)
}
}
} else if _, ok = cacerts.(string); !ok {
return params, fmt.Errorf("invalid tls cacerts value: '%#v',"+
" it needs to be a string or an array of PEM formatted strings", v)
}
}
default:
return params, fmt.Errorf("unknown connect param: %q", k)
}
Expand Down
Loading