From 0aa66383cd48358b643e7415266b8b9e32ba748d Mon Sep 17 00:00:00 2001 From: enguer Date: Sun, 9 Mar 2025 11:39:34 +0100 Subject: [PATCH 1/4] permit non-standard region remove white space on authorization header compare --- handler.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/handler.go b/handler.go index 9bd8b0f..f7061f5 100644 --- a/handler.go +++ b/handler.go @@ -17,7 +17,7 @@ import ( log "github.com/sirupsen/logrus" ) -var awsAuthorizationCredentialRegexp = regexp.MustCompile("Credential=([a-zA-Z0-9]+)/[0-9]+/([a-z]+-?[a-z]+-?[0-9]+)/s3/aws4_request") +var awsAuthorizationCredentialRegexp = regexp.MustCompile("Credential=([a-zA-Z0-9]+)/[0-9]+/([a-zA-Z-0-9]+)/s3/aws4_request") var awsAuthorizationSignedHeadersRegexp = regexp.MustCompile("SignedHeaders=([a-zA-Z0-9;-]+)") // Handler is a special handler that re-signs any AWS S3 request and sends it upstream @@ -225,9 +225,12 @@ func (h *Handler) buildUpstreamRequest(req *http.Request) (*http.Request, error) return nil, err } + //Sanitize fakeReq to remove some white spaces + fakeAuthorizationStr := strings.Replace(strings.Replace(fakeReq.Header["Authorization"][0],", Signature",",Signature",1),", SignedHeaders",",SignedHeaders",1); + // Verify that the fake request and the incoming request have the same signature // This ensures it was sent and signed by a client with correct AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY - cmpResult := subtle.ConstantTimeCompare([]byte(fakeReq.Header["Authorization"][0]), []byte(req.Header["Authorization"][0])) + cmpResult := subtle.ConstantTimeCompare([]byte(fakeAuthorizationStr), []byte(req.Header["Authorization"][0])) if cmpResult == 0 { v, _ := httputil.DumpRequest(fakeReq, false) log.Debugf("Fake request: %v", string(v)) From 45546ce06f66b576dc155d04583c738ec8200331 Mon Sep 17 00:00:00 2001 From: enguer Date: Sun, 9 Mar 2025 13:26:30 +0100 Subject: [PATCH 2/4] add tests to validate s3cmd compatibility add comments on less restrictive region regexp add /health path on metrics httpserve --- handler.go | 14 ++++++++++---- handler_test.go | 20 ++++++++++++++++++++ main.go | 7 ++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/handler.go b/handler.go index f7061f5..7f6a7af 100644 --- a/handler.go +++ b/handler.go @@ -16,7 +16,9 @@ import ( v4 "github.com/aws/aws-sdk-go/aws/signer/v4" log "github.com/sirupsen/logrus" ) - +// - new less strict regexp in order to allow different region naming (compatibility with other providers) +// - east-eu-1 => pass (aws style) +// - gra => pass (ceph style) var awsAuthorizationCredentialRegexp = regexp.MustCompile("Credential=([a-zA-Z0-9]+)/[0-9]+/([a-zA-Z-0-9]+)/s3/aws4_request") var awsAuthorizationSignedHeadersRegexp = regexp.MustCompile("SignedHeaders=([a-zA-Z0-9;-]+)") @@ -225,12 +227,16 @@ func (h *Handler) buildUpstreamRequest(req *http.Request) (*http.Request, error) return nil, err } - //Sanitize fakeReq to remove some white spaces - fakeAuthorizationStr := strings.Replace(strings.Replace(fakeReq.Header["Authorization"][0],", Signature",",Signature",1),", SignedHeaders",",SignedHeaders",1); + // WORKAROUND S3CMD which dont use white space before the some commas in the authorization header + fakeAuthorizationStr := fakeReq.Header.Get("Authorization") + // Sanitize fakeReq to remove white spaces before the comma signature + authorizationStr := strings.Replace(req.Header["Authorization"][0],",Signature",", Signature",1) + // Sanitize fakeReq to remove white spaces before the comma signheaders + authorizationStr = strings.Replace(authorizationStr,",SignedHeaders",", SignedHeaders",1) // Verify that the fake request and the incoming request have the same signature // This ensures it was sent and signed by a client with correct AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY - cmpResult := subtle.ConstantTimeCompare([]byte(fakeAuthorizationStr), []byte(req.Header["Authorization"][0])) + cmpResult := subtle.ConstantTimeCompare([]byte(fakeAuthorizationStr), []byte(authorizationStr)) if cmpResult == 0 { v, _ := httputil.DumpRequest(fakeReq, false) log.Debugf("Fake request: %v", string(v)) diff --git a/handler_test.go b/handler_test.go index 8bfc3af..22846a6 100644 --- a/handler_test.go +++ b/handler_test.go @@ -79,6 +79,12 @@ func verifySignature(w http.ResponseWriter, r *http.Request) { signer.Sign(r, body, "s3", "eu-test-1", signTime) expectedAuthorization := r.Header["Authorization"][0] + // WORKAROUND S3CMD who dont use white space before the comma in the authorization header + // Sanitize fakeReq to remove white spaces before the comma signature + receivedAuthorization = strings.Replace(receivedAuthorization,",Signature",", Signature",1) + // Sanitize fakeReq to remove white spaces before the comma signheaders + receivedAuthorization = strings.Replace(receivedAuthorization,",SignedHeaders",", SignedHeaders",1) + // verify signature fmt.Fprintln(w, receivedAuthorization, expectedAuthorization) if receivedAuthorization == expectedAuthorization { @@ -143,6 +149,20 @@ func TestHandlerValidSignature(t *testing.T) { assert.Equal(t, 200, resp.Code) assert.Contains(t, resp.Body.String(), "Hello, client") } +func TestHandlerValidSignatureS3cmd(t *testing.T) { + h := newTestProxy(t) + + req := httptest.NewRequest(http.MethodGet, "http://foobar.example.com", nil) + signRequest(req) + authorizationReq := req.Header.Get("Authorization"); + authorizationReq = strings.Replace(authorizationReq,", Signature",",Signature",1) + authorizationReq = strings.Replace(authorizationReq,", SignedHeaders",",SignedHeaders",1) + req.Header.Set("Authorization",authorizationReq); + resp := httptest.NewRecorder() + h.ServeHTTP(resp, req) + assert.Equal(t, 200, resp.Code) + assert.Contains(t, resp.Body.String(), "Hello, client") +} func TestHandlerInvalidCredential(t *testing.T) { h := newTestProxy(t) diff --git a/main.go b/main.go index 3ba6826..aaca239 100644 --- a/main.go +++ b/main.go @@ -100,7 +100,10 @@ func NewAwsS3ReverseProxy(opts Options) (*Handler, error) { } return handler, nil } - +//handle /health path +func health(w http.ResponseWriter, req *http.Request){ + fmt.Fprintf(w,"ok") +} func main() { opts := NewOptions() handler, err := NewAwsS3ReverseProxy(opts) @@ -136,6 +139,8 @@ func main() { var wrappedHandler http.Handler = handler if len(opts.MetricsListenAddr) > 0 && len(strings.Split(opts.MetricsListenAddr, ":")) == 2 { metricsHandler := http.NewServeMux() + //add health on metrics http to serve k8s liveness + metricsHandler.HandleFunc("/health",health) metricsHandler.Handle("/metrics", promhttp.Handler()) log.Infof("Listening for secure Prometheus metrics on %s", opts.MetricsListenAddr) From 284d0dc50ca47ee845cafea3df47cd3074e31a35 Mon Sep 17 00:00:00 2001 From: enguer Date: Sun, 9 Mar 2025 14:32:28 +0100 Subject: [PATCH 3/4] add tests s3cmd comments refactor indentations --- handler.go | 13 +++++++------ handler_test.go | 22 +++++++++++++--------- main.go | 6 ------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/handler.go b/handler.go index 7f6a7af..3132a23 100644 --- a/handler.go +++ b/handler.go @@ -16,6 +16,7 @@ import ( v4 "github.com/aws/aws-sdk-go/aws/signer/v4" log "github.com/sirupsen/logrus" ) + // - new less strict regexp in order to allow different region naming (compatibility with other providers) // - east-eu-1 => pass (aws style) // - gra => pass (ceph style) @@ -227,12 +228,12 @@ func (h *Handler) buildUpstreamRequest(req *http.Request) (*http.Request, error) return nil, err } - // WORKAROUND S3CMD which dont use white space before the some commas in the authorization header - fakeAuthorizationStr := fakeReq.Header.Get("Authorization") - // Sanitize fakeReq to remove white spaces before the comma signature - authorizationStr := strings.Replace(req.Header["Authorization"][0],",Signature",", Signature",1) - // Sanitize fakeReq to remove white spaces before the comma signheaders - authorizationStr = strings.Replace(authorizationStr,",SignedHeaders",", SignedHeaders",1) + // WORKAROUND S3CMD which dont use white space before the some commas in the authorization header + fakeAuthorizationStr := fakeReq.Header.Get("Authorization") + // Sanitize fakeReq to remove white spaces before the comma signature + authorizationStr := strings.Replace(req.Header["Authorization"][0], ",Signature", ", Signature", 1) + // Sanitize fakeReq to remove white spaces before the comma signheaders + authorizationStr = strings.Replace(authorizationStr, ",SignedHeaders", ", SignedHeaders", 1) // Verify that the fake request and the incoming request have the same signature // This ensures it was sent and signed by a client with correct AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY diff --git a/handler_test.go b/handler_test.go index 22846a6..da01641 100644 --- a/handler_test.go +++ b/handler_test.go @@ -79,11 +79,11 @@ func verifySignature(w http.ResponseWriter, r *http.Request) { signer.Sign(r, body, "s3", "eu-test-1", signTime) expectedAuthorization := r.Header["Authorization"][0] - // WORKAROUND S3CMD who dont use white space before the comma in the authorization header - // Sanitize fakeReq to remove white spaces before the comma signature - receivedAuthorization = strings.Replace(receivedAuthorization,",Signature",", Signature",1) - // Sanitize fakeReq to remove white spaces before the comma signheaders - receivedAuthorization = strings.Replace(receivedAuthorization,",SignedHeaders",", SignedHeaders",1) + // WORKAROUND S3CMD who dont use white space before the comma in the authorization header + // Sanitize fakeReq to remove white spaces before the comma signature + receivedAuthorization = strings.Replace(receivedAuthorization, ",Signature", ", Signature", 1) + // Sanitize fakeReq to remove white spaces before the comma signheaders + receivedAuthorization = strings.Replace(receivedAuthorization, ",SignedHeaders", ", SignedHeaders", 1) // verify signature fmt.Fprintln(w, receivedAuthorization, expectedAuthorization) @@ -154,10 +154,14 @@ func TestHandlerValidSignatureS3cmd(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "http://foobar.example.com", nil) signRequest(req) - authorizationReq := req.Header.Get("Authorization"); - authorizationReq = strings.Replace(authorizationReq,", Signature",",Signature",1) - authorizationReq = strings.Replace(authorizationReq,", SignedHeaders",",SignedHeaders",1) - req.Header.Set("Authorization",authorizationReq); + // get the generated signed authorization header in order to simulate the s3cmd syntax + authorizationReq := req.Header.Get("Authorization") + // simulating s3cmd syntax and remove the whites space after the comma of the Signature part + authorizationReq = strings.Replace(authorizationReq, ", Signature", ",Signature", 1) + // simulating s3cmd syntax and remove the whites space before the comma of the SignedHeaders part + authorizationReq = strings.Replace(authorizationReq, ", SignedHeaders", ",SignedHeaders", 1) + // push the edited authorization header + req.Header.Set("Authorization", authorizationReq) resp := httptest.NewRecorder() h.ServeHTTP(resp, req) assert.Equal(t, 200, resp.Code) diff --git a/main.go b/main.go index aaca239..6fb3bd4 100644 --- a/main.go +++ b/main.go @@ -100,10 +100,6 @@ func NewAwsS3ReverseProxy(opts Options) (*Handler, error) { } return handler, nil } -//handle /health path -func health(w http.ResponseWriter, req *http.Request){ - fmt.Fprintf(w,"ok") -} func main() { opts := NewOptions() handler, err := NewAwsS3ReverseProxy(opts) @@ -139,8 +135,6 @@ func main() { var wrappedHandler http.Handler = handler if len(opts.MetricsListenAddr) > 0 && len(strings.Split(opts.MetricsListenAddr, ":")) == 2 { metricsHandler := http.NewServeMux() - //add health on metrics http to serve k8s liveness - metricsHandler.HandleFunc("/health",health) metricsHandler.Handle("/metrics", promhttp.Handler()) log.Infof("Listening for secure Prometheus metrics on %s", opts.MetricsListenAddr) From e31f779f38263d05daf986e5b7e7eea79215b6a7 Mon Sep 17 00:00:00 2001 From: enguer Date: Sun, 16 Mar 2025 14:03:58 +0100 Subject: [PATCH 4/4] add presigned url feature --- handler.go | 247 +++++++++++++++++++++++++++++++++++++++++------- handler_test.go | 53 +++++++++++ 2 files changed, 267 insertions(+), 33 deletions(-) diff --git a/handler.go b/handler.go index 3132a23..75012fd 100644 --- a/handler.go +++ b/handler.go @@ -20,8 +20,9 @@ import ( // - new less strict regexp in order to allow different region naming (compatibility with other providers) // - east-eu-1 => pass (aws style) // - gra => pass (ceph style) -var awsAuthorizationCredentialRegexp = regexp.MustCompile("Credential=([a-zA-Z0-9]+)/[0-9]+/([a-zA-Z-0-9]+)/s3/aws4_request") +var awsAuthorizationCredentialRegexp = regexp.MustCompile("([a-zA-Z0-9]+)/([0-9]+)/([a-zA-Z-0-9]+)/s3/aws4_request") var awsAuthorizationSignedHeadersRegexp = regexp.MustCompile("SignedHeaders=([a-zA-Z0-9;-]+)") +var awsAuthorizationSignatureRegexp = regexp.MustCompile("Signature=([a-zA-Z0-9;-]+)") // Handler is a special handler that re-signs any AWS S3 request and sends it upstream type Handler struct { @@ -69,6 +70,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, proxyReq) } +func (h *Handler) presign(signer *v4.Signer, req *http.Request, region string, signDuration time.Duration, signTime time.Time) error { + body := bytes.NewReader([]byte{}) + req.URL.RawPath = req.URL.Path + _, err := signer.Presign(req, body, "s3", region, signDuration, signTime) + return err +} func (h *Handler) sign(signer *v4.Signer, req *http.Request, region string) error { return h.signWithTime(signer, req, region, time.Now()) } @@ -111,23 +118,50 @@ func (h *Handler) validateIncomingSourceIP(req *http.Request) error { } return nil } +func (h *Handler) validateIncomingQueryGetParameters(req *http.Request) (string, string, error) { + // get the query GET parameters + query := req.URL.Query() + + amzDateParam := query.Get("X-Amz-Date") + if len(amzDateParam) < 1 { + return "", "", fmt.Errorf("X-Amz-Date GET param missing: %v", req) + } + + credentialParam := query.Get("X-Amz-Credential") + if len(credentialParam) < 1 { + return "", "", fmt.Errorf("Credential GET param missing: %v", req) + } + match := awsAuthorizationCredentialRegexp.FindStringSubmatch(credentialParam) + if len(match) < 4 { + return "", "", fmt.Errorf("invalid Credential presigned param: Credential not found: %v", req) + } + receivedAccessKeyID := match[1] + region := match[3] + // Validate the received Credential (ACCESS_KEY_ID) is allowed + for accessKeyID := range h.AWSCredentials { + if subtle.ConstantTimeCompare([]byte(receivedAccessKeyID), []byte(accessKeyID)) == 1 { + return accessKeyID, region, nil + } + } + return "", "", fmt.Errorf("invalid AccessKeyID in Credential: %v", req) +} func (h *Handler) validateIncomingHeaders(req *http.Request) (string, string, error) { amzDateHeader := req.Header["X-Amz-Date"] - if len(amzDateHeader) != 1 { + if len(amzDateHeader) < 1 { return "", "", fmt.Errorf("X-Amz-Date header missing or set multiple times: %v", req) } - authorizationHeader := req.Header["Authorization"] - if len(authorizationHeader) != 1 { + authorizationHeader := req.Header.Get("Authorization") + if len(authorizationHeader) < 1 { return "", "", fmt.Errorf("Authorization header missing or set multiple times: %v", req) } - match := awsAuthorizationCredentialRegexp.FindStringSubmatch(authorizationHeader[0]) - if len(match) != 3 { + match := awsAuthorizationCredentialRegexp.FindStringSubmatch(authorizationHeader) + if len(match) != 4 { return "", "", fmt.Errorf("invalid Authorization header: Credential not found: %v", req) } receivedAccessKeyID := match[1] - region := match[2] + region := match[3] // Validate the received Credential (ACCESS_KEY_ID) is allowed for accessKeyID := range h.AWSCredentials { @@ -138,6 +172,49 @@ func (h *Handler) validateIncomingHeaders(req *http.Request) (string, string, er return "", "", fmt.Errorf("invalid AccessKeyID in Credential: %v", req) } +func (h *Handler) generateFakeIncomingPresignRequest(signer *v4.Signer, req *http.Request, region string) (*http.Request, error) { + fakeReq, err := http.NewRequest(req.Method, req.URL.String(), nil) + if err != nil { + return nil, err + } + // fakeReq.URL.RawPath = req.URL.Path + query := req.URL.Query() + fakeQuery := fakeReq.URL.Query() + // fakeReq.Header.Add("X-Amz-Date", query.Get("X-Amz-Date")) + fakeQuery.Del("X-Amz-Signature") + + + // We already validated there there is exactly one Authorization header + credentialParam := query.Get("X-Amz-Credential") + match := awsAuthorizationCredentialRegexp.FindStringSubmatch(credentialParam) + fakeQuery.Set("X-Amz-Credential",match[1]+"/"+match[2]+"/"+region+"/s3/aws4_request") + + // Delete a potentially double-added header + fakeReq.Header.Del("host") + fakeReq.Host = h.AllowedSourceEndpoint + + // The X-Amz-Date header contains a timestamp, such as: 20190929T182805Z + signTime, err := time.Parse("20060102T150405Z", query.Get("X-Amz-Date")) + if err != nil { + return nil, fmt.Errorf("error parsing X-Amz-Date %v - %v", query.Get("X-Amz-Date"), err) + } + + // Extract the duration + signDuration, err := time.ParseDuration(query.Get("X-Amz-Expires")+"s") + if err != nil { + return nil, fmt.Errorf("error parsing X-Amz-Expires %v - %v", query.Get("X-Amz-Expires"), err) + } + + // Save the query parameters + fakeReq.URL.RawQuery = fakeQuery.Encode() + + // Sign the fake request with the original timestamp + if err := h.presign(signer, fakeReq, region, signDuration, signTime); err != nil { + return nil, err + } + return fakeReq, nil +} + func (h *Handler) generateFakeIncomingRequest(signer *v4.Signer, req *http.Request, region string) (*http.Request, error) { fakeReq, err := http.NewRequest(req.Method, req.URL.String(), nil) if err != nil { @@ -159,10 +236,10 @@ func (h *Handler) generateFakeIncomingRequest(signer *v4.Signer, req *http.Reque fakeReq.Host = h.AllowedSourceEndpoint // The X-Amz-Date header contains a timestamp, such as: 20190929T182805Z - signTime, err := time.Parse("20060102T150405Z", req.Header["X-Amz-Date"][0]) - if err != nil { - return nil, fmt.Errorf("error parsing X-Amz-Date %v - %v", req.Header["X-Amz-Date"][0], err) - } + signTime, err := time.Parse("20060102T150405Z", req.Header["X-Amz-Date"][0]) + if err != nil { + return nil, fmt.Errorf("error parsing X-Amz-Date %v - %v", req.Header["X-Amz-Date"][0], err) + } // Sign the fake request with the original timestamp if err := h.signWithTime(signer, fakeReq, region, signTime); err != nil { @@ -204,40 +281,134 @@ func (h *Handler) assembleUpstreamReq(signer *v4.Signer, req *http.Request, regi return proxyReq, nil } +func (h *Handler) assembleUpstreamPresignReq(signer *v4.Signer, req *http.Request, region string) (*http.Request, error) { + upstreamEndpoint := h.UpstreamEndpoint + if len(upstreamEndpoint) == 0 { + upstreamEndpoint = fmt.Sprintf("s3.%s.amazonaws.com", region) + log.Infof("Using %s as upstream endpoint", upstreamEndpoint) + } + + proxyURL := *req.URL + proxyURL.Scheme = h.UpstreamScheme + proxyURL.Host = upstreamEndpoint + proxyURL.RawPath = req.URL.Path + proxyReq, err := http.NewRequest(req.Method, proxyURL.String(), req.Body) + query := proxyReq.URL.Query() + query.Del("X-Amz-Signature") + if err != nil { + return nil, err + } + if val, ok := req.Header["Content-Type"]; ok { + proxyReq.Header["Content-Type"] = val + } + if val, ok := req.Header["Content-Md5"]; ok { + proxyReq.Header["Content-Md5"] = val + } + + // The X-Amz-Date parameter contains a timestamp, such as: 20190929T182805Z + signTime, err := time.Parse("20060102T150405Z", query.Get("X-Amz-Date")) + if err != nil { + return nil, fmt.Errorf("error parsing X-Amz-Date %v - %v", query.Get("X-Amz-Date"), err) + } + + // Extract the duration + signDuration, err := time.ParseDuration(query.Get("X-Amz-Expires")+"s") + if err != nil { + return nil, fmt.Errorf("error parsing X-Amz-Expires %v - %v", query.Get("X-Amz-Expires"), err) + } + + // Save the query parameters + proxyReq.URL.RawQuery = query.Encode() + + // Sign the upstream request + if err := h.presign(signer, proxyReq, region, signDuration, signTime); err != nil { + return nil, err + } + + // Add origin headers after request is signed (no overwrite) + copyHeaderWithoutOverwrite(proxyReq.Header, req.Header) + + return proxyReq, nil +} // Do validates the incoming request and create a new request for an upstream server func (h *Handler) buildUpstreamRequest(req *http.Request) (*http.Request, error) { + query := req.URL.Query() + // queryType may be Sign Or Presign + queryType := "Sign" // Ensure the request was sent from an allowed IP address err := h.validateIncomingSourceIP(req) if err != nil { return nil, err } - // Validate incoming headers and extract AWS_ACCESS_KEY_ID - accessKeyID, region, err := h.validateIncomingHeaders(req) - if err != nil { - return nil, err - } + // Validate incoming headers and extract AWS_ACCESS_KEY_ID + // Detect type of request + accessKeyID, region, err := h.validateIncomingHeaders(req) + if err != nil { + // check if the query has params + query := req.URL.Query() + if len(query) > 0 && len(query.Get("X-Amz-Signature")) > 0{ + // check if the presign url is valid + accessKeyID, region, err = h.validateIncomingQueryGetParameters(req) + if err != nil { + return nil, err + } + // define the queryType to presign + queryType = "Presign" + }else{ + return nil, err + } + } // Get the AWS Signature signer for this AccessKey signer := h.Signers[accessKeyID] - // Assemble a signed fake request to verify the incoming requests signature - fakeReq, err := h.generateFakeIncomingRequest(signer, req, region) - if err != nil { - return nil, err - } - - // WORKAROUND S3CMD which dont use white space before the some commas in the authorization header - fakeAuthorizationStr := fakeReq.Header.Get("Authorization") - // Sanitize fakeReq to remove white spaces before the comma signature - authorizationStr := strings.Replace(req.Header["Authorization"][0], ",Signature", ", Signature", 1) - // Sanitize fakeReq to remove white spaces before the comma signheaders - authorizationStr = strings.Replace(authorizationStr, ",SignedHeaders", ", SignedHeaders", 1) + var fakeAuthorizationSignature string + var authorizationSignature string + var fakeReq *http.Request + if (queryType == "Presign"){ + // Assemble a signed fake request to verify the incoming requests signature + var err error + fakeReq, err = h.generateFakeIncomingPresignRequest(signer, req, region) + if err != nil { + return nil, err + } + + // Extract Signature from fakeReq Header in order to compare it with real req + fakeQuery := fakeReq.URL.Query() + fakeAuthorizationSignature = fakeQuery.Get("X-Amz-Signature") + + // Extract Signature from get in case of presign req + authorizationSignature = query.Get("X-Amz-Signature") + // check non empty signature + if (len(authorizationSignature) < 1) { + return nil, fmt.Errorf("missing signature in Authorization parameter") + } + }else{ + // Assemble a signed fake request to verify the incoming requests signature + fakeReq, err = h.generateFakeIncomingRequest(signer, req, region) + if err != nil { + return nil, err + } + + // Extract Signature from fakeReq Header in order to compare it with real req + fakeAuthorizationStr := fakeReq.Header.Get("Authorization") + fakeAuthorizationSignature = awsAuthorizationSignatureRegexp.FindStringSubmatch(fakeAuthorizationStr)[1] + + // Extract Signature from req Header in order to compare it with fake req + authorizationStr := req.Header.Get("Authorization") + // Extract Signature from header + authorizationSignature = awsAuthorizationSignatureRegexp.FindStringSubmatch(authorizationStr)[1] + // check non empty signature + if (len(authorizationSignature) < 1) { + return nil, fmt.Errorf("missing signature in Authorization header") + } + } // Verify that the fake request and the incoming request have the same signature // This ensures it was sent and signed by a client with correct AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY - cmpResult := subtle.ConstantTimeCompare([]byte(fakeAuthorizationStr), []byte(authorizationStr)) + cmpResult := subtle.ConstantTimeCompare([]byte(fakeAuthorizationSignature), []byte(authorizationSignature)) if cmpResult == 0 { v, _ := httputil.DumpRequest(fakeReq, false) log.Debugf("Fake request: %v", string(v)) @@ -253,10 +424,20 @@ func (h *Handler) buildUpstreamRequest(req *http.Request) (*http.Request, error) } // Assemble a new upstream request - proxyReq, err := h.assembleUpstreamReq(signer, req, region) - if err != nil { - return nil, err - } + var proxyReq *http.Request + if (queryType == "Presign"){ + var err error + proxyReq, err = h.assembleUpstreamPresignReq(signer, req, region) + if err != nil { + return nil, err + } + }else{ + var err error + proxyReq, err = h.assembleUpstreamReq(signer, req, region) + if err != nil { + return nil, err + } + } // Disable Go's "Transfer-Encoding: chunked" madness proxyReq.ContentLength = req.ContentLength diff --git a/handler_test.go b/handler_test.go index da01641..9997a2a 100644 --- a/handler_test.go +++ b/handler_test.go @@ -43,7 +43,25 @@ func newTestProxyWithHandler(t *testing.T, thf *http.HandlerFunc) *Handler { assert.Nil(t, err) return h } +func presignRequest(r *http.Request) { + query := r.URL.Query() + // delete headers to get clean signature + r.Header.Del("accept-encoding") + r.Header.Del("authorization") + // query.Set("X-Amz-Date", "20060102T150405Z") + // r.URL.RawPath = r.URL.Path + // compute the expected signature with valid credentials + body := bytes.NewReader([]byte{}) + signTime, _ := time.Parse("20060102T150405Z", query.Get("X-Amz-Date")) + durationTime, _ := time.ParseDuration(query.Get("X-Amz-Expires")+"s") + signer := v4.NewSigner(credentials.NewStaticCredentialsFromCreds(credentials.Value{ + AccessKeyID: "fooooooooooooooo", + SecretAccessKey: "bar", + })) + signer.Presign(r, body, "s3", "eu-test-1", durationTime, signTime) + // query.Add("X-Amz-Signature",test) +} func signRequest(r *http.Request) { // delete headers to get clean signature r.Header.Del("accept-encoding") @@ -167,6 +185,41 @@ func TestHandlerValidSignatureS3cmd(t *testing.T) { assert.Equal(t, 200, resp.Code) assert.Contains(t, resp.Body.String(), "Hello, client") } +func TestHandlerInvalidSignaturePresignUrl(t *testing.T) { + h := newTestProxy(t) + + req := httptest.NewRequest(http.MethodGet, "http://foobar.example.com/test.txt", nil) + query := req.URL.Query() + query.Add("X-Amz-Algorithm", "AWS4-HMAC-SHA256") + query.Add("X-Amz-Credential", "fooooooooooooooo/20060102/eu-test-1/s3/aws4_request") + query.Add("X-Amz-Date", "20250315T113827Z") + query.Add("X-Amz-Expires", "3600") + query.Add("X-Amz-SignedHeaders", "host") + query.Add("X-Amz-Signature", "6151ad46b27da29a5f0174e3035b4e31af1de7887aaefe8fbf6e0eb653d924a1") + req.URL.RawQuery = query.Encode() + resp := httptest.NewRecorder() + h.ServeHTTP(resp, req) + assert.Equal(t, 400, resp.Code) + assert.Contains(t, resp.Body.String(), "invalid signature in Authorization header") +} +func TestHandlerValidSignaturePresignUrl(t *testing.T) { + h := newTestProxy(t) + + req := httptest.NewRequest(http.MethodGet, "http://foobar.example.com/testvalid.txt", nil) + query := req.URL.Query() + query.Add("X-Amz-Algorithm", "AWS4-HMAC-SHA256") + query.Add("X-Amz-Credential", "fooooooooooooooo/20250315/eu-test-1/s3/aws4_request") + query.Add("X-Amz-Date", "20250315T113827Z") + query.Add("X-Amz-Expires", "3600") + query.Add("X-Amz-SignedHeaders", "host") + req.URL.RawQuery = query.Encode() + presignRequest(req) + + resp := httptest.NewRecorder() + h.ServeHTTP(resp, req) + assert.Equal(t, 200, resp.Code) + assert.Contains(t, resp.Body.String(), "Hello, client") +} func TestHandlerInvalidCredential(t *testing.T) { h := newTestProxy(t)