aboutsummaryrefslogtreecommitdiff
path: root/test/s3/sse/s3_sse_range_server_test.go
blob: 0b02ec62b6824ce1d764c88e3cfab92a4514699f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
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")
}