storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/signature-v2.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 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 18 19 import ( 20 "context" 21 "crypto/hmac" 22 "crypto/sha1" 23 "crypto/subtle" 24 "encoding/base64" 25 "fmt" 26 "net/http" 27 "net/url" 28 "sort" 29 "strconv" 30 "strings" 31 32 xhttp "storj.io/minio/cmd/http" 33 "storj.io/minio/pkg/auth" 34 ) 35 36 // Whitelist resource list that will be used in query string for signature-V2 calculation. 37 // 38 // This list should be kept alphabetically sorted, do not hastily edit. 39 var resourceList = []string{ 40 "acl", 41 "cors", 42 "delete", 43 "encryption", 44 "legal-hold", 45 "lifecycle", 46 "location", 47 "logging", 48 "notification", 49 "partNumber", 50 "policy", 51 "requestPayment", 52 "response-cache-control", 53 "response-content-disposition", 54 "response-content-encoding", 55 "response-content-language", 56 "response-content-type", 57 "response-expires", 58 "retention", 59 "select", 60 "select-type", 61 "tagging", 62 "torrent", 63 "uploadId", 64 "uploads", 65 "versionId", 66 "versioning", 67 "versions", 68 "website", 69 } 70 71 // Signature and API related constants. 72 const ( 73 signV2Algorithm = "AWS" 74 ) 75 76 // AWS S3 Signature V2 calculation rule is give here: 77 // http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign 78 func doesPolicySignatureV2Match(ctx context.Context, formValues http.Header) (auth.Credentials, APIErrorCode) { 79 accessKey := formValues.Get(xhttp.AmzAccessKeyID) 80 cred, _, s3Err := checkKeyValid(ctx, accessKey) 81 if s3Err != ErrNone { 82 return cred, s3Err 83 } 84 policy := formValues.Get("Policy") 85 signature := formValues.Get(xhttp.AmzSignatureV2) 86 if !compareSignatureV2(signature, calculateSignatureV2(policy, cred.SecretKey)) { 87 return cred, ErrSignatureDoesNotMatch 88 } 89 return cred, ErrNone 90 } 91 92 // Escape encodedQuery string into unescaped list of query params, returns error 93 // if any while unescaping the values. 94 func unescapeQueries(encodedQuery string) (unescapedQueries []string, err error) { 95 for _, query := range strings.Split(encodedQuery, "&") { 96 var unescapedQuery string 97 unescapedQuery, err = url.QueryUnescape(query) 98 if err != nil { 99 return nil, err 100 } 101 unescapedQueries = append(unescapedQueries, unescapedQuery) 102 } 103 return unescapedQueries, nil 104 } 105 106 // doesPresignV2SignatureMatch - Verify query headers with presigned signature 107 // - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth 108 // returns ErrNone if matches. S3 errors otherwise. 109 func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode { 110 // r.RequestURI will have raw encoded URI as sent by the client. 111 tokens := strings.SplitN(r.RequestURI, "?", 2) 112 encodedResource := tokens[0] 113 encodedQuery := "" 114 if len(tokens) == 2 { 115 encodedQuery = tokens[1] 116 } 117 118 var ( 119 filteredQueries []string 120 gotSignature string 121 expires string 122 accessKey string 123 err error 124 ) 125 126 var unescapedQueries []string 127 unescapedQueries, err = unescapeQueries(encodedQuery) 128 if err != nil { 129 return ErrInvalidQueryParams 130 } 131 132 // Extract the necessary values from presigned query, construct a list of new filtered queries. 133 for _, query := range unescapedQueries { 134 keyval := strings.SplitN(query, "=", 2) 135 if len(keyval) != 2 { 136 return ErrInvalidQueryParams 137 } 138 switch keyval[0] { 139 case xhttp.AmzAccessKeyID: 140 accessKey = keyval[1] 141 case xhttp.AmzSignatureV2: 142 gotSignature = keyval[1] 143 case xhttp.Expires: 144 expires = keyval[1] 145 default: 146 filteredQueries = append(filteredQueries, query) 147 } 148 } 149 150 // Invalid values returns error. 151 if accessKey == "" || gotSignature == "" || expires == "" { 152 return ErrInvalidQueryParams 153 } 154 155 cred, _, s3Err := checkKeyValid(r.Context(), accessKey) 156 if s3Err != ErrNone { 157 return s3Err 158 } 159 160 // Make sure the request has not expired. 161 expiresInt, err := strconv.ParseInt(expires, 10, 64) 162 if err != nil { 163 return ErrMalformedExpires 164 } 165 166 // Check if the presigned URL has expired. 167 if expiresInt < UTCNow().Unix() { 168 return ErrExpiredPresignRequest 169 } 170 171 encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames) 172 if err != nil { 173 return ErrInvalidRequest 174 } 175 176 expectedSignature := preSignatureV2(cred, r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires) 177 if !compareSignatureV2(gotSignature, expectedSignature) { 178 return ErrSignatureDoesNotMatch 179 } 180 181 return ErrNone 182 } 183 184 func getReqAccessKeyV2(r *http.Request) (auth.Credentials, bool, APIErrorCode) { 185 if accessKey := r.URL.Query().Get(xhttp.AmzAccessKeyID); accessKey != "" { 186 return checkKeyValid(r.Context(), accessKey) 187 } 188 189 // below is V2 Signed Auth header format, splitting on `space` (after the `AWS` string). 190 // Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature 191 authFields := strings.Split(r.Header.Get(xhttp.Authorization), " ") 192 if len(authFields) != 2 { 193 return auth.Credentials{}, false, ErrAuthHeaderEmpty 194 } 195 196 // Then will be splitting on ":", this will seprate `AWSAccessKeyId` and `Signature` string. 197 keySignFields := strings.Split(strings.TrimSpace(authFields[1]), ":") 198 if len(keySignFields) != 2 { 199 return auth.Credentials{}, false, ErrMissingFieldsV2 200 } 201 202 return checkKeyValid(r.Context(), keySignFields[0]) 203 } 204 205 // Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature; 206 // Signature = Base64( HMAC-SHA1( YourSecretKey, UTF-8-Encoding-Of( StringToSign ) ) ); 207 // 208 // StringToSign = HTTP-Verb + "\n" + 209 // Content-Md5 + "\n" + 210 // Content-Type + "\n" + 211 // Date + "\n" + 212 // CanonicalizedProtocolHeaders + 213 // CanonicalizedResource; 214 // 215 // CanonicalizedResource = [ SlashSeparator + Bucket ] + 216 // <HTTP-Request-URI, from the protocol name up to the query string> + 217 // [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 218 // 219 // CanonicalizedProtocolHeaders = <described below> 220 221 // doesSignV2Match - Verify authorization header with calculated header in accordance with 222 // - http://docs.aws.amazon.com/AmazonS3/latest/dev/auth-request-sig-v2.html 223 // returns true if matches, false otherwise. if error is not nil then it is always false 224 225 func validateV2AuthHeader(r *http.Request) (auth.Credentials, APIErrorCode) { 226 var cred auth.Credentials 227 v2Auth := r.Header.Get(xhttp.Authorization) 228 if v2Auth == "" { 229 return cred, ErrAuthHeaderEmpty 230 } 231 232 // Verify if the header algorithm is supported or not. 233 if !strings.HasPrefix(v2Auth, signV2Algorithm) { 234 return cred, ErrSignatureVersionNotSupported 235 } 236 237 cred, _, apiErr := getReqAccessKeyV2(r) 238 if apiErr != ErrNone { 239 return cred, apiErr 240 } 241 242 return cred, ErrNone 243 } 244 245 func doesSignV2Match(r *http.Request) APIErrorCode { 246 v2Auth := r.Header.Get(xhttp.Authorization) 247 cred, apiError := validateV2AuthHeader(r) 248 if apiError != ErrNone { 249 return apiError 250 } 251 252 // r.RequestURI will have raw encoded URI as sent by the client. 253 tokens := strings.SplitN(r.RequestURI, "?", 2) 254 encodedResource := tokens[0] 255 encodedQuery := "" 256 if len(tokens) == 2 { 257 encodedQuery = tokens[1] 258 } 259 260 unescapedQueries, err := unescapeQueries(encodedQuery) 261 if err != nil { 262 return ErrInvalidQueryParams 263 } 264 265 encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames) 266 if err != nil { 267 return ErrInvalidRequest 268 } 269 270 prefix := fmt.Sprintf("%s %s:", signV2Algorithm, cred.AccessKey) 271 if !strings.HasPrefix(v2Auth, prefix) { 272 return ErrSignatureDoesNotMatch 273 } 274 v2Auth = v2Auth[len(prefix):] 275 expectedAuth := signatureV2(cred, r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header) 276 if !compareSignatureV2(v2Auth, expectedAuth) { 277 return ErrSignatureDoesNotMatch 278 } 279 return ErrNone 280 } 281 282 func calculateSignatureV2(stringToSign string, secret string) string { 283 hm := hmac.New(sha1.New, []byte(secret)) 284 hm.Write([]byte(stringToSign)) 285 return base64.StdEncoding.EncodeToString(hm.Sum(nil)) 286 } 287 288 // Return signature-v2 for the presigned request. 289 func preSignatureV2(cred auth.Credentials, method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string { 290 stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, expires) 291 return calculateSignatureV2(stringToSign, cred.SecretKey) 292 } 293 294 // Return the signature v2 of a given request. 295 func signatureV2(cred auth.Credentials, method string, encodedResource string, encodedQuery string, headers http.Header) string { 296 stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, "") 297 signature := calculateSignatureV2(stringToSign, cred.SecretKey) 298 return signature 299 } 300 301 // compareSignatureV2 returns true if and only if both signatures 302 // are equal. The signatures are expected to be base64 encoded strings 303 // according to the AWS S3 signature V2 spec. 304 func compareSignatureV2(sig1, sig2 string) bool { 305 // Decode signature string to binary byte-sequence representation is required 306 // as Base64 encoding of a value is not unique: 307 // For example "aGVsbG8=" and "aGVsbG8=\r" will result in the same byte slice. 308 signature1, err := base64.StdEncoding.DecodeString(sig1) 309 if err != nil { 310 return false 311 } 312 signature2, err := base64.StdEncoding.DecodeString(sig2) 313 if err != nil { 314 return false 315 } 316 return subtle.ConstantTimeCompare(signature1, signature2) == 1 317 } 318 319 // Return canonical headers. 320 func canonicalizedAmzHeadersV2(headers http.Header) string { 321 var keys []string 322 keyval := make(map[string]string, len(headers)) 323 for key := range headers { 324 lkey := strings.ToLower(key) 325 if !strings.HasPrefix(lkey, "x-amz-") { 326 continue 327 } 328 keys = append(keys, lkey) 329 keyval[lkey] = strings.Join(headers[key], ",") 330 } 331 sort.Strings(keys) 332 var canonicalHeaders []string 333 for _, key := range keys { 334 canonicalHeaders = append(canonicalHeaders, key+":"+keyval[key]) 335 } 336 return strings.Join(canonicalHeaders, "\n") 337 } 338 339 // Return canonical resource string. 340 func canonicalizedResourceV2(encodedResource, encodedQuery string) string { 341 queries := strings.Split(encodedQuery, "&") 342 keyval := make(map[string]string) 343 for _, query := range queries { 344 key := query 345 val := "" 346 index := strings.Index(query, "=") 347 if index != -1 { 348 key = query[:index] 349 val = query[index+1:] 350 } 351 keyval[key] = val 352 } 353 354 var canonicalQueries []string 355 for _, key := range resourceList { 356 val, ok := keyval[key] 357 if !ok { 358 continue 359 } 360 if val == "" { 361 canonicalQueries = append(canonicalQueries, key) 362 continue 363 } 364 canonicalQueries = append(canonicalQueries, key+"="+val) 365 } 366 367 // The queries will be already sorted as resourceList is sorted, if canonicalQueries 368 // is empty strings.Join returns empty. 369 canonicalQuery := strings.Join(canonicalQueries, "&") 370 if canonicalQuery != "" { 371 return encodedResource + "?" + canonicalQuery 372 } 373 return encodedResource 374 } 375 376 // Return string to sign under two different conditions. 377 // - if expires string is set then string to sign includes date instead of the Date header. 378 // - if expires string is empty then string to sign includes date header instead. 379 func getStringToSignV2(method string, encodedResource, encodedQuery string, headers http.Header, expires string) string { 380 canonicalHeaders := canonicalizedAmzHeadersV2(headers) 381 if len(canonicalHeaders) > 0 { 382 canonicalHeaders += "\n" 383 } 384 385 date := expires // Date is set to expires date for presign operations. 386 if date == "" { 387 // If expires date is empty then request header Date is used. 388 date = headers.Get(xhttp.Date) 389 } 390 391 // From the Amazon docs: 392 // 393 // StringToSign = HTTP-Verb + "\n" + 394 // Content-Md5 + "\n" + 395 // Content-Type + "\n" + 396 // Date/Expires + "\n" + 397 // CanonicalizedProtocolHeaders + 398 // CanonicalizedResource; 399 stringToSign := strings.Join([]string{ 400 method, 401 headers.Get(xhttp.ContentMD5), 402 headers.Get(xhttp.ContentType), 403 date, 404 canonicalHeaders, 405 }, "\n") 406 407 return stringToSign + canonicalizedResourceV2(encodedResource, encodedQuery) 408 }