From 614a8cf7adfb453e50b93ca59dd2889027ca21f8 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Tue, 18 Oct 2016 14:15:49 -0700 Subject: [PATCH] Sign V2 support. --- cmd/auth-handler.go | 57 ++++- cmd/auth-handler_test.go | 148 ++++++++++++- cmd/bucket-handlers-listobjects.go | 16 +- cmd/bucket-handlers.go | 37 +++- cmd/bucket-policy-handlers.go | 38 ++-- cmd/object-handlers.go | 82 +++++++- cmd/signature-v2.go | 326 +++++++++++++++++++++++++++++ cmd/signature-v2_test.go | 182 ++++++++++++++++ cmd/signature-v4.go | 45 ++-- cmd/signature-v4_test.go | 42 ++-- cmd/signature-verify-reader.go | 12 +- cmd/test-utils_test.go | 14 +- 12 files changed, 883 insertions(+), 116 deletions(-) create mode 100644 cmd/signature-v2.go create mode 100644 cmd/signature-v2_test.go diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index a428c929c..5b42678b9 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -58,6 +58,18 @@ func isRequestSignStreamingV4(r *http.Request) bool { return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 && r.Method == "PUT" } +// Verify if request has AWS Signature Version '2'. +func isRequestSignatureV2(r *http.Request) bool { + return (!strings.HasPrefix(r.Header.Get("Authorization"), signV4Algorithm) && + strings.HasPrefix(r.Header.Get("Authorization"), signV2Algorithm)) +} + +// Verify request has AWS PreSign Version '2'. +func isRequestPresignedSignatureV2(r *http.Request) bool { + _, ok := r.URL.Query()["AWSAccessKeyId"] + return ok +} + // Authorization type. type authType int @@ -66,15 +78,21 @@ const ( authTypeUnknown authType = iota authTypeAnonymous authTypePresigned + authTypePresignedV2 authTypePostPolicy authTypeStreamingSigned authTypeSigned + authTypeSignedV2 authTypeJWT ) // Get request authentication type. func getRequestAuthType(r *http.Request) authType { - if isRequestSignStreamingV4(r) { + if isRequestSignatureV2(r) { + return authTypeSignedV2 + } else if isRequestPresignedSignatureV2(r) { + return authTypePresignedV2 + } else if isRequestSignStreamingV4(r) { return authTypeStreamingSigned } else if isRequestSignatureV4(r) { return authTypeSigned @@ -104,8 +122,16 @@ func sumMD5(data []byte) []byte { return hash.Sum(nil) } +// Verify if request has valid AWS Signature Version '2'. +func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) { + if isRequestSignatureV2(r) { + return doesSignV2Match(r) + } + return doesPresignV2SignatureMatch(r) +} + // Verify if request has valid AWS Signature Version '4'. -func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) { +func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) { if r == nil { return ErrInternalError } @@ -121,7 +147,6 @@ func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) { } // Populate back the payload. r.Body = ioutil.NopCloser(bytes.NewReader(payload)) - validateRegion := true // Validate region. var sha256sum string // Skips calculating sha256 on the payload on server, // if client requested for it. @@ -131,9 +156,9 @@ func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) { sha256sum = hex.EncodeToString(sum256(payload)) } if isRequestSignatureV4(r) { - return doesSignatureMatch(sha256sum, r, validateRegion) + return doesSignatureMatch(sha256sum, r, region) } else if isRequestPresignedSignatureV4(r) { - return doesPresignedSignatureMatch(sha256sum, r, validateRegion) + return doesPresignedSignatureMatch(sha256sum, r, region) } return ErrAccessDenied } @@ -145,13 +170,21 @@ func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) { // request headers and body are used to calculate the signature validating // the client signature present in request. func checkAuth(r *http.Request) APIErrorCode { - aType := getRequestAuthType(r) - if aType != authTypePresigned && aType != authTypeSigned { - // For all unhandled auth types return error AccessDenied. - return ErrAccessDenied - } + return checkAuthWithRegion(r, serverConfig.GetRegion()) +} + +// checkAuthWithRegion - similar to checkAuth but takes a custom region. +func checkAuthWithRegion(r *http.Request, region string) APIErrorCode { // Validates the request for both Presigned and Signed. - return isReqAuthenticated(r) + aType := getRequestAuthType(r) + switch aType { + case authTypeSignedV2, authTypePresignedV2: // Signature V2. + return isReqAuthenticatedV2(r) + case authTypeSigned, authTypePresigned: // Signature V4. + return isReqAuthenticated(r, region) + } + // For all unhandled auth types return error AccessDenied. + return ErrAccessDenied } // authHandler - handles all the incoming authorization headers and validates them if possible. @@ -168,7 +201,9 @@ func setAuthHandler(h http.Handler) http.Handler { var supportedS3AuthTypes = map[authType]struct{}{ authTypeAnonymous: {}, authTypePresigned: {}, + authTypePresignedV2: {}, authTypeSigned: {}, + authTypeSignedV2: {}, authTypePostPolicy: {}, authTypeStreamingSigned: {}, } diff --git a/cmd/auth-handler_test.go b/cmd/auth-handler_test.go index ada9bf3c4..e9d82ab9f 100644 --- a/cmd/auth-handler_test.go +++ b/cmd/auth-handler_test.go @@ -20,9 +20,104 @@ import ( "bytes" "io" "net/http" + "net/url" "testing" ) +// Test get request auth type. +func TestGetRequestAuthType(t *testing.T) { + type testCase struct { + req *http.Request + authT authType + } + testCases := []testCase{ + // Test case - 1 + // Check for generic signature v4 header. + { + req: &http.Request{ + URL: &url.URL{ + Host: "localhost:9000", + Scheme: "http", + Path: "/", + }, + Header: http.Header{ + "Authorization": []string{"AWS4-HMAC-SHA256 "}, + "X-Amz-Content-Sha256": []string{streamingContentSHA256}, + }, + Method: "PUT", + }, + authT: authTypeStreamingSigned, + }, + // Test case - 2 + // Check for JWT header. + { + req: &http.Request{ + URL: &url.URL{ + Host: "localhost:9000", + Scheme: "http", + Path: "/", + }, + Header: http.Header{ + "Authorization": []string{"Bearer 12313123"}, + }, + }, + authT: authTypeJWT, + }, + // Test case - 3 + // Empty authorization header. + { + req: &http.Request{ + URL: &url.URL{ + Host: "localhost:9000", + Scheme: "http", + Path: "/", + }, + Header: http.Header{ + "Authorization": []string{""}, + }, + }, + authT: authTypeUnknown, + }, + // Test case - 4 + // Check for presigned. + { + req: &http.Request{ + URL: &url.URL{ + Host: "localhost:9000", + Scheme: "http", + Path: "/", + RawQuery: "X-Amz-Credential=EXAMPLEINVALIDEXAMPL%2Fs3%2F20160314%2Fus-east-1", + }, + }, + authT: authTypePresigned, + }, + // Test case - 5 + // Check for post policy. + { + req: &http.Request{ + URL: &url.URL{ + Host: "localhost:9000", + Scheme: "http", + Path: "/", + }, + Header: http.Header{ + "Content-Type": []string{"multipart/form-data"}, + }, + Method: "POST", + }, + authT: authTypePostPolicy, + }, + } + + // .. Tests all request auth type. + for i, testc := range testCases { + authT := getRequestAuthType(testc.req) + if authT != testc.authT { + t.Errorf("Test %d: Expected %d, got %d", i+1, testc.authT, authT) + } + } +} + // Test all s3 supported auth types. func TestS3SupportedAuthType(t *testing.T) { type testCase struct { @@ -56,19 +151,29 @@ func TestS3SupportedAuthType(t *testing.T) { authT: authTypeStreamingSigned, pass: true, }, - // Test 6 - JWT is not supported s3 type. + // Test 6 - supported s3 type with signature v2. + { + authT: authTypeSignedV2, + pass: true, + }, + // Test 7 - supported s3 type with presign v2. + { + authT: authTypePresignedV2, + pass: true, + }, + // Test 8 - JWT is not supported s3 type. { authT: authTypeJWT, pass: false, }, - // Test 7 - unknown auth header is not supported s3 type. + // Test 9 - unknown auth header is not supported s3 type. { authT: authTypeUnknown, pass: false, }, - // Test 8 - some new auth type is not supported s3 type. + // Test 10 - some new auth type is not supported s3 type. { - authT: authType(7), + authT: authType(9), pass: false, }, } @@ -115,6 +220,39 @@ func TestIsRequestUnsignedPayload(t *testing.T) { } } +func TestIsRequestPresignedSignatureV2(t *testing.T) { + testCases := []struct { + inputQueryKey string + inputQueryValue string + expectedResult bool + }{ + // Test case - 1. + // Test case with query key "AWSAccessKeyId" set. + {"", "", false}, + // Test case - 2. + {"AWSAccessKeyId", "", true}, + // Test case - 3. + {"X-Amz-Content-Sha256", "", false}, + } + + for i, testCase := range testCases { + // creating an input HTTP request. + // Only the query parameters are relevant for this particular test. + inputReq, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Fatalf("Error initializing input HTTP request: %v", err) + } + q := inputReq.URL.Query() + q.Add(testCase.inputQueryKey, testCase.inputQueryValue) + inputReq.URL.RawQuery = q.Encode() + + actualResult := isRequestPresignedSignatureV2(inputReq) + if testCase.expectedResult != actualResult { + t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult) + } + } +} + // TestIsRequestPresignedSignatureV4 - Test validates the logic for presign signature verision v4 detection. func TestIsRequestPresignedSignatureV4(t *testing.T) { testCases := []struct { @@ -199,7 +337,7 @@ func TestIsReqAuthenticated(t *testing.T) { if testCase.s3Error == ErrBadDigest { testCase.req.Header.Set("Content-Md5", "garbage") } - if s3Error := isReqAuthenticated(testCase.req); s3Error != testCase.s3Error { + if s3Error := isReqAuthenticated(testCase.req, serverConfig.GetRegion()); s3Error != testCase.s3Error { t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error) } } diff --git a/cmd/bucket-handlers-listobjects.go b/cmd/bucket-handlers-listobjects.go index dd406e711..a8fa85d1e 100644 --- a/cmd/bucket-handlers-listobjects.go +++ b/cmd/bucket-handlers-listobjects.go @@ -75,8 +75,14 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypeSigned, authTypePresigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -135,8 +141,14 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypeSigned, authTypePresigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 83a21cfcb..5b071ae1c 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -75,8 +75,14 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r * writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypeSigned, authTypePresigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, "us-east-1"); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -124,8 +130,14 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -165,7 +177,8 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, // owned by the authenticated sender of the request. func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { // List buckets does not support bucket policies, no need to enforce it. - if s3Error := checkAuth(r); s3Error != ErrNone { + // Proceed to validate signature. Validates the request for both Presigned and Signed. + if s3Error := checkAuthWithRegion(r, ""); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -202,8 +215,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -312,7 +331,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, // This implementation of the PUT operation creates a new bucket for authenticated request func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) { // PutBucket does not support policies, use checkAuth to validate signature. - if s3Error := checkAuth(r); s3Error != ErrNone { + if s3Error := checkAuthWithRegion(r, "us-east-1"); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -444,8 +463,14 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } diff --git a/cmd/bucket-policy-handlers.go b/cmd/bucket-policy-handlers.go index 5cb206115..68460d0d4 100644 --- a/cmd/bucket-policy-handlers.go +++ b/cmd/bucket-policy-handlers.go @@ -134,7 +134,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path) return case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -203,21 +203,15 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht // This implementation of the DELETE operation uses the policy // subresource to add to remove a policy on a bucket. func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { + // DeleteBucketPolicy does not support bucket policies, use checkAuth to validate signature. + if s3Error := checkAuth(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } + vars := mux.Vars(r) bucket := vars["bucket"] - switch getRequestAuthType(r) { - default: - // For all unknown auth types return error. - writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path) - return - case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { - writeErrorResponse(w, r, s3Error, r.URL.Path) - return - } - } - // Delete bucket access policy. if err := removeBucketPolicy(bucket, api.ObjectAPI); err != nil { errorIf(err, "Unable to remove bucket policy.") @@ -244,21 +238,15 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r // This operation uses the policy // subresource to return the policy of a specified bucket. func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { + // GetBucketPolicy does not support bucket policies, use checkAuth to validate signature. + if s3Error := checkAuth(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } + vars := mux.Vars(r) bucket := vars["bucket"] - switch getRequestAuthType(r) { - default: - // For all unknown auth types return error. - writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path) - return - case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { - writeErrorResponse(w, r, s3Error, r.URL.Path) - return - } - } - // Read bucket access policy. policy, err := readBucketPolicy(bucket, api.ObjectAPI) if err != nil { diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 90d0702f4..3d0ef5737 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -95,8 +95,14 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -201,8 +207,14 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -251,8 +263,14 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -440,9 +458,16 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req return } md5Sum, err = api.ObjectAPI.PutObject(bucket, object, size, reader, metadata) + case authTypeSignedV2, authTypePresignedV2: + s3Error := isReqAuthenticatedV2(r) + if s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } + md5Sum, err = api.ObjectAPI.PutObject(bucket, object, size, r.Body, metadata) case authTypePresigned, authTypeSigned: // Initialize signature verifier. - reader := newSignVerify(r) + reader := newSignVerify(r, serverConfig.GetRegion()) // Create object. md5Sum, err = api.ObjectAPI.PutObject(bucket, object, size, reader, metadata) } @@ -496,8 +521,14 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -597,9 +628,16 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http return } partMD5, err = api.ObjectAPI.PutObjectPart(bucket, object, uploadID, partID, size, reader, incomingMD5) + case authTypeSignedV2, authTypePresignedV2: + s3Error := isReqAuthenticatedV2(r) + if s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } + partMD5, err = api.ObjectAPI.PutObjectPart(bucket, object, uploadID, partID, size, r.Body, incomingMD5) case authTypePresigned, authTypeSigned: // Initialize signature verifier. - reader := newSignVerify(r) + reader := newSignVerify(r, serverConfig.GetRegion()) partMD5, err = api.ObjectAPI.PutObjectPart(bucket, object, uploadID, partID, size, reader, incomingMD5) } if err != nil { @@ -631,8 +669,14 @@ func (api objectAPIHandlers) AbortMultipartUploadHandler(w http.ResponseWriter, writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -664,8 +708,14 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -715,8 +765,14 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypePresigned, authTypeSigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } @@ -836,8 +892,14 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http. writeErrorResponse(w, r, s3Error, r.URL.Path) return } + case authTypePresignedV2, authTypeSignedV2: + // Signature V2 validation. + if s3Error := isReqAuthenticatedV2(r); s3Error != ErrNone { + writeErrorResponse(w, r, s3Error, r.URL.Path) + return + } case authTypeSigned, authTypePresigned: - if s3Error := isReqAuthenticated(r); s3Error != ErrNone { + if s3Error := isReqAuthenticated(r, serverConfig.GetRegion()); s3Error != ErrNone { writeErrorResponse(w, r, s3Error, r.URL.Path) return } diff --git a/cmd/signature-v2.go b/cmd/signature-v2.go new file mode 100644 index 000000000..8957c117e --- /dev/null +++ b/cmd/signature-v2.go @@ -0,0 +1,326 @@ +/* + * Minio Cloud Storage, (C) 2016 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" +) + +// Signature and API related constants. +const ( + signV2Algorithm = "AWS" +) + +// AWS S3 Signature V2 calculation rule is give here: +// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign + +// Whitelist resource list that will be used in query string for signature-V2 calculation. +var resourceList = []string{ + "acl", + "delete", + "lifecycle", + "location", + "logging", + "notification", + "partNumber", + "policy", + "requestPayment", + "torrent", + "uploadId", + "uploads", + "versionId", + "versioning", + "versions", + "website", +} + +// TODO add post policy signature. + +// doesPresignV2SignatureMatch - Verify query headers with presigned signature +// - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth +// returns ErrNone if matches. S3 errors otherwise. +func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode { + // Access credentials. + cred := serverConfig.GetCredential() + + // url.RawPath will be valid if path has any encoded characters, if not it will + // be empty - in which case we need to consider url.Path (bug in net/http?) + encodedResource := r.URL.RawPath + encodedQuery := r.URL.RawQuery + if encodedResource == "" { + splits := strings.Split(r.URL.Path, "?") + if len(splits) > 0 { + encodedResource = splits[0] + } + } + queries := strings.Split(encodedQuery, "&") + var filteredQueries []string + var gotSignature string + var expires string + var accessKey string + for _, query := range queries { + keyval := strings.Split(query, "=") + switch keyval[0] { + case "AWSAccessKeyId": + accessKey = keyval[1] + case "Signature": + gotSignature = keyval[1] + case "Expires": + expires = keyval[1] + default: + filteredQueries = append(filteredQueries, query) + } + } + + if accessKey == "" { + return ErrInvalidQueryParams + } + + // Validate if access key id same. + if accessKey != cred.AccessKeyID { + return ErrInvalidAccessKeyID + } + + // Make sure the request has not expired. + expiresInt, err := strconv.ParseInt(expires, 10, 64) + if err != nil { + return ErrMalformedExpires + } + + if expiresInt < time.Now().UTC().Unix() { + return ErrExpiredPresignRequest + } + + expectedSignature := preSignatureV2(r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires) + if gotSignature != getURLEncodedName(expectedSignature) { + return ErrSignatureDoesNotMatch + } + + return ErrNone +} + +// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature; +// Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ); +// +// StringToSign = HTTP-Verb + "\n" + +// Content-Md5 + "\n" + +// Content-Type + "\n" + +// Date + "\n" + +// CanonicalizedProtocolHeaders + +// CanonicalizedResource; +// +// CanonicalizedResource = [ "/" + Bucket ] + +// + +// [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; +// +// CanonicalizedProtocolHeaders = + +// doesSignV2Match - Verify authorization header with calculated header in accordance with +// - http://docs.aws.amazon.com/AmazonS3/latest/dev/auth-request-sig-v2.html +// returns true if matches, false otherwise. if error is not nil then it is always false + +func validateV2AuthHeader(v2Auth string) APIErrorCode { + if v2Auth == "" { + return ErrAuthHeaderEmpty + } + // Verify if the header algorithm is supported or not. + if !strings.HasPrefix(v2Auth, signV2Algorithm) { + return ErrSignatureVersionNotSupported + } + + // below is V2 Signed Auth header format, splitting on `space` (after the `AWS` string). + // Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature + authFields := strings.Split(v2Auth, " ") + if len(authFields) != 2 { + return ErrMissingFields + } + + // Then will be splitting on ":", this will seprate `AWSAccessKeyId` and `Signature` string. + keySignFields := strings.Split(strings.TrimSpace(authFields[1]), ":") + if len(keySignFields) != 2 { + return ErrMissingFields + } + + // Access credentials. + cred := serverConfig.GetCredential() + if keySignFields[0] != cred.AccessKeyID { + return ErrInvalidAccessKeyID + } + + return ErrNone +} + +func doesSignV2Match(r *http.Request) APIErrorCode { + v2Auth := r.Header.Get("Authorization") + + if apiError := validateV2AuthHeader(v2Auth); apiError != ErrNone { + return apiError + } + + // url.RawPath will be valid if path has any encoded characters, if not it will + // be empty - in which case we need to consider url.Path (bug in net/http?) + encodedResource := r.URL.RawPath + encodedQuery := r.URL.RawQuery + if encodedResource == "" { + splits := strings.Split(r.URL.Path, "?") + if len(splits) > 0 { + encodedResource = splits[0] + } + } + + expectedAuth := signatureV2(r.Method, encodedResource, encodedQuery, r.Header) + if v2Auth != expectedAuth { + return ErrSignatureDoesNotMatch + } + + return ErrNone +} + +// Return signature-v2 for the presigned request. +func preSignatureV2(method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string { + cred := serverConfig.GetCredential() + + stringToSign := presignV2STS(method, encodedResource, encodedQuery, headers, expires) + hm := hmac.New(sha1.New, []byte(cred.SecretAccessKey)) + hm.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(hm.Sum(nil)) + return signature +} + +// Return signature-v2 authrization header. +func signatureV2(method string, encodedResource string, encodedQuery string, headers http.Header) string { + cred := serverConfig.GetCredential() + + stringToSign := signV2STS(method, encodedResource, encodedQuery, headers) + + hm := hmac.New(sha1.New, []byte(cred.SecretAccessKey)) + hm.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(hm.Sum(nil)) + return fmt.Sprintf("%s %s:%s", signV2Algorithm, cred.AccessKeyID, signature) +} + +// Return canonical headers. +func canonicalizedAmzHeadersV2(headers http.Header) string { + var keys []string + keyval := make(map[string]string) + for key := range headers { + lkey := strings.ToLower(key) + if !strings.HasPrefix(lkey, "x-amz-") { + continue + } + keys = append(keys, lkey) + keyval[lkey] = strings.Join(headers[key], ",") + } + sort.Strings(keys) + var canonicalHeaders []string + for _, key := range keys { + canonicalHeaders = append(canonicalHeaders, key+":"+keyval[key]) + } + return strings.Join(canonicalHeaders, "\n") +} + +// Return canonical resource string. +func canonicalizedResourceV2(encodedPath string, encodedQuery string) string { + queries := strings.Split(encodedQuery, "&") + keyval := make(map[string]string) + for _, query := range queries { + key := query + val := "" + index := strings.Index(query, "=") + if index != -1 { + key = query[:index] + val = query[index+1:] + } + keyval[key] = val + } + var canonicalQueries []string + for _, key := range resourceList { + val, ok := keyval[key] + if !ok { + continue + } + if val == "" { + canonicalQueries = append(canonicalQueries, key) + continue + } + canonicalQueries = append(canonicalQueries, key+"="+val) + } + if len(canonicalQueries) == 0 { + return encodedPath + } + // the queries will be already sorted as resourceList is sorted. + return encodedPath + "?" + strings.Join(canonicalQueries, "&") +} + +// Return string to sign for authz header calculation. +func signV2STS(method string, encodedResource string, encodedQuery string, headers http.Header) string { + canonicalHeaders := canonicalizedAmzHeadersV2(headers) + if len(canonicalHeaders) > 0 { + canonicalHeaders += "\n" + } + + // From the Amazon docs: + // + // StringToSign = HTTP-Verb + "\n" + + // Content-Md5 + "\n" + + // Content-Type + "\n" + + // Date + "\n" + + // CanonicalizedProtocolHeaders + + // CanonicalizedResource; + stringToSign := strings.Join([]string{ + method, + headers.Get("Content-MD5"), + headers.Get("Content-Type"), + headers.Get("Date"), + canonicalHeaders, + }, "\n") + canonicalizedResourceV2(encodedResource, encodedQuery) + + return stringToSign +} + +// Return string to sign for pre-sign signature calculation. +func presignV2STS(method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string { + canonicalHeaders := canonicalizedAmzHeadersV2(headers) + if len(canonicalHeaders) > 0 { + canonicalHeaders += "\n" + } + + // From the Amazon docs: + // + // StringToSign = HTTP-Verb + "\n" + + // Content-Md5 + "\n" + + // Content-Type + "\n" + + // Expires + "\n" + + // CanonicalizedProtocolHeaders + + // CanonicalizedResource; + stringToSign := strings.Join([]string{ + method, + headers.Get("Content-MD5"), + headers.Get("Content-Type"), + expires, + canonicalHeaders, + }, "\n") + canonicalizedResourceV2(encodedResource, encodedQuery) + return stringToSign +} diff --git a/cmd/signature-v2_test.go b/cmd/signature-v2_test.go new file mode 100644 index 000000000..64966507f --- /dev/null +++ b/cmd/signature-v2_test.go @@ -0,0 +1,182 @@ +package cmd + +import ( + "fmt" + "net/http" + "net/url" + "sort" + "testing" + "time" +) + +// Tests for 'func TestResourceListSorting(t *testing.T)'. +func TestResourceListSorting(t *testing.T) { + sortedResourceList := make([]string, len(resourceList)) + copy(sortedResourceList, resourceList) + sort.Strings(sortedResourceList) + for i := 0; i < len(resourceList); i++ { + if resourceList[i] != sortedResourceList[i] { + t.Errorf("Expected resourceList[%d] = \"%s\", resourceList is not correctly sorted.", i, sortedResourceList[i]) + break + } + } +} + +func TestDoesPresignedV2SignatureMatch(t *testing.T) { + root, err := newTestConfig("us-east-1") + if err != nil { + t.Fatal("Unable to initialize test config.") + } + defer removeAll(root) + + now := time.Now().UTC() + + testCases := []struct { + queryParams map[string]string + headers map[string]string + expected APIErrorCode + }{ + // (0) Should error without a set URL query. + { + expected: ErrInvalidQueryParams, + }, + // (1) Should error on an invalid access key. + { + queryParams: map[string]string{ + "Expires": "60", + "Signature": "badsignature", + "AWSAccessKeyId": "Z7IXGOO6BZ0REAN1Q26I", + }, + expected: ErrInvalidAccessKeyID, + }, + // (2) Should error with malformed expires. + { + queryParams: map[string]string{ + "Expires": "60s", + "Signature": "badsignature", + "AWSAccessKeyId": serverConfig.GetCredential().AccessKeyID, + }, + expected: ErrMalformedExpires, + }, + // (3) Should give an expired request if it has expired. + { + queryParams: map[string]string{ + "Expires": "60", + "Signature": "badsignature", + "AWSAccessKeyId": serverConfig.GetCredential().AccessKeyID, + }, + expected: ErrExpiredPresignRequest, + }, + // (4) Should error when the signature does not match. + { + queryParams: map[string]string{ + "Expires": fmt.Sprintf("%d", now.Unix()+60), + "Signature": "badsignature", + "AWSAccessKeyId": serverConfig.GetCredential().AccessKeyID, + }, + expected: ErrSignatureDoesNotMatch, + }, + } + + // Run each test case individually. + for i, testCase := range testCases { + // Turn the map[string]string into map[string][]string, because Go. + query := url.Values{} + for key, value := range testCase.queryParams { + query.Set(key, value) + } + + // Create a request to use. + req, e := http.NewRequest(http.MethodGet, "http://host/a/b?"+query.Encode(), nil) + if e != nil { + t.Errorf("(%d) failed to create http.Request, got %v", i, e) + } + + // Do the same for the headers. + for key, value := range testCase.headers { + req.Header.Set(key, value) + } + + // Check if it matches! + err := doesPresignV2SignatureMatch(req) + if err != testCase.expected { + t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err)) + } + } +} + +// TestValidateV2AuthHeader - Tests validate the logic of V2 Authorization header validator. +func TestValidateV2AuthHeader(t *testing.T) { + // Initialize server config. + if err := initConfig(); err != nil { + t.Fatal(err) + } + + // Save config. + if err := serverConfig.Save(); err != nil { + t.Fatal(err) + } + accessID := serverConfig.GetCredential().AccessKeyID + + testCases := []struct { + authString string + expectedError APIErrorCode + }{ + // Test case - 1. + // Case with empty V2AuthString. + { + + authString: "", + expectedError: ErrAuthHeaderEmpty, + }, + // Test case - 2. + // Test case with `signV2Algorithm` ("AWS") not being the prefix. + { + + authString: "NoV2Prefix", + expectedError: ErrSignatureVersionNotSupported, + }, + // Test case - 3. + // Test case with missing parts in the Auth string. + // below is the correct format of V2 Authorization header. + // Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature + { + + authString: signV2Algorithm, + expectedError: ErrMissingFields, + }, + // Test case - 4. + // Test case with signature part missing. + { + + authString: fmt.Sprintf("%s %s", signV2Algorithm, accessID), + expectedError: ErrMissingFields, + }, + // Test case - 5. + // Test case with wrong accessID. + { + + authString: fmt.Sprintf("%s %s:%s", signV2Algorithm, "InvalidAccessID", "signature"), + expectedError: ErrInvalidAccessKeyID, + }, + // Test case - 6. + // Case with right accessID and format. + { + + authString: fmt.Sprintf("%s %s:%s", signV2Algorithm, accessID, "signature"), + expectedError: ErrNone, + }, + } + + for i, testCase := range testCases { + t.Run(fmt.Sprintf("Case %d AuthStr \"%s\".", i+1, testCase.authString), func(t *testing.T) { + + actualErrCode := validateV2AuthHeader(testCase.authString) + + if testCase.expectedError != actualErrCode { + t.Errorf("Expected the error code to be %v, got %v.", testCase.expectedError, actualErrCode) + } + }) + } + +} diff --git a/cmd/signature-v4.go b/cmd/signature-v4.go index d1bd96ab4..1c44c53ae 100644 --- a/cmd/signature-v4.go +++ b/cmd/signature-v4.go @@ -147,7 +147,7 @@ func getSignature(signingKey []byte, stringToSign string) string { // doesPolicySignatureMatch - Verify query headers with post policy // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html -// returns true if matches, false otherwise. if error is not nil then it is always false +// returns ErrNone if the signature matches. func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode { // Access credentials. cred := serverConfig.GetCredential() @@ -193,14 +193,11 @@ func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode { // doesPresignedSignatureMatch - Verify query headers with presigned signature // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html -// returns true if matches, false otherwise. if error is not nil then it is always false -func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, validateRegion bool) APIErrorCode { +// returns ErrNone if the signature matches. +func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode { // Access credentials. cred := serverConfig.GetCredential() - // Server region. - region := serverConfig.GetRegion() - // Copy request req := *r @@ -223,15 +220,13 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, validate // Verify if region is valid. sRegion := pSignValues.Credential.scope.region - // Should validate region, only if region is set. Some operations - // do not need region validated for example GetBucketLocation. - if validateRegion { - if !isValidRegion(sRegion, region) { - return ErrInvalidRegion - } - } else { + // Should validate region, only if region is set. + if region == "" { region = sRegion } + if !isValidRegion(sRegion, region) { + return ErrInvalidRegion + } // Extract all the signed headers along with its values. extractedSignedHeaders, errCode := extractSignedHeaders(pSignValues.SignedHeaders, req.Header) @@ -321,14 +316,11 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, validate // doesSignatureMatch - Verify authorization header with calculated header in accordance with // - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html -// returns true if matches, false otherwise. if error is not nil then it is always false -func doesSignatureMatch(hashedPayload string, r *http.Request, validateRegion bool) APIErrorCode { +// returns ErrNone if signature matches. +func doesSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode { // Access credentials. cred := serverConfig.GetCredential() - // Server region. - region := serverConfig.GetRegion() - // Copy request. req := *r @@ -372,14 +364,17 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, validateRegion bo // Verify if region is valid. sRegion := signV4Values.Credential.scope.region - // Should validate region, only if region is set. Some operations - // do not need region validated for example GetBucketLocation. - if validateRegion { - if !isValidRegion(sRegion, region) { - return ErrInvalidRegion - } + // Region is set to be empty, we use whatever was sent by the + // request and proceed further. This is a work-around to address + // an important problem for ListBuckets() getting signed with + // different regions. + if region == "" { + region = sRegion + } + // Should validate region, only if region is set. + if !isValidRegion(sRegion, region) { + return ErrInvalidRegion } - region = sRegion // Extract date, if not present throw error. var date string diff --git a/cmd/signature-v4_test.go b/cmd/signature-v4_test.go index e066a3d5c..8f6d91f85 100644 --- a/cmd/signature-v4_test.go +++ b/cmd/signature-v4_test.go @@ -106,15 +106,15 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { credentialTemplate := "%s/%s/%s/s3/aws4_request" testCases := []struct { - queryParams map[string]string - headers map[string]string - verifyRegion bool - expected APIErrorCode + queryParams map[string]string + headers map[string]string + region string + expected APIErrorCode }{ // (0) Should error without a set URL query. { - verifyRegion: false, - expected: ErrInvalidQueryParams, + region: "us-east-1", + expected: ErrInvalidQueryParams, }, // (1) Should error on an invalid access key. { @@ -126,8 +126,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date", "X-Amz-Credential": fmt.Sprintf(credentialTemplate, "Z7IXGOO6BZ0REAN1Q26I", now.Format(yyyymmdd), "us-west-1"), }, - verifyRegion: false, - expected: ErrInvalidAccessKeyID, + region: "us-west-1", + expected: ErrInvalidAccessKeyID, }, // (2) Should error when the payload sha256 doesn't match. { @@ -140,8 +140,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-Credential": fmt.Sprintf(credentialTemplate, serverConfig.GetCredential().AccessKeyID, now.Format(yyyymmdd), "us-west-1"), "X-Amz-Content-Sha256": "ThisIsNotThePayloadHash", }, - verifyRegion: false, - expected: ErrContentSHA256Mismatch, + region: "us-west-1", + expected: ErrContentSHA256Mismatch, }, // (3) Should fail with an invalid region. { @@ -154,8 +154,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-Credential": fmt.Sprintf(credentialTemplate, serverConfig.GetCredential().AccessKeyID, now.Format(yyyymmdd), "us-west-1"), "X-Amz-Content-Sha256": payload, }, - verifyRegion: true, - expected: ErrInvalidRegion, + region: "us-east-1", + expected: ErrInvalidRegion, }, // (4) Should NOT fail with an invalid region if it doesn't verify it. { @@ -168,8 +168,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-Credential": fmt.Sprintf(credentialTemplate, serverConfig.GetCredential().AccessKeyID, now.Format(yyyymmdd), "us-west-1"), "X-Amz-Content-Sha256": payload, }, - verifyRegion: false, - expected: ErrUnsignedHeaders, + region: "us-west-1", + expected: ErrUnsignedHeaders, }, // (5) Should fail to extract headers if the host header is not signed. { @@ -182,8 +182,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-Credential": fmt.Sprintf(credentialTemplate, serverConfig.GetCredential().AccessKeyID, now.Format(yyyymmdd), serverConfig.GetRegion()), "X-Amz-Content-Sha256": payload, }, - verifyRegion: true, - expected: ErrUnsignedHeaders, + region: serverConfig.GetRegion(), + expected: ErrUnsignedHeaders, }, // (6) Should give an expired request if it has expired. { @@ -200,8 +200,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-Date": now.AddDate(0, 0, -2).Format(iso8601Format), "X-Amz-Content-Sha256": payload, }, - verifyRegion: false, - expected: ErrExpiredPresignRequest, + region: serverConfig.GetRegion(), + expected: ErrExpiredPresignRequest, }, // (7) Should error if the signature is incorrect. { @@ -218,8 +218,8 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { "X-Amz-Date": now.Format(iso8601Format), "X-Amz-Content-Sha256": payload, }, - verifyRegion: false, - expected: ErrSignatureDoesNotMatch, + region: serverConfig.GetRegion(), + expected: ErrSignatureDoesNotMatch, }, } @@ -243,7 +243,7 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { } // Check if it matches! - err := doesPresignedSignatureMatch(payload, req, testCase.verifyRegion) + err := doesPresignedSignatureMatch(payload, req, testCase.region) if err != testCase.expected { t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err)) } diff --git a/cmd/signature-verify-reader.go b/cmd/signature-verify-reader.go index e50af8291..10c0517e0 100644 --- a/cmd/signature-verify-reader.go +++ b/cmd/signature-verify-reader.go @@ -19,10 +19,11 @@ package cmd import ( "encoding/hex" "fmt" - "github.com/minio/sha256-simd" "hash" "io" "net/http" + + "github.com/minio/sha256-simd" ) // signVerifyReader represents an io.Reader compatible interface which @@ -31,13 +32,15 @@ import ( type signVerifyReader struct { Request *http.Request // HTTP request to be validated and read. HashWriter hash.Hash // sha256 hash writer. + Region string } // Initializes a new signature verify reader. -func newSignVerify(req *http.Request) *signVerifyReader { +func newSignVerify(req *http.Request, region string) *signVerifyReader { return &signVerifyReader{ Request: req, // Save the request. HashWriter: sha256.New(), // Inititalize sha256. + Region: region, } } @@ -49,7 +52,6 @@ func isSignVerify(reader io.Reader) bool { // Verify - verifies signature and returns error upon signature mismatch. func (v *signVerifyReader) Verify() error { - validateRegion := true // Defaults to validating region. shaPayloadHex := hex.EncodeToString(v.HashWriter.Sum(nil)) if skipContentSha256Cksum(v.Request) { // Sets 'UNSIGNED-PAYLOAD' if client requested to not calculated sha256. @@ -58,9 +60,9 @@ func (v *signVerifyReader) Verify() error { // Signature verification block. var s3Error APIErrorCode if isRequestSignatureV4(v.Request) { - s3Error = doesSignatureMatch(shaPayloadHex, v.Request, validateRegion) + s3Error = doesSignatureMatch(shaPayloadHex, v.Request, v.Region) } else if isRequestPresignedSignatureV4(v.Request) { - s3Error = doesPresignedSignatureMatch(shaPayloadHex, v.Request, validateRegion) + s3Error = doesPresignedSignatureMatch(shaPayloadHex, v.Request, v.Region) } else { // Couldn't figure out the request type, set the error as AccessDenied. s3Error = ErrAccessDenied diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index c538fd908..4319b54f6 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -206,6 +206,8 @@ func signRequest(req *http.Request, accessKey, secretKey string) error { } sort.Strings(headers) + region := serverConfig.GetRegion() + // Get canonical headers. var buf bytes.Buffer for _, k := range headers { @@ -257,7 +259,7 @@ func signRequest(req *http.Request, accessKey, secretKey string) error { // Get scope. scope := strings.Join([]string{ currTime.Format(yyyymmdd), - "us-east-1", + region, "s3", "aws4_request", }, "/") @@ -267,8 +269,8 @@ func signRequest(req *http.Request, accessKey, secretKey string) error { stringToSign = stringToSign + hex.EncodeToString(sum256([]byte(canonicalRequest))) date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd))) - region := sumHMAC(date, []byte("us-east-1")) - service := sumHMAC(region, []byte("s3")) + regionHMAC := sumHMAC(date, []byte(region)) + service := sumHMAC(regionHMAC, []byte("s3")) signingKey := sumHMAC(service, []byte("aws4_request")) signature := hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) @@ -305,9 +307,9 @@ func newTestRequest(method, urlStr string, contentLength int64, body io.ReadSeek case body == nil: hashedPayload = hex.EncodeToString(sum256([]byte{})) default: - payloadBytes, e := ioutil.ReadAll(body) - if e != nil { - return nil, e + payloadBytes, err := ioutil.ReadAll(body) + if err != nil { + return nil, err } hashedPayload = hex.EncodeToString(sum256(payloadBytes)) md5Base64 := base64.StdEncoding.EncodeToString(sumMD5(payloadBytes))