request-signature-v2.go (9033B)
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 "crypto/hmac" 23 "crypto/sha1" 24 "encoding/base64" 25 "fmt" 26 "net/http" 27 "net/url" 28 "sort" 29 "strconv" 30 "strings" 31 "time" 32 33 "github.com/minio/minio-go/v7/pkg/s3utils" 34 ) 35 36 // Signature and API related constants. 37 const ( 38 signV2Algorithm = "AWS" 39 ) 40 41 // Encode input URL path to URL encoded path. 42 func encodeURL2Path(req *http.Request, virtualHost bool) (path string) { 43 if virtualHost { 44 reqHost := getHostAddr(req) 45 dotPos := strings.Index(reqHost, ".") 46 if dotPos > -1 { 47 bucketName := reqHost[:dotPos] 48 path = "/" + bucketName 49 path += req.URL.Path 50 path = s3utils.EncodePath(path) 51 return 52 } 53 } 54 path = s3utils.EncodePath(req.URL.Path) 55 return 56 } 57 58 // PreSignV2 - presign the request in following style. 59 // https://${S3_BUCKET}.s3.amazonaws.com/${S3_OBJECT}?AWSAccessKeyId=${S3_ACCESS_KEY}&Expires=${TIMESTAMP}&Signature=${SIGNATURE}. 60 func PreSignV2(req http.Request, accessKeyID, secretAccessKey string, expires int64, virtualHost bool) *http.Request { 61 // Presign is not needed for anonymous credentials. 62 if accessKeyID == "" || secretAccessKey == "" { 63 return &req 64 } 65 66 d := time.Now().UTC() 67 // Find epoch expires when the request will expire. 68 epochExpires := d.Unix() + expires 69 70 // Add expires header if not present. 71 if expiresStr := req.Header.Get("Expires"); expiresStr == "" { 72 req.Header.Set("Expires", strconv.FormatInt(epochExpires, 10)) 73 } 74 75 // Get presigned string to sign. 76 stringToSign := preStringToSignV2(req, virtualHost) 77 hm := hmac.New(sha1.New, []byte(secretAccessKey)) 78 hm.Write([]byte(stringToSign)) 79 80 // Calculate signature. 81 signature := base64.StdEncoding.EncodeToString(hm.Sum(nil)) 82 83 query := req.URL.Query() 84 // Handle specially for Google Cloud Storage. 85 if strings.Contains(getHostAddr(&req), ".storage.googleapis.com") { 86 query.Set("GoogleAccessId", accessKeyID) 87 } else { 88 query.Set("AWSAccessKeyId", accessKeyID) 89 } 90 91 // Fill in Expires for presigned query. 92 query.Set("Expires", strconv.FormatInt(epochExpires, 10)) 93 94 // Encode query and save. 95 req.URL.RawQuery = s3utils.QueryEncode(query) 96 97 // Save signature finally. 98 req.URL.RawQuery += "&Signature=" + s3utils.EncodePath(signature) 99 100 // Return. 101 return &req 102 } 103 104 // PostPresignSignatureV2 - presigned signature for PostPolicy 105 // request. 106 func PostPresignSignatureV2(policyBase64, secretAccessKey string) string { 107 hm := hmac.New(sha1.New, []byte(secretAccessKey)) 108 hm.Write([]byte(policyBase64)) 109 signature := base64.StdEncoding.EncodeToString(hm.Sum(nil)) 110 return signature 111 } 112 113 // Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature; 114 // Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ); 115 // 116 // StringToSign = HTTP-Verb + "\n" + 117 // Content-Md5 + "\n" + 118 // Content-Type + "\n" + 119 // Date + "\n" + 120 // CanonicalizedProtocolHeaders + 121 // CanonicalizedResource; 122 // 123 // CanonicalizedResource = [ "/" + Bucket ] + 124 // <HTTP-Request-URI, from the protocol name up to the query string> + 125 // [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 126 // 127 // CanonicalizedProtocolHeaders = <described below> 128 129 // SignV2 sign the request before Do() (AWS Signature Version 2). 130 func SignV2(req http.Request, accessKeyID, secretAccessKey string, virtualHost bool) *http.Request { 131 // Signature calculation is not needed for anonymous credentials. 132 if accessKeyID == "" || secretAccessKey == "" { 133 return &req 134 } 135 136 // Initial time. 137 d := time.Now().UTC() 138 139 // Add date if not present. 140 if date := req.Header.Get("Date"); date == "" { 141 req.Header.Set("Date", d.Format(http.TimeFormat)) 142 } 143 144 // Calculate HMAC for secretAccessKey. 145 stringToSign := stringToSignV2(req, virtualHost) 146 hm := hmac.New(sha1.New, []byte(secretAccessKey)) 147 hm.Write([]byte(stringToSign)) 148 149 // Prepare auth header. 150 authHeader := new(bytes.Buffer) 151 authHeader.WriteString(fmt.Sprintf("%s %s:", signV2Algorithm, accessKeyID)) 152 encoder := base64.NewEncoder(base64.StdEncoding, authHeader) 153 encoder.Write(hm.Sum(nil)) 154 encoder.Close() 155 156 // Set Authorization header. 157 req.Header.Set("Authorization", authHeader.String()) 158 159 return &req 160 } 161 162 // From the Amazon docs: 163 // 164 // StringToSign = HTTP-Verb + "\n" + 165 // 166 // Content-Md5 + "\n" + 167 // Content-Type + "\n" + 168 // Expires + "\n" + 169 // CanonicalizedProtocolHeaders + 170 // CanonicalizedResource; 171 func preStringToSignV2(req http.Request, virtualHost bool) string { 172 buf := new(bytes.Buffer) 173 // Write standard headers. 174 writePreSignV2Headers(buf, req) 175 // Write canonicalized protocol headers if any. 176 writeCanonicalizedHeaders(buf, req) 177 // Write canonicalized Query resources if any. 178 writeCanonicalizedResource(buf, req, virtualHost) 179 return buf.String() 180 } 181 182 // writePreSignV2Headers - write preSign v2 required headers. 183 func writePreSignV2Headers(buf *bytes.Buffer, req http.Request) { 184 buf.WriteString(req.Method + "\n") 185 buf.WriteString(req.Header.Get("Content-Md5") + "\n") 186 buf.WriteString(req.Header.Get("Content-Type") + "\n") 187 buf.WriteString(req.Header.Get("Expires") + "\n") 188 } 189 190 // From the Amazon docs: 191 // 192 // StringToSign = HTTP-Verb + "\n" + 193 // 194 // Content-Md5 + "\n" + 195 // Content-Type + "\n" + 196 // Date + "\n" + 197 // CanonicalizedProtocolHeaders + 198 // CanonicalizedResource; 199 func stringToSignV2(req http.Request, virtualHost bool) string { 200 buf := new(bytes.Buffer) 201 // Write standard headers. 202 writeSignV2Headers(buf, req) 203 // Write canonicalized protocol headers if any. 204 writeCanonicalizedHeaders(buf, req) 205 // Write canonicalized Query resources if any. 206 writeCanonicalizedResource(buf, req, virtualHost) 207 return buf.String() 208 } 209 210 // writeSignV2Headers - write signV2 required headers. 211 func writeSignV2Headers(buf *bytes.Buffer, req http.Request) { 212 buf.WriteString(req.Method + "\n") 213 buf.WriteString(req.Header.Get("Content-Md5") + "\n") 214 buf.WriteString(req.Header.Get("Content-Type") + "\n") 215 buf.WriteString(req.Header.Get("Date") + "\n") 216 } 217 218 // writeCanonicalizedHeaders - write canonicalized headers. 219 func writeCanonicalizedHeaders(buf *bytes.Buffer, req http.Request) { 220 var protoHeaders []string 221 vals := make(map[string][]string) 222 for k, vv := range req.Header { 223 // All the AMZ headers should be lowercase 224 lk := strings.ToLower(k) 225 if strings.HasPrefix(lk, "x-amz") { 226 protoHeaders = append(protoHeaders, lk) 227 vals[lk] = vv 228 } 229 } 230 sort.Strings(protoHeaders) 231 for _, k := range protoHeaders { 232 buf.WriteString(k) 233 buf.WriteByte(':') 234 for idx, v := range vals[k] { 235 if idx > 0 { 236 buf.WriteByte(',') 237 } 238 buf.WriteString(v) 239 } 240 buf.WriteByte('\n') 241 } 242 } 243 244 // AWS S3 Signature V2 calculation rule is give here: 245 // http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign 246 247 // Whitelist resource list that will be used in query string for signature-V2 calculation. 248 // 249 // This list should be kept alphabetically sorted, do not hastily edit. 250 var resourceList = []string{ 251 "acl", 252 "cors", 253 "delete", 254 "encryption", 255 "legal-hold", 256 "lifecycle", 257 "location", 258 "logging", 259 "notification", 260 "partNumber", 261 "policy", 262 "replication", 263 "requestPayment", 264 "response-cache-control", 265 "response-content-disposition", 266 "response-content-encoding", 267 "response-content-language", 268 "response-content-type", 269 "response-expires", 270 "retention", 271 "select", 272 "select-type", 273 "tagging", 274 "torrent", 275 "uploadId", 276 "uploads", 277 "versionId", 278 "versioning", 279 "versions", 280 "website", 281 } 282 283 // From the Amazon docs: 284 // 285 // CanonicalizedResource = [ "/" + Bucket ] + 286 // 287 // <HTTP-Request-URI, from the protocol name up to the query string> + 288 // [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 289 func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request, virtualHost bool) { 290 // Save request URL. 291 requestURL := req.URL 292 // Get encoded URL path. 293 buf.WriteString(encodeURL2Path(&req, virtualHost)) 294 if requestURL.RawQuery != "" { 295 var n int 296 vals, _ := url.ParseQuery(requestURL.RawQuery) 297 // Verify if any sub resource queries are present, if yes 298 // canonicallize them. 299 for _, resource := range resourceList { 300 if vv, ok := vals[resource]; ok && len(vv) > 0 { 301 n++ 302 // First element 303 switch n { 304 case 1: 305 buf.WriteByte('?') 306 // The rest 307 default: 308 buf.WriteByte('&') 309 } 310 buf.WriteString(resource) 311 // Request parameters 312 if len(vv[0]) > 0 { 313 buf.WriteByte('=') 314 buf.WriteString(vv[0]) 315 } 316 } 317 } 318 } 319 }