mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-09-24 17:23:40 +08:00
add s3 signature tests and prepare implementation of STREAMING-UNSIGNED-PAYLOAD-TRAILER (#6525)
* add tests for s3 signature * add test for newSignV4ChunkedReader.Read() * add glog import
This commit is contained in:
@@ -364,6 +364,9 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action)
|
|||||||
glog.V(3).Infof("post policy auth type")
|
glog.V(3).Infof("post policy auth type")
|
||||||
r.Header.Set(s3_constants.AmzAuthType, "PostPolicy")
|
r.Header.Set(s3_constants.AmzAuthType, "PostPolicy")
|
||||||
return identity, s3err.ErrNone
|
return identity, s3err.ErrNone
|
||||||
|
case authTypeStreamingUnsigned:
|
||||||
|
glog.V(3).Infof("unsigned streaming upload")
|
||||||
|
return identity, s3err.ErrNone
|
||||||
case authTypeJWT:
|
case authTypeJWT:
|
||||||
glog.V(3).Infof("jwt auth type")
|
glog.V(3).Infof("jwt auth type")
|
||||||
r.Header.Set(s3_constants.AmzAuthType, "Jwt")
|
r.Header.Set(s3_constants.AmzAuthType, "Jwt")
|
||||||
@@ -412,6 +415,10 @@ func (iam *IdentityAccessManagement) authUser(r *http.Request) (*Identity, s3err
|
|||||||
var authType string
|
var authType string
|
||||||
switch getRequestAuthType(r) {
|
switch getRequestAuthType(r) {
|
||||||
case authTypeStreamingSigned:
|
case authTypeStreamingSigned:
|
||||||
|
glog.V(3).Infof("signed streaming upload")
|
||||||
|
return identity, s3err.ErrNone
|
||||||
|
case authTypeStreamingUnsigned:
|
||||||
|
glog.V(3).Infof("unsigned streaming upload")
|
||||||
return identity, s3err.ErrNone
|
return identity, s3err.ErrNone
|
||||||
case authTypeUnknown:
|
case authTypeUnknown:
|
||||||
glog.V(3).Infof("unknown auth type")
|
glog.V(3).Infof("unknown auth type")
|
||||||
|
@@ -56,9 +56,10 @@ const (
|
|||||||
streamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
streamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
||||||
signV4ChunkedAlgorithm = "AWS4-HMAC-SHA256-PAYLOAD"
|
signV4ChunkedAlgorithm = "AWS4-HMAC-SHA256-PAYLOAD"
|
||||||
|
|
||||||
// http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the
|
// http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" or "STREAMING-UNSIGNED-PAYLOAD-TRAILER" indicates that the
|
||||||
// client did not calculate sha256 of the payload.
|
// client did not calculate sha256 of the payload.
|
||||||
unsignedPayload = "UNSIGNED-PAYLOAD"
|
unsignedPayload = "UNSIGNED-PAYLOAD"
|
||||||
|
streamingUnsignedPayload = "STREAMING-UNSIGNED-PAYLOAD-TRAILER"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns SHA256 for calculating canonical-request.
|
// Returns SHA256 for calculating canonical-request.
|
||||||
|
@@ -8,8 +8,6 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -21,7 +19,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestIsRequestPresignedSignatureV4 - Test validates the logic for presign signature version v4 detection.
|
// TestIsRequestPresignedSignatureV4 - Test validates the logic for presign signature version v4 detection.
|
||||||
@@ -288,6 +290,73 @@ var ignoredHeaders = map[string]bool{
|
|||||||
"User-Agent": true,
|
"User-Agent": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests the test helper with an example from the AWS Doc.
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||||
|
// This time it's a PUT request uploading the file with content "Welcome to Amazon S3."
|
||||||
|
func TestGetStringToSignPUT(t *testing.T) {
|
||||||
|
|
||||||
|
canonicalRequest := `PUT
|
||||||
|
/test%24file.text
|
||||||
|
|
||||||
|
date:Fri, 24 May 2013 00:00:00 GMT
|
||||||
|
host:examplebucket.s3.amazonaws.com
|
||||||
|
x-amz-content-sha256:44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072
|
||||||
|
x-amz-date:20130524T000000Z
|
||||||
|
x-amz-storage-class:REDUCED_REDUNDANCY
|
||||||
|
|
||||||
|
date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class
|
||||||
|
44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072`
|
||||||
|
|
||||||
|
date, err := time.Parse(iso8601Format, "20130524T000000Z")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing date: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := "20130524/us-east-1/s3/aws4_request"
|
||||||
|
stringToSign := getStringToSign(canonicalRequest, date, scope)
|
||||||
|
|
||||||
|
expected := `AWS4-HMAC-SHA256
|
||||||
|
20130524T000000Z
|
||||||
|
20130524/us-east-1/s3/aws4_request
|
||||||
|
9e0e90d9c76de8fa5b200d8c849cd5b8dc7a3be3951ddb7f6a76b4158342019d`
|
||||||
|
|
||||||
|
assert.Equal(t, expected, stringToSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests the test helper with an example from the AWS Doc.
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||||
|
// The GET request example with empty string hash.
|
||||||
|
func TestGetStringToSignGETEmptyStringHash(t *testing.T) {
|
||||||
|
|
||||||
|
canonicalRequest := `GET
|
||||||
|
/test.txt
|
||||||
|
|
||||||
|
host:examplebucket.s3.amazonaws.com
|
||||||
|
range:bytes=0-9
|
||||||
|
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||||
|
x-amz-date:20130524T000000Z
|
||||||
|
|
||||||
|
host;range;x-amz-content-sha256;x-amz-date
|
||||||
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||||
|
|
||||||
|
date, err := time.Parse(iso8601Format, "20130524T000000Z")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing date: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := "20130524/us-east-1/s3/aws4_request"
|
||||||
|
stringToSign := getStringToSign(canonicalRequest, date, scope)
|
||||||
|
|
||||||
|
expected := `AWS4-HMAC-SHA256
|
||||||
|
20130524T000000Z
|
||||||
|
20130524/us-east-1/s3/aws4_request
|
||||||
|
7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972`
|
||||||
|
|
||||||
|
assert.Equal(t, expected, stringToSign)
|
||||||
|
}
|
||||||
|
|
||||||
// Sign given request using Signature V4.
|
// Sign given request using Signature V4.
|
||||||
func signRequestV4(req *http.Request, accessKey, secretKey string) error {
|
func signRequestV4(req *http.Request, accessKey, secretKey string) error {
|
||||||
// Get hashed payload.
|
// Get hashed payload.
|
||||||
|
@@ -29,6 +29,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
|
|
||||||
@@ -54,14 +55,21 @@ func (iam *IdentityAccessManagement) calculateSeedSignature(r *http.Request) (cr
|
|||||||
return nil, "", "", time.Time{}, errCode
|
return nil, "", "", time.Time{}, errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload streaming.
|
contentSha256Header := req.Header.Get("X-Amz-Content-Sha256")
|
||||||
payload := streamingContentSHA256
|
|
||||||
|
|
||||||
|
switch contentSha256Header {
|
||||||
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
|
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
|
||||||
if payload != req.Header.Get("X-Amz-Content-Sha256") {
|
case streamingContentSHA256:
|
||||||
|
glog.V(3).Infof("streaming content sha256")
|
||||||
|
case streamingUnsignedPayload:
|
||||||
|
glog.V(3).Infof("streaming unsigned payload")
|
||||||
|
default:
|
||||||
return nil, "", "", time.Time{}, s3err.ErrContentSHA256Mismatch
|
return nil, "", "", time.Time{}, s3err.ErrContentSHA256Mismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Payload streaming.
|
||||||
|
payload := contentSha256Header
|
||||||
|
|
||||||
// Extract all the signed headers along with its values.
|
// Extract all the signed headers along with its values.
|
||||||
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
||||||
if errCode != s3err.ErrNone {
|
if errCode != s3err.ErrNone {
|
||||||
|
107
weed/s3api/chunked_reader_v4_test.go
Normal file
107
weed/s3api/chunked_reader_v4_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package s3api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This test will implement the following scenario:
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#example-signature-calculations-streaming
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimestamp = "20130524T000000Z"
|
||||||
|
defaultBucketName = "examplebucket"
|
||||||
|
defaultAccessKeyId = "AKIAIOSFODNN7EXAMPLE"
|
||||||
|
defaultSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||||
|
defaultRegion = "us-east-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generatePayload() string {
|
||||||
|
chunk1 := "10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\r\n" +
|
||||||
|
strings.Repeat("a", 65536) + "\r\n"
|
||||||
|
chunk2 := "400;chunk-signature=0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497\r\n" +
|
||||||
|
strings.Repeat("a", 1024) + "\r\n"
|
||||||
|
chunk3 := "0;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\r\n\r\n"
|
||||||
|
|
||||||
|
payload := chunk1 + chunk2 + chunk3
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequest() (*http.Request, error) {
|
||||||
|
payload := generatePayload()
|
||||||
|
req, err := http.NewRequest("PUT", "http://s3.amazonaws.com/examplebucket/chunkObject.txt", bytes.NewReader([]byte(payload)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Host", "s3.amazonaws.com")
|
||||||
|
req.Header.Set("x-amz-date", defaultTimestamp)
|
||||||
|
req.Header.Set("x-amz-storage-class", "REDUCED_REDUNDANCY")
|
||||||
|
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class,Signature=4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9")
|
||||||
|
req.Header.Set("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
|
||||||
|
req.Header.Set("Content-Encoding", "aws-chunked")
|
||||||
|
req.Header.Set("x-amz-decoded-content-length", "66560")
|
||||||
|
req.Header.Set("Content-Length", "66824")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSignV4ChunkedReader(t *testing.T) {
|
||||||
|
req, err := NewRequest()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an IdentityAccessManagement instance
|
||||||
|
iam := IdentityAccessManagement{
|
||||||
|
identities: []*Identity{},
|
||||||
|
accessKeyIdent: map[string]*Identity{},
|
||||||
|
accounts: map[string]*Account{},
|
||||||
|
emailAccount: map[string]*Account{},
|
||||||
|
hashes: map[string]*sync.Pool{},
|
||||||
|
hashCounters: map[string]*int32{},
|
||||||
|
identityAnonymous: nil,
|
||||||
|
domain: "",
|
||||||
|
isAuthEnabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default access keys and secrets
|
||||||
|
iam.identities = append(iam.identities, &Identity{
|
||||||
|
Name: "default",
|
||||||
|
Credentials: []*Credential{
|
||||||
|
{
|
||||||
|
AccessKey: defaultAccessKeyId,
|
||||||
|
SecretKey: defaultSecretAccessKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Actions: []Action{
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"List",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
iam.accessKeyIdent[defaultAccessKeyId] = iam.identities[0]
|
||||||
|
|
||||||
|
// Call newSignV4ChunkedReader
|
||||||
|
reader, errCode := iam.newSignV4ChunkedReader(req)
|
||||||
|
assert.NotNil(t, reader)
|
||||||
|
assert.Equal(t, s3err.ErrNone, errCode)
|
||||||
|
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The expected payload a long string of 'a's
|
||||||
|
expectedPayload := strings.Repeat("a", 66560)
|
||||||
|
assert.Equal(t, expectedPayload, string(data))
|
||||||
|
|
||||||
|
}
|
@@ -53,6 +53,11 @@ func isRequestSignStreamingV4(r *http.Request) bool {
|
|||||||
r.Method == http.MethodPut
|
r.Method == http.MethodPut
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isRequestUnsignedStreaming(r *http.Request) bool {
|
||||||
|
return r.Header.Get("x-amz-content-sha256") == streamingUnsignedPayload &&
|
||||||
|
r.Method == http.MethodPut
|
||||||
|
}
|
||||||
|
|
||||||
// Authorization type.
|
// Authorization type.
|
||||||
type authType int
|
type authType int
|
||||||
|
|
||||||
@@ -64,6 +69,7 @@ const (
|
|||||||
authTypePresignedV2
|
authTypePresignedV2
|
||||||
authTypePostPolicy
|
authTypePostPolicy
|
||||||
authTypeStreamingSigned
|
authTypeStreamingSigned
|
||||||
|
authTypeStreamingUnsigned
|
||||||
authTypeSigned
|
authTypeSigned
|
||||||
authTypeSignedV2
|
authTypeSignedV2
|
||||||
authTypeJWT
|
authTypeJWT
|
||||||
@@ -77,6 +83,8 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
return authTypePresignedV2
|
return authTypePresignedV2
|
||||||
} else if isRequestSignStreamingV4(r) {
|
} else if isRequestSignStreamingV4(r) {
|
||||||
return authTypeStreamingSigned
|
return authTypeStreamingSigned
|
||||||
|
} else if isRequestUnsignedStreaming(r) {
|
||||||
|
return authTypeStreamingUnsigned
|
||||||
} else if isRequestSignatureV4(r) {
|
} else if isRequestSignatureV4(r) {
|
||||||
return authTypeSigned
|
return authTypeSigned
|
||||||
} else if isRequestPresignedSignatureV4(r) {
|
} else if isRequestPresignedSignatureV4(r) {
|
||||||
|
@@ -52,7 +52,7 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
if s3a.iam.isEnabled() {
|
if s3a.iam.isEnabled() {
|
||||||
var s3ErrCode s3err.ErrorCode
|
var s3ErrCode s3err.ErrorCode
|
||||||
switch rAuthType {
|
switch rAuthType {
|
||||||
case authTypeStreamingSigned:
|
case authTypeStreamingSigned, authTypeStreamingUnsigned:
|
||||||
dataReader, s3ErrCode = s3a.iam.newSignV4ChunkedReader(r)
|
dataReader, s3ErrCode = s3a.iam.newSignV4ChunkedReader(r)
|
||||||
case authTypeSignedV2, authTypePresignedV2:
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
_, s3ErrCode = s3a.iam.isReqAuthenticatedV2(r)
|
_, s3ErrCode = s3a.iam.isReqAuthenticatedV2(r)
|
||||||
|
Reference in New Issue
Block a user