Files
versitygw/tests/integration/unsigned_streaming_payload_trailer.go
Ben McClelland c6d2360e21 Merge pull request #1679 from versity/sis/unsigned-streaming-upload-not-allowed
fix: rejects STREAMING-UNSIGNED-PAYLOAD-TRAILER for all actions, except for PutObject and UploadPart
2025-12-08 18:08:46 -08:00

593 lines
20 KiB
Go

package integration
import (
"bytes"
"context"
"fmt"
"net/http"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
func UnsignedStreaminPayloadTrailer_malformed_trailer(s *S3Conf) error {
testName := "UnsignedStreaminPayloadTrailer_malformed_trailer"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-obj"
for i, test := range []struct {
trailer string
decContentLength string
payload string
}{
// missing trailer in the payload
{"x-amz-checksum-crc64nvme", "5", "5\r\nhello\r\n0\r\n\r\n"},
// empty checksum key
{"x-amz-checksum-crc64nvme", "5", "5\r\nhello\r\n0\r\n:M3eFcAZSQlc=\r\n\r\n"},
// missing x-amz-trailer
{"", "5", "5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n"},
// invalid trailer in payload
{"x-amz-checksum-crc64nvme", "5", "5\r\nhello\r\n0\r\ninvalid_trailer:M3eFcAZSQlc=\r\n\r\n"},
} {
reqHeaders := map[string]string{
"x-amz-decoded-content-length": test.decContentLength,
}
if test.trailer != "" {
reqHeaders["x-amz-trailer"] = test.trailer
}
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, []byte(test.payload), reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrMalformedTrailer), apiErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_missing_invalid_dec_content_length(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_missing_invalid_dec_content_length"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "object"
for i, clength := range []string{"", "abc", "12x"} {
reqHeaders := map[string]string{
"x-amz-trailer": "x-amz-checksum-crc64nvme",
}
if clength != "" {
reqHeaders["x-amz-decoded-content-length"] = clength
}
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n")
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrMissingContentLength), apiErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_invalid_trailing_checksum(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_invalid_trailing_checksum"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "5",
"x-amz-trailer": "x-amz-checksum-crc64nvme",
}
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:invalid_checksum\r\n\r\n")
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return err
}
return compareS3ApiError(s3err.GetInvalidTrailingChecksumHeaderErr("x-amz-checksum-crc64nvme"), apiErr)
})
}
func UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "5",
"x-amz-trailer": "x-amz-checksum-crc64nvme",
}
// valid crc64nvme, but incorrect
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:QFRKMGE3tuw=\r\n\r\n")
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return err
}
return compareS3ApiError(s3err.GetChecksumBadDigestErr("CRC64NVME"), apiErr)
})
}
func UnsignedStreamingPayloadTrailer_multiple_checksum_headers(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_multiple_checksum_headers"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n")
for i, test := range []struct {
key string
value string
}{
{"crc32", "NhCmhg=="},
{"crc32c", "+Cy97w=="},
{"crc64nvme", "QFRKMGE3tuw="},
{"sha1", "qvTGHdzF6KLavt4PO0gs2a6pQ00="},
{"sha256", "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="},
} {
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "5",
"x-amz-trailer": "x-amz-checksum-crc64nvme",
fmt.Sprintf("x-amz-checksum-%s", test.key): test.value,
}
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrMultipleChecksumHeaders), apiErr); err != nil {
return err
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "5",
"x-amz-trailer": "x-amz-checksum-crc64nvme",
"x-amz-sdk-checksum-algorithm": "sha1",
}
// valid crc64nvme, but incorrect
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n")
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return err
}
return compareS3ApiError(s3err.GetInvalidChecksumHeaderErr("x-amz-sdk-checksum-algorithm"), apiErr)
})
}
func UnsignedStreamingPayloadTrailer_incomplete_body(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_incomplete_body"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
for i, body := range [][]byte{
[]byte("A\ndummy data\r\n0\r\n\r\n"),
[]byte("A\r\ndummy data0\r\n\r\n"),
[]byte("A\r\nXYZ\r\ndummy data\r\n0\r\n\r\n"),
[]byte("B\r\ndummy data\r\n0\r\n\r\n"),
[]byte("A\r\ndummy data\r\n0\r\n"),
[]byte("A\r\ndummy data\r\n0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n"),
[]byte("A\r\n"),
[]byte("A\r\nA\r\ndummy data\r\n0\r\n\r\n"),
// invalid chunk size
[]byte("invalid_chunk_size\r\ndummy data\r\n0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n"),
[]byte("A\r\ndummy data\r\nJ\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n"),
} {
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "10",
"x-amz-trailer": "x-amz-checksum-crc64nvme",
}
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrIncompleteBody), apiErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_invalid_chunk_size(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_invalid_chunk_size"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
chSizeErr := s3err.GetAPIError(s3err.ErrInvalidChunkSize)
for i, test := range []struct {
chunkSizes []int64
expectedErr error
}{
{[]int64{8192, 8192, 100, 0}, nil},
{[]int64{100, 0}, nil},
{[]int64{10000, 9500, 20, 0}, nil},
{[]int64{8200, 8201, 10000, 10, 0}, nil},
{[]int64{100, 8192, 100, 0}, chSizeErr},
{[]int64{8192, 10, 100, 0}, chSizeErr},
{[]int64{10, 10, 10, 0}, chSizeErr},
{[]int64{8192, 10, 100, 0}, chSizeErr},
{[]int64{10000, 10, 8192, 8192, 0}, chSizeErr},
{[]int64{8192, 10, 10, 8192, 20, 20000, 0}, chSizeErr},
} {
cLength, payload, err := constructUnsignedPaylod(test.chunkSizes...)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
reqHeaders := map[string]string{
"x-amz-decoded-content-length": fmt.Sprint(cLength),
}
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, payload, reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if test.expectedErr == nil && apiErr != nil {
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
}
if test.expectedErr != nil {
if apiErr == nil {
return fmt.Errorf("test %v failed: expected %w, instead got nil", i+1, test.expectedErr)
}
expErr, ok := test.expectedErr.(s3err.APIError)
if !ok {
return fmt.Errorf("test %v failed: invalid expected error type", i+1)
}
if err := compareS3ApiError(expErr, apiErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_content_length_payload_size_mismatch(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_content_length_payload_size_mismatch"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
for i, test := range []struct {
payload string
cLength int64
trailer string
}{
{"b\r\nabcdefghijk\r\n0\r\n\r\n", 5, ""},
{"b\r\nabcdefghijk\r\n0\r\n\r\n", 200, ""},
{"a\r\ndummy data\r\n0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n", 128, "crc64nvme"},
{"a\r\ndummy data\r\n0\r\nx-amz-checksum-sha256:eXuwq/95jXIAr3aF3KeQHt/8Ur8mUA1b2XKCZY7iQVI=\r\n\r\n", 7, "crc64nvme"},
} {
reqHeaders := map[string]string{
"x-amz-decoded-content-length": fmt.Sprint(test.cLength),
}
if test.trailer != "" {
reqHeaders["x-amz-trailer"] = fmt.Sprintf("x-amz-checksum-%s", test.trailer)
}
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, []byte(test.payload), reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrContentLengthMismatch), apiErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "11",
}
body := []byte("B\r\nhello world\r\n0\r\n\r\n")
headers, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("%s: %s", apiErr.Code, apiErr.Message)
}
csum := headers["x-amz-checksum-crc64nvme"]
expectedCsum := "jSnVw/bqjr4="
if csum != expectedCsum {
return fmt.Errorf("expected the crc64nvme to be %s, instead got %s", expectedCsum, csum)
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
body := []byte("7\r\nabcdefg\r\n0\r\n\r\n")
for i, test := range []struct {
key string
value string
}{
{"crc32", "MSpqpg=="},
{"crc32c", "5if0QQ=="},
{"crc64nvme", "SmzZ/LTp1CA="},
{"sha1", "L7XhNBn8iSRoZeejJPR27GJOh0A="},
{"sha256", "fRpUEnsiJQL1t5tfsIAwYRUqRPkrN+I8ZSe69mXU2po="},
} {
csumHdr := fmt.Sprintf("x-amz-checksum-%s", test.key)
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "7",
csumHdr: test.value,
}
headers, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if apiErr != nil {
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
}
if headers[csumHdr] != test.value {
return fmt.Errorf("expected the %s to be %s, instead got %s", csumHdr, test.value, headers[csumHdr])
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
for i, test := range []struct {
key string
value string
}{
{"crc32", "MSpqpg=="},
{"crc32c", "5if0QQ=="},
{"crc64nvme", "SmzZ/LTp1CA="},
{"sha1", "L7XhNBn8iSRoZeejJPR27GJOh0A="},
{"sha256", "fRpUEnsiJQL1t5tfsIAwYRUqRPkrN+I8ZSe69mXU2po="},
} {
csumHdr := fmt.Sprintf("x-amz-checksum-%s", test.key)
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "7",
"x-amz-sdk-checksum-algorithm": strings.ToUpper(test.key),
"x-amz-trailer": csumHdr,
}
body := bytes.NewBuffer([]byte("7\r\nabcdefg\r\n0\r\n"))
_, err := body.WriteString(fmt.Sprintf("%s:%s\r\n\r\n", csumHdr, test.value))
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
headers, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body.Bytes(), reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if apiErr != nil {
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
}
if headers[csumHdr] != test.value {
return fmt.Errorf("expected the %s to be %s, instead got %s", csumHdr, test.value, headers[csumHdr])
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_composite_checksum(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_composite_checksum"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
mp, err := createMp(s3client, bucket, object, withChecksumType(types.ChecksumTypeComposite), withChecksum(types.ChecksumAlgorithmCrc32))
if err != nil {
return err
}
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "7",
}
body := []byte("7\r\nabcdefg\r\n0\r\n\r\n")
_, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body, reqHeaders)
if err != nil {
return err
}
return compareS3ApiError(s3err.GetChecksumTypeMismatchErr(types.ChecksumAlgorithmCrc32, "null"), apiErr)
})
}
func UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_full_object(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_composite_checksum"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
mp, err := createMp(s3client, bucket, object, withChecksumType(types.ChecksumTypeFullObject), withChecksum(types.ChecksumAlgorithmCrc32))
if err != nil {
return err
}
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "7",
}
body := []byte("7\r\nabcdefg\r\n0\r\n\r\n")
headers, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body, reqHeaders)
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("(%s) %s", apiErr.Code, apiErr.Message)
}
expectedCsum := "MSpqpg=="
actualCsum := headers["x-amz-checksum-crc32"]
if expectedCsum != actualCsum {
return fmt.Errorf("expected the crc32 checksum to be %s, instead got %s", expectedCsum, actualCsum)
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
mp, err := createMp(s3client, bucket, object, withChecksumType(types.ChecksumTypeFullObject), withChecksum(types.ChecksumAlgorithmCrc32))
if err != nil {
return err
}
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "7",
"x-amz-trailer": "x-amz-checksum-sha256",
}
body := []byte("7\r\nabcdefg\r\n0\r\nx-amz-checksum-sha256:fRpUEnsiJQL1t5tfsIAwYRUqRPkrN+I8ZSe69mXU2po=\r\n\r\n")
_, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body, reqHeaders)
if err != nil {
return err
}
return compareS3ApiError(s3err.GetChecksumTypeMismatchErr(types.ChecksumAlgorithmCrc32, types.ChecksumAlgorithmSha256), apiErr)
})
}
func UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
object := "my-object"
for i, test := range []struct {
key string
value string
}{
{"crc32", "QWaN2w=="},
{"crc32c", "R/I7iQ=="},
{"crc64nvme", "dPVWc2vU1+Q="},
{"sha1", "YR/1TvTYOJz5gtqVFoBJBtmTibY="},
{"sha256", "eXuwq/95jXIAr3aF3KeQHt/8Ur8mUA1b2XKCZY7iQVI="},
} {
mp, err := createMp(s3client, bucket, object, withChecksum(types.ChecksumAlgorithm(strings.ToUpper(test.key))))
if err != nil {
return err
}
csumHdr := fmt.Sprintf("x-amz-checksum-%s", test.key)
reqHeaders := map[string]string{
"x-amz-decoded-content-length": "10",
"x-amz-sdk-checksum-algorithm": test.key,
"x-amz-trailer": csumHdr,
}
body := bytes.NewBuffer([]byte("A\r\ndummy data\r\n0\r\n"))
_, err = body.WriteString(fmt.Sprintf("%s:%s\r\n\r\n", csumHdr, test.value))
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
headers, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body.Bytes(), reqHeaders)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if apiErr != nil {
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
}
if headers[csumHdr] != test.value {
return fmt.Errorf("expected the %s to be %s, instead got %s", csumHdr, test.value, headers[csumHdr])
}
}
return nil
})
}
func UnsignedStreamingPayloadTrailer_not_allowed(s *S3Conf) error {
testName := "UnsignedStreamingPayloadTrailer_not_allowed"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// doesn't matter what data is sent in the body
body := []byte("5\r\nabcde\r\n0\r\n\r\n")
// tests a couple of bucket PUT actions, where
// STREAMING-UNSIGNED-PAYLOAD-TRAILER is not allowed
for i, query := range []string{
"cors", // PutBucketCors
"tagging", // PutBucketTagging
"object-lock", // PutObjectLockConfiguration
"ownershipControls", // PutBucketOwnership
"versioning", // PutBucketVersioning
"policy", // PutBucketPolicy
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("%s/%s?%s", s.endpoint, bucket, query), bytes.NewReader(body))
if err != nil {
cancel()
return fmt.Errorf("test %v failed: %w", i+1, err)
}
req.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER")
req.Header.Set("x-amz-decoded-content-length", "5")
_, apiErr, err := sendSignedRequest(s, req, cancel)
if err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrInvalidSHA256PayloadUsage), apiErr); err != nil {
return fmt.Errorf("test %v failed: %w", i+1, err)
}
}
return nil
})
}