mirror of
https://github.com/versity/versitygw.git
synced 2026-09-19 06:32:10 +00:00
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:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user