From 039c0ff7b11be2b2d94477e47e56d980a9f1e3e6 Mon Sep 17 00:00:00 2001 From: Jason Del Ponte Date: Tue, 8 Dec 2020 13:35:56 -0800 Subject: [PATCH] aws/session: Add support for client TLS certs on HTTP client (#3654) Adds support for the SDK to automatically modify the HTTP client to include TLS configuration of custom Client TLS certificate. This configuration can be provide via the environment variable or directly in code via the `session.Options` struct. These options are compatible with the AWS_CA_BUNDLE configuration. #### Environment variable configuration Both `AWS_SDK_GO_CLIENT_TLS_CERT`, and `AWS_SDK_GO_CLIENT_TLS_KEY` must be provided together, and must point to valid PEM encoded file containing the certificate, and key respectively. ``` AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key ``` #### In code configuration via session.Options Alternative configuration is to specify the `ClientTLSCert` and `ClientTLSKey` fields on the `session.Options` struction. These are `io.Reader`s that provide the PEM encoded content for the certificate and key files. ```go sess, err := session.NewSessionWithOptions(session.Options{ ClientTLSCert: myCertFile, ClientTLSKey: myKeyFile, }) ``` --- CHANGELOG_PENDING.md | 2 + aws/session/client_tls_cert_test.go | 191 ++++++++++++++++++ aws/session/custom_transport.go | 27 +++ ...ransport.go => custom_transport_go1.12.go} | 4 +- ...sport_1_5.go => custom_transport_go1.5.go} | 2 +- ...sport_1_6.go => custom_transport_go1.6.go} | 2 +- aws/session/doc.go | 27 +++ aws/session/env_config.go | 25 ++- aws/session/env_config_test.go | 78 +++++-- aws/session/session.go | 187 ++++++++++++++--- aws/session/shared_config.go | 13 ++ awstesting/client_tls_cert.go | 182 +++++++++++++++++ 12 files changed, 685 insertions(+), 55 deletions(-) create mode 100644 aws/session/client_tls_cert_test.go create mode 100644 aws/session/custom_transport.go rename aws/session/{cabundle_transport.go => custom_transport_go1.12.go} (88%) rename aws/session/{cabundle_transport_1_5.go => custom_transport_go1.5.go} (88%) rename aws/session/{cabundle_transport_1_6.go => custom_transport_go1.6.go} (90%) create mode 100644 awstesting/client_tls_cert.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8a1927a39ca..d8f161a6bc4 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,7 @@ ### SDK Features ### SDK Enhancements +* `aws/session`: Add support for client TLS certs on HTTP client ([#3654](https://github.com/aws/aws-sdk-go/pull/3654)) + * Adds support for the SDK to automatically modify the HTTP client to include TLS configuration of custom Client TLS certificate. ### SDK Bugs diff --git a/aws/session/client_tls_cert_test.go b/aws/session/client_tls_cert_test.go new file mode 100644 index 00000000000..177d5643273 --- /dev/null +++ b/aws/session/client_tls_cert_test.go @@ -0,0 +1,191 @@ +// +build go1.9 + +package session + +import ( + "crypto/x509" + "io" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/awstesting" +) + +func TestNewSession_WithClientTLSCert(t *testing.T) { + type testCase struct { + // Params + setup func(certFilename, keyFilename string) (Options, func(), error) + ExpectErr string + } + + cases := map[string]testCase{ + "env": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + os.Setenv(useClientTLSCert[0], certFilename) + os.Setenv(useClientTLSKey[0], keyFilename) + return Options{}, func() {}, nil + }, + }, + "env file not found": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + os.Setenv(useClientTLSCert[0], "some-cert-file-not-exists") + os.Setenv(useClientTLSKey[0], "some-key-file-not-exists") + return Options{}, func() {}, nil + }, + ExpectErr: "LoadClientTLSCertError", + }, + "env cert file only": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + os.Setenv(useClientTLSCert[0], certFilename) + return Options{}, func() {}, nil + }, + ExpectErr: "must both be provided", + }, + "env key file only": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + os.Setenv(useClientTLSKey[0], keyFilename) + return Options{}, func() {}, nil + }, + ExpectErr: "must both be provided", + }, + + "session options": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + certFile, err := os.Open(certFilename) + if err != nil { + return Options{}, nil, err + } + keyFile, err := os.Open(keyFilename) + if err != nil { + return Options{}, nil, err + } + + return Options{ + ClientTLSCert: certFile, + ClientTLSKey: keyFile, + }, func() { + certFile.Close() + keyFile.Close() + }, nil + }, + }, + "session cert load error": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + certFile, err := os.Open(certFilename) + if err != nil { + return Options{}, nil, err + } + keyFile, err := os.Open(keyFilename) + if err != nil { + return Options{}, nil, err + } + + stat, _ := certFile.Stat() + return Options{ + ClientTLSCert: io.LimitReader(certFile, stat.Size()/2), + ClientTLSKey: keyFile, + }, func() { + certFile.Close() + keyFile.Close() + }, nil + }, + ExpectErr: "unable to load x509 key pair", + }, + "session key load error": { + setup: func(certFilename, keyFilename string) (Options, func(), error) { + certFile, err := os.Open(certFilename) + if err != nil { + return Options{}, nil, err + } + keyFile, err := os.Open(keyFilename) + if err != nil { + return Options{}, nil, err + } + + stat, _ := keyFile.Stat() + return Options{ + ClientTLSCert: certFile, + ClientTLSKey: io.LimitReader(keyFile, stat.Size()/2), + }, func() { + certFile.Close() + keyFile.Close() + }, nil + }, + ExpectErr: "unable to load x509 key pair", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + // Asserts + restoreEnvFn := initSessionTestEnv() + defer restoreEnvFn() + + certFilename, keyFilename, err := awstesting.CreateClientTLSCertFiles() + if err != nil { + t.Fatalf("failed to create client certificate files, %v", err) + } + defer func() { + if err := awstesting.CleanupTLSBundleFiles(certFilename, keyFilename); err != nil { + t.Errorf("failed to cleanup client TLS cert files, %v", err) + } + }() + + opts, cleanup, err := c.setup(certFilename, keyFilename) + if err != nil { + t.Fatalf("test case failed setup, %v", err) + } + if cleanup != nil { + defer cleanup() + } + + server, err := awstesting.NewTLSClientCertServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + })) + if err != nil { + t.Fatalf("failed to load session, %v", err) + } + server.StartTLS() + defer server.Close() + + // Give server change to start + time.Sleep(time.Second) + + // Load SDK session with options configured. + sess, err := NewSessionWithOptions(opts) + if len(c.ExpectErr) != 0 { + if err == nil { + t.Fatalf("expect error, got none") + } + if e, a := c.ExpectErr, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expect error to contain %v, got %v", e, a) + } + return + } + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + // Clients need to add ca bundle for test service. + p := x509.NewCertPool() + p.AddCert(server.Certificate()) + client := sess.Config.HTTPClient + client.Transport.(*http.Transport).TLSClientConfig.RootCAs = p + + // Send request + req, _ := http.NewRequest("GET", server.URL, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("failed to send request, %v", err) + } + + if e, a := 200, resp.StatusCode; e != a { + t.Errorf("expect %v status code, got %v", e, a) + } + }) + } +} diff --git a/aws/session/custom_transport.go b/aws/session/custom_transport.go new file mode 100644 index 00000000000..593aedc4218 --- /dev/null +++ b/aws/session/custom_transport.go @@ -0,0 +1,27 @@ +// +build go1.13 + +package session + +import ( + "net" + "net/http" + "time" +) + +// Transport that should be used when a custom CA bundle is specified with the +// SDK. +func getCustomTransport() *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} diff --git a/aws/session/cabundle_transport.go b/aws/session/custom_transport_go1.12.go similarity index 88% rename from aws/session/cabundle_transport.go rename to aws/session/custom_transport_go1.12.go index ea9ebb6f6a2..1bf31cf8e56 100644 --- a/aws/session/cabundle_transport.go +++ b/aws/session/custom_transport_go1.12.go @@ -1,4 +1,4 @@ -// +build go1.7 +// +build !go1.13,go1.7 package session @@ -10,7 +10,7 @@ import ( // Transport that should be used when a custom CA bundle is specified with the // SDK. -func getCABundleTransport() *http.Transport { +func getCustomTransport() *http.Transport { return &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ diff --git a/aws/session/cabundle_transport_1_5.go b/aws/session/custom_transport_go1.5.go similarity index 88% rename from aws/session/cabundle_transport_1_5.go rename to aws/session/custom_transport_go1.5.go index fec39dfc126..253d7bc9d55 100644 --- a/aws/session/cabundle_transport_1_5.go +++ b/aws/session/custom_transport_go1.5.go @@ -10,7 +10,7 @@ import ( // Transport that should be used when a custom CA bundle is specified with the // SDK. -func getCABundleTransport() *http.Transport { +func getCustomTransport() *http.Transport { return &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ diff --git a/aws/session/cabundle_transport_1_6.go b/aws/session/custom_transport_go1.6.go similarity index 90% rename from aws/session/cabundle_transport_1_6.go rename to aws/session/custom_transport_go1.6.go index 1c5a5391e65..db240605441 100644 --- a/aws/session/cabundle_transport_1_6.go +++ b/aws/session/custom_transport_go1.6.go @@ -10,7 +10,7 @@ import ( // Transport that should be used when a custom CA bundle is specified with the // SDK. -func getCABundleTransport() *http.Transport { +func getCustomTransport() *http.Transport { return &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ diff --git a/aws/session/doc.go b/aws/session/doc.go index cc461bd3230..9419b518d58 100644 --- a/aws/session/doc.go +++ b/aws/session/doc.go @@ -208,6 +208,8 @@ env values as well. AWS_SDK_LOAD_CONFIG=1 +Custom Shared Config and Credential Files + Shared credentials file path can be set to instruct the SDK to use an alternative file for the shared credentials. If not set the file will be loaded from $HOME/.aws/credentials on Linux/Unix based systems, and @@ -222,6 +224,8 @@ $HOME/.aws/config on Linux/Unix based systems, and AWS_CONFIG_FILE=$HOME/my_shared_config +Custom CA Bundle + Path to a custom Credentials Authority (CA) bundle PEM file that the SDK will use instead of the default system's root CA bundle. Use this only if you want to replace the CA bundle the SDK uses for TLS requests. @@ -242,6 +246,29 @@ Setting a custom HTTPClient in the aws.Config options will override this setting To use this option and custom HTTP client, the HTTP client needs to be provided when creating the session. Not the service client. +Custom Client TLS Certificate + +The SDK supports the environment and session option being configured with +Client TLS certificates that are sent as a part of the client's TLS handshake +for client authentication. If used, both Cert and Key values are required. If +one is missing, or either fail to load the contents of the file an error will +be returned. + +HTTP Client's Transport concrete implementation must be a http.Transport +or creating the session will fail. + + AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key + AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert + +This can also be configured via the session.Options ClientTLSCert and ClientTLSKey. + + sess, err := session.NewSessionWithOptions(session.Options{ + ClientTLSCert: myCertFile, + ClientTLSKey: myKeyFile, + }) + +Custom EC2 IMDS Endpoint + The endpoint of the EC2 IMDS client can be configured via the environment variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a Session. See Options.EC2IMDSEndpoint for more details. diff --git a/aws/session/env_config.go b/aws/session/env_config.go index d67c261d74f..3cd5d4b5ae1 100644 --- a/aws/session/env_config.go +++ b/aws/session/env_config.go @@ -101,6 +101,18 @@ type envConfig struct { // AWS_CA_BUNDLE=$HOME/my_custom_ca_bundle CustomCABundle string + // Sets the TLC client certificate that should be used by the SDK's HTTP transport + // when making requests. The certificate must be paired with a TLS client key file. + // + // AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert + ClientTLSCert string + + // Sets the TLC client key that should be used by the SDK's HTTP transport + // when making requests. The key must be paired with a TLS client certificate file. + // + // AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key + ClientTLSKey string + csmEnabled string CSMEnabled *bool CSMPort string @@ -219,6 +231,15 @@ var ( ec2IMDSEndpointEnvKey = []string{ "AWS_EC2_METADATA_SERVICE_ENDPOINT", } + useCABundleKey = []string{ + "AWS_CA_BUNDLE", + } + useClientTLSCert = []string{ + "AWS_SDK_GO_CLIENT_TLS_CERT", + } + useClientTLSKey = []string{ + "AWS_SDK_GO_CLIENT_TLS_KEY", + } ) // loadEnvConfig retrieves the SDK's environment configuration. @@ -302,7 +323,9 @@ func envConfigLoad(enableSharedConfig bool) (envConfig, error) { cfg.SharedConfigFile = defaults.SharedConfigFilename() } - cfg.CustomCABundle = os.Getenv("AWS_CA_BUNDLE") + setFromEnvVal(&cfg.CustomCABundle, useCABundleKey) + setFromEnvVal(&cfg.ClientTLSCert, useClientTLSCert) + setFromEnvVal(&cfg.ClientTLSKey, useClientTLSKey) var err error // STS Regional Endpoint variable diff --git a/aws/session/env_config_test.go b/aws/session/env_config_test.go index ebc294ad8a0..a1d30a45e83 100644 --- a/aws/session/env_config_test.go +++ b/aws/session/env_config_test.go @@ -107,7 +107,7 @@ func TestLoadEnvConfig(t *testing.T) { UseSharedConfigCall bool Config envConfig }{ - { + 0: { Env: map[string]string{ "AWS_REGION": "region", "AWS_PROFILE": "profile", @@ -118,7 +118,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 1: { Env: map[string]string{ "AWS_REGION": "region", "AWS_DEFAULT_REGION": "default_region", @@ -131,7 +131,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 2: { Env: map[string]string{ "AWS_REGION": "region", "AWS_DEFAULT_REGION": "default_region", @@ -146,7 +146,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 3: { Env: map[string]string{ "AWS_DEFAULT_REGION": "default_region", "AWS_DEFAULT_PROFILE": "default_profile", @@ -156,7 +156,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 4: { Env: map[string]string{ "AWS_DEFAULT_REGION": "default_region", "AWS_DEFAULT_PROFILE": "default_profile", @@ -169,7 +169,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 5: { Env: map[string]string{ "AWS_REGION": "region", "AWS_PROFILE": "profile", @@ -182,7 +182,7 @@ func TestLoadEnvConfig(t *testing.T) { }, UseSharedConfigCall: true, }, - { + 6: { Env: map[string]string{ "AWS_REGION": "region", "AWS_DEFAULT_REGION": "default_region", @@ -197,7 +197,7 @@ func TestLoadEnvConfig(t *testing.T) { }, UseSharedConfigCall: true, }, - { + 7: { Env: map[string]string{ "AWS_REGION": "region", "AWS_DEFAULT_REGION": "default_region", @@ -213,7 +213,7 @@ func TestLoadEnvConfig(t *testing.T) { }, UseSharedConfigCall: true, }, - { + 8: { Env: map[string]string{ "AWS_DEFAULT_REGION": "default_region", "AWS_DEFAULT_PROFILE": "default_profile", @@ -226,7 +226,7 @@ func TestLoadEnvConfig(t *testing.T) { }, UseSharedConfigCall: true, }, - { + 9: { Env: map[string]string{ "AWS_DEFAULT_REGION": "default_region", "AWS_DEFAULT_PROFILE": "default_profile", @@ -240,7 +240,7 @@ func TestLoadEnvConfig(t *testing.T) { }, UseSharedConfigCall: true, }, - { + 10: { Env: map[string]string{ "AWS_CA_BUNDLE": "custom_ca_bundle", }, @@ -250,7 +250,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 11: { Env: map[string]string{ "AWS_CA_BUNDLE": "custom_ca_bundle", }, @@ -262,7 +262,51 @@ func TestLoadEnvConfig(t *testing.T) { }, UseSharedConfigCall: true, }, - { + 12: { + Env: map[string]string{ + "AWS_SDK_GO_CLIENT_TLS_CERT": "client_tls_cert", + }, + Config: envConfig{ + ClientTLSCert: "client_tls_cert", + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, + 13: { + Env: map[string]string{ + "AWS_SDK_GO_CLIENT_TLS_CERT": "client_tls_cert", + }, + Config: envConfig{ + ClientTLSCert: "client_tls_cert", + EnableSharedConfig: true, + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + UseSharedConfigCall: true, + }, + 14: { + Env: map[string]string{ + "AWS_SDK_GO_CLIENT_TLS_KEY": "client_tls_key", + }, + Config: envConfig{ + ClientTLSKey: "client_tls_key", + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + }, + 15: { + Env: map[string]string{ + "AWS_SDK_GO_CLIENT_TLS_KEY": "client_tls_key", + }, + Config: envConfig{ + ClientTLSKey: "client_tls_key", + EnableSharedConfig: true, + SharedCredentialsFile: shareddefaults.SharedCredentialsFilename(), + SharedConfigFile: shareddefaults.SharedConfigFilename(), + }, + UseSharedConfigCall: true, + }, + 16: { Env: map[string]string{ "AWS_SHARED_CREDENTIALS_FILE": "/path/to/credentials/file", "AWS_CONFIG_FILE": "/path/to/config/file", @@ -272,7 +316,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: "/path/to/config/file", }, }, - { + 17: { Env: map[string]string{ "AWS_STS_REGIONAL_ENDPOINTS": "regional", }, @@ -282,7 +326,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 18: { Env: map[string]string{ "AWS_S3_US_EAST_1_REGIONAL_ENDPOINT": "regional", }, @@ -292,7 +336,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 19: { Env: map[string]string{ "AWS_S3_USE_ARN_REGION": "true", }, @@ -302,7 +346,7 @@ func TestLoadEnvConfig(t *testing.T) { SharedConfigFile: shareddefaults.SharedConfigFilename(), }, }, - { + 20: { Env: map[string]string{ "AWS_EC2_METADATA_SERVICE_ENDPOINT": "http://example.aws", }, diff --git a/aws/session/session.go b/aws/session/session.go index 6430a7f1526..08713cc3474 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -25,6 +25,13 @@ const ( // ErrCodeSharedConfig represents an error that occurs in the shared // configuration logic ErrCodeSharedConfig = "SharedConfigErr" + + // ErrCodeLoadCustomCABundle error code for unable to load custom CA bundle. + ErrCodeLoadCustomCABundle = "LoadCustomCABundleError" + + // ErrCodeLoadClientTLSCert error code for unable to load client TLS + // certificate or key + ErrCodeLoadClientTLSCert = "LoadClientTLSCertError" ) // ErrSharedConfigSourceCollision will be returned if a section contains both @@ -229,17 +236,46 @@ type Options struct { // the SDK will use instead of the default system's root CA bundle. Use this // only if you want to replace the CA bundle the SDK uses for TLS requests. // - // Enabling this option will attempt to merge the Transport into the SDK's HTTP - // client. If the client's Transport is not a http.Transport an error will be - // returned. If the Transport's TLS config is set this option will cause the SDK + // HTTP Client's Transport concrete implementation must be a http.Transport + // or creating the session will fail. + // + // If the Transport's TLS config is set this option will cause the SDK // to overwrite the Transport's TLS config's RootCAs value. If the CA // bundle reader contains multiple certificates all of them will be loaded. // - // The Session option CustomCABundle is also available when creating sessions - // to also enable this feature. CustomCABundle session option field has priority - // over the AWS_CA_BUNDLE environment variable, and will be used if both are set. + // Can also be specified via the environment variable: + // + // AWS_CA_BUNDLE=$HOME/ca_bundle + // + // Can also be specified via the shared config field: + // + // ca_bundle = $HOME/ca_bundle CustomCABundle io.Reader + // Reader for the TLC client certificate that should be used by the SDK's + // HTTP transport when making requests. The certificate must be paired with + // a TLS client key file. Will be ignored if both are not provided. + // + // HTTP Client's Transport concrete implementation must be a http.Transport + // or creating the session will fail. + // + // Can also be specified via the environment variable: + // + // AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert + ClientTLSCert io.Reader + + // Reader for the TLC client key that should be used by the SDK's HTTP + // transport when making requests. The key must be paired with a TLS client + // certificate file. Will be ignored if both are not provided. + // + // HTTP Client's Transport concrete implementation must be a http.Transport + // or creating the session will fail. + // + // Can also be specified via the environment variable: + // + // AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key + ClientTLSKey io.Reader + // The handlers that the session and all API clients will be created with. // This must be a complete set of handlers. Use the defaults.Handlers() // function to initialize this value before changing the handlers to be @@ -319,17 +355,6 @@ func NewSessionWithOptions(opts Options) (*Session, error) { envCfg.EnableSharedConfig = true } - // Only use AWS_CA_BUNDLE if session option is not provided. - if len(envCfg.CustomCABundle) != 0 && opts.CustomCABundle == nil { - f, err := os.Open(envCfg.CustomCABundle) - if err != nil { - return nil, awserr.New("LoadCustomCABundleError", - "failed to open custom CA bundle PEM file", err) - } - defer f.Close() - opts.CustomCABundle = f - } - return newSession(opts, envCfg, &opts.Config) } @@ -460,6 +485,10 @@ func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session, return nil, err } + if err := setTLSOptions(&opts, cfg, envCfg, sharedCfg); err != nil { + return nil, err + } + s := &Session{ Config: cfg, Handlers: handlers, @@ -479,13 +508,6 @@ func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session, } } - // Setup HTTP client with custom cert bundle if enabled - if opts.CustomCABundle != nil { - if err := loadCustomCABundle(s, opts.CustomCABundle); err != nil { - return nil, err - } - } - return s, nil } @@ -529,22 +551,83 @@ func loadCSMConfig(envCfg envConfig, cfgFiles []string) (csmConfig, error) { return csmConfig{}, nil } -func loadCustomCABundle(s *Session, bundle io.Reader) error { +func setTLSOptions(opts *Options, cfg *aws.Config, envCfg envConfig, sharedCfg sharedConfig) error { + // CA Bundle can be specified in both environment variable shared config file. + var caBundleFilename = envCfg.CustomCABundle + if len(caBundleFilename) == 0 { + caBundleFilename = sharedCfg.CustomCABundle + } + + // Only use environment value if session option is not provided. + customTLSOptions := map[string]struct { + filename string + field *io.Reader + errCode string + }{ + "custom CA bundle PEM": {filename: caBundleFilename, field: &opts.CustomCABundle, errCode: ErrCodeLoadCustomCABundle}, + "custom client TLS cert": {filename: envCfg.ClientTLSCert, field: &opts.ClientTLSCert, errCode: ErrCodeLoadClientTLSCert}, + "custom client TLS key": {filename: envCfg.ClientTLSKey, field: &opts.ClientTLSKey, errCode: ErrCodeLoadClientTLSCert}, + } + for name, v := range customTLSOptions { + if len(v.filename) != 0 && *v.field == nil { + f, err := os.Open(v.filename) + if err != nil { + return awserr.New(v.errCode, fmt.Sprintf("failed to open %s file", name), err) + } + defer f.Close() + *v.field = f + } + } + + // Setup HTTP client with custom cert bundle if enabled + if opts.CustomCABundle != nil { + if err := loadCustomCABundle(cfg.HTTPClient, opts.CustomCABundle); err != nil { + return err + } + } + + // Setup HTTP client TLS certificate and key for client TLS authentication. + if opts.ClientTLSCert != nil && opts.ClientTLSKey != nil { + if err := loadClientTLSCert(cfg.HTTPClient, opts.ClientTLSCert, opts.ClientTLSKey); err != nil { + return err + } + } else if opts.ClientTLSCert == nil && opts.ClientTLSKey == nil { + // Do nothing if neither values are available. + + } else { + return awserr.New(ErrCodeLoadClientTLSCert, + fmt.Sprintf("client TLS cert(%t) and key(%t) must both be provided", + opts.ClientTLSCert != nil, opts.ClientTLSKey != nil), nil) + } + + return nil +} + +func getHTTPTransport(client *http.Client) (*http.Transport, error) { var t *http.Transport - switch v := s.Config.HTTPClient.Transport.(type) { + switch v := client.Transport.(type) { case *http.Transport: t = v default: - if s.Config.HTTPClient.Transport != nil { - return awserr.New("LoadCustomCABundleError", - "unable to load custom CA bundle, HTTPClient's transport unsupported type", nil) + if client.Transport != nil { + return nil, fmt.Errorf("unsupported transport, %T", client.Transport) } } if t == nil { // Nil transport implies `http.DefaultTransport` should be used. Since // the SDK cannot modify, nor copy the `DefaultTransport` specifying // the values the next closest behavior. - t = getCABundleTransport() + t = getCustomTransport() + } + + return t, nil +} + +func loadCustomCABundle(client *http.Client, bundle io.Reader) error { + t, err := getHTTPTransport(client) + if err != nil { + return awserr.New(ErrCodeLoadCustomCABundle, + "unable to load custom CA bundle, HTTPClient's transport unsupported type", err) } p, err := loadCertPool(bundle) @@ -556,7 +639,7 @@ func loadCustomCABundle(s *Session, bundle io.Reader) error { } t.TLSClientConfig.RootCAs = p - s.Config.HTTPClient.Transport = t + client.Transport = t return nil } @@ -564,19 +647,57 @@ func loadCustomCABundle(s *Session, bundle io.Reader) error { func loadCertPool(r io.Reader) (*x509.CertPool, error) { b, err := ioutil.ReadAll(r) if err != nil { - return nil, awserr.New("LoadCustomCABundleError", + return nil, awserr.New(ErrCodeLoadCustomCABundle, "failed to read custom CA bundle PEM file", err) } p := x509.NewCertPool() if !p.AppendCertsFromPEM(b) { - return nil, awserr.New("LoadCustomCABundleError", + return nil, awserr.New(ErrCodeLoadCustomCABundle, "failed to load custom CA bundle PEM file", err) } return p, nil } +func loadClientTLSCert(client *http.Client, certFile, keyFile io.Reader) error { + t, err := getHTTPTransport(client) + if err != nil { + return awserr.New(ErrCodeLoadClientTLSCert, + "unable to get usable HTTP transport from client", err) + } + + cert, err := ioutil.ReadAll(certFile) + if err != nil { + return awserr.New(ErrCodeLoadClientTLSCert, + "unable to get read client TLS cert file", err) + } + + key, err := ioutil.ReadAll(keyFile) + if err != nil { + return awserr.New(ErrCodeLoadClientTLSCert, + "unable to get read client TLS key file", err) + } + + clientCert, err := tls.X509KeyPair(cert, key) + if err != nil { + return awserr.New(ErrCodeLoadClientTLSCert, + "unable to load x509 key pair from client cert", err) + } + + tlsCfg := t.TLSClientConfig + if tlsCfg == nil { + tlsCfg = &tls.Config{} + } + + tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) + + t.TLSClientConfig = tlsCfg + client.Transport = t + + return nil +} + func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg sharedConfig, handlers request.Handlers, diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index 680805a38ad..be7daacf308 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -34,6 +34,9 @@ const ( // Additional Config fields regionKey = `region` + // custom CA Bundle filename + customCABundleKey = `ca_bundle` + // endpoint discovery group enableEndpointDiscoveryKey = `endpoint_discovery_enabled` // optional @@ -90,6 +93,15 @@ type sharedConfig struct { // region Region string + // CustomCABundle is the file path to a PEM file the SDK will read and + // use to configure the HTTP transport with additional CA certs that are + // not present in the platforms default CA store. + // + // This value will be ignored if the file does not exist. + // + // ca_bundle + CustomCABundle string + // EnableEndpointDiscovery can be enabled in the shared config by setting // endpoint_discovery_enabled to true // @@ -276,6 +288,7 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e updateString(&cfg.SourceProfileName, section, sourceProfileKey) updateString(&cfg.CredentialSource, section, credentialSourceKey) updateString(&cfg.Region, section, regionKey) + updateString(&cfg.CustomCABundle, section, customCABundleKey) if section.Has(roleDurationSecondsKey) { d := time.Duration(section.Int(roleDurationSecondsKey)) * time.Second diff --git a/awstesting/client_tls_cert.go b/awstesting/client_tls_cert.go new file mode 100644 index 00000000000..9c83053b47b --- /dev/null +++ b/awstesting/client_tls_cert.go @@ -0,0 +1,182 @@ +package awstesting + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "net/http/httptest" +) + +// NewTLSClientCertServer creates a new HTTP test server initialize to require +// HTTP clients authenticate with TLS client certificates. +func NewTLSClientCertServer(handler http.Handler) (*httptest.Server, error) { + server := httptest.NewUnstartedServer(handler) + + if server.TLS == nil { + server.TLS = &tls.Config{} + } + server.TLS.ClientAuth = tls.RequireAndVerifyClientCert + + if server.TLS.ClientCAs == nil { + server.TLS.ClientCAs = x509.NewCertPool() + } + certPem := append(ClientTLSCert, ClientTLSKey...) + if ok := server.TLS.ClientCAs.AppendCertsFromPEM(certPem); !ok { + return nil, fmt.Errorf("failed to append client certs") + } + + return server, nil +} + +// CreateClientTLSCertFiles returns a set of temporary files for the client +// certificate and key files. +func CreateClientTLSCertFiles() (cert, key string, err error) { + cert, err = createTmpFile(ClientTLSCert) + if err != nil { + return "", "", err + } + + key, err = createTmpFile(ClientTLSKey) + if err != nil { + return "", "", err + } + + return cert, key, nil +} + +/* +Client certificate generation + +# Create CA +openssl genrsa -aes256 -passout pass:xxxx -out ca.pass.key 4096 +openssl rsa -passin pass:xxxx -in ca.pass.key -out ca.key +rm ca.pass.key +openssl req -new -x509 -days 3650 -key ca.key -out ca.pem + +# Create key for client +openssl genrsa -aes256 -passout pass:xxxx -out 01-client.pass.key 4096 +openssl rsa -passin pass:xxxx -in 01-client.pass.key -out 01-client.key +rm 01-client.pass.key + +# create csr for client +openssl req -new -key 01-client.key -out 01-client.csr +openssl x509 -req -days 3650 -in 01-client.csr -CA ca.pem -CAkey ca.key -set_serial 01 -out 01-client.pem +cat 01-client.key 01-client.pem ca.pem > 01-client.full.pem +*/ + +var ( + // ClientTLSCert 01-client.pem + ClientTLSCert = []byte(`-----BEGIN CERTIFICATE----- +MIIEjjCCAnYCAQEwDQYJKoZIhvcNAQEFBQAwDTELMAkGA1UEBhMCVVMwHhcNMjAx +MTI0MDAyODI5WhcNMzAxMTIyMDAyODI5WjANMQswCQYDVQQGEwJVUzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAN8gy1UtBR73fCJ9JWIREfBtqW/+hfNn +ZyIu7bc4MTWoP1dYG3CVV+HALijfVNeFQaohXjWaUIaXAa4idtM1AAf+J8GADqHp +z4qnAoIWLqfWRwtFJyggB2tnzmFA/yxR2jlpe3yT/OL0aXtYgS9bVeH6nWWjNuAo +D6qlTGSB/7ns8iDUK0WRJsodRGPi8OHNm4q5Pxqbzfvzu2vmF66NcvNb/96yIngl +Sjv6CSTz16hbbmqQQJAXurjkOLbSFCYZ76D2pYmqS/hLpUlFH/Bd/BcVP/3H5INA +fodY9Rx1oXETNuC69QgLA+2zlhGmbICh+OexIqNb2RH6vwi7EV5/Y4v7CKwzypre +OgOtkYQiDjhG3CxB+8E4q5t43SKpft7KFUXWmTaxOxZr7gmuBZGV5Lxzg+NgnFnV +tkPCVxKYsSSdhs11z0Ne/BBsGXCw0YoJ7HacFuVCf//C/vqT7y2ivhao3oMlv3ae +HjfHi9WIsZbDBB37Kk4UFXlXO0WXijrH09wILDW3IQ65fYMUBIyKFt9hKGjWKfcg +BWuTgJ98eG+BmxP6PIWgZTo1XdWKcxPblidLkwU4OzzuHONSsoGL8eeTBC0WcUT0 +5H3bSVbkYQObKHe4fxCVUC/xEPgQga0NBlXLq0Zr8UnNPio7Vip5pzJ99ma4t4PN +TnP6f2B1zrjLAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAB2ei7140M68glYQk9vp +oOUz+8Ibg9ogo7LCk95jsFxGvBJu62mFfa1s1NMsWq4QHK5XG5nkq4ZUues6dl/B +LpSv8uwIwV7aNgZathJLxb2M4s32xPUodGbfaeVbk08qyEGMCo4QE000Hace/iFZ +jbNT6rkU6Edv/gsvHkVkCouMTsZhpMHezyrnSBAyxwqU82QVHbC2ByEQFNJ+0rCJ +gAzcXuWI/6X3+LQSQ44Y0n7nj7Rx6YidtwCoFoQ1oIAdlt6LyUKTtEUa3uN9Cdb6 +nO4VGNC5p4URImHTMdqxDn0xpTYw0q9P+hierZYViuCaEokNlaWNk2wGHBqRlgxv +ci2qox1GCtabhRGyWEUzC9N6coVQPh1xuay8oQB/oXzcwk8LnUaOdVgwhKya1fEt +MQrlS/Vsv6e18UQXN0OM3V6mUFa+5wu+C4Ly7XQJ6EUwYZ6LYqO5ypsfXr8GrS0p +32l5nB7r80Q6mjKCG6MB827rIqWQvfadUX5q0xizb/RDKk+SmqxnffY38WpqLWec +WpEghlkp2IYQFdg7WxoKXCpz1rv+BI28rowRkVeW6chGqO9zx6Sk/twosiamgRK1 +s2MhHZnvl1x4h+uPsST2b4FAyzuDXB39g7pUnAq9XVhWA6J4ndFduIh8jmVWdZBg +KJTU5ZEXpuI0w7WDrPwaIUbU +-----END CERTIFICATE----- +`) + + // ClientTLSKey 01-client.key + ClientTLSKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- +`) + + // ClientCA ca.pem + ClientCA = []byte(`-----BEGIN CERTIFICATE----- +MIIEljCCAn4CCQDzkVB8uGX1GDANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJV +UzAeFw0yMDExMjQwMDI3NDlaFw0zMDExMjIwMDI3NDlaMA0xCzAJBgNVBAYTAlVT +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6gfPPxYl/6n/GuPkQwiT +RTnH0mohDgBT8vA9RpE3ffI/qY8zO0rD/bHJIsN2neus+bFIciro5S/LxaZ2amx5 +Y1WZPOeNKW2r73FfwlhyhQ6a9noiXJYnMqcT3Hn7FLeEMtt3kYkXJw+9k230mBhn +L1vWP0KXNoi0B3T0SCJGhJAdjV/tTsTOKVnzaxaRfeXH//+S7McbEAA6m902+VDy +tLGjyjB4Ed+AKeQg59FOn6/Q+NBaCDCGPK1+R0NgVreT8yI0tIgDaApZUuKl6Iid +QNEHuGt77o8jgxKh93PDiVsgbnfRIkpHLwrJM8aDEMI34lSTt21MH6hbvBq6nA84 +HkK4oAhYhV+qK5/I3INjP6cEIuIgxYqxYpIY31zmqkF0g0BtDy20zIwLohNVdxiB +1tMBRl1c4A1G2E1oZG+0BhO10xCWvk3pR5cdOsJCB7SnwQV895V3R1R12HqKNW11 +8e5e5Vef7GnAbACgZKQwZGRnpa7BiClC4j5BOUgN33G8mUK1j19/8fo7HOg+qOLk +WTp+u0Dr/12WKrJc+p413ltwhbbxtpTsBKnqeRvp628pT3YY1aUP5iC4Ph7bAN/1 +ziMgaKA/97A3UWgTEmLwzrhIAPsMU/zDa3FhI0cY3dDHD10iz303mZRfC97F6c8C +25VXx8/3pqpoLfYHhh9HtR8CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAANq6OnTW +xzxzcjB4UY0+XlXtgqSUy9DAmAAgfbFy+dgBtsYb45vvkKWLVrDov/spYch3S61I +Ba7bNWoqTkuOVVXcZtvG8zA8oH8xrU/Fgt+MIDCEFxuWttvroVEsomWyBh903fSB +y5c+gj4XvM5aYuLfljEi0R6qJNANIyyfSZkj6qR2yYm+Q7zK6SBCTlEfNdwuJfzy +ef4GJLotvx2+my8/DnUN4isDCQIdndXXhk2jlkQX839J84xOdGg2LtfjJPv/yDoY +ZkXcZF939jgg1Y7ppMg0BwhgqgfYCEf063O0C3elX41TL53hEIpu6/Qc9BbfkuxD +OO4mH2fGNXOGFo/liU+vQ9WNYHfPur1DcaMF2cKkaiK8EU53i+INU/94infU57fE +o2q6Wyzk82ozuyFsauKpXIUY5AiP2ovoMPcIE9Rfg38LpNtRLW/mFPuPK8hoQYdl +BKI5TeWiX0SvzsqlrMP814uwhFe/0l7heVuiDTIh4+rzXew5v8JmsPjFWAQvaNL8 +tCTTIWUmJSMLbnQeZocDgp/vQUrCgj0OUgt9ScfZfevnhsUz1KvKO6gXyJamcs0S +zPTgPDpOZoBCbJdkM3J02ypSyQou2HYW+6C2CRZF+E3/Ef98RUembqiu2djP03ma +qhpIGyqpydp464PMJJsCSGEwGA3SDMFhc5E= +-----END CERTIFICATE----- +`) +)