|
|
|
@ -25,7 +25,6 @@ import ( |
|
|
|
"encoding/hex" |
|
|
|
"io" |
|
|
|
"net/http" |
|
|
|
"path" |
|
|
|
"regexp" |
|
|
|
"sort" |
|
|
|
"strconv" |
|
|
|
@ -33,17 +32,20 @@ import ( |
|
|
|
"time" |
|
|
|
"unicode/utf8" |
|
|
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
|
|
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
|
|
|
) |
|
|
|
|
|
|
|
func (iam *IdentityAccessManagement) reqSignatureV4Verify(r *http.Request) (*Identity, s3err.ErrorCode) { |
|
|
|
sha256sum := getContentSha256Cksum(r) |
|
|
|
switch { |
|
|
|
case isRequestSignatureV4(r): |
|
|
|
return iam.doesSignatureMatch(sha256sum, r) |
|
|
|
identity, _, errCode := iam.doesSignatureMatch(r) |
|
|
|
return identity, errCode |
|
|
|
case isRequestPresignedSignatureV4(r): |
|
|
|
return iam.doesPresignedSignatureMatch(sha256sum, r) |
|
|
|
identity, _, errCode := iam.doesPresignedSignatureMatch(r) |
|
|
|
return identity, errCode |
|
|
|
} |
|
|
|
return nil, s3err.ErrAccessDenied |
|
|
|
} |
|
|
|
@ -154,236 +156,298 @@ func parseSignV4(v4Auth string) (sv signValues, aec s3err.ErrorCode) { |
|
|
|
return signV4Values, s3err.ErrNone |
|
|
|
} |
|
|
|
|
|
|
|
// doesSignatureMatch verifies the request signature.
|
|
|
|
func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r *http.Request) (*Identity, s3err.ErrorCode) { |
|
|
|
|
|
|
|
// Copy request
|
|
|
|
req := *r |
|
|
|
// buildPathWithForwardedPrefix combines forwarded prefix with URL path while preserving S3 key semantics.
|
|
|
|
// This function avoids path.Clean which would collapse "//" and dot segments, breaking S3 signatures.
|
|
|
|
// It only normalizes the join boundary to avoid double slashes between prefix and path.
|
|
|
|
func buildPathWithForwardedPrefix(forwardedPrefix, urlPath string) string { |
|
|
|
if forwardedPrefix == "" { |
|
|
|
return urlPath |
|
|
|
} |
|
|
|
// Ensure single leading slash on prefix
|
|
|
|
if !strings.HasPrefix(forwardedPrefix, "/") { |
|
|
|
forwardedPrefix = "/" + forwardedPrefix |
|
|
|
} |
|
|
|
// Join without collapsing interior segments; only fix a double slash at the boundary
|
|
|
|
var joined string |
|
|
|
if strings.HasSuffix(forwardedPrefix, "/") && strings.HasPrefix(urlPath, "/") { |
|
|
|
joined = forwardedPrefix + urlPath[1:] |
|
|
|
} else if !strings.HasSuffix(forwardedPrefix, "/") && !strings.HasPrefix(urlPath, "/") { |
|
|
|
joined = forwardedPrefix + "/" + urlPath |
|
|
|
} else { |
|
|
|
joined = forwardedPrefix + urlPath |
|
|
|
} |
|
|
|
// Trailing slash semantics inherited from urlPath (already present if needed)
|
|
|
|
return joined |
|
|
|
} |
|
|
|
|
|
|
|
// Save authorization header.
|
|
|
|
v4Auth := req.Header.Get("Authorization") |
|
|
|
// v4AuthInfo holds the parsed authentication data from a request,
|
|
|
|
// whether it's from the Authorization header or presigned URL query parameters.
|
|
|
|
type v4AuthInfo struct { |
|
|
|
Signature string |
|
|
|
AccessKey string |
|
|
|
SignedHeaders []string |
|
|
|
Date time.Time |
|
|
|
Region string |
|
|
|
Service string |
|
|
|
Scope string |
|
|
|
HashedPayload string |
|
|
|
IsPresigned bool |
|
|
|
} |
|
|
|
|
|
|
|
// Parse signature version '4' header.
|
|
|
|
signV4Values, errCode := parseSignV4(v4Auth) |
|
|
|
// verifyV4Signature is the single entry point for verifying any AWS Signature V4 request.
|
|
|
|
// It handles standard requests, presigned URLs, and the seed signature for streaming uploads.
|
|
|
|
func (iam *IdentityAccessManagement) verifyV4Signature(r *http.Request, shouldCheckPermissions bool) (identity *Identity, credential *Credential, calculatedSignature string, authInfo *v4AuthInfo, errCode s3err.ErrorCode) { |
|
|
|
// 1. Extract authentication information from header or query parameters
|
|
|
|
authInfo, errCode = extractV4AuthInfo(r) |
|
|
|
if errCode != s3err.ErrNone { |
|
|
|
return nil, errCode |
|
|
|
} |
|
|
|
|
|
|
|
// Compute payload hash for non-S3 services
|
|
|
|
if signV4Values.Credential.scope.service != "s3" && hashedPayload == emptySHA256 && r.Body != nil { |
|
|
|
var err error |
|
|
|
hashedPayload, err = streamHashRequestBody(r, iamRequestBodyLimit) |
|
|
|
if err != nil { |
|
|
|
return nil, s3err.ErrInternalError |
|
|
|
} |
|
|
|
return nil, nil, "", nil, errCode |
|
|
|
} |
|
|
|
|
|
|
|
// Extract all the signed headers along with its values.
|
|
|
|
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r) |
|
|
|
if errCode != s3err.ErrNone { |
|
|
|
return nil, errCode |
|
|
|
// 2. Lookup user and credentials
|
|
|
|
identity, cred, found := iam.lookupByAccessKey(authInfo.AccessKey) |
|
|
|
if !found { |
|
|
|
return nil, nil, "", nil, s3err.ErrInvalidAccessKeyID |
|
|
|
} |
|
|
|
|
|
|
|
cred := signV4Values.Credential |
|
|
|
identity, foundCred, found := iam.lookupByAccessKey(cred.accessKey) |
|
|
|
if !found { |
|
|
|
return nil, s3err.ErrInvalidAccessKeyID |
|
|
|
// 3. Perform permission check
|
|
|
|
if shouldCheckPermissions { |
|
|
|
bucket, object := s3_constants.GetBucketAndObject(r) |
|
|
|
action := s3_constants.ACTION_READ |
|
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead { |
|
|
|
action = s3_constants.ACTION_WRITE |
|
|
|
} |
|
|
|
if !identity.canDo(Action(action), bucket, object) { |
|
|
|
return nil, nil, "", nil, s3err.ErrAccessDenied |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Extract date, if not present throw error.
|
|
|
|
var dateStr string |
|
|
|
if dateStr = req.Header.Get("x-amz-date"); dateStr == "" { |
|
|
|
if dateStr = r.Header.Get("Date"); dateStr == "" { |
|
|
|
return nil, s3err.ErrMissingDateHeader |
|
|
|
// 4. Handle presigned request expiration
|
|
|
|
if authInfo.IsPresigned { |
|
|
|
if errCode = checkPresignedRequestExpiry(r, authInfo.Date); errCode != s3err.ErrNone { |
|
|
|
return nil, nil, "", nil, errCode |
|
|
|
} |
|
|
|
} |
|
|
|
// Parse date header.
|
|
|
|
t, e := time.Parse(iso8601Format, dateStr) |
|
|
|
if e != nil { |
|
|
|
return nil, s3err.ErrMalformedDate |
|
|
|
|
|
|
|
// 5. Extract headers that were part of the signature
|
|
|
|
extractedSignedHeaders, errCode := extractSignedHeaders(authInfo.SignedHeaders, r) |
|
|
|
if errCode != s3err.ErrNone { |
|
|
|
return nil, nil, "", nil, errCode |
|
|
|
} |
|
|
|
|
|
|
|
// Query string.
|
|
|
|
queryStr := req.URL.Query().Encode() |
|
|
|
// 6. Get the query string for the canonical request
|
|
|
|
queryStr := getCanonicalQueryString(r, authInfo.IsPresigned) |
|
|
|
|
|
|
|
// 7. Define a closure for the core verification logic to avoid repetition
|
|
|
|
verify := func(urlPath string) (string, s3err.ErrorCode) { |
|
|
|
return calculateAndVerifySignature( |
|
|
|
cred.SecretKey, |
|
|
|
r.Method, |
|
|
|
urlPath, |
|
|
|
queryStr, |
|
|
|
extractedSignedHeaders, |
|
|
|
authInfo, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// Check if reverse proxy is forwarding with prefix
|
|
|
|
// 8. Verify the signature, trying with X-Forwarded-Prefix first
|
|
|
|
if forwardedPrefix := r.Header.Get("X-Forwarded-Prefix"); forwardedPrefix != "" { |
|
|
|
// Try signature verification with the forwarded prefix first.
|
|
|
|
// This handles cases where reverse proxies strip URL prefixes and add the X-Forwarded-Prefix header.
|
|
|
|
cleanedPath := buildPathWithForwardedPrefix(forwardedPrefix, req.URL.Path) |
|
|
|
errCode = iam.verifySignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, cleanedPath, req.Method, foundCred.SecretKey, t, signV4Values) |
|
|
|
cleanedPath := buildPathWithForwardedPrefix(forwardedPrefix, r.URL.Path) |
|
|
|
calculatedSignature, errCode = verify(cleanedPath) |
|
|
|
if errCode == s3err.ErrNone { |
|
|
|
return identity, errCode |
|
|
|
return identity, cred, calculatedSignature, authInfo, s3err.ErrNone |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Try normal signature verification (without prefix)
|
|
|
|
errCode = iam.verifySignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method, foundCred.SecretKey, t, signV4Values) |
|
|
|
if errCode == s3err.ErrNone { |
|
|
|
return identity, errCode |
|
|
|
// 9. Verify with the original path
|
|
|
|
calculatedSignature, errCode = verify(r.URL.Path) |
|
|
|
if errCode != s3err.ErrNone { |
|
|
|
return nil, nil, "", nil, errCode |
|
|
|
} |
|
|
|
|
|
|
|
return nil, errCode |
|
|
|
return identity, cred, calculatedSignature, authInfo, s3err.ErrNone |
|
|
|
} |
|
|
|
|
|
|
|
// buildPathWithForwardedPrefix combines forwarded prefix with URL path while preserving trailing slashes.
|
|
|
|
// This ensures compatibility with S3 SDK signatures that include trailing slashes for directory operations.
|
|
|
|
func buildPathWithForwardedPrefix(forwardedPrefix, urlPath string) string { |
|
|
|
fullPath := forwardedPrefix + urlPath |
|
|
|
hasTrailingSlash := strings.HasSuffix(urlPath, "/") && urlPath != "/" |
|
|
|
cleanedPath := path.Clean(fullPath) |
|
|
|
if hasTrailingSlash && !strings.HasSuffix(cleanedPath, "/") { |
|
|
|
cleanedPath += "/" |
|
|
|
} |
|
|
|
return cleanedPath |
|
|
|
} |
|
|
|
|
|
|
|
// verifySignatureWithPath verifies signature with a given path (used for both normal and prefixed paths).
|
|
|
|
func (iam *IdentityAccessManagement) verifySignatureWithPath(extractedSignedHeaders http.Header, hashedPayload, queryStr, urlPath, method, secretKey string, t time.Time, signV4Values signValues) s3err.ErrorCode { |
|
|
|
// Get canonical request.
|
|
|
|
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, urlPath, method) |
|
|
|
|
|
|
|
// Get string to sign from canonical request.
|
|
|
|
stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) |
|
|
|
|
|
|
|
// Get hmac signing key.
|
|
|
|
signingKey := getSigningKey(secretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), signV4Values.Credential.scope.region, signV4Values.Credential.scope.service) |
|
|
|
|
|
|
|
// Calculate signature.
|
|
|
|
// calculateAndVerifySignature contains the core logic for creating the canonical request,
|
|
|
|
// string-to-sign, and comparing the final signature.
|
|
|
|
func calculateAndVerifySignature(secretKey, method, urlPath, queryStr string, extractedSignedHeaders http.Header, authInfo *v4AuthInfo) (string, s3err.ErrorCode) { |
|
|
|
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, authInfo.HashedPayload, queryStr, urlPath, method) |
|
|
|
stringToSign := getStringToSign(canonicalRequest, authInfo.Date, authInfo.Scope) |
|
|
|
signingKey := getSigningKey(secretKey, authInfo.Date.Format(yyyymmdd), authInfo.Region, authInfo.Service) |
|
|
|
newSignature := getSignature(signingKey, stringToSign) |
|
|
|
|
|
|
|
// Verify if signature match.
|
|
|
|
if !compareSignatureV4(newSignature, signV4Values.Signature) { |
|
|
|
return s3err.ErrSignatureDoesNotMatch |
|
|
|
if !compareSignatureV4(newSignature, authInfo.Signature) { |
|
|
|
glog.V(4).Infof("Signature mismatch. Details:\n- CanonicalRequest: %q\n- StringToSign: %q\n- Calculated: %s, Provided: %s", |
|
|
|
canonicalRequest, stringToSign, newSignature, authInfo.Signature) |
|
|
|
return "", s3err.ErrSignatureDoesNotMatch |
|
|
|
} |
|
|
|
|
|
|
|
return s3err.ErrNone |
|
|
|
return newSignature, s3err.ErrNone |
|
|
|
} |
|
|
|
|
|
|
|
// verifyPresignedSignatureWithPath verifies presigned signature with a given path (used for both normal and prefixed paths).
|
|
|
|
func (iam *IdentityAccessManagement) verifyPresignedSignatureWithPath(extractedSignedHeaders http.Header, hashedPayload, queryStr, urlPath, method, secretKey string, t time.Time, credHeader credentialHeader, signature string) s3err.ErrorCode { |
|
|
|
// Get canonical request.
|
|
|
|
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, urlPath, method) |
|
|
|
func extractV4AuthInfo(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) { |
|
|
|
if isRequestPresignedSignatureV4(r) { |
|
|
|
return extractV4AuthInfoFromQuery(r) |
|
|
|
} |
|
|
|
return extractV4AuthInfoFromHeader(r) |
|
|
|
} |
|
|
|
|
|
|
|
// Get string to sign from canonical request.
|
|
|
|
stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope()) |
|
|
|
func extractV4AuthInfoFromHeader(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) { |
|
|
|
authHeader := r.Header.Get("Authorization") |
|
|
|
signV4Values, errCode := parseSignV4(authHeader) |
|
|
|
if errCode != s3err.ErrNone { |
|
|
|
return nil, errCode |
|
|
|
} |
|
|
|
|
|
|
|
// Get hmac signing key.
|
|
|
|
signingKey := getSigningKey(secretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, credHeader.scope.service) |
|
|
|
var t time.Time |
|
|
|
if xamz := r.Header.Get("x-amz-date"); xamz != "" { |
|
|
|
parsed, err := time.Parse(iso8601Format, xamz) |
|
|
|
if err != nil { |
|
|
|
return nil, s3err.ErrMalformedDate |
|
|
|
} |
|
|
|
t = parsed |
|
|
|
} else { |
|
|
|
ds := r.Header.Get("Date") |
|
|
|
if ds == "" { |
|
|
|
return nil, s3err.ErrMissingDateHeader |
|
|
|
} |
|
|
|
parsed, err := http.ParseTime(ds) |
|
|
|
if err != nil { |
|
|
|
return nil, s3err.ErrMalformedDate |
|
|
|
} |
|
|
|
t = parsed.UTC() |
|
|
|
} |
|
|
|
|
|
|
|
// Calculate expected signature.
|
|
|
|
expectedSignature := getSignature(signingKey, stringToSign) |
|
|
|
// Validate clock skew: requests cannot be older than 15 minutes from server time to prevent replay attacks
|
|
|
|
const maxSkew = 15 * time.Minute |
|
|
|
now := time.Now().UTC() |
|
|
|
if now.Sub(t) > maxSkew || t.Sub(now) > maxSkew { |
|
|
|
return nil, s3err.ErrRequestTimeTooSkewed |
|
|
|
} |
|
|
|
|
|
|
|
// Verify if signature match.
|
|
|
|
if !compareSignatureV4(expectedSignature, signature) { |
|
|
|
return s3err.ErrSignatureDoesNotMatch |
|
|
|
hashedPayload := getContentSha256Cksum(r) |
|
|
|
if signV4Values.Credential.scope.service != "s3" && hashedPayload == emptySHA256 && r.Body != nil { |
|
|
|
var hashErr error |
|
|
|
hashedPayload, hashErr = streamHashRequestBody(r, iamRequestBodyLimit) |
|
|
|
if hashErr != nil { |
|
|
|
return nil, s3err.ErrInternalError |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return s3err.ErrNone |
|
|
|
return &v4AuthInfo{ |
|
|
|
Signature: signV4Values.Signature, |
|
|
|
AccessKey: signV4Values.Credential.accessKey, |
|
|
|
SignedHeaders: signV4Values.SignedHeaders, |
|
|
|
Date: t, |
|
|
|
Region: signV4Values.Credential.scope.region, |
|
|
|
Service: signV4Values.Credential.scope.service, |
|
|
|
Scope: signV4Values.Credential.getScope(), |
|
|
|
HashedPayload: hashedPayload, |
|
|
|
IsPresigned: false, |
|
|
|
}, s3err.ErrNone |
|
|
|
} |
|
|
|
|
|
|
|
// Simple implementation for presigned signature verification
|
|
|
|
func (iam *IdentityAccessManagement) doesPresignedSignatureMatch(hashedPayload string, r *http.Request) (*Identity, s3err.ErrorCode) { |
|
|
|
// Parse presigned signature values from query parameters
|
|
|
|
func extractV4AuthInfoFromQuery(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) { |
|
|
|
query := r.URL.Query() |
|
|
|
|
|
|
|
// Check required parameters
|
|
|
|
algorithm := query.Get("X-Amz-Algorithm") |
|
|
|
if algorithm != signV4Algorithm { |
|
|
|
// Validate all required query parameters upfront for fail-fast behavior
|
|
|
|
if query.Get("X-Amz-Algorithm") != signV4Algorithm { |
|
|
|
return nil, s3err.ErrSignatureVersionNotSupported |
|
|
|
} |
|
|
|
|
|
|
|
credential := query.Get("X-Amz-Credential") |
|
|
|
if credential == "" { |
|
|
|
return nil, s3err.ErrMissingFields |
|
|
|
if query.Get("X-Amz-Date") == "" { |
|
|
|
return nil, s3err.ErrMissingDateHeader |
|
|
|
} |
|
|
|
|
|
|
|
signature := query.Get("X-Amz-Signature") |
|
|
|
if signature == "" { |
|
|
|
if query.Get("X-Amz-Credential") == "" { |
|
|
|
return nil, s3err.ErrMissingFields |
|
|
|
} |
|
|
|
|
|
|
|
signedHeadersStr := query.Get("X-Amz-SignedHeaders") |
|
|
|
if signedHeadersStr == "" { |
|
|
|
if query.Get("X-Amz-Signature") == "" { |
|
|
|
return nil, s3err.ErrMissingFields |
|
|
|
} |
|
|
|
|
|
|
|
dateStr := query.Get("X-Amz-Date") |
|
|
|
if dateStr == "" { |
|
|
|
return nil, s3err.ErrMissingDateHeader |
|
|
|
} |
|
|
|
|
|
|
|
// Parse credential
|
|
|
|
credHeader, err := parseCredentialHeader("Credential=" + credential) |
|
|
|
if err != s3err.ErrNone { |
|
|
|
return nil, err |
|
|
|
if query.Get("X-Amz-SignedHeaders") == "" { |
|
|
|
return nil, s3err.ErrMissingFields |
|
|
|
} |
|
|
|
|
|
|
|
// Look up identity by access key
|
|
|
|
identity, foundCred, found := iam.lookupByAccessKey(credHeader.accessKey) |
|
|
|
if !found { |
|
|
|
return nil, s3err.ErrInvalidAccessKeyID |
|
|
|
if query.Get("X-Amz-Expires") == "" { |
|
|
|
return nil, s3err.ErrInvalidQueryParams |
|
|
|
} |
|
|
|
|
|
|
|
// Parse date
|
|
|
|
t, e := time.Parse(iso8601Format, dateStr) |
|
|
|
if e != nil { |
|
|
|
dateStr := query.Get("X-Amz-Date") |
|
|
|
t, err := time.Parse(iso8601Format, dateStr) |
|
|
|
if err != nil { |
|
|
|
return nil, s3err.ErrMalformedDate |
|
|
|
} |
|
|
|
|
|
|
|
// Check expiration
|
|
|
|
expiresStr := query.Get("X-Amz-Expires") |
|
|
|
if expiresStr != "" { |
|
|
|
expires, parseErr := strconv.ParseInt(expiresStr, 10, 64) |
|
|
|
if parseErr != nil { |
|
|
|
return nil, s3err.ErrMalformedDate |
|
|
|
} |
|
|
|
// Check if current time is after the expiration time
|
|
|
|
expirationTime := t.Add(time.Duration(expires) * time.Second) |
|
|
|
if time.Now().UTC().After(expirationTime) { |
|
|
|
return nil, s3err.ErrExpiredPresignRequest |
|
|
|
} |
|
|
|
// Parse credential header
|
|
|
|
credHeader, errCode := parseCredentialHeader("Credential=" + query.Get("X-Amz-Credential")) |
|
|
|
if errCode != s3err.ErrNone { |
|
|
|
return nil, errCode |
|
|
|
} |
|
|
|
|
|
|
|
// Parse signed headers
|
|
|
|
signedHeaders := strings.Split(signedHeadersStr, ";") |
|
|
|
// For presigned URLs, X-Amz-Content-Sha256 must come from the query parameter
|
|
|
|
// (or default to UNSIGNED-PAYLOAD) because that's what was used for signing.
|
|
|
|
// We must NOT check the request header as it wasn't part of the signature calculation.
|
|
|
|
hashedPayload := query.Get("X-Amz-Content-Sha256") |
|
|
|
if hashedPayload == "" { |
|
|
|
hashedPayload = unsignedPayload |
|
|
|
} |
|
|
|
|
|
|
|
return &v4AuthInfo{ |
|
|
|
Signature: query.Get("X-Amz-Signature"), |
|
|
|
AccessKey: credHeader.accessKey, |
|
|
|
SignedHeaders: strings.Split(query.Get("X-Amz-SignedHeaders"), ";"), |
|
|
|
Date: t, |
|
|
|
Region: credHeader.scope.region, |
|
|
|
Service: credHeader.scope.service, |
|
|
|
Scope: credHeader.getScope(), |
|
|
|
HashedPayload: hashedPayload, |
|
|
|
IsPresigned: true, |
|
|
|
}, s3err.ErrNone |
|
|
|
} |
|
|
|
|
|
|
|
// Extract signed headers from request
|
|
|
|
extractedSignedHeaders := make(http.Header) |
|
|
|
for _, header := range signedHeaders { |
|
|
|
if header == "host" { |
|
|
|
extractedSignedHeaders[header] = []string{extractHostHeader(r)} |
|
|
|
continue |
|
|
|
} |
|
|
|
if values := r.Header[http.CanonicalHeaderKey(header)]; len(values) > 0 { |
|
|
|
extractedSignedHeaders[http.CanonicalHeaderKey(header)] = values |
|
|
|
} |
|
|
|
func getCanonicalQueryString(r *http.Request, isPresigned bool) string { |
|
|
|
var queryToEncode string |
|
|
|
if !isPresigned { |
|
|
|
queryToEncode = r.URL.Query().Encode() |
|
|
|
} else { |
|
|
|
queryForCanonical := r.URL.Query() |
|
|
|
queryForCanonical.Del("X-Amz-Signature") |
|
|
|
queryToEncode = queryForCanonical.Encode() |
|
|
|
} |
|
|
|
return queryToEncode |
|
|
|
} |
|
|
|
|
|
|
|
// Remove signature from query for canonical request calculation
|
|
|
|
queryForCanonical := r.URL.Query() |
|
|
|
queryForCanonical.Del("X-Amz-Signature") |
|
|
|
queryStr := strings.Replace(queryForCanonical.Encode(), "+", "%20", -1) |
|
|
|
func checkPresignedRequestExpiry(r *http.Request, t time.Time) s3err.ErrorCode { |
|
|
|
expiresStr := r.URL.Query().Get("X-Amz-Expires") |
|
|
|
// X-Amz-Expires is validated as required in extractV4AuthInfoFromQuery,
|
|
|
|
// so it should never be empty here
|
|
|
|
expires, err := strconv.ParseInt(expiresStr, 10, 64) |
|
|
|
if err != nil { |
|
|
|
return s3err.ErrMalformedDate |
|
|
|
} |
|
|
|
|
|
|
|
var errCode s3err.ErrorCode |
|
|
|
// Check if reverse proxy is forwarding with prefix for presigned URLs
|
|
|
|
if forwardedPrefix := r.Header.Get("X-Forwarded-Prefix"); forwardedPrefix != "" { |
|
|
|
// Try signature verification with the forwarded prefix first.
|
|
|
|
// This handles cases where reverse proxies strip URL prefixes and add the X-Forwarded-Prefix header.
|
|
|
|
cleanedPath := buildPathWithForwardedPrefix(forwardedPrefix, r.URL.Path) |
|
|
|
errCode = iam.verifyPresignedSignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, cleanedPath, r.Method, foundCred.SecretKey, t, credHeader, signature) |
|
|
|
if errCode == s3err.ErrNone { |
|
|
|
return identity, errCode |
|
|
|
} |
|
|
|
// The maximum value for X-Amz-Expires is 604800 seconds (7 days)
|
|
|
|
// Allow 0 but it will immediately fail expiration check
|
|
|
|
if expires < 0 { |
|
|
|
return s3err.ErrNegativeExpires |
|
|
|
} |
|
|
|
if expires > 604800 { |
|
|
|
return s3err.ErrMaximumExpires |
|
|
|
} |
|
|
|
|
|
|
|
// Try normal signature verification (without prefix)
|
|
|
|
errCode = iam.verifyPresignedSignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, r.URL.Path, r.Method, foundCred.SecretKey, t, credHeader, signature) |
|
|
|
if errCode == s3err.ErrNone { |
|
|
|
return identity, errCode |
|
|
|
expirationTime := t.Add(time.Duration(expires) * time.Second) |
|
|
|
if time.Now().UTC().After(expirationTime) { |
|
|
|
return s3err.ErrExpiredPresignRequest |
|
|
|
} |
|
|
|
return s3err.ErrNone |
|
|
|
} |
|
|
|
|
|
|
|
func (iam *IdentityAccessManagement) doesSignatureMatch(r *http.Request) (*Identity, string, s3err.ErrorCode) { |
|
|
|
identity, _, calculatedSignature, _, errCode := iam.verifyV4Signature(r, false) |
|
|
|
return identity, calculatedSignature, errCode |
|
|
|
} |
|
|
|
|
|
|
|
return nil, errCode |
|
|
|
func (iam *IdentityAccessManagement) doesPresignedSignatureMatch(r *http.Request) (*Identity, string, s3err.ErrorCode) { |
|
|
|
identity, _, calculatedSignature, _, errCode := iam.verifyV4Signature(r, false) |
|
|
|
return identity, calculatedSignature, errCode |
|
|
|
} |
|
|
|
|
|
|
|
// credentialHeader data type represents structured form of Credential
|
|
|
|
@ -531,7 +595,7 @@ func extractHostHeader(r *http.Request) string { |
|
|
|
// Check if reverse proxy also forwarded the port
|
|
|
|
if forwardedPort := r.Header.Get("X-Forwarded-Port"); forwardedPort != "" { |
|
|
|
// Determine the protocol to check for standard ports
|
|
|
|
proto := r.Header.Get("X-Forwarded-Proto") |
|
|
|
proto := strings.ToLower(r.Header.Get("X-Forwarded-Proto")) |
|
|
|
// Only add port if it's not the standard port for the protocol
|
|
|
|
if (proto == "https" && forwardedPort != "443") || (proto != "https" && forwardedPort != "80") { |
|
|
|
return forwardedHost + ":" + forwardedPort |
|
|
|
|