diff options
Diffstat (limited to 'weed/s3api/auth_signature_v4.go')
| -rw-r--r-- | weed/s3api/auth_signature_v4.go | 900 |
1 files changed, 296 insertions, 604 deletions
diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 0ddbaa917..8d9011f0d 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -23,21 +23,15 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/hex" - "hash" - "io" - "net" "net/http" - "net/url" "regexp" "sort" "strconv" "strings" - "sync" - "sync/atomic" "time" "unicode/utf8" - "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) @@ -52,77 +46,99 @@ func (iam *IdentityAccessManagement) reqSignatureV4Verify(r *http.Request) (*Ide return nil, s3err.ErrAccessDenied } -// Streaming AWS Signature Version '4' constants. +// Constants specific to this file const ( - emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - streamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" - signV4ChunkedAlgorithm = "AWS4-HMAC-SHA256-PAYLOAD" - - // http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" or "STREAMING-UNSIGNED-PAYLOAD-TRAILER" indicates that the - // client did not calculate sha256 of the payload. - unsignedPayload = "UNSIGNED-PAYLOAD" + emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + streamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" streamingUnsignedPayload = "STREAMING-UNSIGNED-PAYLOAD-TRAILER" + unsignedPayload = "UNSIGNED-PAYLOAD" ) -// AWS S3 authentication headers that should be skipped when signing the request -// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html -var awsS3AuthHeaders = map[string]struct{}{ - "x-amz-content-sha256": {}, - "x-amz-security-token": {}, - "x-amz-algorithm": {}, - "x-amz-date": {}, - "x-amz-expires": {}, - "x-amz-signedheaders": {}, - "x-amz-credential": {}, - "x-amz-signature": {}, -} - -// Returns SHA256 for calculating canonical-request. +// getContentSha256Cksum retrieves the "x-amz-content-sha256" header value. func getContentSha256Cksum(r *http.Request) string { - var ( - defaultSha256Cksum string - v []string - ok bool - ) + // 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, if not set in presigned requests, checksum - // will default to 'UNSIGNED-PAYLOAD'. - defaultSha256Cksum = unsignedPayload - v, ok = r.URL.Query()["X-Amz-Content-Sha256"] - if !ok { - v, ok = r.Header["X-Amz-Content-Sha256"] - } - } else { - // X-Amz-Content-Sha256, if not set in signed requests, checksum - // will default to sha256([]byte("")). - defaultSha256Cksum = emptySHA256 - v, ok = r.Header["X-Amz-Content-Sha256"] + // X-Amz-Content-Sha256 header value is optional for presigned requests. + return unsignedPayload } - // We found 'X-Amz-Content-Sha256' return the captured value. - if ok { - return v[0] + // 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 } - // We couldn't find 'X-Amz-Content-Sha256'. - return defaultSha256Cksum + // 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 } -// Verify authorization header - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html +// Wrapper to verify if request came with a valid signature. func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r *http.Request) (*Identity, s3err.ErrorCode) { - // Copy request. + // Copy request req := *r // Save authorization header. v4Auth := req.Header.Get("Authorization") // Parse signature version '4' header. - signV4Values, err := parseSignV4(v4Auth) - if err != s3err.ErrNone { - return nil, err + signV4Values, errCode := parseSignV4(v4Auth) + if errCode != s3err.ErrNone { + return nil, errCode } // Extract all the signed headers along with its values. @@ -131,21 +147,26 @@ func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r return nil, errCode } - // Verify if the access key id matches. - identity, cred, found := iam.lookupByAccessKey(signV4Values.Credential.accessKey) + cred := signV4Values.Credential + identity, foundCred, found := iam.lookupByAccessKey(cred.accessKey) if !found { return nil, s3err.ErrInvalidAccessKeyID } + bucket, object := s3_constants.GetBucketAndObject(r) + if !identity.canDo(s3_constants.ACTION_WRITE, bucket, object) { + return nil, s3err.ErrAccessDenied + } + // Extract date, if not present throw error. - var date string - if date = req.Header.Get(http.CanonicalHeaderKey("X-Amz-Date")); date == "" { - if date = r.Header.Get("Date"); date == "" { + var dateStr string + if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" { + if dateStr = r.Header.Get("Date"); dateStr == "" { return nil, s3err.ErrMissingDateHeader } } // Parse date header. - t, e := time.Parse(iso8601Format, date) + t, e := time.Parse(iso8601Format, dateStr) if e != nil { return nil, s3err.ErrMalformedDate } @@ -153,62 +174,134 @@ func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r // Query string. queryStr := req.URL.Query().Encode() - // Get hashed Payload - if signV4Values.Credential.scope.service != "s3" && hashedPayload == emptySHA256 && r.Body != nil { - buf, _ := io.ReadAll(r.Body) - r.Body = io.NopCloser(bytes.NewBuffer(buf)) - b, _ := io.ReadAll(bytes.NewBuffer(buf)) - if len(b) != 0 { - bodyHash := sha256.Sum256(b) - hashedPayload = hex.EncodeToString(bodyHash[:]) - } + // Get canonical request. + canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method) + + // Get string to sign from canonical request. + stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) + + // Get hmac signing key. + signingKey := getSigningKey(foundCred.SecretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), signV4Values.Credential.scope.region, "s3") + + // Calculate signature. + newSignature := getSignature(signingKey, stringToSign) + + // Verify if signature match. + if !compareSignatureV4(newSignature, signV4Values.Signature) { + return nil, s3err.ErrSignatureDoesNotMatch } - if forwardedPrefix := r.Header.Get("X-Forwarded-Prefix"); forwardedPrefix != "" { - // Handling usage of reverse proxy at prefix. - // Trying with prefix before main path. + // Return error none. + return identity, s3err.ErrNone +} - // Get canonical request. - glog.V(4).Infof("Forwarded Prefix: %s", forwardedPrefix) +// 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 + query := r.URL.Query() - canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, forwardedPrefix+req.URL.Path, req.Method) - errCode = iam.genAndCompareSignatureV4(canonicalRequest, cred.SecretKey, t, signV4Values) - if errCode == s3err.ErrNone { - return identity, errCode - } + // Check required parameters + algorithm := query.Get("X-Amz-Algorithm") + if algorithm != signV4Algorithm { + return nil, s3err.ErrSignatureVersionNotSupported } - // Get canonical request. - canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method) + credential := query.Get("X-Amz-Credential") + if credential == "" { + return nil, s3err.ErrMissingFields + } - errCode = iam.genAndCompareSignatureV4(canonicalRequest, cred.SecretKey, t, signV4Values) + signature := query.Get("X-Amz-Signature") + if signature == "" { + return nil, s3err.ErrMissingFields + } - if errCode == s3err.ErrNone { - return identity, errCode + signedHeadersStr := query.Get("X-Amz-SignedHeaders") + if signedHeadersStr == "" { + return nil, s3err.ErrMissingFields } - return nil, errCode -} -// Generate and compare signature for request. -func (iam *IdentityAccessManagement) genAndCompareSignatureV4(canonicalRequest, secretKey string, t time.Time, signV4Values signValues) s3err.ErrorCode { - // Get string to sign from canonical request. - stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) - glog.V(4).Infof("String to Sign:\n%s", stringToSign) - // Calculate signature. - newSignature := iam.getSignature( - secretKey, - signV4Values.Credential.scope.date, - signV4Values.Credential.scope.region, - signV4Values.Credential.scope.service, - stringToSign, - ) - glog.V(4).Infof("Signature:\n%s", newSignature) + dateStr := query.Get("X-Amz-Date") + if dateStr == "" { + return nil, s3err.ErrMissingDateHeader + } - // Verify if signature match. - if !compareSignatureV4(newSignature, signV4Values.Signature) { - return s3err.ErrSignatureDoesNotMatch + // Parse credential + credHeader, err := parseCredentialHeader("Credential=" + credential) + if err != s3err.ErrNone { + return nil, err } - return s3err.ErrNone + + // Look up identity by access key + identity, foundCred, found := iam.lookupByAccessKey(credHeader.accessKey) + if !found { + return nil, s3err.ErrInvalidAccessKeyID + } + + // Check permissions + bucket, object := s3_constants.GetBucketAndObject(r) + if !identity.canDo(s3_constants.ACTION_READ, bucket, object) { + return nil, s3err.ErrAccessDenied + } + + // Parse date + t, e := time.Parse(iso8601Format, dateStr) + if e != 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 signed headers + signedHeaders := strings.Split(signedHeadersStr, ";") + + // Extract signed headers from request + extractedSignedHeaders := make(http.Header) + for _, header := range signedHeaders { + if header == "host" { + extractedSignedHeaders[header] = []string{r.Host} + continue + } + if values := r.Header[http.CanonicalHeaderKey(header)]; len(values) > 0 { + extractedSignedHeaders[http.CanonicalHeaderKey(header)] = values + } + } + + // Remove signature from query for canonical request calculation + queryForCanonical := r.URL.Query() + queryForCanonical.Del("X-Amz-Signature") + queryStr := strings.Replace(queryForCanonical.Encode(), "+", "%20", -1) + + // Get canonical request + canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, r.URL.Path, r.Method) + + // Get string to sign + stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope()) + + // Get signing key + signingKey := getSigningKey(foundCred.SecretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, "s3") + + // Calculate expected signature + expectedSignature := getSignature(signingKey, stringToSign) + + // Verify signature + if !compareSignatureV4(expectedSignature, signature) { + return nil, s3err.ErrSignatureDoesNotMatch + } + + return identity, s3err.ErrNone } // credentialHeader data type represents structured form of Credential @@ -223,14 +316,6 @@ type credentialHeader struct { } } -// signValues data type represents structured form of AWS Signature V4 header. -type signValues struct { - Credential credentialHeader - SignedHeaders []string - Signature string -} - -// Return scope string. func (c credentialHeader) getScope() string { return strings.Join([]string{ c.scope.date.Format(yyyymmdd), @@ -240,58 +325,9 @@ func (c credentialHeader) getScope() string { }, "/") } -// Authorization: algorithm Credential=accessKeyID/credScope, \ -// SignedHeaders=signedHeaders, Signature=signature -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 -} - // parse credentialHeader string into its structured form. func parseCredentialHeader(credElement string) (ch credentialHeader, aec s3err.ErrorCode) { - creds := strings.Split(strings.TrimSpace(credElement), "=") + creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2) if len(creds) != 2 { return ch, s3err.ErrMissingFields } @@ -318,22 +354,6 @@ func parseCredentialHeader(credElement string) (ch credentialHeader, aec s3err.E return cred, 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 -} - // Parse signature from signature tag. func parseSignature(signElement string) (string, s3err.ErrorCode) { signFields := strings.Split(strings.TrimSpace(signElement), "=") @@ -350,437 +370,90 @@ func parseSignature(signElement string) (string, s3err.ErrorCode) { return signature, s3err.ErrNone } -// doesPolicySignatureV4Match - Verify query headers with post policy -// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html -// -// returns ErrNone if the signature matches. -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 s3err.ErrMissingFields +// 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 } - - _, cred, found := iam.lookupByAccessKey(credHeader.accessKey) - if !found { - return s3err.ErrInvalidAccessKeyID + if signedHdrFields[0] != "SignedHeaders" { + return nil, s3err.ErrMissingSignHeadersTag } - - // Get signature. - newSignature := iam.getSignature( - cred.SecretKey, - credHeader.scope.date, - credHeader.scope.region, - credHeader.scope.service, - formValues.Get("Policy"), - ) - - // Verify signature. - if !compareSignatureV4(newSignature, formValues.Get("X-Amz-Signature")) { - return s3err.ErrSignatureDoesNotMatch + if signedHdrFields[1] == "" { + return nil, s3err.ErrMissingFields } - - // Success. - return s3err.ErrNone + signedHeaders := strings.Split(signedHdrFields[1], ";") + return signedHeaders, s3err.ErrNone } -// check query headers with presigned signature -// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html -func (iam *IdentityAccessManagement) doesPresignedSignatureMatch(hashedPayload string, r *http.Request) (*Identity, s3err.ErrorCode) { - - // Copy request - req := *r +func (iam *IdentityAccessManagement) doesPolicySignatureV4Match(formValues http.Header) s3err.ErrorCode { - // Parse request query string. - pSignValues, err := parsePreSignV4(req.URL.Query()) + // Parse credential tag. + credHeader, err := parseCredentialHeader("Credential=" + formValues.Get("X-Amz-Credential")) if err != s3err.ErrNone { - return nil, err + return err } - // Verify if the access key id matches. - identity, cred, found := iam.lookupByAccessKey(pSignValues.Credential.accessKey) + identity, cred, found := iam.lookupByAccessKey(credHeader.accessKey) if !found { - return nil, s3err.ErrInvalidAccessKeyID - } - - // Extract all the signed headers along with its values. - extractedSignedHeaders, errCode := extractSignedHeaders(pSignValues.SignedHeaders, r) - if errCode != s3err.ErrNone { - return nil, errCode - } - // Construct new query. - query := make(url.Values) - if req.URL.Query().Get("X-Amz-Content-Sha256") != "" { - query.Set("X-Amz-Content-Sha256", hashedPayload) - } - - query.Set("X-Amz-Algorithm", signV4Algorithm) - - now := time.Now().UTC() - - // If the host which signed the request is slightly ahead in time (by less than globalMaxSkewTime) the - // request should still be allowed. - if pSignValues.Date.After(now.Add(15 * time.Minute)) { - return nil, s3err.ErrRequestNotReadyYet - } - - if now.Sub(pSignValues.Date) > pSignValues.Expires { - return nil, s3err.ErrExpiredPresignRequest - } - - // Save the date and expires. - t := pSignValues.Date - expireSeconds := int(pSignValues.Expires / time.Second) - - // Construct the query. - query.Set("X-Amz-Date", t.Format(iso8601Format)) - query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds)) - query.Set("X-Amz-SignedHeaders", getSignedHeaders(extractedSignedHeaders)) - query.Set("X-Amz-Credential", cred.AccessKey+"/"+getScope(t, pSignValues.Credential.scope.region)) - - // Save other headers available in the request parameters. - for k, v := range req.URL.Query() { - // Skip AWS S3 authentication headers - if _, ok := awsS3AuthHeaders[strings.ToLower(k)]; ok { - continue - } - - query[k] = v + return s3err.ErrInvalidAccessKeyID } - // Get the encoded query. - encodedQuery := query.Encode() - - // Verify if date query is same. - if req.URL.Query().Get("X-Amz-Date") != query.Get("X-Amz-Date") { - return nil, s3err.ErrSignatureDoesNotMatch - } - // Verify if expires query is same. - if req.URL.Query().Get("X-Amz-Expires") != query.Get("X-Amz-Expires") { - return nil, s3err.ErrSignatureDoesNotMatch - } - // Verify if signed headers query is same. - if req.URL.Query().Get("X-Amz-SignedHeaders") != query.Get("X-Amz-SignedHeaders") { - return nil, s3err.ErrSignatureDoesNotMatch - } - // Verify if credential query is same. - if req.URL.Query().Get("X-Amz-Credential") != query.Get("X-Amz-Credential") { - return nil, s3err.ErrSignatureDoesNotMatch - } - // Verify if sha256 payload query is same. - if req.URL.Query().Get("X-Amz-Content-Sha256") != "" { - if req.URL.Query().Get("X-Amz-Content-Sha256") != query.Get("X-Amz-Content-Sha256") { - return nil, s3err.ErrContentSHA256Mismatch - } + bucket := formValues.Get("bucket") + if !identity.canDo(s3_constants.ACTION_WRITE, bucket, "") { + return s3err.ErrAccessDenied } - // / Verify finally if signature is same. - - // Get canonical request. - presignedCanonicalReq := getCanonicalRequest(extractedSignedHeaders, hashedPayload, encodedQuery, req.URL.Path, req.Method) - - // Get string to sign from canonical request. - presignedStringToSign := getStringToSign(presignedCanonicalReq, t, pSignValues.Credential.getScope()) + // Get signing key. + signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, "s3") - // Get new signature. - newSignature := iam.getSignature( - cred.SecretKey, - pSignValues.Credential.scope.date, - pSignValues.Credential.scope.region, - pSignValues.Credential.scope.service, - presignedStringToSign, - ) + // Get signature. + newSignature := getSignature(signingKey, formValues.Get("Policy")) // Verify signature. - if !compareSignatureV4(req.URL.Query().Get("X-Amz-Signature"), newSignature) { - return nil, s3err.ErrSignatureDoesNotMatch - } - return identity, s3err.ErrNone -} - -func (iam *IdentityAccessManagement) getSignature(secretKey string, t time.Time, region string, service string, stringToSign string) string { - pool := iam.getSignatureHashPool(secretKey, t, region, service) - h := pool.Get().(hash.Hash) - defer pool.Put(h) - - h.Reset() - h.Write([]byte(stringToSign)) - sig := hex.EncodeToString(h.Sum(nil)) - - return sig -} - -func (iam *IdentityAccessManagement) getSignatureHashPool(secretKey string, t time.Time, region string, service string) *sync.Pool { - // Build a caching key for the pool. - date := t.Format(yyyymmdd) - hashID := "AWS4" + secretKey + "/" + date + "/" + region + "/" + service + "/" + "aws4_request" - - // Try to find an existing pool and return it. - iam.hashMu.RLock() - pool, ok := iam.hashes[hashID] - iam.hashMu.RUnlock() - - if !ok { - iam.hashMu.Lock() - defer iam.hashMu.Unlock() - pool, ok = iam.hashes[hashID] - } - - if ok { - atomic.StoreInt32(iam.hashCounters[hashID], 1) - return pool - } - - // Create a pool that returns HMAC hashers for the requested parameters to avoid expensive re-initializing - // of new instances on every request. - iam.hashes[hashID] = &sync.Pool{ - New: func() any { - signingKey := getSigningKey(secretKey, date, region, service) - return hmac.New(sha256.New, signingKey) - }, - } - iam.hashCounters[hashID] = new(int32) - - // Clean up unused pools automatically after one hour of inactivity - ticker := time.NewTicker(time.Hour) - go func() { - for range ticker.C { - old := atomic.SwapInt32(iam.hashCounters[hashID], 0) - if old == 0 { - break - } - } - - ticker.Stop() - iam.hashMu.Lock() - delete(iam.hashes, hashID) - delete(iam.hashCounters, hashID) - iam.hashMu.Unlock() - }() - - return iam.hashes[hashID] -} - -func contains(list []string, elem string) bool { - for _, t := range list { - if t == elem { - return true - } - } - return false -} - -// preSignValues data type represents structured form of AWS Signature V4 query string. -type preSignValues struct { - signValues - Date time.Time - Expires time.Duration -} - -// Parses signature version '4' query string of the following form. -// -// querystring = X-Amz-Algorithm=algorithm -// querystring += &X-Amz-Credential= urlencode(accessKey + '/' + credential_scope) -// querystring += &X-Amz-Date=date -// querystring += &X-Amz-Expires=timeout interval -// querystring += &X-Amz-SignedHeaders=signed_headers -// querystring += &X-Amz-Signature=signature -// -// verifies if any of the necessary query params are missing in the presigned request. -func doesV4PresignParamsExist(query url.Values) s3err.ErrorCode { - v4PresignQueryParams := []string{"X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Signature", "X-Amz-Date", "X-Amz-SignedHeaders", "X-Amz-Expires"} - for _, v4PresignQueryParam := range v4PresignQueryParams { - if _, ok := query[v4PresignQueryParam]; !ok { - return s3err.ErrInvalidQueryParams - } + if !compareSignatureV4(newSignature, formValues.Get("X-Amz-Signature")) { + return s3err.ErrSignatureDoesNotMatch } return s3err.ErrNone } -// Parses all the presigned signature values into separate elements. -func parsePreSignV4(query url.Values) (psv preSignValues, aec s3err.ErrorCode) { - var err s3err.ErrorCode - // verify whether the required query params exist. - err = doesV4PresignParamsExist(query) - if err != s3err.ErrNone { - return psv, err - } - - // Verify if the query algorithm is supported or not. - if query.Get("X-Amz-Algorithm") != signV4Algorithm { - return psv, s3err.ErrInvalidQuerySignatureAlgo - } - - // Initialize signature version '4' structured header. - preSignV4Values := preSignValues{} - - // Save credential. - preSignV4Values.Credential, err = parseCredentialHeader("Credential=" + query.Get("X-Amz-Credential")) - if err != s3err.ErrNone { - return psv, err - } - - var e error - // Save date in native time.Time. - preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get("X-Amz-Date")) - if e != nil { - return psv, s3err.ErrMalformedPresignedDate - } - - // Save expires in native time.Duration. - preSignV4Values.Expires, e = time.ParseDuration(query.Get("X-Amz-Expires") + "s") - if e != nil { - return psv, s3err.ErrMalformedExpires - } - - if preSignV4Values.Expires < 0 { - return psv, s3err.ErrNegativeExpires - } - - // Check if Expiry time is less than 7 days (value in seconds). - if preSignV4Values.Expires.Seconds() > 604800 { - return psv, s3err.ErrMaximumExpires - } - - // Save signed headers. - preSignV4Values.SignedHeaders, err = parseSignedHeader("SignedHeaders=" + query.Get("X-Amz-SignedHeaders")) - if err != s3err.ErrNone { - return psv, err - } - - // Save signature. - preSignV4Values.Signature, err = parseSignature("Signature=" + query.Get("X-Amz-Signature")) - if err != s3err.ErrNone { - return psv, err - } - - // Return structured form of signature query string. - return preSignV4Values, s3err.ErrNone -} - -// extractSignedHeaders extract signed headers from Authorization header +// Verify if extracted signed headers are not properly signed. func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, s3err.ErrorCode) { reqHeaders := r.Header - // find whether "host" is part of list of signed headers. - // if not return ErrUnsignedHeaders. "host" is mandatory. - if !contains(signedHeaders, "host") { - return nil, s3err.ErrUnsignedHeaders + // 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` will not be found in the headers, can be found in r.Host. - // but its alway necessary that the list of signed headers containing host in it. - val, ok := reqHeaders[http.CanonicalHeaderKey(header)] - if ok { - for _, enc := range val { - extractedSignedHeaders.Add(header, enc) - } + // `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 } - switch header { - case "expect": - // Set the default value of the Expect header for compatibility. - // - // In NGINX v1.1, the Expect header is removed when handling 100-continue requests. - // - // `aws-cli` sets this as part of signed headers. - // - // According to - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 - // Expect header is always of form: - // - // Expect = "Expect" ":" 1#expectation - // expectation = "100-continue" | expectation-extension - // - // So it safe to assume that '100-continue' is what would - // be sent, for the time being keep this work around. - extractedSignedHeaders.Set(header, "100-continue") - case "host": - extractedHost := extractHostHeader(r) - extractedSignedHeaders.Set(header, extractedHost) - case "transfer-encoding": - for _, enc := range r.TransferEncoding { - extractedSignedHeaders.Add(header, enc) - } - case "content-length": - // Signature-V4 spec excludes Content-Length from signed headers list for signature calculation. - // But some clients deviate from this rule. Hence we consider Content-Length for signature - // calculation to be compatible with such clients. - extractedSignedHeaders.Set(header, strconv.FormatInt(r.ContentLength, 10)) - default: - return nil, s3err.ErrUnsignedHeaders + // 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 { - - forwardedHost := r.Header.Get("X-Forwarded-Host") - forwardedPort := r.Header.Get("X-Forwarded-Port") - forwardedProto := r.Header.Get("X-Forwarded-Proto") - - // If X-Forwarded-Host is set, use that as the host. - // If X-Forwarded-Port is set, use that too to form the host. - // If X-Forwarded-Proto is set, check if is it default to omit the port. - if forwardedHost != "" { - extractedHost := forwardedHost - host, port, err := net.SplitHostPort(extractedHost) - if err == nil { - extractedHost = host - if forwardedPort == "" { - forwardedPort = port - } - } - scheme := r.URL.Scheme - if forwardedProto != "" { - scheme = forwardedProto - } - if !isDefaultPort(scheme, forwardedPort) { - extractedHost = net.JoinHostPort(extractedHost, forwardedPort) - } - return extractedHost - } else { - // Go http server removes "host" from Request.Header - host := r.Host - if host == "" { - host = r.URL.Host - } - h, port, err := net.SplitHostPort(host) - if err != nil { - return host - } - if isDefaultPort(r.URL.Scheme, port) { - return h - } - return host + hostHeaderValue := r.Host + // For standard requests, this should be fine. + if r.Host != "" { + return hostHeaderValue } -} - -func isDefaultPort(scheme, port string) bool { - if port == "" { - return true - } - - switch port { - case "80": - return strings.EqualFold(scheme, "http") - case "443": - return strings.EqualFold(scheme, "https") - default: - return false - } -} - -// 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)) + // If no host header is found, then check for host URL value. + if r.URL.Host != "" { + hostHeaderValue = r.URL.Host } - sort.Strings(headers) - return strings.Join(headers, ";") + return hostHeaderValue } // getScope generate a string of a specific date, an AWS region, and a service. @@ -815,8 +488,6 @@ func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, getSignedHeaders(extractedSignedHeaders), payload, }, "\n") - - glog.V(4).Infof("Canonical Request:\n%s", canonicalRequest) return canonicalRequest } @@ -824,11 +495,16 @@ func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, func getStringToSign(canonicalRequest string, t time.Time, scope string) string { stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n" stringToSign = stringToSign + scope + "\n" - canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest)) - stringToSign = stringToSign + hex.EncodeToString(canonicalRequestBytes[:]) + 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) @@ -850,9 +526,11 @@ func getCanonicalHeaders(signedHeaders http.Header) string { var headers []string vals := make(http.Header) for k, vv := range signedHeaders { - headers = append(headers, strings.ToLower(k)) vals[strings.ToLower(k)] = vv } + for k := range vals { + headers = append(headers, k) + } sort.Strings(headers) var buf bytes.Buffer @@ -870,18 +548,28 @@ func getCanonicalHeaders(signedHeaders http.Header) string { return buf.String() } -// Trim leading and trailing spaces and replace sequential spaces with one space, following Trimall() -// in http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html +// 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 one space and return + // 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 encode the strings from UTF-8 byte representations to HTML hex escape sequences +// 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 @@ -896,34 +584,38 @@ func encodePath(pathName string) 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) - continue - } - switch s { - case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) - encodedPathname = encodedPathname + string(s) - continue - default: - len := utf8.RuneLen(s) - if len < 0 { - // if utf8 cannot convert return the same string as is - return pathName - } - u := make([]byte, len) - utf8.EncodeRune(u, s) - for _, r := range u { - hex := hex.EncodeToString([]byte{r}) - encodedPathname = encodedPathname + "%" + strings.ToUpper(hex) + } 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 +// 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 - // is unique for a sequence of bytes. See also compareSignatureV2. + // 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 } |
