Sign V2 support.
This commit is contained in:
@@ -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: {},
|
||||
}
|
||||
|
||||
@@ -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 <cred_string>"},
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
326
cmd/signature-v2.go
Normal file
326
cmd/signature-v2.go
Normal file
@@ -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 ] +
|
||||
// <HTTP-Request-URI, from the protocol name up to the query string> +
|
||||
// [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
|
||||
//
|
||||
// CanonicalizedProtocolHeaders = <described below>
|
||||
|
||||
// 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
|
||||
}
|
||||
182
cmd/signature-v2_test.go
Normal file
182
cmd/signature-v2_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user