storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/signature-v4.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2015, 2016, 2017 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 // Package cmd This file implements helper functions to validate AWS 18 // Signature Version '4' authorization header. 19 // 20 // This package provides comprehensive helpers for following signature 21 // types. 22 // - Based on Authorization header. 23 // - Based on Query parameters. 24 // - Based on Form POST policy. 25 package cmd 26 27 import ( 28 "bytes" 29 "context" 30 "crypto/sha256" 31 "crypto/subtle" 32 "encoding/hex" 33 "net/http" 34 "net/url" 35 "sort" 36 "strconv" 37 "strings" 38 "time" 39 40 "github.com/minio/minio-go/v7/pkg/s3utils" 41 "github.com/minio/minio-go/v7/pkg/set" 42 43 xhttp "storj.io/minio/cmd/http" 44 "storj.io/minio/cmd/logger" 45 "storj.io/minio/pkg/auth" 46 ) 47 48 // AWS Signature Version '4' constants. 49 const ( 50 signV4Algorithm = "AWS4-HMAC-SHA256" 51 iso8601Format = "20060102T150405Z" 52 yyyymmdd = "20060102" 53 ) 54 55 type serviceType string 56 57 const ( 58 serviceS3 serviceType = "s3" 59 serviceSTS serviceType = "sts" 60 ) 61 62 // getCanonicalHeaders generate a list of request headers with their values 63 func getCanonicalHeaders(signedHeaders http.Header) string { 64 var headers []string 65 vals := make(http.Header) 66 for k, vv := range signedHeaders { 67 headers = append(headers, strings.ToLower(k)) 68 vals[strings.ToLower(k)] = vv 69 } 70 sort.Strings(headers) 71 72 var buf bytes.Buffer 73 for _, k := range headers { 74 buf.WriteString(k) 75 buf.WriteByte(':') 76 for idx, v := range vals[k] { 77 if idx > 0 { 78 buf.WriteByte(',') 79 } 80 buf.WriteString(signV4TrimAll(v)) 81 } 82 buf.WriteByte('\n') 83 } 84 return buf.String() 85 } 86 87 // getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names 88 func getSignedHeaders(signedHeaders http.Header) string { 89 var headers []string 90 for k := range signedHeaders { 91 headers = append(headers, strings.ToLower(k)) 92 } 93 sort.Strings(headers) 94 return strings.Join(headers, ";") 95 } 96 97 // getCanonicalRequest generate a canonical request of style 98 // 99 // canonicalRequest = 100 // <HTTPMethod>\n 101 // <CanonicalURI>\n 102 // <CanonicalQueryString>\n 103 // <CanonicalHeaders>\n 104 // <SignedHeaders>\n 105 // <HashedPayload> 106 // 107 func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, urlPath, method string) string { 108 rawQuery := strings.Replace(queryStr, "+", "%20", -1) 109 encodedPath := s3utils.EncodePath(urlPath) 110 canonicalRequest := strings.Join([]string{ 111 method, 112 encodedPath, 113 rawQuery, 114 getCanonicalHeaders(extractedSignedHeaders), 115 getSignedHeaders(extractedSignedHeaders), 116 payload, 117 }, "\n") 118 return canonicalRequest 119 } 120 121 // getScope generate a string of a specific date, an AWS region, and a service. 122 func getScope(t time.Time, region string) string { 123 scope := strings.Join([]string{ 124 t.Format(yyyymmdd), 125 region, 126 string(serviceS3), 127 "aws4_request", 128 }, SlashSeparator) 129 return scope 130 } 131 132 // getStringToSign a string based on selected query values. 133 func getStringToSign(canonicalRequest string, t time.Time, scope string) string { 134 stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n" 135 stringToSign = stringToSign + scope + "\n" 136 canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest)) 137 stringToSign = stringToSign + hex.EncodeToString(canonicalRequestBytes[:]) 138 return stringToSign 139 } 140 141 // getSigningKey hmac seed to calculate final signature. 142 func getSigningKey(secretKey string, t time.Time, region string, stype serviceType) []byte { 143 date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd))) 144 regionBytes := sumHMAC(date, []byte(region)) 145 service := sumHMAC(regionBytes, []byte(stype)) 146 signingKey := sumHMAC(service, []byte("aws4_request")) 147 return signingKey 148 } 149 150 // getSignature final signature in hexadecimal form. 151 func getSignature(signingKey []byte, stringToSign string) string { 152 return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) 153 } 154 155 // Check to see if Policy is signed correctly. 156 func doesPolicySignatureMatch(ctx context.Context, formValues http.Header) (auth.Credentials, APIErrorCode) { 157 // For SignV2 - Signature field will be valid 158 if _, ok := formValues["Signature"]; ok { 159 return doesPolicySignatureV2Match(ctx, formValues) 160 } 161 return doesPolicySignatureV4Match(ctx, formValues) 162 } 163 164 // compareSignatureV4 returns true if and only if both signatures 165 // are equal. The signatures are expected to be HEX encoded strings 166 // according to the AWS S3 signature V4 spec. 167 func compareSignatureV4(sig1, sig2 string) bool { 168 // The CTC using []byte(str) works because the hex encoding 169 // is unique for a sequence of bytes. See also compareSignatureV2. 170 return subtle.ConstantTimeCompare([]byte(sig1), []byte(sig2)) == 1 171 } 172 173 // doesPolicySignatureMatch - Verify query headers with post policy 174 // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html 175 // returns ErrNone if the signature matches. 176 func doesPolicySignatureV4Match(ctx context.Context, formValues http.Header) (auth.Credentials, APIErrorCode) { 177 // Server region. 178 region := globalServerRegion 179 180 // Parse credential tag. 181 credHeader, s3Err := parseCredentialHeader("Credential="+formValues.Get(xhttp.AmzCredential), region, serviceS3) 182 if s3Err != ErrNone { 183 return auth.Credentials{}, s3Err 184 } 185 186 cred, _, s3Err := checkKeyValid(ctx, credHeader.accessKey) 187 if s3Err != ErrNone { 188 return cred, s3Err 189 } 190 191 // Get signing key. 192 signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date, credHeader.scope.region, serviceS3) 193 194 // Get signature. 195 newSignature := getSignature(signingKey, formValues.Get("Policy")) 196 197 // Verify signature. 198 if !compareSignatureV4(newSignature, formValues.Get(xhttp.AmzSignature)) { 199 return cred, ErrSignatureDoesNotMatch 200 } 201 if cred.AccessKey != "" { 202 logger.GetReqInfo(ctx).AccessKey = cred.AccessKey 203 } 204 if cred.AccessGrant != "" { 205 logger.GetReqInfo(ctx).AccessGrant = cred.AccessGrant 206 } 207 208 // Success. 209 return cred, ErrNone 210 } 211 212 // doesPresignedSignatureMatch - Verify query headers with presigned signature 213 // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html 214 // returns ErrNone if the signature matches. 215 func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string, stype serviceType) APIErrorCode { 216 // Copy request 217 req := *r 218 219 // Parse request query string. 220 pSignValues, err := parsePreSignV4(req.URL.Query(), region, stype) 221 if err != ErrNone { 222 return err 223 } 224 225 cred, _, s3Err := checkKeyValid(req.Context(), pSignValues.Credential.accessKey) 226 if s3Err != ErrNone { 227 return s3Err 228 } 229 230 // Extract all the signed headers along with its values. 231 extractedSignedHeaders, errCode := extractSignedHeaders(pSignValues.SignedHeaders, r) 232 if errCode != ErrNone { 233 return errCode 234 } 235 236 // If the host which signed the request is slightly ahead in time (by less than globalMaxSkewTime) the 237 // request should still be allowed. 238 if pSignValues.Date.After(UTCNow().Add(globalMaxSkewTime)) { 239 return ErrRequestNotReadyYet 240 } 241 242 if UTCNow().Sub(pSignValues.Date) > pSignValues.Expires { 243 return ErrExpiredPresignRequest 244 } 245 246 // Save the date and expires. 247 t := pSignValues.Date 248 expireSeconds := int(pSignValues.Expires / time.Second) 249 250 // Construct new query. 251 query := make(url.Values) 252 clntHashedPayload := req.URL.Query().Get(xhttp.AmzContentSha256) 253 if clntHashedPayload != "" { 254 query.Set(xhttp.AmzContentSha256, hashedPayload) 255 } 256 257 token := req.URL.Query().Get(xhttp.AmzSecurityToken) 258 if token != "" { 259 query.Set(xhttp.AmzSecurityToken, cred.SessionToken) 260 } 261 262 query.Set(xhttp.AmzAlgorithm, signV4Algorithm) 263 264 // Construct the query. 265 query.Set(xhttp.AmzDate, t.Format(iso8601Format)) 266 query.Set(xhttp.AmzExpires, strconv.Itoa(expireSeconds)) 267 query.Set(xhttp.AmzSignedHeaders, getSignedHeaders(extractedSignedHeaders)) 268 query.Set(xhttp.AmzCredential, cred.AccessKey+SlashSeparator+pSignValues.Credential.getScope()) 269 270 defaultSigParams := set.CreateStringSet( 271 xhttp.AmzContentSha256, 272 xhttp.AmzSecurityToken, 273 xhttp.AmzAlgorithm, 274 xhttp.AmzDate, 275 xhttp.AmzExpires, 276 xhttp.AmzSignedHeaders, 277 xhttp.AmzCredential, 278 xhttp.AmzSignature, 279 ) 280 281 // Add missing query parameters if any provided in the request URL 282 for k, v := range req.URL.Query() { 283 if !defaultSigParams.Contains(k) { 284 query[k] = v 285 } 286 } 287 288 // Get the encoded query. 289 encodedQuery := query.Encode() 290 291 // Verify if date query is same. 292 if req.URL.Query().Get(xhttp.AmzDate) != query.Get(xhttp.AmzDate) { 293 return ErrSignatureDoesNotMatch 294 } 295 // Verify if expires query is same. 296 if req.URL.Query().Get(xhttp.AmzExpires) != query.Get(xhttp.AmzExpires) { 297 return ErrSignatureDoesNotMatch 298 } 299 // Verify if signed headers query is same. 300 if req.URL.Query().Get(xhttp.AmzSignedHeaders) != query.Get(xhttp.AmzSignedHeaders) { 301 return ErrSignatureDoesNotMatch 302 } 303 // Verify if credential query is same. 304 if req.URL.Query().Get(xhttp.AmzCredential) != query.Get(xhttp.AmzCredential) { 305 return ErrSignatureDoesNotMatch 306 } 307 // Verify if sha256 payload query is same. 308 if clntHashedPayload != "" && clntHashedPayload != query.Get(xhttp.AmzContentSha256) { 309 return ErrContentSHA256Mismatch 310 } 311 // Verify if security token is correct. 312 if token != "" && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 { 313 return ErrInvalidToken 314 } 315 316 /// Verify finally if signature is same. 317 318 // Get canonical request. 319 presignedCanonicalReq := getCanonicalRequest(extractedSignedHeaders, hashedPayload, encodedQuery, req.URL.Path, req.Method) 320 321 // Get string to sign from canonical request. 322 presignedStringToSign := getStringToSign(presignedCanonicalReq, t, pSignValues.Credential.getScope()) 323 324 // Get hmac presigned signing key. 325 presignedSigningKey := getSigningKey(cred.SecretKey, pSignValues.Credential.scope.date, 326 pSignValues.Credential.scope.region, stype) 327 328 // Get new signature. 329 newSignature := getSignature(presignedSigningKey, presignedStringToSign) 330 331 // Verify signature. 332 if !compareSignatureV4(req.URL.Query().Get(xhttp.AmzSignature), newSignature) { 333 return ErrSignatureDoesNotMatch 334 } 335 return ErrNone 336 } 337 338 // doesSignatureMatch - Verify authorization header with calculated header in accordance with 339 // - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html 340 // returns ErrNone if signature matches. 341 func doesSignatureMatch(hashedPayload string, r *http.Request, region string, stype serviceType) APIErrorCode { 342 // Copy request. 343 req := *r 344 345 // Save authorization header. 346 v4Auth := req.Header.Get(xhttp.Authorization) 347 348 // Parse signature version '4' header. 349 signV4Values, err := parseSignV4(v4Auth, region, stype) 350 if err != ErrNone { 351 return err 352 } 353 354 // Extract all the signed headers along with its values. 355 extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r) 356 if errCode != ErrNone { 357 return errCode 358 } 359 360 cred, _, s3Err := checkKeyValid(req.Context(), signV4Values.Credential.accessKey) 361 if s3Err != ErrNone { 362 return s3Err 363 } 364 365 // Extract date, if not present throw error. 366 var date string 367 if date = req.Header.Get(xhttp.AmzDate); date == "" { 368 if date = r.Header.Get(xhttp.Date); date == "" { 369 return ErrMissingDateHeader 370 } 371 } 372 373 // Parse date header. 374 t, e := time.Parse(iso8601Format, date) 375 if e != nil { 376 return ErrMissingDateHeader 377 } 378 379 // Query string. 380 queryStr := req.URL.Query().Encode() 381 382 // Get canonical request. 383 canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method) 384 385 // Get string to sign from canonical request. 386 stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) 387 388 // Get hmac signing key. 389 signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, 390 signV4Values.Credential.scope.region, stype) 391 392 // Calculate signature. 393 newSignature := getSignature(signingKey, stringToSign) 394 395 // Verify if signature match. 396 if !compareSignatureV4(newSignature, signV4Values.Signature) { 397 return ErrSignatureDoesNotMatch 398 } 399 400 // Return error none. 401 return ErrNone 402 }