You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							782 lines
						
					
					
						
							25 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							782 lines
						
					
					
						
							25 KiB
						
					
					
				| /* | |
|  * The following code tries to reverse engineer the Amazon S3 APIs, | |
|  * and is mostly copied from minio implementation. | |
|  */ | |
| 
 | |
| // 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 s3api | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"crypto/hmac" | |
| 	"crypto/sha256" | |
| 	"crypto/subtle" | |
| 	"encoding/hex" | |
| 	"io" | |
| 	"net/http" | |
| 	"regexp" | |
| 	"sort" | |
| 	"strconv" | |
| 	"strings" | |
| 	"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) { | |
| 	switch { | |
| 	case isRequestSignatureV4(r): | |
| 		identity, _, errCode := iam.doesSignatureMatch(r) | |
| 		return identity, errCode | |
| 	case isRequestPresignedSignatureV4(r): | |
| 		identity, _, errCode := iam.doesPresignedSignatureMatch(r) | |
| 		return identity, errCode | |
| 	} | |
| 	return nil, s3err.ErrAccessDenied | |
| } | |
| 
 | |
| // Constants specific to this file | |
| const ( | |
| 	emptySHA256              = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" | |
| 	streamingContentSHA256   = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" | |
| 	streamingUnsignedPayload = "STREAMING-UNSIGNED-PAYLOAD-TRAILER" | |
| 	unsignedPayload          = "UNSIGNED-PAYLOAD" | |
| 	// Limit for IAM/STS request body size to prevent DoS attacks | |
| 	iamRequestBodyLimit = 10 * (1 << 20) // 10 MiB | |
| ) | |
| 
 | |
| // streamHashRequestBody computes SHA256 hash incrementally while preserving the body. | |
| func streamHashRequestBody(r *http.Request, sizeLimit int64) (string, error) { | |
| 	if r.Body == nil { | |
| 		return emptySHA256, nil | |
| 	} | |
| 
 | |
| 	limitedReader := io.LimitReader(r.Body, sizeLimit) | |
| 	hasher := sha256.New() | |
| 	var bodyBuffer bytes.Buffer | |
| 
 | |
| 	// Use io.Copy with an io.MultiWriter to hash and buffer the body simultaneously. | |
| 	if _, err := io.Copy(io.MultiWriter(hasher, &bodyBuffer), limitedReader); err != nil { | |
| 		return "", err | |
| 	} | |
| 
 | |
| 	r.Body = io.NopCloser(&bodyBuffer) | |
| 
 | |
| 	if bodyBuffer.Len() == 0 { | |
| 		return emptySHA256, nil | |
| 	} | |
| 
 | |
| 	return hex.EncodeToString(hasher.Sum(nil)), nil | |
| } | |
| 
 | |
| // getContentSha256Cksum retrieves the "x-amz-content-sha256" header value. | |
| func getContentSha256Cksum(r *http.Request) string { | |
| 	// If the client sends a SHA256 checksum of the object in this header, use it. | |
| 	if v := r.Header.Get("X-Amz-Content-Sha256"); v != "" { | |
| 		return v | |
| 	} | |
| 
 | |
| 	// For a presigned request we look at the query param for sha256. | |
| 	if isRequestPresignedSignatureV4(r) { | |
| 		// X-Amz-Content-Sha256 header value is optional for presigned requests. | |
| 		return unsignedPayload | |
| 	} | |
| 
 | |
| 	// X-Amz-Content-Sha256 header value is required for all non-presigned requests. | |
| 	return emptySHA256 | |
| } | |
| 
 | |
| // signValues data type represents structured form of AWS Signature V4 header. | |
| type signValues struct { | |
| 	Credential    credentialHeader | |
| 	SignedHeaders []string | |
| 	Signature     string | |
| } | |
| 
 | |
| // parseSignV4 parses the authorization header for signature v4. | |
| func parseSignV4(v4Auth string) (sv signValues, aec s3err.ErrorCode) { | |
| 	// Replace all spaced strings, some clients can send spaced | |
| 	// parameters and some won't. So we pro-actively remove any spaces | |
| 	// to make parsing easier. | |
| 	v4Auth = strings.Replace(v4Auth, " ", "", -1) | |
| 	if v4Auth == "" { | |
| 		return sv, s3err.ErrAuthHeaderEmpty | |
| 	} | |
| 
 | |
| 	// Verify if the header algorithm is supported or not. | |
| 	if !strings.HasPrefix(v4Auth, signV4Algorithm) { | |
| 		return sv, s3err.ErrSignatureVersionNotSupported | |
| 	} | |
| 
 | |
| 	// Strip off the Algorithm prefix. | |
| 	v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm) | |
| 	authFields := strings.Split(strings.TrimSpace(v4Auth), ",") | |
| 	if len(authFields) != 3 { | |
| 		return sv, s3err.ErrMissingFields | |
| 	} | |
| 
 | |
| 	// Initialize signature version '4' structured header. | |
| 	signV4Values := signValues{} | |
| 
 | |
| 	var err s3err.ErrorCode | |
| 	// Save credential values. | |
| 	signV4Values.Credential, err = parseCredentialHeader(authFields[0]) | |
| 	if err != s3err.ErrNone { | |
| 		return sv, err | |
| 	} | |
| 
 | |
| 	// Save signed headers. | |
| 	signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1]) | |
| 	if err != s3err.ErrNone { | |
| 		return sv, err | |
| 	} | |
| 
 | |
| 	// Save signature. | |
| 	signV4Values.Signature, err = parseSignature(authFields[2]) | |
| 	if err != s3err.ErrNone { | |
| 		return sv, err | |
| 	} | |
| 
 | |
| 	// Return the structure here. | |
| 	return signV4Values, s3err.ErrNone | |
| } | |
| 
 | |
| // 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 | |
| } | |
| 
 | |
| // 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 | |
| } | |
| 
 | |
| // 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, nil, "", nil, errCode | |
| 	} | |
| 
 | |
| 	// 2. Lookup user and credentials | |
| 	identity, cred, found := iam.lookupByAccessKey(authInfo.AccessKey) | |
| 	if !found { | |
| 		return nil, nil, "", 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 | |
| 		} | |
| 	} | |
| 
 | |
| 	// 4. Handle presigned request expiration | |
| 	if authInfo.IsPresigned { | |
| 		if errCode = checkPresignedRequestExpiry(r, authInfo.Date); errCode != s3err.ErrNone { | |
| 			return nil, nil, "", nil, errCode | |
| 		} | |
| 	} | |
| 
 | |
| 	// 5. Extract headers that were part of the signature | |
| 	extractedSignedHeaders, errCode := extractSignedHeaders(authInfo.SignedHeaders, r) | |
| 	if errCode != s3err.ErrNone { | |
| 		return nil, nil, "", nil, errCode | |
| 	} | |
| 
 | |
| 	// 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, | |
| 		) | |
| 	} | |
| 
 | |
| 	// 8. Verify the signature, trying with X-Forwarded-Prefix first | |
| 	if forwardedPrefix := r.Header.Get("X-Forwarded-Prefix"); forwardedPrefix != "" { | |
| 		cleanedPath := buildPathWithForwardedPrefix(forwardedPrefix, r.URL.Path) | |
| 		calculatedSignature, errCode = verify(cleanedPath) | |
| 		if errCode == s3err.ErrNone { | |
| 			return identity, cred, calculatedSignature, authInfo, s3err.ErrNone | |
| 		} | |
| 	} | |
| 
 | |
| 	// 9. Verify with the original path | |
| 	calculatedSignature, errCode = verify(r.URL.Path) | |
| 	if errCode != s3err.ErrNone { | |
| 		return nil, nil, "", nil, errCode | |
| 	} | |
| 
 | |
| 	return identity, cred, calculatedSignature, authInfo, s3err.ErrNone | |
| } | |
| 
 | |
| // 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) | |
| 
 | |
| 	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 newSignature, s3err.ErrNone | |
| } | |
| 
 | |
| func extractV4AuthInfo(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) { | |
| 	if isRequestPresignedSignatureV4(r) { | |
| 		return extractV4AuthInfoFromQuery(r) | |
| 	} | |
| 	return extractV4AuthInfoFromHeader(r) | |
| } | |
| 
 | |
| func extractV4AuthInfoFromHeader(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) { | |
| 	authHeader := r.Header.Get("Authorization") | |
| 	signV4Values, errCode := parseSignV4(authHeader) | |
| 	if errCode != s3err.ErrNone { | |
| 		return nil, errCode | |
| 	} | |
| 
 | |
| 	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() | |
| 	} | |
| 
 | |
| 	// 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 | |
| 	} | |
| 
 | |
| 	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 &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 | |
| } | |
| 
 | |
| func extractV4AuthInfoFromQuery(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) { | |
| 	query := r.URL.Query() | |
| 
 | |
| 	// Validate all required query parameters upfront for fail-fast behavior | |
| 	if query.Get("X-Amz-Algorithm") != signV4Algorithm { | |
| 		return nil, s3err.ErrSignatureVersionNotSupported | |
| 	} | |
| 	if query.Get("X-Amz-Date") == "" { | |
| 		return nil, s3err.ErrMissingDateHeader | |
| 	} | |
| 	if query.Get("X-Amz-Credential") == "" { | |
| 		return nil, s3err.ErrMissingFields | |
| 	} | |
| 	if query.Get("X-Amz-Signature") == "" { | |
| 		return nil, s3err.ErrMissingFields | |
| 	} | |
| 	if query.Get("X-Amz-SignedHeaders") == "" { | |
| 		return nil, s3err.ErrMissingFields | |
| 	} | |
| 	if query.Get("X-Amz-Expires") == "" { | |
| 		return nil, s3err.ErrInvalidQueryParams | |
| 	} | |
| 
 | |
| 	// Parse date | |
| 	dateStr := query.Get("X-Amz-Date") | |
| 	t, err := time.Parse(iso8601Format, dateStr) | |
| 	if err != nil { | |
| 		return nil, s3err.ErrMalformedDate | |
| 	} | |
| 
 | |
| 	// Parse credential header | |
| 	credHeader, errCode := parseCredentialHeader("Credential=" + query.Get("X-Amz-Credential")) | |
| 	if errCode != s3err.ErrNone { | |
| 		return nil, errCode | |
| 	} | |
| 
 | |
| 	// 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 | |
| } | |
| 
 | |
| 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 | |
| } | |
| 
 | |
| 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 | |
| 	} | |
| 
 | |
| 	// 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 | |
| 	} | |
| 
 | |
| 	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 | |
| } | |
| 
 | |
| 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 | |
| // string from authorization header. | |
| type credentialHeader struct { | |
| 	accessKey string | |
| 	scope     struct { | |
| 		date    time.Time | |
| 		region  string | |
| 		service string | |
| 		request string | |
| 	} | |
| } | |
| 
 | |
| func (c credentialHeader) getScope() string { | |
| 	return strings.Join([]string{ | |
| 		c.scope.date.Format(yyyymmdd), | |
| 		c.scope.region, | |
| 		c.scope.service, | |
| 		c.scope.request, | |
| 	}, "/") | |
| } | |
| 
 | |
| // parse credentialHeader string into its structured form. | |
| func parseCredentialHeader(credElement string) (ch credentialHeader, aec s3err.ErrorCode) { | |
| 	creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2) | |
| 	if len(creds) != 2 { | |
| 		return ch, s3err.ErrMissingFields | |
| 	} | |
| 	if creds[0] != "Credential" { | |
| 		return ch, s3err.ErrMissingCredTag | |
| 	} | |
| 	credElements := strings.Split(strings.TrimSpace(creds[1]), "/") | |
| 	if len(credElements) != 5 { | |
| 		return ch, s3err.ErrCredMalformed | |
| 	} | |
| 	// Save access key id. | |
| 	cred := credentialHeader{ | |
| 		accessKey: credElements[0], | |
| 	} | |
| 	var e error | |
| 	cred.scope.date, e = time.Parse(yyyymmdd, credElements[1]) | |
| 	if e != nil { | |
| 		return ch, s3err.ErrMalformedCredentialDate | |
| 	} | |
| 
 | |
| 	cred.scope.region = credElements[2] | |
| 	cred.scope.service = credElements[3] // "s3" | |
| 	cred.scope.request = credElements[4] // "aws4_request" | |
| 	return cred, s3err.ErrNone | |
| } | |
| 
 | |
| // Parse signature from signature tag. | |
| func parseSignature(signElement string) (string, s3err.ErrorCode) { | |
| 	signFields := strings.Split(strings.TrimSpace(signElement), "=") | |
| 	if len(signFields) != 2 { | |
| 		return "", s3err.ErrMissingFields | |
| 	} | |
| 	if signFields[0] != "Signature" { | |
| 		return "", s3err.ErrMissingSignTag | |
| 	} | |
| 	if signFields[1] == "" { | |
| 		return "", s3err.ErrMissingFields | |
| 	} | |
| 	signature := signFields[1] | |
| 	return signature, s3err.ErrNone | |
| } | |
| 
 | |
| // Parse slice of signed headers from signed headers tag. | |
| func parseSignedHeader(signedHdrElement string) ([]string, s3err.ErrorCode) { | |
| 	signedHdrFields := strings.Split(strings.TrimSpace(signedHdrElement), "=") | |
| 	if len(signedHdrFields) != 2 { | |
| 		return nil, s3err.ErrMissingFields | |
| 	} | |
| 	if signedHdrFields[0] != "SignedHeaders" { | |
| 		return nil, s3err.ErrMissingSignHeadersTag | |
| 	} | |
| 	if signedHdrFields[1] == "" { | |
| 		return nil, s3err.ErrMissingFields | |
| 	} | |
| 	signedHeaders := strings.Split(signedHdrFields[1], ";") | |
| 	return signedHeaders, s3err.ErrNone | |
| } | |
| 
 | |
| func (iam *IdentityAccessManagement) doesPolicySignatureV4Match(formValues http.Header) s3err.ErrorCode { | |
| 
 | |
| 	// Parse credential tag. | |
| 	credHeader, err := parseCredentialHeader("Credential=" + formValues.Get("X-Amz-Credential")) | |
| 	if err != s3err.ErrNone { | |
| 		return err | |
| 	} | |
| 
 | |
| 	identity, cred, found := iam.lookupByAccessKey(credHeader.accessKey) | |
| 	if !found { | |
| 		return s3err.ErrInvalidAccessKeyID | |
| 	} | |
| 
 | |
| 	bucket := formValues.Get("bucket") | |
| 	if !identity.canDo(s3_constants.ACTION_WRITE, bucket, "") { | |
| 		return s3err.ErrAccessDenied | |
| 	} | |
| 
 | |
| 	// Get signing key. | |
| 	signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, credHeader.scope.service) | |
| 
 | |
| 	// Get signature. | |
| 	newSignature := getSignature(signingKey, formValues.Get("Policy")) | |
| 
 | |
| 	// Verify signature. | |
| 	if !compareSignatureV4(newSignature, formValues.Get("X-Amz-Signature")) { | |
| 		return s3err.ErrSignatureDoesNotMatch | |
| 	} | |
| 	return s3err.ErrNone | |
| } | |
| 
 | |
| // Verify if extracted signed headers are not properly signed. | |
| func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, s3err.ErrorCode) { | |
| 	reqHeaders := r.Header | |
| 	// If no signed headers are provided, then return an error. | |
| 	if len(signedHeaders) == 0 { | |
| 		return nil, s3err.ErrMissingFields | |
| 	} | |
| 	extractedSignedHeaders := make(http.Header) | |
| 	for _, header := range signedHeaders { | |
| 		// `host` is not a case-sensitive header, unlike other headers such as `x-amz-date`. | |
| 		if header == "host" { | |
| 			// Get host value. | |
| 			hostHeaderValue := extractHostHeader(r) | |
| 			extractedSignedHeaders[header] = []string{hostHeaderValue} | |
| 			continue | |
| 		} | |
| 		// For all other headers we need to find them in the HTTP headers and copy them over. | |
| 		// We skip non-existent headers to be compatible with AWS signatures. | |
| 		if values, ok := reqHeaders[http.CanonicalHeaderKey(header)]; ok { | |
| 			extractedSignedHeaders[header] = values | |
| 		} | |
| 	} | |
| 	return extractedSignedHeaders, s3err.ErrNone | |
| } | |
| 
 | |
| // extractHostHeader returns the value of host header if available. | |
| func extractHostHeader(r *http.Request) string { | |
| 	// Check for X-Forwarded-Host header first, which is set by reverse proxies | |
| 	if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" { | |
| 		// 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 := 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 | |
| 			} | |
| 		} | |
| 		// Using reverse proxy with X-Forwarded-Host (standard port or no port forwarded). | |
| 		return forwardedHost | |
| 	} | |
| 
 | |
| 	hostHeaderValue := r.Host | |
| 	// For standard requests, this should be fine. | |
| 	if r.Host != "" { | |
| 		return hostHeaderValue | |
| 	} | |
| 	// If no host header is found, then check for host URL value. | |
| 	if r.URL.Host != "" { | |
| 		hostHeaderValue = r.URL.Host | |
| 	} | |
| 	return hostHeaderValue | |
| } | |
| 
 | |
| // getScope generate a string of a specific date, an AWS region, and a service. | |
| func getScope(t time.Time, region string, service string) string { | |
| 	scope := strings.Join([]string{ | |
| 		t.Format(yyyymmdd), | |
| 		region, | |
| 		service, | |
| 		"aws4_request", | |
| 	}, "/") | |
| 	return scope | |
| } | |
| 
 | |
| // getCanonicalRequest generate a canonical request of style | |
| // | |
| // canonicalRequest = | |
| // | |
| //	<HTTPMethod>\n | |
| //	<CanonicalURI>\n | |
| //	<CanonicalQueryString>\n | |
| //	<CanonicalHeaders>\n | |
| //	<SignedHeaders>\n | |
| //	<HashedPayload> | |
| func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, urlPath, method string) string { | |
| 	rawQuery := strings.Replace(queryStr, "+", "%20", -1) | |
| 	encodedPath := encodePath(urlPath) | |
| 	canonicalRequest := strings.Join([]string{ | |
| 		method, | |
| 		encodedPath, | |
| 		rawQuery, | |
| 		getCanonicalHeaders(extractedSignedHeaders), | |
| 		getSignedHeaders(extractedSignedHeaders), | |
| 		payload, | |
| 	}, "\n") | |
| 	return canonicalRequest | |
| } | |
| 
 | |
| // getStringToSign a string based on selected query values. | |
| func getStringToSign(canonicalRequest string, t time.Time, scope string) string { | |
| 	stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n" | |
| 	stringToSign = stringToSign + scope + "\n" | |
| 	stringToSign = stringToSign + getSHA256Hash([]byte(canonicalRequest)) | |
| 	return stringToSign | |
| } | |
| 
 | |
| // getSHA256Hash returns hex-encoded SHA256 hash of the input data. | |
| func getSHA256Hash(data []byte) string { | |
| 	hash := sha256.Sum256(data) | |
| 	return hex.EncodeToString(hash[:]) | |
| } | |
| 
 | |
| // sumHMAC calculate hmac between two input byte array. | |
| func sumHMAC(key []byte, data []byte) []byte { | |
| 	hash := hmac.New(sha256.New, key) | |
| 	hash.Write(data) | |
| 	return hash.Sum(nil) | |
| } | |
| 
 | |
| // getSigningKey hmac seed to calculate final signature. | |
| func getSigningKey(secretKey string, time string, region string, service string) []byte { | |
| 	date := sumHMAC([]byte("AWS4"+secretKey), []byte(time)) | |
| 	regionBytes := sumHMAC(date, []byte(region)) | |
| 	serviceBytes := sumHMAC(regionBytes, []byte(service)) | |
| 	signingKey := sumHMAC(serviceBytes, []byte("aws4_request")) | |
| 	return signingKey | |
| } | |
| 
 | |
| // getCanonicalHeaders generate a list of request headers with their values | |
| func getCanonicalHeaders(signedHeaders http.Header) string { | |
| 	var headers []string | |
| 	vals := make(http.Header) | |
| 	for k, vv := range signedHeaders { | |
| 		vals[strings.ToLower(k)] = vv | |
| 	} | |
| 	for k := range vals { | |
| 		headers = append(headers, k) | |
| 	} | |
| 	sort.Strings(headers) | |
| 
 | |
| 	var buf bytes.Buffer | |
| 	for _, k := range headers { | |
| 		buf.WriteString(k) | |
| 		buf.WriteByte(':') | |
| 		for idx, v := range vals[k] { | |
| 			if idx > 0 { | |
| 				buf.WriteByte(',') | |
| 			} | |
| 			buf.WriteString(signV4TrimAll(v)) | |
| 		} | |
| 		buf.WriteByte('\n') | |
| 	} | |
| 	return buf.String() | |
| } | |
| 
 | |
| // signV4TrimAll trims leading and trailing spaces from each string in the slice, and trims sequential spaces. | |
| func signV4TrimAll(input string) string { | |
| 	// Compress adjacent spaces (a space is determined by | |
| 	// unicode.IsSpace() internally here) to a single space and trim | |
| 	// leading and trailing spaces. | |
| 	return strings.Join(strings.Fields(input), " ") | |
| } | |
| 
 | |
| // getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names | |
| func getSignedHeaders(signedHeaders http.Header) string { | |
| 	var headers []string | |
| 	for k := range signedHeaders { | |
| 		headers = append(headers, strings.ToLower(k)) | |
| 	} | |
| 	sort.Strings(headers) | |
| 	return strings.Join(headers, ";") | |
| } | |
| 
 | |
| // if object matches reserved string, no need to encode them | |
| var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") | |
| 
 | |
| // encodePath encodes the strings from UTF-8 byte representations to HTML hex escape sequences | |
| // | |
| // This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 | |
| // non english characters cannot be parsed due to the nature in which url.Encode() is written | |
| // | |
| // This function on the other hand is a direct replacement for url.Encode() technique to support | |
| // pretty much every UTF-8 character. | |
| func encodePath(pathName string) string { | |
| 	if reservedObjectNames.MatchString(pathName) { | |
| 		return pathName | |
| 	} | |
| 	var encodedPathname string | |
| 	for _, s := range pathName { | |
| 		if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) | |
| 			encodedPathname = encodedPathname + string(s) | |
| 		} else { | |
| 			switch s { | |
| 			case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) | |
| 				encodedPathname = encodedPathname + string(s) | |
| 			default: | |
| 				runeLen := utf8.RuneLen(s) | |
| 				if runeLen < 0 { | |
| 					return pathName | |
| 				} | |
| 				u := make([]byte, runeLen) | |
| 				utf8.EncodeRune(u, s) | |
| 				for _, r := range u { | |
| 					hex := hex.EncodeToString([]byte{r}) | |
| 					encodedPathname = encodedPathname + "%" + strings.ToUpper(hex) | |
| 				} | |
| 			} | |
| 		} | |
| 	} | |
| 	return encodedPathname | |
| } | |
| 
 | |
| // getSignature final signature in hexadecimal form. | |
| func getSignature(signingKey []byte, stringToSign string) string { | |
| 	return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) | |
| } | |
| 
 | |
| // compareSignatureV4 returns true if and only if both signatures | |
| // are equal. The signatures are expected to be hex-encoded strings | |
| // according to the AWS S3 signature V4 spec. | |
| func compareSignatureV4(sig1, sig2 string) bool { | |
| 	// The CTC using []byte(str) works because the hex encoding doesn't use | |
| 	// non-ASCII characters. Otherwise, we'd need to convert the strings to | |
| 	// a []rune of UTF-8 characters. | |
| 	return subtle.ConstantTimeCompare([]byte(sig1), []byte(sig2)) == 1 | |
| }
 |