From d61b9c9b56e3c7e49ce2c3f20bb602966416e987 Mon Sep 17 00:00:00 2001 From: itsHenry <2671230065@qq.com> Date: Sun, 7 Jul 2024 04:16:26 +0000 Subject: [PATCH] feat(auth): enhance AWS Signature V4 authentication support - Add query-based authentication alongside header-based - Support "UNSIGNED-PAYLOAD" - Add fallback options for date header extraction - Fix ordering of query keys by using req.URL.Query().Encode() instead of req.URL.RawQuery --- signature/signature-v4-utils.go | 2 +- signature/signature-v4.go | 60 +++++++++++++++++++-------- signature/signature-v4_test.go | 73 +++++++++++++++++++++++++++++---- 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/signature/signature-v4-utils.go b/signature/signature-v4-utils.go index 92a0a685..6ed15f48 100644 --- a/signature/signature-v4-utils.go +++ b/signature/signature-v4-utils.go @@ -12,7 +12,7 @@ var ( // extractSignedHeaders extract signed headers from Authorization header func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, ErrorCode) { reqHeaders := r.Header - reqQueries := r.Form + reqQueries := r.URL.Query() // find whether "host" is part of list of signed headers. // if not return ErrUnsignedHeaders. "host" is mandatory. if !contains(signedHeaders, "host") { diff --git a/signature/signature-v4.go b/signature/signature-v4.go index 7f34eafd..f26ea40f 100644 --- a/signature/signature-v4.go +++ b/signature/signature-v4.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/hex" + "fmt" "net/http" "sort" "strings" @@ -25,6 +26,11 @@ const ( headerDate = "Date" amzContentSha256 = "X-Amz-Content-Sha256" amzDate = "X-Amz-Date" + amzAlgorithm = "X-Amz-Algorithm" + amzCredential = "X-Amz-Credential" + amzSignedHeaders = "X-Amz-SignedHeaders" + amzSignature = "X-Amz-Signature" + amzexpires = "X-Amz-Expires" ) // getCanonicalHeaders generate a list of request headers with their values @@ -133,11 +139,22 @@ func getSigningKey(secretKey string, t time.Time, region string) []byte { func V4SignVerify(r *http.Request) ErrorCode { // Copy request. req := *r - hashedPayload := getContentSha256Cksum(r) + queryf := req.URL.Query() + isUnsignedPayload := req.Header.Get("X-Amz-Content-Sha256") == "UNSIGNED-PAYLOAD" // Save authorization header. v4Auth := req.Header.Get(headerAuth) + // If the header is empty but the query string has the signature, then it's QueryString authentication. (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) + if v4Auth == "" && queryf.Get(amzSignature) != "" { + // QueryString authentications are always "UNSIGNED-PAYLOAD". + isUnsignedPayload = true + v4Auth = fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", queryf.Get(amzAlgorithm), queryf.Get(amzCredential), queryf.Get(amzSignedHeaders), queryf.Get(amzSignature)) + if queryf.Get(amzCredential) == "" { + return errMissingCredTag + } + } + // Parse signature version '4' header. signV4Values, Err := ParseSignV4(v4Auth) if Err != ErrNone { @@ -155,12 +172,18 @@ func V4SignVerify(r *http.Request) ErrorCode { return ErrCode } - // Extract date, if not present throw Error. - var date string - if date = req.Header.Get(amzDate); date == "" { - if date = r.Header.Get(headerDate); date == "" { - return errMissingDateHeader - } + // Extract date from various possible sources + date := req.Header.Get(amzDate) + if date == "" { + date = req.Header.Get(headerDate) + } + if date == "" { + date = queryf.Get(amzDate) + } + + // If date is still empty after checking all sources, return an error + if date == "" { + return errMissingDateHeader } // Parse date header. @@ -170,19 +193,24 @@ func V4SignVerify(r *http.Request) ErrorCode { } // Query string. - queryStr := req.URL.RawQuery - - // Get canonical request. - canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method) - - // Get string to sign from canonical request. - stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) + queryf.Del(amzSignature) + rawquery := queryf.Encode() // Get hmac signing key. signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, signV4Values.Credential.scope.region) - // Calculate signature. - newSignature := getSignature(signingKey, stringToSign) + var newSignature string + if isUnsignedPayload { + hashedPayload := "UNSIGNED-PAYLOAD" + canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, rawquery, req.URL.Path, req.Method) + stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) + newSignature = getSignature(signingKey, stringToSign) + } else { + hashedPayload := getContentSha256Cksum(r) + canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, rawquery, req.URL.Path, req.Method) + stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) + newSignature = getSignature(signingKey, stringToSign) + } // Verify if signature match. if !compareSignatureV4(newSignature, signV4Values.Signature) { diff --git a/signature/signature-v4_test.go b/signature/signature-v4_test.go index 020f49c3..2e9783ef 100644 --- a/signature/signature-v4_test.go +++ b/signature/signature-v4_test.go @@ -6,6 +6,7 @@ import ( "fmt" "math/rand" "net/http" + "net/url" "testing" "time" @@ -37,28 +38,86 @@ func RandString(n int) string { } func TestSignatureMatch(t *testing.T) { + testCases := []struct { + name string + useQueryString bool + }{ + { + name: "Header-based Authentication", + useQueryString: false, + }, + { + name: "Query-based Authentication", + useQueryString: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + Body := bytes.NewReader(nil) + ak := RandString(32) + sk := RandString(64) + region := RandString(16) + + creds := credentials.NewStaticCredentials(ak, sk, "") + signature.ReloadKeys(map[string]string{ak: sk}) + signer := v4.NewSigner(creds) + + req, err := http.NewRequest(http.MethodPost, "https://s3-endpoint.example.com/bin", Body) + if err != nil { + t.Error(err) + } + + if tc.useQueryString { + // For query-based authentication + req.URL.RawQuery = url.Values{ + "X-Amz-Algorithm": []string{signV4Algorithm}, + "X-Amz-Credential": []string{fmt.Sprintf("%s/%s/%s/%s/aws4_request", ak, time.Now().Format(yyyymmdd), region, serviceS3)}, + "X-Amz-Date": []string{time.Now().Format(iso8601Format)}, + "X-Amz-Expires": []string{"900"}, + "X-Amz-SignedHeaders": []string{"host"}, + }.Encode() + _, err = signer.Sign(req, Body, serviceS3, region, time.Now()) + } else { + // For header-based authentication + _, err = signer.Sign(req, Body, serviceS3, region, time.Now()) + } - Body := bytes.NewReader(nil) + if err != nil { + t.Error(err) + } + + if result := signature.V4SignVerify(req); result != signature.ErrNone { + t.Errorf("invalid result: expect none but got %+v", signature.GetAPIError(result)) + } + }) + } +} + +func TestUnsignedPayload(t *testing.T) { + Body := bytes.NewReader([]byte("test data")) ak := RandString(32) sk := RandString(64) region := RandString(16) - credentials := credentials.NewStaticCredentials(ak, sk, "") + creds := credentials.NewStaticCredentials(ak, sk, "") signature.ReloadKeys(map[string]string{ak: sk}) - signer := v4.NewSigner(credentials) + signer := v4.NewSigner(creds) - req, err := http.NewRequest(http.MethodPost, "https://s3-endpoint.exmaple.com/bin", Body) + req, err := http.NewRequest(http.MethodPost, "https://s3-endpoint.example.com/bin", Body) if err != nil { - t.Error(err) + t.Fatal(err) } + req.Header.Set("X-Amz-Content-Sha256", unsignedPayload) + _, err = signer.Sign(req, Body, serviceS3, region, time.Now()) if err != nil { - t.Error(err) + t.Fatal(err) } if result := signature.V4SignVerify(req); result != signature.ErrNone { - t.Error(fmt.Errorf("invalid result: expect none but got %+v", signature.GetAPIError(result))) + t.Errorf("invalid result for unsigned payload: expect none but got %+v", signature.GetAPIError(result)) } }