fix: fixes sigv4 and presigned url auth errors.

Fixes #1540
Fixes #1538
Fixes #1513
Fixes #1425

Fixes SigV4 authentication and presigned URL error handling. Adds two sets of errors in the `s3err` package for these authentication mechanisms.

* Adds a check to return a custom "not supported" error when `X-Amz-Security-Token` is present in presigned URLs.
* Adds a check to return a custom "not supported" error when the `AWS4-ECDSA-P256-SHA256` algorithm is used in presigned URLs.
This commit is contained in:
niksis02
2025-09-18 00:11:12 +04:00
parent c2c359e9f0
commit 6176d9eb46
11 changed files with 599 additions and 381 deletions
+22 -28
View File
@@ -17,9 +17,7 @@ package middlewares
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"time"
@@ -52,9 +50,27 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string)
return nil
}
// Check X-Amz-Date header
date := ctx.Get("X-Amz-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.GetAPIError(s3err.ErrAuthHeaderEmpty)
return s3err.GetAPIError(s3err.ErrInvalidAuthHeader)
}
authData, err := utils.ParseAuthorization(authorization)
@@ -63,11 +79,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string)
}
if authData.Region != region {
return s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", authData.Region),
HTTPStatusCode: http.StatusForbidden,
}
return s3err.MalformedAuth.IncorrectRegion(region, authData.Region)
}
utils.ContextKeyIsRoot.Set(ctx, authData.Access == root.Access)
@@ -80,29 +92,11 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string)
return err
}
utils.ContextKeyAccount.Set(ctx, account)
// Check X-Amz-Date header
date := ctx.Get("X-Amz-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.ErrMalformedDate)
}
if date[:8] != authData.Date {
return s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)
return s3err.MalformedAuth.DateMismatch()
}
// Validate the dates difference
err = utils.ValidateDate(tdate)
if err != nil {
return err
}
utils.ContextKeyAccount.Set(ctx, account)
var contentLength int64
contentLengthStr := ctx.Get("Content-Length")
+6 -1
View File
@@ -32,10 +32,15 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, region
if utils.ContextKeyPublicBucket.IsSet(ctx) {
return nil
}
if ctx.Query("X-Amz-Signature") == "" {
if !utils.IsPresignedURLAuth(ctx) {
return nil
}
if ctx.Request().URI().QueryArgs().Has("X-Amz-Security-Token") {
// OIDC Authorization with X-Amz-Security-Token is not supported
return s3err.QueryAuthErrors.SecurityTokenNotSupported()
}
// Set in the context the "authenticated" key, in case the authentication succeeds,
// otherwise the middleware will return the caucht error
utils.ContextKeyAuthenticated.Set(ctx, true)
+1 -1
View File
@@ -31,7 +31,7 @@ import (
func AuthorizePublicBucketAccess(be backend.Backend, s3action string, policyPermission auth.Action, permission auth.Permission) fiber.Handler {
return func(ctx *fiber.Ctx) error {
// skip for authenticated requests
if ctx.Query("X-Amz-Algorithm") != "" || ctx.Get("Authorization") != "" {
if utils.IsPresignedURLAuth(ctx) || ctx.Get("Authorization") != "" {
return nil
}
+25 -18
View File
@@ -103,7 +103,7 @@ func (ar *AuthReader) validateSignature() error {
// Parse the date and check the date validity
tdate, err := time.Parse(iso8601Format, date)
if err != nil {
return s3err.GetAPIError(s3err.ErrMalformedDate)
return s3err.GetAPIError(s3err.ErrMissingDateHeader)
}
return CheckValidSignature(ar.ctx, ar.auth, ar.secret, hashPayload, tdate, int64(ar.size))
@@ -184,54 +184,61 @@ func ParseAuthorization(authorization string) (AuthData, error) {
}
if len(authParts) < 2 {
return a, s3err.GetAPIError(s3err.ErrMissingFields)
return a, s3err.GetAPIError(s3err.ErrInvalidAuthHeader)
}
algo := authParts[0]
if algo != "AWS4-HMAC-SHA256" {
return a, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported)
return a, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationType)
}
kvData := authParts[1]
kvPairs := strings.Split(kvData, ",")
// we are expecting at least Credential, SignedHeaders, and Signature
// key value pairs here
if len(kvPairs) < 3 {
return a, s3err.GetAPIError(s3err.ErrMissingFields)
if len(kvPairs) != 3 {
return a, s3err.MalformedAuth.MissingComponents()
}
var access, region, signedHeaders, signature, date string
for _, kv := range kvPairs {
for i, kv := range kvPairs {
keyValue := strings.Split(kv, "=")
if len(keyValue) != 2 {
switch {
case strings.HasPrefix(kv, "Credential"):
return a, s3err.GetAPIError(s3err.ErrCredMalformed)
case strings.HasPrefix(kv, "SignedHeaders"):
return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams)
return a, s3err.MalformedAuth.MalformedComponent(kv)
}
key, value := keyValue[0], keyValue[1]
switch i {
case 0:
if key != "Credential" {
return a, s3err.MalformedAuth.MissingCredential()
}
return a, s3err.GetAPIError(s3err.ErrMissingFields)
case 1:
if key != "SignedHeaders" {
return a, s3err.MalformedAuth.MissingSignedHeaders()
}
case 2:
if key != "Signature" {
return a, s3err.MalformedAuth.MissingSignature()
}
}
key := strings.TrimSpace(keyValue[0])
value := strings.TrimSpace(keyValue[1])
switch key {
case "Credential":
creds := strings.Split(value, "/")
if len(creds) != 5 {
return a, s3err.GetAPIError(s3err.ErrCredMalformed)
return a, s3err.MalformedAuth.MalformedCredential()
}
if creds[3] != "s3" {
return a, s3err.GetAPIError(s3err.ErrSignatureIncorrService)
return a, s3err.MalformedAuth.IncorrectService(creds[3])
}
if creds[4] != "aws4_request" {
return a, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)
return a, s3err.MalformedAuth.InvalidTerminal(creds[4])
}
_, err := time.Parse(yyyymmdd, creds[1])
if err != nil {
return a, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)
return a, s3err.MalformedAuth.InvalidDateFormat(creds[1])
}
access = creds[0]
date = creds[1]
+76 -29
View File
@@ -18,7 +18,6 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
@@ -35,6 +34,9 @@ import (
const (
unsignedPayload string = "UNSIGNED-PAYLOAD"
algoHMAC string = "AWS4-HMAC-SHA256"
algoECDSA string = "AWS4-ECDSA-P256-SHA256"
)
// PresignedAuthReader is an io.Reader that validates presigned request authorization
@@ -136,65 +138,70 @@ func ParsePresignedURIParts(ctx *fiber.Ctx) (AuthData, error) {
// Get and verify algorithm query parameter
algo := ctx.Query("X-Amz-Algorithm")
if algo == "" {
return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams)
}
if algo != "AWS4-HMAC-SHA256" {
return a, s3err.GetAPIError(s3err.ErrInvalidQuerySignatureAlgo)
err := validateAlgorithm(algo)
if err != nil {
return a, err
}
// Parse and validate credentials query parameter
credsQuery := ctx.Query("X-Amz-Credential")
if credsQuery == "" {
return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams)
return a, s3err.QueryAuthErrors.MissingRequiredParams()
}
creds := strings.Split(credsQuery, "/")
if len(creds) != 5 {
return a, s3err.GetAPIError(s3err.ErrCredMalformed)
return a, s3err.QueryAuthErrors.MalformedCredential()
}
// validate the service
if creds[3] != "s3" {
return a, s3err.GetAPIError(s3err.ErrSignatureIncorrService)
return a, s3err.QueryAuthErrors.IncorrectService(creds[3])
}
// validate the terminal
if creds[4] != "aws4_request" {
return a, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)
return a, s3err.QueryAuthErrors.IncorrectTerminal(creds[4])
}
_, err := time.Parse(yyyymmdd, creds[1])
// validate the date
_, err = time.Parse(yyyymmdd, creds[1])
if err != nil {
return a, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)
return a, s3err.QueryAuthErrors.InvalidDateFormat(creds[1])
}
region, ok := ContextKeyRegion.Get(ctx).(string)
if !ok {
region = ""
}
// validate the region
if creds[2] != region {
return a, s3err.QueryAuthErrors.IncorrectRegion(region, creds[2])
}
// Parse and validate Date query param
date := ctx.Query("X-Amz-Date")
if date == "" {
return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams)
return a, s3err.QueryAuthErrors.MissingRequiredParams()
}
tdate, err := time.Parse(iso8601Format, date)
if err != nil {
return a, s3err.GetAPIError(s3err.ErrMalformedDate)
return a, s3err.QueryAuthErrors.InvalidXAmzDateFormat()
}
if date[:8] != creds[1] {
return a, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)
}
if ContextKeyRegion.Get(ctx) != creds[2] {
return a, s3err.APIError{
Code: "SignatureDoesNotMatch",
Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", creds[2]),
HTTPStatusCode: http.StatusForbidden,
}
return a, s3err.QueryAuthErrors.DateMismatch(creds[1], date[:8])
}
signature := ctx.Query("X-Amz-Signature")
if signature == "" {
return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams)
return a, s3err.QueryAuthErrors.MissingRequiredParams()
}
signedHdrs := ctx.Query("X-Amz-SignedHeaders")
if signedHdrs == "" {
return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams)
return a, s3err.QueryAuthErrors.MissingRequiredParams()
}
// Validate X-Amz-Expires query param and check if request is expired
@@ -215,20 +222,20 @@ func ParsePresignedURIParts(ctx *fiber.Ctx) (AuthData, error) {
func validateExpiration(str string, date time.Time) error {
if str == "" {
return s3err.GetAPIError(s3err.ErrInvalidQueryParams)
return s3err.QueryAuthErrors.MissingRequiredParams()
}
exp, err := strconv.Atoi(str)
if err != nil {
return s3err.GetAPIError(s3err.ErrMalformedExpires)
return s3err.QueryAuthErrors.ExpiresNumber()
}
if exp < 0 {
return s3err.GetAPIError(s3err.ErrNegativeExpires)
return s3err.QueryAuthErrors.ExpiresNegative()
}
if exp > 604800 {
return s3err.GetAPIError(s3err.ErrMaximumExpires)
return s3err.QueryAuthErrors.ExpiresTooLarge()
}
now := time.Now()
@@ -240,3 +247,43 @@ func validateExpiration(str string, date time.Time) error {
return nil
}
// validateAlgorithm validates the algorithm
// for AWS4-ECDSA-P256-SHA256 it returns a custom non AWS error
// currently only AWS4-HMAC-SHA256 algorithm is supported
func validateAlgorithm(algo string) error {
switch algo {
case "":
return s3err.QueryAuthErrors.MissingRequiredParams()
case algoHMAC:
return nil
case algoECDSA:
return s3err.QueryAuthErrors.OnlyHMACSupported()
default:
// all other algorithms are considerd as invalid
return s3err.QueryAuthErrors.UnsupportedAlgorithm()
}
}
// IsPresignedURLAuth determines if the request is presigned:
// which is authorization with query params
func IsPresignedURLAuth(ctx *fiber.Ctx) bool {
algo := ctx.Query("X-Amz-Algorithm")
creds := ctx.Query("X-Amz-Credential")
signature := ctx.Query("X-Amz-Signature")
signedHeaders := ctx.Query("X-Amz-SignedHeaders")
expires := ctx.Query("X-Amz-Expires")
return !isEmpty(algo, creds, signature, signedHeaders, expires)
}
// isEmpty checks if all the given strings are empty
func isEmpty(args ...string) bool {
for _, a := range args {
if a != "" {
return false
}
}
return true
}
+24 -4
View File
@@ -18,6 +18,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/versity/versitygw/s3err"
)
@@ -37,7 +38,7 @@ func Test_validateExpiration(t *testing.T) {
str: "",
date: time.Now(),
},
err: s3err.GetAPIError(s3err.ErrInvalidQueryParams),
err: s3err.QueryAuthErrors.MissingRequiredParams(),
},
{
name: "invalid-expiration",
@@ -45,7 +46,7 @@ func Test_validateExpiration(t *testing.T) {
str: "invalid_expiration",
date: time.Now(),
},
err: s3err.GetAPIError(s3err.ErrMalformedExpires),
err: s3err.QueryAuthErrors.ExpiresNumber(),
},
{
name: "negative-expiration",
@@ -53,7 +54,7 @@ func Test_validateExpiration(t *testing.T) {
str: "-320",
date: time.Now(),
},
err: s3err.GetAPIError(s3err.ErrNegativeExpires),
err: s3err.QueryAuthErrors.ExpiresNegative(),
},
{
name: "exceeding-expiration",
@@ -61,7 +62,7 @@ func Test_validateExpiration(t *testing.T) {
str: "6048000",
date: time.Now(),
},
err: s3err.GetAPIError(s3err.ErrMaximumExpires),
err: s3err.QueryAuthErrors.ExpiresTooLarge(),
},
{
name: "expired value",
@@ -98,3 +99,22 @@ func Test_validateExpiration(t *testing.T) {
})
}
}
func Test_validateAlgorithm(t *testing.T) {
tests := []struct {
name string
algo string
err error
}{
{"empty", "", s3err.QueryAuthErrors.MissingRequiredParams()},
{"AWS4-HMAC-SHA256", "AWS4-HMAC-SHA256", nil},
{"AWS4-ECDSA-P256-SHA256", "AWS4-ECDSA-P256-SHA256", s3err.QueryAuthErrors.OnlyHMACSupported()},
{"invalid", "invalid algo", s3err.QueryAuthErrors.UnsupportedAlgorithm()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateAlgorithm(tt.algo)
assert.EqualValues(t, tt.err, err)
})
}
}