Files
versitygw/s3api/middlewares/authentication.go
T
niksis02 888fd5c1ad fix: match S3's Content-Encoding error details for aws-chunked with UNSIGNED-PAYLOAD
The rejection now reports `Content-Encoding` as the `ArgumentName` and the bare `aws-chunked` token as the `ArgumentValue`, rather than `x-amz-content-sha256` and the payload type, so a request sending `gzip,aws-chunked` gets back just the offending coding, and the message carries S3's trailing period.

Adds integration tests for the three cases: `UnsignedStreamingPayloadTrailer_strips_aws_chunked_content_encoding` for a framed upload where only `aws-chunked` is dropped and the remaining codings keep their order, `PutObject_plain_stores_aws_chunked_content_encoding` for a hex-payload PUT that stores the token as sent, and `PutObject_unsigned_payload_with_aws_chunked_content_encoding` for the full error shape across four header spellings.
2026-09-22 02:04:34 +04:00

205 lines
6.3 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package middlewares
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"strconv"
"time"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/internal/sigv4auth"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
const (
iso8601Format = "20060102T150405Z"
maxObjSizeLimit = 5 * 1024 * 1024 * 1024 // 5gb
defaultRegion = "us-east-1"
)
type RootUserConfig struct {
Access string
Secret string
}
func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, streamBody, requireContentSha256, allowDefaultRegion bool) fiber.Handler {
rootAccount := auth.Account{Access: root.Access, Secret: root.Secret, Role: auth.RoleAdmin}
return func(ctx fiber.Ctx) error {
// The bucket is public, no need to check this signature
if utils.ContextKeyPublicBucket.IsSet(ctx) {
return nil
}
// If ContextKeyAuthenticated is set in context locals, it means it was presigned url case
if utils.ContextKeyAuthenticated.IsSet(ctx) {
return nil
}
// Check X-Amz-Date header
date := ctx.Get("X-Amz-Date")
if date == "" {
// Fall back to `Date` header if `X-Amz-Date` is not set
date = ctx.Get("Date")
}
if date == "" {
return s3err.GetAPIError(s3err.ErrMissingDateHeader)
}
// Parse the date and check the date validity
tdate, err := time.Parse(iso8601Format, date)
if err != nil {
return s3err.GetAPIError(s3err.ErrMissingDateHeader)
}
// Validate the dates difference
err = utils.ValidateDate(tdate)
if err != nil {
return err
}
authorization := ctx.Get("Authorization")
if authorization == "" {
return s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, authorization)
}
authData, err := utils.ParseAuthorization(authorization)
if err != nil {
return err
}
if authData.Region != region && !(allowDefaultRegion && authData.Region == defaultRegion) {
return s3err.MalformedAuth.IncorrectRegion(region, authData.Region)
}
utils.ContextKeyIsRoot.Set(ctx, authData.Access == root.Access)
sessionToken := ctx.Get(sigv4auth.HeaderSecurityToken)
derivedKey, account, err := auth.ResolveDerivedKey(iam, rootAccount, authData.Access, sessionToken, authData.Date, authData.Region, sigv4auth.ServiceS3)
if err == auth.ErrNoSuchUser {
return s3err.GetInvalidAccessKeyIdErr(authData.Access)
}
if errors.Is(err, auth.ErrInvalidSessionToken) {
return s3err.GetAPIError(s3err.ErrInvalidToken)
}
if err != nil {
return err
}
if date[:8] != authData.Date {
return s3err.MalformedAuth.DateMismatch()
}
utils.ContextKeyAccount.Set(ctx, account)
var contentLength int64
contentLengthStr := ctx.Get("Content-Length")
if contentLengthStr != "" {
contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
//TODO: not sure if InvalidRequest should be returned in this case
if err != nil {
return s3err.GetAPIError(s3err.ErrInvalidRequest)
}
}
hashPayload := ctx.Get("X-Amz-Content-Sha256")
if requireContentSha256 && hashPayload == "" {
return s3err.GetAPIError(s3err.ErrMissingContentSha256)
}
if !utils.IsValidSha256PayloadHeader(hashPayload) {
return s3err.GetInvalidArgumentErr(s3err.InvalidArgSHA256Payload, hashPayload)
}
// the streaming payload type is allowed only in PutObject and UploadPart
// e.g. STREAMING-UNSIGNED-PAYLOAD-TRAILER
if !streamBody && utils.IsStreamingPayload(hashPayload) {
return s3err.GetAPIError(s3err.ErrInvalidSHA256PayloadUsage)
}
// aws-chunked frames the body, so it contradicts an unsigned payload,
// which declares the body is sent as-is
if utils.IsUnsignedPaylod(hashPayload) && utils.HasAwsChunkedEncoding(ctx.Get("Content-Encoding")) {
return s3err.GetInvalidArgumentErr(s3err.InvalidArgAwsChunkedUnsignedPayload, utils.AwsChunkedEncoding)
}
canonicalString, err := utils.CheckValidSignature(ctx, authData, derivedKey, hashPayload, tdate, contentLength)
if err != nil {
return err
}
if streamBody {
// store the request body stream reader in context locals
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
return r
})
// wrap the io.Reader with sha256 hex hash reader, if x-amz-content-sha256
// is the content sha256 - not a special payload type
if !utils.IsSpecialPayload(hashPayload) {
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
var cr io.Reader
cr, err = utils.NewHashReader(r, hashPayload, utils.HashTypeSha256Hex)
return cr
})
if err != nil {
return err
}
}
// wrap the io.Reader with ChunkReader if x-amz-content-sha256
// provide chunk encoding value
if utils.IsStreamingPayload(hashPayload) {
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
var cr io.Reader
cr, err = utils.NewChunkReader(ctx, r, authData, canonicalString, derivedKey, tdate)
return cr
})
if err != nil {
return err
}
return nil
}
// Content-Length has to be set for data uploads: PutObject, UploadPart
if contentLengthStr == "" {
return s3err.GetAPIError(s3err.ErrMissingContentLength)
}
// the upload limit for big data actions: PutObject, UploadPart
// is 5gb. If the size exceeds the limit, return 'EntityTooLarge' err
if contentLength > maxObjSizeLimit {
return s3err.GetEntityTooLargeErr(contentLength, maxObjSizeLimit)
}
return nil
}
if !utils.IsSpecialPayload(hashPayload) {
// Calculate the hash of the request payload
hashedPayload := sha256.Sum256(ctx.BodyRaw())
hexPayload := hex.EncodeToString(hashedPayload[:])
// Compare the calculated hash with the hash provided
if hashPayload != hexPayload {
return s3err.GetContentSHA256MismatchErr(hashPayload, hexPayload)
}
}
return nil
}
}