package sse_test import ( "bytes" "context" "crypto/sha256" "fmt" "io" "net/http" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/service/s3" s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // signRawHTTPRequest signs a raw HTTP request with AWS Signature V4 func signRawHTTPRequest(ctx context.Context, req *http.Request, cfg *S3SSETestConfig) error { // Create credentials creds := aws.Credentials{ AccessKeyID: cfg.AccessKey, SecretAccessKey: cfg.SecretKey, } // Create signer signer := v4.NewSigner() // Calculate payload hash (empty for GET requests) payloadHash := fmt.Sprintf("%x", sha256.Sum256([]byte{})) // Sign the request err := signer.SignHTTP(ctx, creds, req, payloadHash, "s3", cfg.Region, time.Now()) if err != nil { return fmt.Errorf("failed to sign request: %w", err) } return nil } // TestSSECRangeRequestsServerBehavior tests that the server correctly handles Range requests // for SSE-C encrypted objects by checking actual HTTP response (not SDK-processed response) func TestSSECRangeRequestsServerBehavior(t *testing.T) { ctx := context.Background() client, err := createS3Client(ctx, defaultConfig) require.NoError(t, err, "Failed to create S3 client") bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssec-range-server-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) sseKey := generateSSECKey() testData := generateTestData(2048) // 2KB test file objectKey := "test-range-server-validation" // Upload with SSE-C _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err, "Failed to upload SSE-C object") // Test cases for range requests testCases := []struct { name string rangeHeader string expectedStart int64 expectedEnd int64 expectedTotal int64 }{ { name: "First 100 bytes", rangeHeader: "bytes=0-99", expectedStart: 0, expectedEnd: 99, expectedTotal: 2048, }, { name: "Middle range", rangeHeader: "bytes=500-699", expectedStart: 500, expectedEnd: 699, expectedTotal: 2048, }, { name: "Last 100 bytes", rangeHeader: "bytes=1948-2047", expectedStart: 1948, expectedEnd: 2047, expectedTotal: 2048, }, { name: "Single byte", rangeHeader: "bytes=1000-1000", expectedStart: 1000, expectedEnd: 1000, expectedTotal: 2048, }, { name: "AES block boundary crossing", rangeHeader: "bytes=15-17", expectedStart: 15, expectedEnd: 17, expectedTotal: 2048, }, { name: "Open-ended range", rangeHeader: "bytes=2000-", expectedStart: 2000, expectedEnd: 2047, expectedTotal: 2048, }, { name: "Suffix range (last 100 bytes)", rangeHeader: "bytes=-100", expectedStart: 1948, expectedEnd: 2047, expectedTotal: 2048, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Build object URL (Endpoint already includes http://) objectURL := fmt.Sprintf("%s/%s/%s", defaultConfig.Endpoint, bucketName, objectKey, ) // Create raw HTTP request req, err := http.NewRequest("GET", objectURL, nil) require.NoError(t, err, "Failed to create HTTP request") // Add Range header req.Header.Set("Range", tc.rangeHeader) // Add SSE-C headers req.Header.Set("x-amz-server-side-encryption-customer-algorithm", "AES256") req.Header.Set("x-amz-server-side-encryption-customer-key", sseKey.KeyB64) req.Header.Set("x-amz-server-side-encryption-customer-key-MD5", sseKey.KeyMD5) // Sign the request with AWS Signature V4 err = signRawHTTPRequest(ctx, req, defaultConfig) require.NoError(t, err, "Failed to sign HTTP request") // Make request with raw HTTP client httpClient := &http.Client{} resp, err := httpClient.Do(req) require.NoError(t, err, "Failed to execute range request") defer resp.Body.Close() // CRITICAL CHECK 1: Status code must be 206 Partial Content assert.Equal(t, http.StatusPartialContent, resp.StatusCode, "Server must return 206 Partial Content for range request, got %d", resp.StatusCode) // CRITICAL CHECK 2: Content-Range header must be present and correct expectedContentRange := fmt.Sprintf("bytes %d-%d/%d", tc.expectedStart, tc.expectedEnd, tc.expectedTotal) actualContentRange := resp.Header.Get("Content-Range") assert.Equal(t, expectedContentRange, actualContentRange, "Content-Range header mismatch") // CRITICAL CHECK 3: Content-Length must match requested range size expectedLength := tc.expectedEnd - tc.expectedStart + 1 actualLength := resp.ContentLength assert.Equal(t, expectedLength, actualLength, "Content-Length mismatch: expected %d, got %d", expectedLength, actualLength) // CRITICAL CHECK 4: Actual bytes received from network bodyBytes, err := io.ReadAll(resp.Body) require.NoError(t, err, "Failed to read response body") assert.Equal(t, int(expectedLength), len(bodyBytes), "Actual bytes received from server mismatch: expected %d, got %d", expectedLength, len(bodyBytes)) // CRITICAL CHECK 5: Verify decrypted content matches expected range expectedData := testData[tc.expectedStart : tc.expectedEnd+1] assert.Equal(t, expectedData, bodyBytes, "Decrypted range content doesn't match expected data") // Verify SSE-C headers are present in response assert.Equal(t, "AES256", resp.Header.Get("x-amz-server-side-encryption-customer-algorithm"), "SSE-C algorithm header missing in range response") assert.Equal(t, sseKey.KeyMD5, resp.Header.Get("x-amz-server-side-encryption-customer-key-MD5"), "SSE-C key MD5 header missing in range response") }) } } // TestSSEKMSRangeRequestsServerBehavior tests server-side Range handling for SSE-KMS func TestSSEKMSRangeRequestsServerBehavior(t *testing.T) { ctx := context.Background() client, err := createS3Client(ctx, defaultConfig) require.NoError(t, err, "Failed to create S3 client") bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssekms-range-server-") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) kmsKeyID := "test-range-key" testData := generateTestData(4096) // 4KB test file objectKey := "test-kms-range-server-validation" // Upload with SSE-KMS _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: "aws:kms", SSEKMSKeyId: aws.String(kmsKeyID), }) require.NoError(t, err, "Failed to upload SSE-KMS object") // Test various ranges testCases := []struct { name string rangeHeader string start int64 end int64 }{ {"First KB", "bytes=0-1023", 0, 1023}, {"Second KB", "bytes=1024-2047", 1024, 2047}, {"Last KB", "bytes=3072-4095", 3072, 4095}, {"Unaligned range", "bytes=100-299", 100, 299}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { objectURL := fmt.Sprintf("%s/%s/%s", defaultConfig.Endpoint, bucketName, objectKey, ) req, err := http.NewRequest("GET", objectURL, nil) require.NoError(t, err) req.Header.Set("Range", tc.rangeHeader) // Sign the request with AWS Signature V4 err = signRawHTTPRequest(ctx, req, defaultConfig) require.NoError(t, err, "Failed to sign HTTP request") httpClient := &http.Client{} resp, err := httpClient.Do(req) require.NoError(t, err) defer resp.Body.Close() // Verify 206 status assert.Equal(t, http.StatusPartialContent, resp.StatusCode, "SSE-KMS range request must return 206, got %d", resp.StatusCode) // Verify Content-Range expectedContentRange := fmt.Sprintf("bytes %d-%d/%d", tc.start, tc.end, int64(len(testData))) assert.Equal(t, expectedContentRange, resp.Header.Get("Content-Range")) // Verify actual bytes received bodyBytes, err := io.ReadAll(resp.Body) require.NoError(t, err) expectedLength := tc.end - tc.start + 1 assert.Equal(t, int(expectedLength), len(bodyBytes), "Actual network bytes mismatch") // Verify content expectedData := testData[tc.start : tc.end+1] assert.Equal(t, expectedData, bodyBytes) }) } } // TestSSES3RangeRequestsServerBehavior tests server-side Range handling for SSE-S3 func TestSSES3RangeRequestsServerBehavior(t *testing.T) { ctx := context.Background() client, err := createS3Client(ctx, defaultConfig) require.NoError(t, err, "Failed to create S3 client") bucketName, err := createTestBucket(ctx, client, "sses3-range-server") require.NoError(t, err, "Failed to create test bucket") defer cleanupTestBucket(ctx, client, bucketName) testData := generateTestData(8192) // 8KB test file objectKey := "test-s3-range-server-validation" // Upload with SSE-S3 _, err = client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), Body: bytes.NewReader(testData), ServerSideEncryption: "AES256", }) require.NoError(t, err, "Failed to upload SSE-S3 object") // Test range request objectURL := fmt.Sprintf("%s/%s/%s", defaultConfig.Endpoint, bucketName, objectKey, ) req, err := http.NewRequest("GET", objectURL, nil) require.NoError(t, err) req.Header.Set("Range", "bytes=1000-1999") // Sign the request with AWS Signature V4 err = signRawHTTPRequest(ctx, req, defaultConfig) require.NoError(t, err, "Failed to sign HTTP request") httpClient := &http.Client{} resp, err := httpClient.Do(req) require.NoError(t, err) defer resp.Body.Close() // Verify server response assert.Equal(t, http.StatusPartialContent, resp.StatusCode) assert.Equal(t, "bytes 1000-1999/8192", resp.Header.Get("Content-Range")) assert.Equal(t, int64(1000), resp.ContentLength) bodyBytes, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, 1000, len(bodyBytes)) assert.Equal(t, testData[1000:2000], bodyBytes) } // TestSSEMultipartRangeRequestsServerBehavior tests Range requests on multipart encrypted objects func TestSSEMultipartRangeRequestsServerBehavior(t *testing.T) { ctx := context.Background() client, err := createS3Client(ctx, defaultConfig) require.NoError(t, err) bucketName, err := createTestBucket(ctx, client, defaultConfig.BucketPrefix+"ssec-mp-range-") require.NoError(t, err) defer cleanupTestBucket(ctx, client, bucketName) sseKey := generateSSECKey() objectKey := "test-multipart-range-server" // Create 10MB test data (2 parts of 5MB each) partSize := 5 * 1024 * 1024 part1Data := generateTestData(partSize) part2Data := generateTestData(partSize) fullData := append(part1Data, part2Data...) // Initiate multipart upload createResp, err := client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err) uploadID := aws.ToString(createResp.UploadId) // Upload part 1 part1Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), UploadId: aws.String(uploadID), PartNumber: aws.Int32(1), Body: bytes.NewReader(part1Data), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err) // Upload part 2 part2Resp, err := client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), UploadId: aws.String(uploadID), PartNumber: aws.Int32(2), Body: bytes.NewReader(part2Data), SSECustomerAlgorithm: aws.String("AES256"), SSECustomerKey: aws.String(sseKey.KeyB64), SSECustomerKeyMD5: aws.String(sseKey.KeyMD5), }) require.NoError(t, err) // Complete multipart upload _, err = client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ Bucket: aws.String(bucketName), Key: aws.String(objectKey), UploadId: aws.String(uploadID), MultipartUpload: &s3types.CompletedMultipartUpload{ Parts: []s3types.CompletedPart{ {PartNumber: aws.Int32(1), ETag: part1Resp.ETag}, {PartNumber: aws.Int32(2), ETag: part2Resp.ETag}, }, }, }) require.NoError(t, err) // Test range that crosses part boundary objectURL := fmt.Sprintf("%s/%s/%s", defaultConfig.Endpoint, bucketName, objectKey, ) // Range spanning across the part boundary start := int64(partSize - 1000) end := int64(partSize + 1000) req, err := http.NewRequest("GET", objectURL, nil) require.NoError(t, err) req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) req.Header.Set("x-amz-server-side-encryption-customer-algorithm", "AES256") req.Header.Set("x-amz-server-side-encryption-customer-key", sseKey.KeyB64) req.Header.Set("x-amz-server-side-encryption-customer-key-MD5", sseKey.KeyMD5) // Sign the request with AWS Signature V4 err = signRawHTTPRequest(ctx, req, defaultConfig) require.NoError(t, err, "Failed to sign HTTP request") httpClient := &http.Client{} resp, err := httpClient.Do(req) require.NoError(t, err) defer resp.Body.Close() // Verify server behavior for cross-part range assert.Equal(t, http.StatusPartialContent, resp.StatusCode, "Multipart range request must return 206") expectedLength := end - start + 1 assert.Equal(t, expectedLength, resp.ContentLength, "Content-Length for cross-part range") bodyBytes, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, int(expectedLength), len(bodyBytes), "Actual bytes for cross-part range") // Verify content spans the part boundary correctly expectedData := fullData[start : end+1] assert.Equal(t, expectedData, bodyBytes, "Cross-part range content must be correctly decrypted and assembled") }