302 lines
10 KiB
Go
302 lines
10 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func niceError(code APIErrorCode) string {
|
|
// Special-handle ErrNone
|
|
if code == ErrNone {
|
|
return "ErrNone"
|
|
}
|
|
|
|
return fmt.Sprintf("%s (%s)", errorCodes[code].Code, errorCodes[code].Description)
|
|
}
|
|
|
|
func TestDoesPolicySignatureMatch(t *testing.T) {
|
|
credentialTemplate := "%s/%s/%s/s3/aws4_request"
|
|
now := UTCNow()
|
|
accessKey := globalActiveCred.AccessKey
|
|
|
|
testCases := []struct {
|
|
form http.Header
|
|
expected APIErrorCode
|
|
}{
|
|
// (0) It should fail if 'X-Amz-Credential' is missing.
|
|
{
|
|
form: http.Header{},
|
|
expected: ErrCredMalformed,
|
|
},
|
|
// (1) It should fail if the access key is incorrect.
|
|
{
|
|
form: http.Header{
|
|
"X-Amz-Credential": []string{fmt.Sprintf(credentialTemplate, "EXAMPLEINVALIDEXAMPL", now.Format(yyyymmdd), globalMinioDefaultRegion)},
|
|
},
|
|
expected: ErrInvalidAccessKeyID,
|
|
},
|
|
// (2) It should fail with a bad signature.
|
|
{
|
|
form: http.Header{
|
|
"X-Amz-Credential": []string{fmt.Sprintf(credentialTemplate, accessKey, now.Format(yyyymmdd), globalMinioDefaultRegion)},
|
|
"X-Amz-Date": []string{now.Format(iso8601Format)},
|
|
"X-Amz-Signature": []string{"invalidsignature"},
|
|
"Policy": []string{"policy"},
|
|
},
|
|
expected: ErrSignatureDoesNotMatch,
|
|
},
|
|
// (3) It should succeed if everything is correct.
|
|
{
|
|
form: http.Header{
|
|
"X-Amz-Credential": []string{
|
|
fmt.Sprintf(credentialTemplate, accessKey, now.Format(yyyymmdd), globalMinioDefaultRegion),
|
|
},
|
|
"X-Amz-Date": []string{now.Format(iso8601Format)},
|
|
"X-Amz-Signature": []string{
|
|
getSignature(getSigningKey(globalActiveCred.SecretKey, now,
|
|
globalMinioDefaultRegion, serviceS3), "policy"),
|
|
},
|
|
"Policy": []string{"policy"},
|
|
},
|
|
expected: ErrNone,
|
|
},
|
|
}
|
|
|
|
// Run each test case individually.
|
|
for i, testCase := range testCases {
|
|
_, code := doesPolicySignatureMatch(testCase.form)
|
|
if code != testCase.expected {
|
|
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(code))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoesPresignedSignatureMatch(t *testing.T) {
|
|
obj, fsDir, err := prepareFS()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(fsDir)
|
|
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// sha256 hash of "payload"
|
|
payloadSHA256 := "239f59ed55e737c77147cf55ad0c1b030b6d7ee748a7426952f9b852d5a935e5"
|
|
now := UTCNow()
|
|
credentialTemplate := "%s/%s/%s/s3/aws4_request"
|
|
|
|
region := globalServerRegion
|
|
accessKeyID := globalActiveCred.AccessKey
|
|
testCases := []struct {
|
|
queryParams map[string]string
|
|
headers map[string]string
|
|
region string
|
|
expected APIErrorCode
|
|
}{
|
|
// (0) Should error without a set URL query.
|
|
{
|
|
region: globalMinioDefaultRegion,
|
|
expected: ErrInvalidQueryParams,
|
|
},
|
|
// (1) Should error on an invalid access key.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, "Z7IXGOO6BZ0REAN1Q26I", now.Format(yyyymmdd), "us-west-1"),
|
|
},
|
|
region: "us-west-1",
|
|
expected: ErrInvalidAccessKeyID,
|
|
},
|
|
// (2) Should NOT fail with an invalid region if it doesn't verify it.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), "us-west-1"),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: "us-west-1",
|
|
expected: ErrUnsignedHeaders,
|
|
},
|
|
// (3) Should fail to extract headers if the host header is not signed.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: region,
|
|
expected: ErrUnsignedHeaders,
|
|
},
|
|
// (4) Should give an expired request if it has expired.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.AddDate(0, 0, -2).Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
headers: map[string]string{
|
|
"X-Amz-Date": now.AddDate(0, 0, -2).Format(iso8601Format),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: region,
|
|
expected: ErrExpiredPresignRequest,
|
|
},
|
|
// (5) Should error if the signature is incorrect.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
headers: map[string]string{
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: region,
|
|
expected: ErrSignatureDoesNotMatch,
|
|
},
|
|
// (6) Should error if the request is not ready yet, ie X-Amz-Date is in the future.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Add(1 * time.Hour).Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
headers: map[string]string{
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: region,
|
|
expected: ErrRequestNotReadyYet,
|
|
},
|
|
// (7) Should not error with invalid region instead, call should proceed
|
|
// with sigature does not match.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
headers: map[string]string{
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: "",
|
|
expected: ErrSignatureDoesNotMatch,
|
|
},
|
|
// (8) Should error with signature does not match. But handles
|
|
// query params which do not precede with "x-amz-" header.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
"response-content-type": "application/json",
|
|
},
|
|
headers: map[string]string{
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
},
|
|
region: "",
|
|
expected: ErrSignatureDoesNotMatch,
|
|
},
|
|
// (9) Should error with unsigned headers.
|
|
{
|
|
queryParams: map[string]string{
|
|
"X-Amz-Algorithm": signV4Algorithm,
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
"X-Amz-Expires": "60",
|
|
"X-Amz-Signature": "badsignature",
|
|
"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
|
|
"X-Amz-Credential": fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
|
|
"X-Amz-Content-Sha256": payloadSHA256,
|
|
"response-content-type": "application/json",
|
|
},
|
|
headers: map[string]string{
|
|
"X-Amz-Date": now.Format(iso8601Format),
|
|
},
|
|
region: "",
|
|
expected: ErrUnsignedHeaders,
|
|
},
|
|
}
|
|
|
|
// 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 := doesPresignedSignatureMatch(payloadSHA256, req, testCase.region, serviceS3)
|
|
if err != testCase.expected {
|
|
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err))
|
|
}
|
|
}
|
|
}
|