// 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 ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "os" "strings" "time" "unicode" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/smithy-go/logging" "github.com/gofiber/fiber/v2" v4 "github.com/versity/versitygw/aws/signer/v4" "github.com/versity/versitygw/debuglogger" "github.com/versity/versitygw/s3err" ) const ( iso8601Format = "20060102T150405Z" yyyymmdd = "20060102" ) func HexBytes(s string) string { b := []byte(s) // raw UTF-8 bytes parts := make([]string, len(b)) for i, v := range b { parts[i] = fmt.Sprintf("%02x", v) } return strings.Join(parts, " ") } const ( service = "s3" ) // CheckValidSignature validates the ctx v4 auth signature func CheckValidSignature(ctx *fiber.Ctx, auth AuthData, secret, checksum string, tdate time.Time, contentLen int64) (string, error) { signedHdrs := strings.Split(auth.SignedHeaders, ";") // Create a new http request instance from fasthttp request req, err := createHttpRequestFromCtx(ctx, signedHdrs, contentLen) if err != nil { return "", fmt.Errorf("create http request from context: %w", err) } signer := v4.NewSigner() signMeta, err := signer.SignHTTP(req.Context(), aws.Credentials{ AccessKeyID: auth.Access, SecretAccessKey: secret, }, req, checksum, service, auth.Region, tdate, signedHdrs, func(options *v4.SignerOptions) { options.DisableURIPathEscaping = true if debuglogger.IsDebugEnabled() { options.LogSigning = true options.Logger = logging.NewStandardLogger(os.Stderr) } }) if err != nil { return "", fmt.Errorf("sign generated http request: %w", err) } genAuth, err := ParseAuthorization(req.Header.Get("Authorization")) if err != nil { return "", err } if auth.Signature != genAuth.Signature { return "", s3err.GetSignatureDoesNotMatchErr( auth.Access, signMeta.StringToSign, auth.Signature, HexBytes(signMeta.StringToSign), signMeta.CanonicalString, HexBytes(signMeta.CanonicalString), ) } return signMeta.CanonicalString, nil } // AuthData is the parsed authorization data from the header type AuthData struct { Algorithm string Access string Region string SignedHeaders string Signature string Date string } // ParseAuthorization returns the parsed fields for the aws v4 auth header // example authorization string from aws docs: // Authorization: AWS4-HMAC-SHA256 // Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, // SignedHeaders=host;range;x-amz-date, // Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024 func ParseAuthorization(authorization string) (AuthData, error) { a := AuthData{} // authorization must start with: // Authorization: // followed by key=value pairs separated by "," authParts := strings.SplitN(authorization, " ", 2) for i, el := range authParts { if strings.Contains(el, " ") { authParts[i] = removeSpace(el) } } if len(authParts) < 2 { return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, authorization) } algo := authParts[0] if algo == "AWS" { // SigV2 authorization is not supported by the gateway return a, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationMechanism) } if algo != "AWS4-HMAC-SHA256" { return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthorizationType, algo) } 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.MalformedAuth.MissingComponents() } var access, region, signedHeaders, signature, date string for i, kv := range kvPairs { keyValue := strings.Split(kv, "=") if len(keyValue) != 2 { return a, s3err.MalformedAuth.MalformedComponent(kv) } key, value := keyValue[0], keyValue[1] switch i { case 0: if key != "Credential" { return a, s3err.MalformedAuth.MissingCredential() } case 1: if key != "SignedHeaders" { return a, s3err.MalformedAuth.MissingSignedHeaders() } case 2: if key != "Signature" { return a, s3err.MalformedAuth.MissingSignature() } } switch key { case "Credential": creds, err := ParseCredentials(value, s3err.MalformedAuth) if err != nil { return a, err } access = creds.Access date = creds.Date region = creds.Region case "SignedHeaders": signedHeaders = value case "Signature": signature = value } } return AuthData{ Algorithm: algo, Access: access, Region: region, SignedHeaders: signedHeaders, Signature: signature, Date: date, }, nil } type CredentialsScope struct { Access string Date string Region string } type CredsError interface { MalformedCredential(string) s3err.S3Error IncorrectService(string, string) s3err.S3Error IncorrectTerminal(string, string) s3err.S3Error InvalidDateFormat(string, string) s3err.S3Error } func ParseCredentials(input string, errHandler CredsError) (*CredentialsScope, error) { creds := strings.Split(input, "/") if len(creds) != 5 { return nil, errHandler.MalformedCredential(input) } if creds[3] != "s3" { return nil, errHandler.IncorrectService(input, creds[3]) } if creds[4] != "aws4_request" { return nil, errHandler.IncorrectTerminal(input, creds[4]) } _, err := time.Parse(yyyymmdd, creds[1]) if err != nil { return nil, errHandler.InvalidDateFormat(input, creds[1]) } return &CredentialsScope{ Access: creds[0], Date: creds[1], Region: creds[2], }, nil } func removeSpace(str string) string { var b strings.Builder b.Grow(len(str)) for _, ch := range str { if !unicode.IsSpace(ch) { b.WriteRune(ch) } } return b.String() } func SignPostPolicy(base64Policy, yyyymmdd, region, secretKey string) (string, error) { signingKey := deriveSigningKey(secretKey, yyyymmdd, region) sig := hmacSHA256(signingKey, []byte(base64Policy)) return hex.EncodeToString(sig), nil } func deriveSigningKey(secretKey, yyyymmdd, region string) []byte { kDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(yyyymmdd)) kRegion := hmacSHA256(kDate, []byte(region)) kService := hmacSHA256(kRegion, []byte(service)) kSigning := hmacSHA256(kService, []byte("aws4_request")) return kSigning } func hmacSHA256(key, data []byte) []byte { h := hmac.New(sha256.New, key) h.Write(data) return h.Sum(nil) }