mirror of
https://github.com/versity/versitygw.git
synced 2026-07-02 16:54:25 +00:00
577470214d
Validate required signed headers for both Authorization-header SigV4 requests and presigned URLs. The required signed header set is now `host` plus every incoming header with the `x-amz-` prefix. During request reconstruction, signed headers and explicitly ignored headers are copied into the generated request used for signature verification. If an incoming `x-amz-*` header is present but missing from the client-provided `SignedHeaders`, return `AccessDenied` with a `HeadersNotSigned` field. The `host` header remains part of the canonical request and signed header calculation. Previously, a client could sign a request without an S3 control header and then add that header after signing. For example, a presigned `PUT` URL could be generated with only `host` signed, then the actual request could include an unsigned `X-Amz-Tagging` or `X-Amz-Copy-Source` header. Because the verifier reconstructed the request only from `SignedHeaders`, that extra header was omitted from signature calculation and could pass authentication even though it changed the request semantics. This is now rejected with `AccessDenied`. Expose v4 helper methods for checking required and ignored headers, and update canonical header signing so ignored headers can still be included when a client explicitly lists them in `SignedHeaders`, while `Authorization` remains excluded from signature calculation.
267 lines
6.8 KiB
Go
267 lines
6.8 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 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 "", 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: <ALGORITHM>
|
|
// 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)
|
|
}
|