From e51be73ac7cff94ca0e6d1c657929f736e176286 Mon Sep 17 00:00:00 2001 From: Krishna Srinivas Date: Sat, 22 Oct 2016 21:27:12 +0530 Subject: [PATCH] PresignedPost: Support for Signature V2 presigned POST Policy. (#3043) fixes #2993 --- cmd/post-policy_test.go | 112 ++++++++++++++++++++++++++--- cmd/signature-v2.go | 32 ++++++--- cmd/signature-v2_test.go | 32 +++++++++ cmd/signature-v4-postpolicyform.go | 3 - cmd/signature-v4.go | 11 ++- 5 files changed, 166 insertions(+), 24 deletions(-) diff --git a/cmd/post-policy_test.go b/cmd/post-policy_test.go index 89485b608..6f1bc04eb 100644 --- a/cmd/post-policy_test.go +++ b/cmd/post-policy_test.go @@ -33,8 +33,8 @@ const ( iso8601DateFormat = "20060102T150405Z" ) -// newPostPolicyBytes - creates a bare bones postpolicy string with key and bucket matches. -func newPostPolicyBytes(credential, bucketName, objectKey string, expiration time.Time) []byte { +// newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches. +func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte { t := time.Now().UTC() // Add the expiration date. expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(expirationDateFormat)) @@ -59,6 +59,25 @@ func newPostPolicyBytes(credential, bucketName, objectKey string, expiration tim return []byte(retStr) } +// newPostPolicyBytesV2 - creates a bare bones postpolicy string with key and bucket matches. +func newPostPolicyBytesV2(bucketName, objectKey string, expiration time.Time) []byte { + // Add the expiration date. + expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(expirationDateFormat)) + // Add the bucket condition, only accept buckets equal to the one passed. + bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) + // Add the key condition, only accept keys equal to the one passed. + keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s"]`, objectKey) + + // Combine all conditions into one string. + conditionStr := fmt.Sprintf(`"conditions":[%s, %s]`, bucketConditionStr, keyConditionStr) + retStr := "{" + retStr = retStr + expirationStr + "," + retStr = retStr + conditionStr + retStr = retStr + "}" + + return []byte(retStr) +} + // Wrapper for calling TestPostPolicyHandlerHandler tests for both XL multiple disks and single node setup. func TestPostPolicyHandler(t *testing.T) { ExecObjectLayerTest(t, testPostPolicyHandler) @@ -105,9 +124,34 @@ func testPostPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandle t.Fatalf("%s : %s", instanceType, err.Error()) } - // Collection of non-exhaustive ListMultipartUploads test cases, valid errors - // and success responses. - testCases := []struct { + // Test cases for signature-V2. + testCasesV2 := []struct { + expectedStatus int + accessKey string + secretKey string + }{ + {http.StatusForbidden, "invalidaccesskey", credentials.SecretAccessKey}, + {http.StatusForbidden, credentials.AccessKeyID, "invalidsecretkey"}, + {http.StatusNoContent, credentials.AccessKeyID, credentials.SecretAccessKey}, + } + + for i, test := range testCasesV2 { + // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. + rec := httptest.NewRecorder() + req, perr := newPostRequestV2("", bucketName, "testobject", test.accessKey, test.secretKey) + if perr != nil { + t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: %v", i+1, instanceType, perr) + } + // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. + // Call the ServeHTTP to execute the handler. + apiRouter.ServeHTTP(rec, req) + if rec.Code != test.expectedStatus { + t.Fatalf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, test.expectedStatus, rec.Code) + } + } + + // Test cases for signature-V4. + testCasesV4 := []struct { objectName string data []byte expectedRespStatus int @@ -144,10 +188,10 @@ func testPostPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandle }, } - for i, testCase := range testCases { + for i, testCase := range testCasesV4 { // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. rec := httptest.NewRecorder() - req, perr := newPostRequest("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey) + req, perr := newPostRequestV4("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey) if perr != nil { t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: %v", i+1, instanceType, perr) } @@ -173,7 +217,57 @@ func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, l return signature } -func newPostRequest(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { +func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secretKey string) (*http.Request, error) { + // Expire the request five minutes from now. + expirationTime := time.Now().UTC().Add(time.Minute * 5) + // Create a new post policy. + policy := newPostPolicyBytesV2(bucketName, objectName, expirationTime) + // Only need the encoding. + encodedPolicy := base64.StdEncoding.EncodeToString(policy) + + // Presign with V4 signature based on the policy. + signature := calculateSignatureV2(encodedPolicy, secretKey) + + formData := map[string]string{ + "AWSAccessKeyId": accessKey, + "bucket": bucketName, + "key": objectName, + "policy": encodedPolicy, + "signature": signature, + } + + // Create the multipart form. + var buf bytes.Buffer + w := multipart.NewWriter(&buf) + + // Set the normal formData + for k, v := range formData { + w.WriteField(k, v) + } + // Set the File formData + writer, err := w.CreateFormFile("file", "s3verify/post/object") + if err != nil { + // return nil, err + return nil, err + } + writer.Write([]byte("hello world")) + // Close before creating the new request. + w.Close() + + // Set the body equal to the created policy. + reader := bytes.NewReader(buf.Bytes()) + + req, err := http.NewRequest("POST", makeTestTargetURL(endPoint, bucketName, objectName, nil), reader) + if err != nil { + return nil, err + } + + // Set form content-type. + req.Header.Set("Content-Type", w.FormDataContentType()) + return req, nil +} + +func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { // Keep time. t := time.Now().UTC() // Expire the request five minutes from now. @@ -181,7 +275,7 @@ func newPostRequest(endPoint, bucketName, objectName string, objData []byte, acc // Get the user credential. credStr := getCredential(accessKey, serverConfig.GetRegion(), t) // Create a new post policy. - policy := newPostPolicyBytes(credStr, bucketName, objectName, expirationTime) + policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime) // Only need the encoding. encodedPolicy := base64.StdEncoding.EncodeToString(policy) diff --git a/cmd/signature-v2.go b/cmd/signature-v2.go index 8957c117e..9dea67e97 100644 --- a/cmd/signature-v2.go +++ b/cmd/signature-v2.go @@ -56,7 +56,19 @@ var resourceList = []string{ "website", } -// TODO add post policy signature. +func doesPolicySignatureV2Match(formValues map[string]string) APIErrorCode { + cred := serverConfig.GetCredential() + accessKey := formValues["Awsaccesskeyid"] + if accessKey != cred.AccessKeyID { + return ErrInvalidAccessKeyID + } + signature := formValues["Signature"] + policy := formValues["Policy"] + if signature != calculateSignatureV2(policy, cred.SecretAccessKey) { + return ErrSignatureDoesNotMatch + } + return ErrNone +} // doesPresignV2SignatureMatch - Verify query headers with presigned signature // - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth @@ -198,26 +210,24 @@ func doesSignV2Match(r *http.Request) APIErrorCode { return ErrNone } +func calculateSignatureV2(stringToSign string, secret string) string { + hm := hmac.New(sha1.New, []byte(secret)) + hm.Write([]byte(stringToSign)) + return base64.StdEncoding.EncodeToString(hm.Sum(nil)) +} + // 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 calculateSignatureV2(stringToSign, cred.SecretAccessKey) } // 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)) + signature := calculateSignatureV2(stringToSign, cred.SecretAccessKey) return fmt.Sprintf("%s %s:%s", signV2Algorithm, cred.AccessKeyID, signature) } diff --git a/cmd/signature-v2_test.go b/cmd/signature-v2_test.go index 64966507f..24c8a8d24 100644 --- a/cmd/signature-v2_test.go +++ b/cmd/signature-v2_test.go @@ -180,3 +180,35 @@ func TestValidateV2AuthHeader(t *testing.T) { } } + +func TestDoesPolicySignatureV2Match(t *testing.T) { + if err := initConfig(); err != nil { + t.Fatal(err) + } + + if err := serverConfig.Save(); err != nil { + t.Fatal(err) + } + creds := serverConfig.GetCredential() + policy := "policy" + testCases := []struct { + accessKey string + policy string + signature string + errCode APIErrorCode + }{ + {"invalidAccessKey", policy, calculateSignatureV2(policy, creds.SecretAccessKey), ErrInvalidAccessKeyID}, + {creds.AccessKeyID, policy, calculateSignatureV2("random", creds.SecretAccessKey), ErrSignatureDoesNotMatch}, + {creds.AccessKeyID, policy, calculateSignatureV2(policy, creds.SecretAccessKey), ErrNone}, + } + for i, test := range testCases { + formValues := make(map[string]string) + formValues["Awsaccesskeyid"] = test.accessKey + formValues["Signature"] = test.signature + formValues["Policy"] = test.policy + errCode := doesPolicySignatureV2Match(formValues) + if errCode != test.errCode { + t.Fatalf("(%d) expected to get %s, instead got %s", i+1, niceError(test.errCode), niceError(errCode)) + } + } +} diff --git a/cmd/signature-v4-postpolicyform.go b/cmd/signature-v4-postpolicyform.go index 8f05d6aa5..4f7011535 100644 --- a/cmd/signature-v4-postpolicyform.go +++ b/cmd/signature-v4-postpolicyform.go @@ -153,9 +153,6 @@ func parsePostPolicyFormV4(policy string) (PostPolicyForm, error) { // checkPostPolicy - apply policy conditions and validate input values. func checkPostPolicy(formValues map[string]string) APIErrorCode { - if formValues["X-Amz-Algorithm"] != signV4Algorithm { - return ErrSignatureVersionNotSupported - } /// Decoding policy policyBytes, err := base64.StdEncoding.DecodeString(formValues["Policy"]) if err != nil { diff --git a/cmd/signature-v4.go b/cmd/signature-v4.go index 1c44c53ae..15952e8ac 100644 --- a/cmd/signature-v4.go +++ b/cmd/signature-v4.go @@ -145,10 +145,19 @@ func getSignature(signingKey []byte, stringToSign string) string { return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) } +// Check to see if Policy is signed correctly. +func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode { + // For SignV2 - Signature field will be valid + if formValues["Signature"] != "" { + return doesPolicySignatureV2Match(formValues) + } + return doesPolicySignatureV4Match(formValues) +} + // doesPolicySignatureMatch - Verify query headers with post policy // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html // returns ErrNone if the signature matches. -func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode { +func doesPolicySignatureV4Match(formValues map[string]string) APIErrorCode { // Access credentials. cred := serverConfig.GetCredential()