// 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 utils import ( "fmt" "net/url" "os" "strconv" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/smithy-go/logging" "github.com/gofiber/fiber/v3" v4 "github.com/versity/versitygw/aws/signer/v4" "github.com/versity/versitygw/debuglogger" "github.com/versity/versitygw/s3err" ) const ( unsignedPayload string = "UNSIGNED-PAYLOAD" algoHMAC string = "AWS4-HMAC-SHA256" algoECDSA string = "AWS4-ECDSA-P256-SHA256" ) // CheckPresignedSignature validates presigned request signature func CheckPresignedSignature(ctx fiber.Ctx, auth AuthData, secret string) error { signedHdrs := strings.Split(auth.SignedHeaders, ";") var contentLength int64 var err error contentLengthStr := ctx.Get("Content-Length") if contentLengthStr != "" { contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64) if err != nil { return s3err.GetAPIError(s3err.ErrInvalidRequest) } } // Create a new http request instance from fasthttp request req, err := createPresignedHttpRequestFromCtx(ctx, signedHdrs, contentLength) if err != nil { return err } date, _ := time.Parse(iso8601Format, auth.Date) signer := v4.NewSigner() uri, _, signMeta, signErr := signer.PresignHTTP(ctx.RequestCtx(), aws.Credentials{ AccessKeyID: auth.Access, SecretAccessKey: secret, }, req, unsignedPayload, service, auth.Region, date, signedHdrs, func(options *v4.SignerOptions) { options.DisableURIPathEscaping = true if debuglogger.IsDebugEnabled() { options.LogSigning = true options.Logger = logging.NewStandardLogger(os.Stderr) } }) if signErr != nil { return fmt.Errorf("presign generated http request: %w", err) } urlParts, err := url.Parse(uri) if err != nil { return fmt.Errorf("parse presigned url: %w", err) } signature := urlParts.Query().Get("X-Amz-Signature") if signature != auth.Signature { return s3err.GetSignatureDoesNotMatchErr( auth.Access, signMeta.StringToSign, auth.Signature, HexBytes(signMeta.StringToSign), signMeta.CanonicalString, HexBytes(signMeta.CanonicalString), ) } return nil } // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html // // # ParsePresignedURIParts parses and validates request URL query parameters // // ?X-Amz-Algorithm=AWS4-HMAC-SHA256 // &X-Amz-Credential=access-key-id/20130721/us-east-1/s3/aws4_request // &X-Amz-Date=20130721T201207Z // &X-Amz-Expires=86400 // &X-Amz-SignedHeaders=host // &X-Amz-Signature=1e68ad45c1db540284a4a1eca3884c293ba1a0ff63ab9db9a15b5b29dfa02cd8 func ParsePresignedURIParts(ctx fiber.Ctx, region string) (AuthData, error) { a := AuthData{} // Get and verify algorithm query parameter algo := ctx.Query("X-Amz-Algorithm") 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.QueryAuthErrors.MissingRequiredParams() } creds, err := ParseCredentials(credsQuery, s3err.QueryAuthErrors) if err != nil { return a, err } // validate the region if creds.Region != region { return a, s3err.QueryAuthErrors.IncorrectRegion(region, creds.Region) } // Parse and validate Date query param date := ctx.Query("X-Amz-Date") if date == "" { return a, s3err.QueryAuthErrors.MissingRequiredParams() } tdate, err := time.Parse(iso8601Format, date) if err != nil { return a, s3err.QueryAuthErrors.InvalidXAmzDateFormat() } if date[:8] != creds.Date { return a, s3err.QueryAuthErrors.DateMismatch(creds.Date, date[:8]) } signature := ctx.Query("X-Amz-Signature") if signature == "" { return a, s3err.QueryAuthErrors.MissingRequiredParams() } signedHdrs := ctx.Query("X-Amz-SignedHeaders") if signedHdrs == "" { return a, s3err.QueryAuthErrors.MissingRequiredParams() } // Validate X-Amz-Expires query param and check if request is expired err = validateExpiration(ctx.Query("X-Amz-Expires"), tdate) if err != nil { return a, err } a.Signature = signature a.Access = creds.Access a.Algorithm = algo a.Region = creds.Region a.SignedHeaders = signedHdrs a.Date = date return a, nil } func validateExpiration(str string, date time.Time) error { if str == "" { return s3err.QueryAuthErrors.MissingRequiredParams() } exp, err := strconv.Atoi(str) if err != nil { return s3err.QueryAuthErrors.ExpiresNumber() } if exp < 0 { return s3err.QueryAuthErrors.ExpiresNegative() } if exp > 604800 { return s3err.QueryAuthErrors.ExpiresTooLarge() } now := time.Now().UTC() expiresAt := date.Add(time.Duration(exp) * time.Second) if expiresAt.Before(now) { return s3err.GetExpiredPresignedURLError(exp, expiresAt.Format(time.RFC3339), now.Format(time.RFC3339)) } 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 considered 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 !allEmpty(algo, creds, signature, signedHeaders, expires) || IsPresignedURLAuthV2(ctx) } // IsPresignedURLAuthV2 determines if the request is // query-string signed with aws v2 signer func IsPresignedURLAuthV2(ctx fiber.Ctx) bool { expires := ctx.Query("Expires") access := ctx.Query("AWSAccessKeyId") signature := ctx.Query("Signature") return anyNonEmpty(expires, access, signature) } // allEmpty reports whether every given string is empty. func allEmpty(args ...string) bool { for _, a := range args { if a != "" { return false } } return true } // anyNonEmpty reports whether at least one given string is non-empty. func anyNonEmpty(args ...string) bool { for _, a := range args { if a != "" { return true } } return false }