mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-17 19:48:35 +02:00
* s3: skip seed-signature verification in chunked reader when IAM is off When no credentials are configured there is nothing to verify streaming chunk signatures against. Leave the credential unset so the reader decodes the aws-chunked body and skips per-chunk verification, matching the unsigned-streaming path. This also fixes SigV4-signed unsigned-streaming uploads on open gateways, which previously failed in calculateSeedSignature. setupIam() now sets isAuthEnabled to match the credentials it installs, so the signature-verification tests still exercise the enabled path. * s3: decode signed streaming uploads when IAM is disabled AWS SDK v2 clients (Trino, Spark, modern aws-cli) send PutObject bodies as STREAMING-AWS4-HMAC-SHA256-PAYLOAD. On a gateway without S3 auth this was rejected with ErrAuthNotSetup, breaking writes to table buckets and regular buckets alike. The auth check already short-circuits when IAM is off, so route both streaming payload types through the chunked reader. * s3: cover signed trailer streaming uploads without IAM Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
535 lines
20 KiB
Go
535 lines
20 KiB
Go
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 = UserAccessKeyPrefix + "IOSFODNN7EXAMPLE"
|
|
defaultSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
|
defaultRegion = "us-east-1"
|
|
)
|
|
|
|
func TestExtractChecksumAlgorithmIsCaseInsensitive(t *testing.T) {
|
|
got, err := extractChecksumAlgorithm("X-Amz-Checksum-Crc32")
|
|
if err != nil {
|
|
t.Fatalf("extractChecksumAlgorithm returned error: %v", err)
|
|
}
|
|
if got != ChecksumAlgorithmCRC32 {
|
|
t.Fatalf("extractChecksumAlgorithm() = %v, want %v", got, ChecksumAlgorithmCRC32)
|
|
}
|
|
}
|
|
|
|
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: true,
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
// createTrailerStreamingRequest creates a streaming upload request with trailer for testing.
|
|
// If useValidTrailerSignature is true, uses a correctly calculated trailer signature;
|
|
// otherwise uses an intentionally wrong signature for negative testing.
|
|
func createTrailerStreamingRequest(t *testing.T, useValidTrailerSignature bool) (*http.Request, string) {
|
|
chunk1Data := "hello world\n"
|
|
chunk1DataLen := len(chunk1Data)
|
|
chunk1DataLenHex := fmt.Sprintf("%x", chunk1DataLen)
|
|
|
|
// 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-TRAILER"
|
|
canonicalHeaders := "content-encoding:aws-chunked\n" +
|
|
"host:s3.amazonaws.com\n" +
|
|
"x-amz-content-sha256:" + hashedPayload + "\n" +
|
|
"x-amz-date:" + amzDate + "\n" +
|
|
fmt.Sprintf("x-amz-decoded-content-length:%d\n", chunk1DataLen) +
|
|
"x-amz-trailer:x-amz-checksum-crc32\n"
|
|
signedHeaders := "content-encoding;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-trailer"
|
|
|
|
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)
|
|
|
|
// Final chunk (0 bytes)
|
|
finalStringToSign := "AWS4-HMAC-SHA256-PAYLOAD\n" + amzDate + "\n" + scope + "\n" +
|
|
chunk1Signature + "\n" + emptySHA256 + "\n" + emptySHA256
|
|
finalSignature := getSignature(signingKey, finalStringToSign)
|
|
|
|
// Calculate CRC32 checksum for trailer
|
|
crcWriter := crc32.NewIEEE()
|
|
_, crcErr := crcWriter.Write([]byte(chunk1Data))
|
|
assert.NoError(t, crcErr)
|
|
checksum := crcWriter.Sum(nil)
|
|
base64EncodedChecksum := base64.StdEncoding.EncodeToString(checksum)
|
|
|
|
// The on-wire trailer format uses \r\n (HTTP/aws-chunked convention)
|
|
trailerOnWire := "x-amz-checksum-crc32:" + base64EncodedChecksum + "\r\n"
|
|
|
|
// Calculate or use wrong trailer signature
|
|
var trailerSignature string
|
|
if useValidTrailerSignature {
|
|
// The canonical trailer content uses \n for signing (per AWS SigV4 spec)
|
|
trailerCanonical := "x-amz-checksum-crc32:" + base64EncodedChecksum + "\n"
|
|
trailerHash := getSHA256Hash([]byte(trailerCanonical))
|
|
trailerStringToSign := "AWS4-HMAC-SHA256-TRAILER\n" + amzDate + "\n" + scope + "\n" +
|
|
finalSignature + "\n" + trailerHash
|
|
trailerSignature = getSignature(signingKey, trailerStringToSign)
|
|
} else {
|
|
// Intentionally wrong signature for negative testing
|
|
trailerSignature = "0000000000000000000000000000000000000000000000000000000000000000"
|
|
}
|
|
|
|
// Build the chunked payload with trailer and trailer signature
|
|
payload := fmt.Sprintf("%s;chunk-signature=%s\r\n%s\r\n", chunk1DataLenHex, chunk1Signature, chunk1Data) +
|
|
fmt.Sprintf("0;chunk-signature=%s\r\n", finalSignature) +
|
|
trailerOnWire +
|
|
"x-amz-trailer-signature:" + trailerSignature + "\r\n" +
|
|
"\r\n"
|
|
|
|
// 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", fmt.Sprintf("%d", chunk1DataLen))
|
|
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc32")
|
|
|
|
authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
|
|
defaultAccessKeyId, scope, signedHeaders, seedSignature)
|
|
req.Header.Set("Authorization", authHeader)
|
|
|
|
return req, chunk1Data
|
|
}
|
|
|
|
// TestSignedStreamingUploadWithTrailer tests streaming uploads with signed chunks and trailers
|
|
// This tests the STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER content-sha256 header value
|
|
// which is used by AWS SDK v2 when checksum validation is enabled
|
|
func TestSignedStreamingUploadWithTrailer(t *testing.T) {
|
|
iam := setupIam()
|
|
req, expectedData := createTrailerStreamingRequest(t, true)
|
|
|
|
// 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, expectedData, string(data))
|
|
}
|
|
|
|
// TestSignedStreamingUploadWithTrailerInvalidSignature tests behavior with invalid trailer signatures.
|
|
// This is a negative test case for trailer signature validation. It currently verifies that an invalid
|
|
// signature doesn't break content reading, and is prepared for when validation is implemented.
|
|
func TestSignedStreamingUploadWithTrailerInvalidSignature(t *testing.T) {
|
|
iam := setupIam()
|
|
req, expectedData := createTrailerStreamingRequest(t, false)
|
|
|
|
// Test the chunked reader - it should be created successfully
|
|
reader, errCode := iam.newChunkedReader(req)
|
|
assert.Equal(t, s3err.ErrNone, errCode)
|
|
assert.NotNil(t, reader)
|
|
|
|
// Read the payload - currently trailer signature validation may not be implemented,
|
|
// but this test documents the expected behavior and will catch regressions
|
|
// if trailer signature validation is added in the future
|
|
data, err := io.ReadAll(reader)
|
|
// Note: If trailer signature validation is implemented, this should fail with an error
|
|
// For now, we just verify the content is correctly extracted
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), "signature", "Error should indicate signature mismatch")
|
|
} else {
|
|
// If no error, content should still be correct (trailer sig validation not yet implemented)
|
|
assert.Equal(t, expectedData, 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
|
|
wrongChunkSignatureBytes := []byte(chunk1Signature)
|
|
if len(wrongChunkSignatureBytes) > 0 {
|
|
// Flip the first hex character to guarantee a different signature
|
|
if wrongChunkSignatureBytes[0] == '0' {
|
|
wrongChunkSignatureBytes[0] = '1'
|
|
} else {
|
|
wrongChunkSignatureBytes[0] = '0'
|
|
}
|
|
}
|
|
wrongChunkSignature := string(wrongChunkSignatureBytes)
|
|
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")
|
|
}
|
|
|
|
// TestSignedStreamingUploadWithoutIAM verifies that signed streaming uploads
|
|
// (STREAMING-AWS4-HMAC-SHA256-PAYLOAD) are decoded rather than rejected when no
|
|
// credentials are configured. Trino and other AWS SDK v2 clients always send
|
|
// this payload type, so open gateways must accept it.
|
|
func TestSignedStreamingUploadWithoutIAM(t *testing.T) {
|
|
iam := IdentityAccessManagement{
|
|
identities: []*Identity{},
|
|
accessKeyIdent: map[string]*Identity{},
|
|
isAuthEnabled: false,
|
|
}
|
|
s3a := &S3ApiServer{iam: &iam}
|
|
|
|
chunkData := strings.Repeat("a", 1024)
|
|
payload := fmt.Sprintf("400;chunk-signature=%064x\r\n%s\r\n", 1, chunkData) +
|
|
fmt.Sprintf("0;chunk-signature=%064x\r\n\r\n", 2)
|
|
|
|
req, err := http.NewRequest(http.MethodPut, "http://localhost/test-bucket/test-object",
|
|
bytes.NewReader([]byte(payload)))
|
|
assert.NoError(t, err)
|
|
req.Header.Set("x-amz-content-sha256", streamingContentSHA256)
|
|
req.Header.Set("Content-Encoding", "aws-chunked")
|
|
req.Header.Set("x-amz-decoded-content-length", "1024")
|
|
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=x/20261016/us-east-1/s3/aws4_request, SignedHeaders=host, Signature=abc")
|
|
|
|
reader, errCode := getRequestDataReader(s3a, req)
|
|
assert.Equal(t, s3err.ErrNone, errCode)
|
|
assert.NotNil(t, reader)
|
|
|
|
data, err := io.ReadAll(reader)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, chunkData, string(data))
|
|
}
|
|
|
|
// TestTrailerStreamingUploadWithoutIAM verifies that the trailer variant
|
|
// (STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER) is also decoded when no
|
|
// credentials are configured, and that the checksum trailer is still honored.
|
|
func TestTrailerStreamingUploadWithoutIAM(t *testing.T) {
|
|
iam := IdentityAccessManagement{
|
|
identities: []*Identity{},
|
|
accessKeyIdent: map[string]*Identity{},
|
|
isAuthEnabled: false,
|
|
}
|
|
s3a := &S3ApiServer{iam: &iam}
|
|
|
|
req, expectedData := createTrailerStreamingRequest(t, true)
|
|
|
|
reader, errCode := getRequestDataReader(s3a, req)
|
|
assert.Equal(t, s3err.ErrNone, errCode)
|
|
assert.NotNil(t, reader)
|
|
|
|
data, err := io.ReadAll(reader)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedData, string(data))
|
|
}
|