request-signature-v4.go (11021B)
1 /* 2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage 3 * Copyright 2015-2017 MinIO, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package signer 19 20 import ( 21 "bytes" 22 "encoding/hex" 23 "net/http" 24 "sort" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/minio/minio-go/v7/pkg/s3utils" 30 ) 31 32 // Signature and API related constants. 33 const ( 34 signV4Algorithm = "AWS4-HMAC-SHA256" 35 iso8601DateFormat = "20060102T150405Z" 36 yyyymmdd = "20060102" 37 ) 38 39 // Different service types 40 const ( 41 ServiceTypeS3 = "s3" 42 ServiceTypeSTS = "sts" 43 ) 44 45 // Excerpts from @lsegal - 46 // https:/github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258. 47 // 48 // * User-Agent 49 // This is ignored from signing because signing this causes problems with generating pre-signed 50 // URLs (that are executed by other agents) or when customers pass requests through proxies, which 51 // may modify the user-agent. 52 // 53 // * Authorization 54 // Is skipped for obvious reasons. 55 // 56 // * Accept-Encoding 57 // Some S3 servers like Hitachi Content Platform do not honor this header for signature 58 // calculation. 59 var v4IgnoredHeaders = map[string]bool{ 60 "Accept-Encoding": true, 61 "Authorization": true, 62 "User-Agent": true, 63 } 64 65 // getSigningKey hmac seed to calculate final signature. 66 func getSigningKey(secret, loc string, t time.Time, serviceType string) []byte { 67 date := sumHMAC([]byte("AWS4"+secret), []byte(t.Format(yyyymmdd))) 68 location := sumHMAC(date, []byte(loc)) 69 service := sumHMAC(location, []byte(serviceType)) 70 signingKey := sumHMAC(service, []byte("aws4_request")) 71 return signingKey 72 } 73 74 // getSignature final signature in hexadecimal form. 75 func getSignature(signingKey []byte, stringToSign string) string { 76 return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) 77 } 78 79 // getScope generate a string of a specific date, an AWS region, and a 80 // service. 81 func getScope(location string, t time.Time, serviceType string) string { 82 scope := strings.Join([]string{ 83 t.Format(yyyymmdd), 84 location, 85 serviceType, 86 "aws4_request", 87 }, "/") 88 return scope 89 } 90 91 // GetCredential generate a credential string. 92 func GetCredential(accessKeyID, location string, t time.Time, serviceType string) string { 93 scope := getScope(location, t, serviceType) 94 return accessKeyID + "/" + scope 95 } 96 97 // getHashedPayload get the hexadecimal value of the SHA256 hash of 98 // the request payload. 99 func getHashedPayload(req http.Request) string { 100 hashedPayload := req.Header.Get("X-Amz-Content-Sha256") 101 if hashedPayload == "" { 102 // Presign does not have a payload, use S3 recommended value. 103 hashedPayload = unsignedPayload 104 } 105 return hashedPayload 106 } 107 108 // getCanonicalHeaders generate a list of request headers for 109 // signature. 110 func getCanonicalHeaders(req http.Request, ignoredHeaders map[string]bool) string { 111 var headers []string 112 vals := make(map[string][]string) 113 for k, vv := range req.Header { 114 if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok { 115 continue // ignored header 116 } 117 headers = append(headers, strings.ToLower(k)) 118 vals[strings.ToLower(k)] = vv 119 } 120 if !headerExists("host", headers) { 121 headers = append(headers, "host") 122 } 123 sort.Strings(headers) 124 125 var buf bytes.Buffer 126 // Save all the headers in canonical form <header>:<value> newline 127 // separated for each header. 128 for _, k := range headers { 129 buf.WriteString(k) 130 buf.WriteByte(':') 131 switch { 132 case k == "host": 133 buf.WriteString(getHostAddr(&req)) 134 buf.WriteByte('\n') 135 default: 136 for idx, v := range vals[k] { 137 if idx > 0 { 138 buf.WriteByte(',') 139 } 140 buf.WriteString(signV4TrimAll(v)) 141 } 142 buf.WriteByte('\n') 143 } 144 } 145 return buf.String() 146 } 147 148 func headerExists(key string, headers []string) bool { 149 for _, k := range headers { 150 if k == key { 151 return true 152 } 153 } 154 return false 155 } 156 157 // getSignedHeaders generate all signed request headers. 158 // i.e lexically sorted, semicolon-separated list of lowercase 159 // request header names. 160 func getSignedHeaders(req http.Request, ignoredHeaders map[string]bool) string { 161 var headers []string 162 for k := range req.Header { 163 if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok { 164 continue // Ignored header found continue. 165 } 166 headers = append(headers, strings.ToLower(k)) 167 } 168 if !headerExists("host", headers) { 169 headers = append(headers, "host") 170 } 171 sort.Strings(headers) 172 return strings.Join(headers, ";") 173 } 174 175 // getCanonicalRequest generate a canonical request of style. 176 // 177 // canonicalRequest = 178 // 179 // <HTTPMethod>\n 180 // <CanonicalURI>\n 181 // <CanonicalQueryString>\n 182 // <CanonicalHeaders>\n 183 // <SignedHeaders>\n 184 // <HashedPayload> 185 func getCanonicalRequest(req http.Request, ignoredHeaders map[string]bool, hashedPayload string) string { 186 req.URL.RawQuery = strings.ReplaceAll(req.URL.Query().Encode(), "+", "%20") 187 canonicalRequest := strings.Join([]string{ 188 req.Method, 189 s3utils.EncodePath(req.URL.Path), 190 req.URL.RawQuery, 191 getCanonicalHeaders(req, ignoredHeaders), 192 getSignedHeaders(req, ignoredHeaders), 193 hashedPayload, 194 }, "\n") 195 return canonicalRequest 196 } 197 198 // getStringToSign a string based on selected query values. 199 func getStringToSignV4(t time.Time, location, canonicalRequest, serviceType string) string { 200 stringToSign := signV4Algorithm + "\n" + t.Format(iso8601DateFormat) + "\n" 201 stringToSign = stringToSign + getScope(location, t, serviceType) + "\n" 202 stringToSign += hex.EncodeToString(sum256([]byte(canonicalRequest))) 203 return stringToSign 204 } 205 206 // PreSignV4 presign the request, in accordance with 207 // http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html. 208 func PreSignV4(req http.Request, accessKeyID, secretAccessKey, sessionToken, location string, expires int64) *http.Request { 209 // Presign is not needed for anonymous credentials. 210 if accessKeyID == "" || secretAccessKey == "" { 211 return &req 212 } 213 214 // Initial time. 215 t := time.Now().UTC() 216 217 // Get credential string. 218 credential := GetCredential(accessKeyID, location, t, ServiceTypeS3) 219 220 // Get all signed headers. 221 signedHeaders := getSignedHeaders(req, v4IgnoredHeaders) 222 223 // Set URL query. 224 query := req.URL.Query() 225 query.Set("X-Amz-Algorithm", signV4Algorithm) 226 query.Set("X-Amz-Date", t.Format(iso8601DateFormat)) 227 query.Set("X-Amz-Expires", strconv.FormatInt(expires, 10)) 228 query.Set("X-Amz-SignedHeaders", signedHeaders) 229 query.Set("X-Amz-Credential", credential) 230 // Set session token if available. 231 if sessionToken != "" { 232 query.Set("X-Amz-Security-Token", sessionToken) 233 } 234 req.URL.RawQuery = query.Encode() 235 236 // Get canonical request. 237 canonicalRequest := getCanonicalRequest(req, v4IgnoredHeaders, getHashedPayload(req)) 238 239 // Get string to sign from canonical request. 240 stringToSign := getStringToSignV4(t, location, canonicalRequest, ServiceTypeS3) 241 242 // Gext hmac signing key. 243 signingKey := getSigningKey(secretAccessKey, location, t, ServiceTypeS3) 244 245 // Calculate signature. 246 signature := getSignature(signingKey, stringToSign) 247 248 // Add signature header to RawQuery. 249 req.URL.RawQuery += "&X-Amz-Signature=" + signature 250 251 return &req 252 } 253 254 // PostPresignSignatureV4 - presigned signature for PostPolicy 255 // requests. 256 func PostPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string { 257 // Get signining key. 258 signingkey := getSigningKey(secretAccessKey, location, t, ServiceTypeS3) 259 // Calculate signature. 260 signature := getSignature(signingkey, policyBase64) 261 return signature 262 } 263 264 // SignV4STS - signature v4 for STS request. 265 func SignV4STS(req http.Request, accessKeyID, secretAccessKey, location string) *http.Request { 266 return signV4(req, accessKeyID, secretAccessKey, "", location, ServiceTypeSTS, nil) 267 } 268 269 // Internal function called for different service types. 270 func signV4(req http.Request, accessKeyID, secretAccessKey, sessionToken, location, serviceType string, trailer http.Header) *http.Request { 271 // Signature calculation is not needed for anonymous credentials. 272 if accessKeyID == "" || secretAccessKey == "" { 273 return &req 274 } 275 276 // Initial time. 277 t := time.Now().UTC() 278 279 // Set x-amz-date. 280 req.Header.Set("X-Amz-Date", t.Format(iso8601DateFormat)) 281 282 // Set session token if available. 283 if sessionToken != "" { 284 req.Header.Set("X-Amz-Security-Token", sessionToken) 285 } 286 287 if len(trailer) > 0 { 288 for k := range trailer { 289 req.Header.Add("X-Amz-Trailer", strings.ToLower(k)) 290 } 291 292 req.Header.Set("Content-Encoding", "aws-chunked") 293 req.Header.Set("x-amz-decoded-content-length", strconv.FormatInt(req.ContentLength, 10)) 294 } 295 296 hashedPayload := getHashedPayload(req) 297 if serviceType == ServiceTypeSTS { 298 // Content sha256 header is not sent with the request 299 // but it is expected to have sha256 of payload for signature 300 // in STS service type request. 301 req.Header.Del("X-Amz-Content-Sha256") 302 } 303 304 // Get canonical request. 305 canonicalRequest := getCanonicalRequest(req, v4IgnoredHeaders, hashedPayload) 306 307 // Get string to sign from canonical request. 308 stringToSign := getStringToSignV4(t, location, canonicalRequest, serviceType) 309 310 // Get hmac signing key. 311 signingKey := getSigningKey(secretAccessKey, location, t, serviceType) 312 313 // Get credential string. 314 credential := GetCredential(accessKeyID, location, t, serviceType) 315 316 // Get all signed headers. 317 signedHeaders := getSignedHeaders(req, v4IgnoredHeaders) 318 319 // Calculate signature. 320 signature := getSignature(signingKey, stringToSign) 321 322 // If regular request, construct the final authorization header. 323 parts := []string{ 324 signV4Algorithm + " Credential=" + credential, 325 "SignedHeaders=" + signedHeaders, 326 "Signature=" + signature, 327 } 328 329 // Set authorization header. 330 auth := strings.Join(parts, ", ") 331 req.Header.Set("Authorization", auth) 332 333 if len(trailer) > 0 { 334 // Use custom chunked encoding. 335 req.Trailer = trailer 336 return StreamingUnsignedV4(&req, sessionToken, req.ContentLength, time.Now().UTC()) 337 } 338 return &req 339 } 340 341 // SignV4 sign the request before Do(), in accordance with 342 // http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html. 343 func SignV4(req http.Request, accessKeyID, secretAccessKey, sessionToken, location string) *http.Request { 344 return signV4(req, accessKeyID, secretAccessKey, sessionToken, location, ServiceTypeS3, nil) 345 } 346 347 // SignV4Trailer sign the request before Do(), in accordance with 348 // http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html 349 func SignV4Trailer(req http.Request, accessKeyID, secretAccessKey, sessionToken, location string, trailer http.Header) *http.Request { 350 return signV4(req, accessKeyID, secretAccessKey, sessionToken, location, ServiceTypeS3, trailer) 351 }