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")
							 | 
						|
								}
							 |