diff --git a/go.mod b/go.mod index 74a6e1b..df18cc0 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,17 @@ go 1.20 require ( github.com/alecthomas/assert/v2 v2.3.0 + github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 github.com/sirupsen/logrus v1.9.3 - github.com/superfly/macaroon v0.2.14-0.20240718172852-139f90b76537 + github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648 golang.org/x/crypto v0.12.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 ) require ( github.com/alecthomas/repr v0.2.0 // indirect + github.com/aws/smithy-go v1.20.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect diff --git a/go.sum b/go.sum index 619b9f4..496e1f2 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVd github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,6 +30,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/superfly/macaroon v0.2.14-0.20240718172852-139f90b76537 h1:xL2tIkau3Dr3dd4WOLbGz14kRcF49x15bVIMdOkLTyI= github.com/superfly/macaroon v0.2.14-0.20240718172852-139f90b76537/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo= +github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648 h1:YQG1v1QcTFQxJureNBcbtxosZ98u78ceUNCDQgI/vgM= +github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= diff --git a/processor.go b/processor.go index 377411e..e6cfad9 100644 --- a/processor.go +++ b/processor.go @@ -11,7 +11,10 @@ import ( "net/http" "net/textproto" "strings" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "golang.org/x/exp/slices" ) @@ -145,6 +148,100 @@ func (c *OAuthProcessorConfig) Processor(params map[string]string) (RequestProce }, nil } +type Sigv4ProcessorConfig struct { + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` +} + +var _ ProcessorConfig = (*Sigv4ProcessorConfig)(nil) + +func (c *Sigv4ProcessorConfig) Processor(params map[string]string) (RequestProcessor, error) { + + if len(c.AccessKey) == 0 { + return nil, errors.New("missing access key") + } + if len(c.SecretKey) == 0 { + return nil, errors.New("missing secret key") + } + + return func(r *http.Request) error { + // NOTE: Sigv4 has defenses against request forgery and reuse. + // This does *not* make those guarantees, and likely can not. + + var ( + service string + region string + date time.Time + err error + ) + + // Parse the auth header to get the service, region, and date + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return errors.New("expected request to contain an Authorization header") + } + + for _, section := range strings.Split(authHeader, " ") { + section = strings.TrimRight(section, ",") + keyValuePair := strings.SplitN(section, "=", 2) + if len(keyValuePair) != 2 { + continue + } + + if keyValuePair[0] == "Credential" { + credParts := strings.Split(keyValuePair[1], "/") + if len(credParts) != 5 { + return errors.New("invalid credential in auth header") + } + + dateStr := credParts[1] + date, err = time.Parse("20060102", dateStr) + if err != nil { + return err + } + + service = credParts[2] + region = credParts[3] + break + } + } + if timestamp := r.Header.Get("X-Amz-Date"); timestamp != "" { + date, err = time.Parse("20060102T150405Z", timestamp) + if err != nil { + return err + } + } + if service == "" || region == "" || date.IsZero() { + return errors.New("expected valid sigv4 authentication header in request to tokenizer") + } + + // Strip the Authorization header from the request + r.Header.Del("Authorization") + + credentials := aws.Credentials{ + AccessKeyID: c.AccessKey, + SecretAccessKey: c.SecretKey, + } + + // HACK: We have to strip the filtered headers *before* the request gets signed, + // since sigv4 expects a signature of all the request's headers. + for _, h := range FilteredHeaders { + r.Header.Del(h) + } + // Remove headers that goproxy will strip out later anyway. Otherwise, the header signature check will fail. + // https://github.com/elazarl/goproxy/blob/8b0c205063807802a7ac1d75351a90172a9c83fb/proxy.go#L87-L92 + r.Header.Del("Accept-Encoding") + r.Header.Del("Proxy-Connection") + r.Header.Del("Proxy-Authenticate") + r.Header.Del("Proxy-Authorization") + + signer := v4.NewSigner() + err = signer.SignHTTP(r.Context(), credentials, r, r.Header.Get("X-Amz-Content-Sha256"), service, region, date) + + return err + }, nil +} + // A helper type to be embedded in RequestProcessors wanting to use the `fmt` config/param. type FmtProcessor struct { Fmt string `json:"fmt,omitempty"` diff --git a/secret.go b/secret.go index c3eeb9a..db31fb4 100644 --- a/secret.go +++ b/secret.go @@ -53,6 +53,7 @@ type wireSecret struct { *InjectProcessorConfig `json:"inject_processor,omitempty"` *InjectHMACProcessorConfig `json:"inject_hmac_processor,omitempty"` *OAuthProcessorConfig `json:"oauth2_processor,omitempty"` + *Sigv4ProcessorConfig `json:"sigv4_processor,omitempty"` *BearerAuthConfig `json:"bearer_auth,omitempty"` *MacaroonAuthConfig `json:"macaroon_auth,omitempty"` *FlyioMacaroonAuthConfig `json:"flyio_macaroon_auth,omitempty"` @@ -81,6 +82,8 @@ func (s *Secret) MarshalJSON() ([]byte, error) { ws.InjectHMACProcessorConfig = p case *OAuthProcessorConfig: ws.OAuthProcessorConfig = p + case *Sigv4ProcessorConfig: + ws.Sigv4ProcessorConfig = p default: return nil, errors.New("bad processor config") } @@ -121,6 +124,10 @@ func (s *Secret) UnmarshalJSON(b []byte) error { np += 1 s.ProcessorConfig = ws.OAuthProcessorConfig } + if ws.Sigv4ProcessorConfig != nil { + np += 1 + s.ProcessorConfig = ws.Sigv4ProcessorConfig + } if np != 1 { return errors.New("bad processor config") }