You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							316 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							316 lines
						
					
					
						
							11 KiB
						
					
					
				| package s3api | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"encoding/base64" | |
| 	"fmt" | |
| 	"io" | |
| 	"net/http" | |
| 	"strings" | |
| 	"sync" | |
| 	"testing" | |
| 	"time" | |
| 
 | |
| 	"hash/crc32" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" | |
| 	"github.com/stretchr/testify/assert" | |
| ) | |
| 
 | |
| // getDefaultTimestamp returns a current timestamp for tests | |
| func getDefaultTimestamp() string { | |
| 	return time.Now().UTC().Format(iso8601Format) | |
| } | |
| 
 | |
| const ( | |
| 	defaultTimestamp       = "20130524T000000Z" // Legacy constant for reference | |
| 	defaultBucketName      = "examplebucket" | |
| 	defaultAccessKeyId     = "AKIAIOSFODNN7EXAMPLE" | |
| 	defaultSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" | |
| 	defaultRegion          = "us-east-1" | |
| ) | |
| 
 | |
| func generateStreamingUnsignedPayloadTrailerPayload(includeFinalCRLF bool) string { | |
| 	// This test will implement the following scenario: | |
| 	// https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html | |
|  | |
| 	chunk1 := "2000\r\n" + strings.Repeat("a", 8192) + "\r\n" | |
| 	chunk2 := "2000\r\n" + strings.Repeat("a", 8192) + "\r\n" | |
| 	chunk3 := "400\r\n" + strings.Repeat("a", 1024) + "\r\n" | |
| 
 | |
| 	chunk4 := "0\r\n" /* the last chunk is empty */ | |
| 
 | |
| 	if includeFinalCRLF { | |
| 		// Some clients omit the final CRLF, so we need to test that case as well | |
| 		chunk4 += "\r\n" | |
| 	} | |
| 
 | |
| 	data := strings.Repeat("a", 17408) | |
| 	writer := crc32.NewIEEE() | |
| 	_, err := writer.Write([]byte(data)) | |
| 
 | |
| 	if err != nil { | |
| 		fmt.Println("Error:", err) | |
| 	} | |
| 	checksum := writer.Sum(nil) | |
| 	base64EncodedChecksum := base64.StdEncoding.EncodeToString(checksum) | |
| 	trailer := "x-amz-checksum-crc32:" + base64EncodedChecksum + "\n\r\n\r\n\r\n" | |
| 
 | |
| 	payload := chunk1 + chunk2 + chunk3 + chunk4 + trailer | |
| 	return payload | |
| } | |
| 
 | |
| func NewRequestStreamingUnsignedPayloadTrailer(includeFinalCRLF bool) (*http.Request, error) { | |
| 	// This test will implement the following scenario: | |
| 	// https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html | |
|  | |
| 	payload := generateStreamingUnsignedPayloadTrailerPayload(includeFinalCRLF) | |
| 	req, err := http.NewRequest("PUT", "http://amzn-s3-demo-bucket/Key+", bytes.NewReader([]byte(payload))) | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	req.Header.Set("Host", "amzn-s3-demo-bucket") | |
| 	req.Header.Set("x-amz-date", getDefaultTimestamp()) | |
| 	req.Header.Set("Content-Encoding", "aws-chunked") | |
| 	req.Header.Set("x-amz-decoded-content-length", "17408") | |
| 	req.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER") | |
| 	req.Header.Set("x-amz-trailer", "x-amz-checksum-crc32") | |
| 
 | |
| 	return req, nil | |
| } | |
| 
 | |
| func TestNewSignV4ChunkedReaderStreamingUnsignedPayloadTrailer(t *testing.T) { | |
| 	// This test will implement the following scenario: | |
| 	// https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html | |
| 	iam := setupIam() | |
| 
 | |
| 	req, err := NewRequestStreamingUnsignedPayloadTrailer(true) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to create request: %v", err) | |
| 	} | |
| 	// The expected payload a long string of 'a's | |
| 	expectedPayload := strings.Repeat("a", 17408) | |
| 
 | |
| 	runWithRequest(iam, req, t, expectedPayload) | |
| 
 | |
| 	req, err = NewRequestStreamingUnsignedPayloadTrailer(false) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to create request: %v", err) | |
| 	} | |
| 	runWithRequest(iam, req, t, expectedPayload) | |
| } | |
| 
 | |
| func runWithRequest(iam IdentityAccessManagement, req *http.Request, t *testing.T, expectedPayload string) { | |
| 	reader, errCode := iam.newChunkedReader(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) | |
| 	} | |
| 
 | |
| 	assert.Equal(t, expectedPayload, string(data)) | |
| } | |
| 
 | |
| func setupIam() IdentityAccessManagement { | |
| 	// Create an IdentityAccessManagement instance | |
| 	// Add default access keys and secrets | |
|  | |
| 	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, | |
| 	} | |
| 
 | |
| 	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] | |
| 	return iam | |
| } | |
| 
 | |
| // TestSignedStreamingUpload tests streaming uploads with signed chunks | |
| // This replaces the removed AWS example test with a dynamic signature generation approach | |
| func TestSignedStreamingUpload(t *testing.T) { | |
| 	iam := setupIam() | |
| 
 | |
| 	// Create a simple streaming upload with 2 chunks | |
| 	chunk1Data := strings.Repeat("a", 1024) | |
| 	chunk2Data := strings.Repeat("b", 512) | |
| 
 | |
| 	// Use current time for signatures | |
| 	now := time.Now().UTC() | |
| 	amzDate := now.Format(iso8601Format) | |
| 	dateStamp := now.Format(yyyymmdd) | |
| 
 | |
| 	// Calculate seed signature | |
| 	scope := dateStamp + "/" + defaultRegion + "/s3/aws4_request" | |
| 
 | |
| 	// Build canonical request for seed signature | |
| 	hashedPayload := "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" | |
| 	canonicalHeaders := "content-encoding:aws-chunked\n" + | |
| 		"host:s3.amazonaws.com\n" + | |
| 		"x-amz-content-sha256:" + hashedPayload + "\n" + | |
| 		"x-amz-date:" + amzDate + "\n" + | |
| 		"x-amz-decoded-content-length:1536\n" | |
| 	signedHeaders := "content-encoding;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length" | |
| 
 | |
| 	canonicalRequest := "PUT\n" + | |
| 		"/test-bucket/test-object\n" + | |
| 		"\n" + | |
| 		canonicalHeaders + "\n" + | |
| 		signedHeaders + "\n" + | |
| 		hashedPayload | |
| 
 | |
| 	canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest)) | |
| 	stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + canonicalRequestHash | |
| 
 | |
| 	signingKey := getSigningKey(defaultSecretAccessKey, dateStamp, defaultRegion, "s3") | |
| 	seedSignature := getSignature(signingKey, stringToSign) | |
| 
 | |
| 	// Calculate chunk signatures | |
| 	chunk1Hash := getSHA256Hash([]byte(chunk1Data)) | |
| 	chunk1StringToSign := "AWS4-HMAC-SHA256-PAYLOAD\n" + amzDate + "\n" + scope + "\n" + | |
| 		seedSignature + "\n" + emptySHA256 + "\n" + chunk1Hash | |
| 	chunk1Signature := getSignature(signingKey, chunk1StringToSign) | |
| 
 | |
| 	chunk2Hash := getSHA256Hash([]byte(chunk2Data)) | |
| 	chunk2StringToSign := "AWS4-HMAC-SHA256-PAYLOAD\n" + amzDate + "\n" + scope + "\n" + | |
| 		chunk1Signature + "\n" + emptySHA256 + "\n" + chunk2Hash | |
| 	chunk2Signature := getSignature(signingKey, chunk2StringToSign) | |
| 
 | |
| 	finalStringToSign := "AWS4-HMAC-SHA256-PAYLOAD\n" + amzDate + "\n" + scope + "\n" + | |
| 		chunk2Signature + "\n" + emptySHA256 + "\n" + emptySHA256 | |
| 	finalSignature := getSignature(signingKey, finalStringToSign) | |
| 
 | |
| 	// Build the chunked payload | |
| 	payload := fmt.Sprintf("400;chunk-signature=%s\r\n%s\r\n", chunk1Signature, chunk1Data) + | |
| 		fmt.Sprintf("200;chunk-signature=%s\r\n%s\r\n", chunk2Signature, chunk2Data) + | |
| 		fmt.Sprintf("0;chunk-signature=%s\r\n\r\n", finalSignature) | |
| 
 | |
| 	// Create the request | |
| 	req, err := http.NewRequest("PUT", "http://s3.amazonaws.com/test-bucket/test-object", | |
| 		bytes.NewReader([]byte(payload))) | |
| 	assert.NoError(t, err) | |
| 
 | |
| 	req.Header.Set("Host", "s3.amazonaws.com") | |
| 	req.Header.Set("x-amz-date", amzDate) | |
| 	req.Header.Set("x-amz-content-sha256", hashedPayload) | |
| 	req.Header.Set("Content-Encoding", "aws-chunked") | |
| 	req.Header.Set("x-amz-decoded-content-length", "1536") | |
| 
 | |
| 	authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", | |
| 		defaultAccessKeyId, scope, signedHeaders, seedSignature) | |
| 	req.Header.Set("Authorization", authHeader) | |
| 
 | |
| 	// Test the chunked reader | |
| 	reader, errCode := iam.newChunkedReader(req) | |
| 	assert.Equal(t, s3err.ErrNone, errCode) | |
| 	assert.NotNil(t, reader) | |
| 
 | |
| 	// Read and verify the payload | |
| 	data, err := io.ReadAll(reader) | |
| 	assert.NoError(t, err) | |
| 	assert.Equal(t, chunk1Data+chunk2Data, string(data)) | |
| } | |
| 
 | |
| // TestSignedStreamingUploadInvalidSignature tests that invalid chunk signatures are rejected | |
| // This is a negative test case to ensure signature validation is actually working | |
| func TestSignedStreamingUploadInvalidSignature(t *testing.T) { | |
| 	iam := setupIam() | |
| 
 | |
| 	// Create a simple streaming upload with 1 chunk | |
| 	chunk1Data := strings.Repeat("a", 1024) | |
| 
 | |
| 	// Use current time for signatures | |
| 	now := time.Now().UTC() | |
| 	amzDate := now.Format(iso8601Format) | |
| 	dateStamp := now.Format(yyyymmdd) | |
| 
 | |
| 	// Calculate seed signature | |
| 	scope := dateStamp + "/" + defaultRegion + "/s3/aws4_request" | |
| 
 | |
| 	// Build canonical request for seed signature | |
| 	hashedPayload := "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" | |
| 	canonicalHeaders := "content-encoding:aws-chunked\n" + | |
| 		"host:s3.amazonaws.com\n" + | |
| 		"x-amz-content-sha256:" + hashedPayload + "\n" + | |
| 		"x-amz-date:" + amzDate + "\n" + | |
| 		"x-amz-decoded-content-length:1024\n" | |
| 	signedHeaders := "content-encoding;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length" | |
| 
 | |
| 	canonicalRequest := "PUT\n" + | |
| 		"/test-bucket/test-object\n" + | |
| 		"\n" + | |
| 		canonicalHeaders + "\n" + | |
| 		signedHeaders + "\n" + | |
| 		hashedPayload | |
| 
 | |
| 	canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest)) | |
| 	stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + canonicalRequestHash | |
| 
 | |
| 	signingKey := getSigningKey(defaultSecretAccessKey, dateStamp, defaultRegion, "s3") | |
| 	seedSignature := getSignature(signingKey, stringToSign) | |
| 
 | |
| 	// Calculate chunk signature (correct) | |
| 	chunk1Hash := getSHA256Hash([]byte(chunk1Data)) | |
| 	chunk1StringToSign := "AWS4-HMAC-SHA256-PAYLOAD\n" + amzDate + "\n" + scope + "\n" + | |
| 		seedSignature + "\n" + emptySHA256 + "\n" + chunk1Hash | |
| 	chunk1Signature := getSignature(signingKey, chunk1StringToSign) | |
| 
 | |
| 	// Calculate final signature (correct) | |
| 	finalStringToSign := "AWS4-HMAC-SHA256-PAYLOAD\n" + amzDate + "\n" + scope + "\n" + | |
| 		chunk1Signature + "\n" + emptySHA256 + "\n" + emptySHA256 | |
| 	finalSignature := getSignature(signingKey, finalStringToSign) | |
| 
 | |
| 	// Build the chunked payload with INTENTIONALLY WRONG chunk signature | |
| 	// We'll use a modified signature to simulate a tampered request | |
| 	wrongChunkSignature := strings.Replace(chunk1Signature, "a", "b", 1) | |
| 	payload := fmt.Sprintf("400;chunk-signature=%s\r\n%s\r\n", wrongChunkSignature, chunk1Data) + | |
| 		fmt.Sprintf("0;chunk-signature=%s\r\n\r\n", finalSignature) | |
| 
 | |
| 	// Create the request | |
| 	req, err := http.NewRequest("PUT", "http://s3.amazonaws.com/test-bucket/test-object", | |
| 		bytes.NewReader([]byte(payload))) | |
| 	assert.NoError(t, err) | |
| 
 | |
| 	req.Header.Set("Host", "s3.amazonaws.com") | |
| 	req.Header.Set("x-amz-date", amzDate) | |
| 	req.Header.Set("x-amz-content-sha256", hashedPayload) | |
| 	req.Header.Set("Content-Encoding", "aws-chunked") | |
| 	req.Header.Set("x-amz-decoded-content-length", "1024") | |
| 
 | |
| 	authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", | |
| 		defaultAccessKeyId, scope, signedHeaders, seedSignature) | |
| 	req.Header.Set("Authorization", authHeader) | |
| 
 | |
| 	// Test the chunked reader - it should be created successfully | |
| 	reader, errCode := iam.newChunkedReader(req) | |
| 	assert.Equal(t, s3err.ErrNone, errCode) | |
| 	assert.NotNil(t, reader) | |
| 
 | |
| 	// Try to read the payload - this should fail with signature validation error | |
| 	_, err = io.ReadAll(reader) | |
| 	assert.Error(t, err, "Expected error when reading chunk with invalid signature") | |
| 	assert.Contains(t, err.Error(), "chunk signature does not match", "Error should indicate chunk signature mismatch") | |
| }
 |