diff options
| author | Tom Crasset <25140344+tcrasset@users.noreply.github.com> | 2025-10-25 10:11:45 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-25 01:11:45 -0700 |
| commit | 824dcac3bf5b75fd4b74bf83d7b08895422d4374 (patch) | |
| tree | 6abc89b9b6fa88ba474b9e96e8636a50da85ad30 /weed/s3api/chunked_reader_v4_test.go | |
| parent | 6a8c53bc44beb057f64d5ba1f7ac026f8410fe04 (diff) | |
| download | seaweedfs-824dcac3bf5b75fd4b74bf83d7b08895422d4374.tar.xz seaweedfs-824dcac3bf5b75fd4b74bf83d7b08895422d4374.zip | |
s3: combine all signature verification checks into a single function (#7330)
Diffstat (limited to 'weed/s3api/chunked_reader_v4_test.go')
| -rw-r--r-- | weed/s3api/chunked_reader_v4_test.go | 228 |
1 files changed, 174 insertions, 54 deletions
diff --git a/weed/s3api/chunked_reader_v4_test.go b/weed/s3api/chunked_reader_v4_test.go index 786df3465..c9bad1d8a 100644 --- a/weed/s3api/chunked_reader_v4_test.go +++ b/weed/s3api/chunked_reader_v4_test.go @@ -9,6 +9,7 @@ import ( "strings" "sync" "testing" + "time" "hash/crc32" @@ -16,66 +17,19 @@ import ( "github.com/stretchr/testify/assert" ) +// getDefaultTimestamp returns a current timestamp for tests +func getDefaultTimestamp() string { + return time.Now().UTC().Format(iso8601Format) +} + const ( - defaultTimestamp = "20130524T000000Z" + defaultTimestamp = "20130524T000000Z" // Legacy constant for reference defaultBucketName = "examplebucket" defaultAccessKeyId = "AKIAIOSFODNN7EXAMPLE" defaultSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" defaultRegion = "us-east-1" ) -func generatestreamingAws4HmacSha256Payload() string { - // This test will implement the following scenario: - // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#example-signature-calculations-streaming - - 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" // The last chunk is empty - - payload := chunk1 + chunk2 + chunk3 - return payload -} - -func NewRequeststreamingAws4HmacSha256Payload() (*http.Request, error) { - // This test will implement the following scenario: - // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#example-signature-calculations-streaming - - payload := generatestreamingAws4HmacSha256Payload() - 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 TestNewSignV4ChunkedReaderstreamingAws4HmacSha256Payload(t *testing.T) { - // This test will implement the following scenario: - // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#example-signature-calculations-streaming - req, err := NewRequeststreamingAws4HmacSha256Payload() - if err != nil { - t.Fatalf("Failed to create request: %v", err) - } - iam := setupIam() - - // The expected payload a long string of 'a's - expectedPayload := strings.Repeat("a", 66560) - - runWithRequest(iam, req, t, expectedPayload) -} - func generateStreamingUnsignedPayloadTrailerPayload(includeFinalCRLF bool) string { // This test will implement the following scenario: // https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html @@ -117,7 +71,7 @@ func NewRequestStreamingUnsignedPayloadTrailer(includeFinalCRLF bool) (*http.Req } req.Header.Set("Host", "amzn-s3-demo-bucket") - req.Header.Set("x-amz-date", defaultTimestamp) + 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") @@ -194,3 +148,169 @@ func setupIam() IdentityAccessManagement { 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") +} |
