]>
Commit | Line | Data |
---|---|---|
1 | // Package v4 implements signing for AWS V4 signer | |
2 | // | |
3 | // Provides request signing for request that need to be signed with | |
4 | // AWS V4 Signatures. | |
5 | // | |
6 | // Standalone Signer | |
7 | // | |
8 | // Generally using the signer outside of the SDK should not require any additional | |
9 | // logic when using Go v1.5 or higher. The signer does this by taking advantage | |
10 | // of the URL.EscapedPath method. If your request URI requires additional escaping | |
11 | // you many need to use the URL.Opaque to define what the raw URI should be sent | |
12 | // to the service as. | |
13 | // | |
14 | // The signer will first check the URL.Opaque field, and use its value if set. | |
15 | // The signer does require the URL.Opaque field to be set in the form of: | |
16 | // | |
17 | // "//<hostname>/<path>" | |
18 | // | |
19 | // // e.g. | |
20 | // "//example.com/some/path" | |
21 | // | |
22 | // The leading "//" and hostname are required or the URL.Opaque escaping will | |
23 | // not work correctly. | |
24 | // | |
25 | // If URL.Opaque is not set the signer will fallback to the URL.EscapedPath() | |
26 | // method and using the returned value. If you're using Go v1.4 you must set | |
27 | // URL.Opaque if the URI path needs escaping. If URL.Opaque is not set with | |
28 | // Go v1.5 the signer will fallback to URL.Path. | |
29 | // | |
30 | // AWS v4 signature validation requires that the canonical string's URI path | |
31 | // element must be the URI escaped form of the HTTP request's path. | |
32 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | |
33 | // | |
34 | // The Go HTTP client will perform escaping automatically on the request. Some | |
35 | // of these escaping may cause signature validation errors because the HTTP | |
36 | // request differs from the URI path or query that the signature was generated. | |
37 | // https://golang.org/pkg/net/url/#URL.EscapedPath | |
38 | // | |
39 | // Because of this, it is recommended that when using the signer outside of the | |
40 | // SDK that explicitly escaping the request prior to being signed is preferable, | |
41 | // and will help prevent signature validation errors. This can be done by setting | |
42 | // the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then | |
43 | // call URL.EscapedPath() if Opaque is not set. | |
44 | // | |
45 | // If signing a request intended for HTTP2 server, and you're using Go 1.6.2 | |
46 | // through 1.7.4 you should use the URL.RawPath as the pre-escaped form of the | |
47 | // request URL. https://github.com/golang/go/issues/16847 points to a bug in | |
48 | // Go pre 1.8 that fails to make HTTP2 requests using absolute URL in the HTTP | |
49 | // message. URL.Opaque generally will force Go to make requests with absolute URL. | |
50 | // URL.RawPath does not do this, but RawPath must be a valid escaping of Path | |
51 | // or url.EscapedPath will ignore the RawPath escaping. | |
52 | // | |
53 | // Test `TestStandaloneSign` provides a complete example of using the signer | |
54 | // outside of the SDK and pre-escaping the URI path. | |
55 | package v4 | |
56 | ||
57 | import ( | |
58 | "crypto/hmac" | |
59 | "crypto/sha256" | |
60 | "encoding/hex" | |
61 | "fmt" | |
62 | "io" | |
63 | "io/ioutil" | |
64 | "net/http" | |
65 | "net/url" | |
66 | "sort" | |
67 | "strconv" | |
68 | "strings" | |
69 | "time" | |
70 | ||
71 | "github.com/aws/aws-sdk-go/aws" | |
72 | "github.com/aws/aws-sdk-go/aws/credentials" | |
73 | "github.com/aws/aws-sdk-go/aws/request" | |
74 | "github.com/aws/aws-sdk-go/internal/sdkio" | |
75 | "github.com/aws/aws-sdk-go/private/protocol/rest" | |
76 | ) | |
77 | ||
78 | const ( | |
79 | authHeaderPrefix = "AWS4-HMAC-SHA256" | |
80 | timeFormat = "20060102T150405Z" | |
81 | shortTimeFormat = "20060102" | |
82 | ||
83 | // emptyStringSHA256 is a SHA256 of an empty string | |
84 | emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | |
85 | ) | |
86 | ||
87 | var ignoredHeaders = rules{ | |
88 | blacklist{ | |
89 | mapRule{ | |
90 | "Authorization": struct{}{}, | |
91 | "User-Agent": struct{}{}, | |
92 | "X-Amzn-Trace-Id": struct{}{}, | |
93 | }, | |
94 | }, | |
95 | } | |
96 | ||
97 | // requiredSignedHeaders is a whitelist for build canonical headers. | |
98 | var requiredSignedHeaders = rules{ | |
99 | whitelist{ | |
100 | mapRule{ | |
101 | "Cache-Control": struct{}{}, | |
102 | "Content-Disposition": struct{}{}, | |
103 | "Content-Encoding": struct{}{}, | |
104 | "Content-Language": struct{}{}, | |
105 | "Content-Md5": struct{}{}, | |
106 | "Content-Type": struct{}{}, | |
107 | "Expires": struct{}{}, | |
108 | "If-Match": struct{}{}, | |
109 | "If-Modified-Since": struct{}{}, | |
110 | "If-None-Match": struct{}{}, | |
111 | "If-Unmodified-Since": struct{}{}, | |
112 | "Range": struct{}{}, | |
113 | "X-Amz-Acl": struct{}{}, | |
114 | "X-Amz-Copy-Source": struct{}{}, | |
115 | "X-Amz-Copy-Source-If-Match": struct{}{}, | |
116 | "X-Amz-Copy-Source-If-Modified-Since": struct{}{}, | |
117 | "X-Amz-Copy-Source-If-None-Match": struct{}{}, | |
118 | "X-Amz-Copy-Source-If-Unmodified-Since": struct{}{}, | |
119 | "X-Amz-Copy-Source-Range": struct{}{}, | |
120 | "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{}, | |
121 | "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{}, | |
122 | "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, | |
123 | "X-Amz-Grant-Full-control": struct{}{}, | |
124 | "X-Amz-Grant-Read": struct{}{}, | |
125 | "X-Amz-Grant-Read-Acp": struct{}{}, | |
126 | "X-Amz-Grant-Write": struct{}{}, | |
127 | "X-Amz-Grant-Write-Acp": struct{}{}, | |
128 | "X-Amz-Metadata-Directive": struct{}{}, | |
129 | "X-Amz-Mfa": struct{}{}, | |
130 | "X-Amz-Request-Payer": struct{}{}, | |
131 | "X-Amz-Server-Side-Encryption": struct{}{}, | |
132 | "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{}, | |
133 | "X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{}, | |
134 | "X-Amz-Server-Side-Encryption-Customer-Key": struct{}{}, | |
135 | "X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{}, | |
136 | "X-Amz-Storage-Class": struct{}{}, | |
137 | "X-Amz-Tagging": struct{}{}, | |
138 | "X-Amz-Website-Redirect-Location": struct{}{}, | |
139 | "X-Amz-Content-Sha256": struct{}{}, | |
140 | }, | |
141 | }, | |
142 | patterns{"X-Amz-Meta-"}, | |
143 | } | |
144 | ||
145 | // allowedHoisting is a whitelist for build query headers. The boolean value | |
146 | // represents whether or not it is a pattern. | |
147 | var allowedQueryHoisting = inclusiveRules{ | |
148 | blacklist{requiredSignedHeaders}, | |
149 | patterns{"X-Amz-"}, | |
150 | } | |
151 | ||
152 | // Signer applies AWS v4 signing to given request. Use this to sign requests | |
153 | // that need to be signed with AWS V4 Signatures. | |
154 | type Signer struct { | |
155 | // The authentication credentials the request will be signed against. | |
156 | // This value must be set to sign requests. | |
157 | Credentials *credentials.Credentials | |
158 | ||
159 | // Sets the log level the signer should use when reporting information to | |
160 | // the logger. If the logger is nil nothing will be logged. See | |
161 | // aws.LogLevelType for more information on available logging levels | |
162 | // | |
163 | // By default nothing will be logged. | |
164 | Debug aws.LogLevelType | |
165 | ||
166 | // The logger loging information will be written to. If there the logger | |
167 | // is nil, nothing will be logged. | |
168 | Logger aws.Logger | |
169 | ||
170 | // Disables the Signer's moving HTTP header key/value pairs from the HTTP | |
171 | // request header to the request's query string. This is most commonly used | |
172 | // with pre-signed requests preventing headers from being added to the | |
173 | // request's query string. | |
174 | DisableHeaderHoisting bool | |
175 | ||
176 | // Disables the automatic escaping of the URI path of the request for the | |
177 | // siganture's canonical string's path. For services that do not need additional | |
178 | // escaping then use this to disable the signer escaping the path. | |
179 | // | |
180 | // S3 is an example of a service that does not need additional escaping. | |
181 | // | |
182 | // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | |
183 | DisableURIPathEscaping bool | |
184 | ||
185 | // Disables the automatical setting of the HTTP request's Body field with the | |
186 | // io.ReadSeeker passed in to the signer. This is useful if you're using a | |
187 | // custom wrapper around the body for the io.ReadSeeker and want to preserve | |
188 | // the Body value on the Request.Body. | |
189 | // | |
190 | // This does run the risk of signing a request with a body that will not be | |
191 | // sent in the request. Need to ensure that the underlying data of the Body | |
192 | // values are the same. | |
193 | DisableRequestBodyOverwrite bool | |
194 | ||
195 | // currentTimeFn returns the time value which represents the current time. | |
196 | // This value should only be used for testing. If it is nil the default | |
197 | // time.Now will be used. | |
198 | currentTimeFn func() time.Time | |
199 | ||
200 | // UnsignedPayload will prevent signing of the payload. This will only | |
201 | // work for services that have support for this. | |
202 | UnsignedPayload bool | |
203 | } | |
204 | ||
205 | // NewSigner returns a Signer pointer configured with the credentials and optional | |
206 | // option values provided. If not options are provided the Signer will use its | |
207 | // default configuration. | |
208 | func NewSigner(credentials *credentials.Credentials, options ...func(*Signer)) *Signer { | |
209 | v4 := &Signer{ | |
210 | Credentials: credentials, | |
211 | } | |
212 | ||
213 | for _, option := range options { | |
214 | option(v4) | |
215 | } | |
216 | ||
217 | return v4 | |
218 | } | |
219 | ||
220 | type signingCtx struct { | |
221 | ServiceName string | |
222 | Region string | |
223 | Request *http.Request | |
224 | Body io.ReadSeeker | |
225 | Query url.Values | |
226 | Time time.Time | |
227 | ExpireTime time.Duration | |
228 | SignedHeaderVals http.Header | |
229 | ||
230 | DisableURIPathEscaping bool | |
231 | ||
232 | credValues credentials.Value | |
233 | isPresign bool | |
234 | formattedTime string | |
235 | formattedShortTime string | |
236 | unsignedPayload bool | |
237 | ||
238 | bodyDigest string | |
239 | signedHeaders string | |
240 | canonicalHeaders string | |
241 | canonicalString string | |
242 | credentialString string | |
243 | stringToSign string | |
244 | signature string | |
245 | authorization string | |
246 | } | |
247 | ||
248 | // Sign signs AWS v4 requests with the provided body, service name, region the | |
249 | // request is made to, and time the request is signed at. The signTime allows | |
250 | // you to specify that a request is signed for the future, and cannot be | |
251 | // used until then. | |
252 | // | |
253 | // Returns a list of HTTP headers that were included in the signature or an | |
254 | // error if signing the request failed. Generally for signed requests this value | |
255 | // is not needed as the full request context will be captured by the http.Request | |
256 | // value. It is included for reference though. | |
257 | // | |
258 | // Sign will set the request's Body to be the `body` parameter passed in. If | |
259 | // the body is not already an io.ReadCloser, it will be wrapped within one. If | |
260 | // a `nil` body parameter passed to Sign, the request's Body field will be | |
261 | // also set to nil. Its important to note that this functionality will not | |
262 | // change the request's ContentLength of the request. | |
263 | // | |
264 | // Sign differs from Presign in that it will sign the request using HTTP | |
265 | // header values. This type of signing is intended for http.Request values that | |
266 | // will not be shared, or are shared in a way the header values on the request | |
267 | // will not be lost. | |
268 | // | |
269 | // The requests body is an io.ReadSeeker so the SHA256 of the body can be | |
270 | // generated. To bypass the signer computing the hash you can set the | |
271 | // "X-Amz-Content-Sha256" header with a precomputed value. The signer will | |
272 | // only compute the hash if the request header value is empty. | |
273 | func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) { | |
274 | return v4.signWithBody(r, body, service, region, 0, false, signTime) | |
275 | } | |
276 | ||
277 | // Presign signs AWS v4 requests with the provided body, service name, region | |
278 | // the request is made to, and time the request is signed at. The signTime | |
279 | // allows you to specify that a request is signed for the future, and cannot | |
280 | // be used until then. | |
281 | // | |
282 | // Returns a list of HTTP headers that were included in the signature or an | |
283 | // error if signing the request failed. For presigned requests these headers | |
284 | // and their values must be included on the HTTP request when it is made. This | |
285 | // is helpful to know what header values need to be shared with the party the | |
286 | // presigned request will be distributed to. | |
287 | // | |
288 | // Presign differs from Sign in that it will sign the request using query string | |
289 | // instead of header values. This allows you to share the Presigned Request's | |
290 | // URL with third parties, or distribute it throughout your system with minimal | |
291 | // dependencies. | |
292 | // | |
293 | // Presign also takes an exp value which is the duration the | |
294 | // signed request will be valid after the signing time. This is allows you to | |
295 | // set when the request will expire. | |
296 | // | |
297 | // The requests body is an io.ReadSeeker so the SHA256 of the body can be | |
298 | // generated. To bypass the signer computing the hash you can set the | |
299 | // "X-Amz-Content-Sha256" header with a precomputed value. The signer will | |
300 | // only compute the hash if the request header value is empty. | |
301 | // | |
302 | // Presigning a S3 request will not compute the body's SHA256 hash by default. | |
303 | // This is done due to the general use case for S3 presigned URLs is to share | |
304 | // PUT/GET capabilities. If you would like to include the body's SHA256 in the | |
305 | // presigned request's signature you can set the "X-Amz-Content-Sha256" | |
306 | // HTTP header and that will be included in the request's signature. | |
307 | func (v4 Signer) Presign(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) { | |
308 | return v4.signWithBody(r, body, service, region, exp, true, signTime) | |
309 | } | |
310 | ||
311 | func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, isPresign bool, signTime time.Time) (http.Header, error) { | |
312 | currentTimeFn := v4.currentTimeFn | |
313 | if currentTimeFn == nil { | |
314 | currentTimeFn = time.Now | |
315 | } | |
316 | ||
317 | ctx := &signingCtx{ | |
318 | Request: r, | |
319 | Body: body, | |
320 | Query: r.URL.Query(), | |
321 | Time: signTime, | |
322 | ExpireTime: exp, | |
323 | isPresign: isPresign, | |
324 | ServiceName: service, | |
325 | Region: region, | |
326 | DisableURIPathEscaping: v4.DisableURIPathEscaping, | |
327 | unsignedPayload: v4.UnsignedPayload, | |
328 | } | |
329 | ||
330 | for key := range ctx.Query { | |
331 | sort.Strings(ctx.Query[key]) | |
332 | } | |
333 | ||
334 | if ctx.isRequestSigned() { | |
335 | ctx.Time = currentTimeFn() | |
336 | ctx.handlePresignRemoval() | |
337 | } | |
338 | ||
339 | var err error | |
340 | ctx.credValues, err = v4.Credentials.Get() | |
341 | if err != nil { | |
342 | return http.Header{}, err | |
343 | } | |
344 | ||
345 | ctx.sanitizeHostForHeader() | |
346 | ctx.assignAmzQueryValues() | |
347 | if err := ctx.build(v4.DisableHeaderHoisting); err != nil { | |
348 | return nil, err | |
349 | } | |
350 | ||
351 | // If the request is not presigned the body should be attached to it. This | |
352 | // prevents the confusion of wanting to send a signed request without | |
353 | // the body the request was signed for attached. | |
354 | if !(v4.DisableRequestBodyOverwrite || ctx.isPresign) { | |
355 | var reader io.ReadCloser | |
356 | if body != nil { | |
357 | var ok bool | |
358 | if reader, ok = body.(io.ReadCloser); !ok { | |
359 | reader = ioutil.NopCloser(body) | |
360 | } | |
361 | } | |
362 | r.Body = reader | |
363 | } | |
364 | ||
365 | if v4.Debug.Matches(aws.LogDebugWithSigning) { | |
366 | v4.logSigningInfo(ctx) | |
367 | } | |
368 | ||
369 | return ctx.SignedHeaderVals, nil | |
370 | } | |
371 | ||
372 | func (ctx *signingCtx) sanitizeHostForHeader() { | |
373 | request.SanitizeHostForHeader(ctx.Request) | |
374 | } | |
375 | ||
376 | func (ctx *signingCtx) handlePresignRemoval() { | |
377 | if !ctx.isPresign { | |
378 | return | |
379 | } | |
380 | ||
381 | // The credentials have expired for this request. The current signing | |
382 | // is invalid, and needs to be request because the request will fail. | |
383 | ctx.removePresign() | |
384 | ||
385 | // Update the request's query string to ensure the values stays in | |
386 | // sync in the case retrieving the new credentials fails. | |
387 | ctx.Request.URL.RawQuery = ctx.Query.Encode() | |
388 | } | |
389 | ||
390 | func (ctx *signingCtx) assignAmzQueryValues() { | |
391 | if ctx.isPresign { | |
392 | ctx.Query.Set("X-Amz-Algorithm", authHeaderPrefix) | |
393 | if ctx.credValues.SessionToken != "" { | |
394 | ctx.Query.Set("X-Amz-Security-Token", ctx.credValues.SessionToken) | |
395 | } else { | |
396 | ctx.Query.Del("X-Amz-Security-Token") | |
397 | } | |
398 | ||
399 | return | |
400 | } | |
401 | ||
402 | if ctx.credValues.SessionToken != "" { | |
403 | ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken) | |
404 | } | |
405 | } | |
406 | ||
407 | // SignRequestHandler is a named request handler the SDK will use to sign | |
408 | // service client request with using the V4 signature. | |
409 | var SignRequestHandler = request.NamedHandler{ | |
410 | Name: "v4.SignRequestHandler", Fn: SignSDKRequest, | |
411 | } | |
412 | ||
413 | // SignSDKRequest signs an AWS request with the V4 signature. This | |
414 | // request handler should only be used with the SDK's built in service client's | |
415 | // API operation requests. | |
416 | // | |
417 | // This function should not be used on its on its own, but in conjunction with | |
418 | // an AWS service client's API operation call. To sign a standalone request | |
419 | // not created by a service client's API operation method use the "Sign" or | |
420 | // "Presign" functions of the "Signer" type. | |
421 | // | |
422 | // If the credentials of the request's config are set to | |
423 | // credentials.AnonymousCredentials the request will not be signed. | |
424 | func SignSDKRequest(req *request.Request) { | |
425 | SignSDKRequestWithCurrentTime(req, time.Now) | |
426 | } | |
427 | ||
428 | // BuildNamedHandler will build a generic handler for signing. | |
429 | func BuildNamedHandler(name string, opts ...func(*Signer)) request.NamedHandler { | |
430 | return request.NamedHandler{ | |
431 | Name: name, | |
432 | Fn: func(req *request.Request) { | |
433 | SignSDKRequestWithCurrentTime(req, time.Now, opts...) | |
434 | }, | |
435 | } | |
436 | } | |
437 | ||
438 | // SignSDKRequestWithCurrentTime will sign the SDK's request using the time | |
439 | // function passed in. Behaves the same as SignSDKRequest with the exception | |
440 | // the request is signed with the value returned by the current time function. | |
441 | func SignSDKRequestWithCurrentTime(req *request.Request, curTimeFn func() time.Time, opts ...func(*Signer)) { | |
442 | // If the request does not need to be signed ignore the signing of the | |
443 | // request if the AnonymousCredentials object is used. | |
444 | if req.Config.Credentials == credentials.AnonymousCredentials { | |
445 | return | |
446 | } | |
447 | ||
448 | region := req.ClientInfo.SigningRegion | |
449 | if region == "" { | |
450 | region = aws.StringValue(req.Config.Region) | |
451 | } | |
452 | ||
453 | name := req.ClientInfo.SigningName | |
454 | if name == "" { | |
455 | name = req.ClientInfo.ServiceName | |
456 | } | |
457 | ||
458 | v4 := NewSigner(req.Config.Credentials, func(v4 *Signer) { | |
459 | v4.Debug = req.Config.LogLevel.Value() | |
460 | v4.Logger = req.Config.Logger | |
461 | v4.DisableHeaderHoisting = req.NotHoist | |
462 | v4.currentTimeFn = curTimeFn | |
463 | if name == "s3" { | |
464 | // S3 service should not have any escaping applied | |
465 | v4.DisableURIPathEscaping = true | |
466 | } | |
467 | // Prevents setting the HTTPRequest's Body. Since the Body could be | |
468 | // wrapped in a custom io.Closer that we do not want to be stompped | |
469 | // on top of by the signer. | |
470 | v4.DisableRequestBodyOverwrite = true | |
471 | }) | |
472 | ||
473 | for _, opt := range opts { | |
474 | opt(v4) | |
475 | } | |
476 | ||
477 | curTime := curTimeFn() | |
478 | signedHeaders, err := v4.signWithBody(req.HTTPRequest, req.GetBody(), | |
479 | name, region, req.ExpireTime, req.ExpireTime > 0, curTime, | |
480 | ) | |
481 | if err != nil { | |
482 | req.Error = err | |
483 | req.SignedHeaderVals = nil | |
484 | return | |
485 | } | |
486 | ||
487 | req.SignedHeaderVals = signedHeaders | |
488 | req.LastSignedAt = curTime | |
489 | } | |
490 | ||
491 | const logSignInfoMsg = `DEBUG: Request Signature: | |
492 | ---[ CANONICAL STRING ]----------------------------- | |
493 | %s | |
494 | ---[ STRING TO SIGN ]-------------------------------- | |
495 | %s%s | |
496 | -----------------------------------------------------` | |
497 | const logSignedURLMsg = ` | |
498 | ---[ SIGNED URL ]------------------------------------ | |
499 | %s` | |
500 | ||
501 | func (v4 *Signer) logSigningInfo(ctx *signingCtx) { | |
502 | signedURLMsg := "" | |
503 | if ctx.isPresign { | |
504 | signedURLMsg = fmt.Sprintf(logSignedURLMsg, ctx.Request.URL.String()) | |
505 | } | |
506 | msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg) | |
507 | v4.Logger.Log(msg) | |
508 | } | |
509 | ||
510 | func (ctx *signingCtx) build(disableHeaderHoisting bool) error { | |
511 | ctx.buildTime() // no depends | |
512 | ctx.buildCredentialString() // no depends | |
513 | ||
514 | if err := ctx.buildBodyDigest(); err != nil { | |
515 | return err | |
516 | } | |
517 | ||
518 | unsignedHeaders := ctx.Request.Header | |
519 | if ctx.isPresign { | |
520 | if !disableHeaderHoisting { | |
521 | urlValues := url.Values{} | |
522 | urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends | |
523 | for k := range urlValues { | |
524 | ctx.Query[k] = urlValues[k] | |
525 | } | |
526 | } | |
527 | } | |
528 | ||
529 | ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders) | |
530 | ctx.buildCanonicalString() // depends on canon headers / signed headers | |
531 | ctx.buildStringToSign() // depends on canon string | |
532 | ctx.buildSignature() // depends on string to sign | |
533 | ||
534 | if ctx.isPresign { | |
535 | ctx.Request.URL.RawQuery += "&X-Amz-Signature=" + ctx.signature | |
536 | } else { | |
537 | parts := []string{ | |
538 | authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString, | |
539 | "SignedHeaders=" + ctx.signedHeaders, | |
540 | "Signature=" + ctx.signature, | |
541 | } | |
542 | ctx.Request.Header.Set("Authorization", strings.Join(parts, ", ")) | |
543 | } | |
544 | ||
545 | return nil | |
546 | } | |
547 | ||
548 | func (ctx *signingCtx) buildTime() { | |
549 | ctx.formattedTime = ctx.Time.UTC().Format(timeFormat) | |
550 | ctx.formattedShortTime = ctx.Time.UTC().Format(shortTimeFormat) | |
551 | ||
552 | if ctx.isPresign { | |
553 | duration := int64(ctx.ExpireTime / time.Second) | |
554 | ctx.Query.Set("X-Amz-Date", ctx.formattedTime) | |
555 | ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10)) | |
556 | } else { | |
557 | ctx.Request.Header.Set("X-Amz-Date", ctx.formattedTime) | |
558 | } | |
559 | } | |
560 | ||
561 | func (ctx *signingCtx) buildCredentialString() { | |
562 | ctx.credentialString = strings.Join([]string{ | |
563 | ctx.formattedShortTime, | |
564 | ctx.Region, | |
565 | ctx.ServiceName, | |
566 | "aws4_request", | |
567 | }, "/") | |
568 | ||
569 | if ctx.isPresign { | |
570 | ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString) | |
571 | } | |
572 | } | |
573 | ||
574 | func buildQuery(r rule, header http.Header) (url.Values, http.Header) { | |
575 | query := url.Values{} | |
576 | unsignedHeaders := http.Header{} | |
577 | for k, h := range header { | |
578 | if r.IsValid(k) { | |
579 | query[k] = h | |
580 | } else { | |
581 | unsignedHeaders[k] = h | |
582 | } | |
583 | } | |
584 | ||
585 | return query, unsignedHeaders | |
586 | } | |
587 | func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) { | |
588 | var headers []string | |
589 | headers = append(headers, "host") | |
590 | for k, v := range header { | |
591 | canonicalKey := http.CanonicalHeaderKey(k) | |
592 | if !r.IsValid(canonicalKey) { | |
593 | continue // ignored header | |
594 | } | |
595 | if ctx.SignedHeaderVals == nil { | |
596 | ctx.SignedHeaderVals = make(http.Header) | |
597 | } | |
598 | ||
599 | lowerCaseKey := strings.ToLower(k) | |
600 | if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok { | |
601 | // include additional values | |
602 | ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...) | |
603 | continue | |
604 | } | |
605 | ||
606 | headers = append(headers, lowerCaseKey) | |
607 | ctx.SignedHeaderVals[lowerCaseKey] = v | |
608 | } | |
609 | sort.Strings(headers) | |
610 | ||
611 | ctx.signedHeaders = strings.Join(headers, ";") | |
612 | ||
613 | if ctx.isPresign { | |
614 | ctx.Query.Set("X-Amz-SignedHeaders", ctx.signedHeaders) | |
615 | } | |
616 | ||
617 | headerValues := make([]string, len(headers)) | |
618 | for i, k := range headers { | |
619 | if k == "host" { | |
620 | if ctx.Request.Host != "" { | |
621 | headerValues[i] = "host:" + ctx.Request.Host | |
622 | } else { | |
623 | headerValues[i] = "host:" + ctx.Request.URL.Host | |
624 | } | |
625 | } else { | |
626 | headerValues[i] = k + ":" + | |
627 | strings.Join(ctx.SignedHeaderVals[k], ",") | |
628 | } | |
629 | } | |
630 | stripExcessSpaces(headerValues) | |
631 | ctx.canonicalHeaders = strings.Join(headerValues, "\n") | |
632 | } | |
633 | ||
634 | func (ctx *signingCtx) buildCanonicalString() { | |
635 | ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1) | |
636 | ||
637 | uri := getURIPath(ctx.Request.URL) | |
638 | ||
639 | if !ctx.DisableURIPathEscaping { | |
640 | uri = rest.EscapePath(uri, false) | |
641 | } | |
642 | ||
643 | ctx.canonicalString = strings.Join([]string{ | |
644 | ctx.Request.Method, | |
645 | uri, | |
646 | ctx.Request.URL.RawQuery, | |
647 | ctx.canonicalHeaders + "\n", | |
648 | ctx.signedHeaders, | |
649 | ctx.bodyDigest, | |
650 | }, "\n") | |
651 | } | |
652 | ||
653 | func (ctx *signingCtx) buildStringToSign() { | |
654 | ctx.stringToSign = strings.Join([]string{ | |
655 | authHeaderPrefix, | |
656 | ctx.formattedTime, | |
657 | ctx.credentialString, | |
658 | hex.EncodeToString(makeSha256([]byte(ctx.canonicalString))), | |
659 | }, "\n") | |
660 | } | |
661 | ||
662 | func (ctx *signingCtx) buildSignature() { | |
663 | secret := ctx.credValues.SecretAccessKey | |
664 | date := makeHmac([]byte("AWS4"+secret), []byte(ctx.formattedShortTime)) | |
665 | region := makeHmac(date, []byte(ctx.Region)) | |
666 | service := makeHmac(region, []byte(ctx.ServiceName)) | |
667 | credentials := makeHmac(service, []byte("aws4_request")) | |
668 | signature := makeHmac(credentials, []byte(ctx.stringToSign)) | |
669 | ctx.signature = hex.EncodeToString(signature) | |
670 | } | |
671 | ||
672 | func (ctx *signingCtx) buildBodyDigest() error { | |
673 | hash := ctx.Request.Header.Get("X-Amz-Content-Sha256") | |
674 | if hash == "" { | |
675 | includeSHA256Header := ctx.unsignedPayload || | |
676 | ctx.ServiceName == "s3" || | |
677 | ctx.ServiceName == "glacier" | |
678 | ||
679 | s3Presign := ctx.isPresign && ctx.ServiceName == "s3" | |
680 | ||
681 | if ctx.unsignedPayload || s3Presign { | |
682 | hash = "UNSIGNED-PAYLOAD" | |
683 | includeSHA256Header = !s3Presign | |
684 | } else if ctx.Body == nil { | |
685 | hash = emptyStringSHA256 | |
686 | } else { | |
687 | if !aws.IsReaderSeekable(ctx.Body) { | |
688 | return fmt.Errorf("cannot use unseekable request body %T, for signed request with body", ctx.Body) | |
689 | } | |
690 | hashBytes, err := makeSha256Reader(ctx.Body) | |
691 | if err != nil { | |
692 | return err | |
693 | } | |
694 | hash = hex.EncodeToString(hashBytes) | |
695 | } | |
696 | ||
697 | if includeSHA256Header { | |
698 | ctx.Request.Header.Set("X-Amz-Content-Sha256", hash) | |
699 | } | |
700 | } | |
701 | ctx.bodyDigest = hash | |
702 | ||
703 | return nil | |
704 | } | |
705 | ||
706 | // isRequestSigned returns if the request is currently signed or presigned | |
707 | func (ctx *signingCtx) isRequestSigned() bool { | |
708 | if ctx.isPresign && ctx.Query.Get("X-Amz-Signature") != "" { | |
709 | return true | |
710 | } | |
711 | if ctx.Request.Header.Get("Authorization") != "" { | |
712 | return true | |
713 | } | |
714 | ||
715 | return false | |
716 | } | |
717 | ||
718 | // unsign removes signing flags for both signed and presigned requests. | |
719 | func (ctx *signingCtx) removePresign() { | |
720 | ctx.Query.Del("X-Amz-Algorithm") | |
721 | ctx.Query.Del("X-Amz-Signature") | |
722 | ctx.Query.Del("X-Amz-Security-Token") | |
723 | ctx.Query.Del("X-Amz-Date") | |
724 | ctx.Query.Del("X-Amz-Expires") | |
725 | ctx.Query.Del("X-Amz-Credential") | |
726 | ctx.Query.Del("X-Amz-SignedHeaders") | |
727 | } | |
728 | ||
729 | func makeHmac(key []byte, data []byte) []byte { | |
730 | hash := hmac.New(sha256.New, key) | |
731 | hash.Write(data) | |
732 | return hash.Sum(nil) | |
733 | } | |
734 | ||
735 | func makeSha256(data []byte) []byte { | |
736 | hash := sha256.New() | |
737 | hash.Write(data) | |
738 | return hash.Sum(nil) | |
739 | } | |
740 | ||
741 | func makeSha256Reader(reader io.ReadSeeker) (hashBytes []byte, err error) { | |
742 | hash := sha256.New() | |
743 | start, err := reader.Seek(0, sdkio.SeekCurrent) | |
744 | if err != nil { | |
745 | return nil, err | |
746 | } | |
747 | defer func() { | |
748 | // ensure error is return if unable to seek back to start of payload. | |
749 | _, err = reader.Seek(start, sdkio.SeekStart) | |
750 | }() | |
751 | ||
752 | // Use CopyN to avoid allocating the 32KB buffer in io.Copy for bodies | |
753 | // smaller than 32KB. Fall back to io.Copy if we fail to determine the size. | |
754 | size, err := aws.SeekerLen(reader) | |
755 | if err != nil { | |
756 | io.Copy(hash, reader) | |
757 | } else { | |
758 | io.CopyN(hash, reader, size) | |
759 | } | |
760 | ||
761 | return hash.Sum(nil), nil | |
762 | } | |
763 | ||
764 | const doubleSpace = " " | |
765 | ||
766 | // stripExcessSpaces will rewrite the passed in slice's string values to not | |
767 | // contain multiple side-by-side spaces. | |
768 | func stripExcessSpaces(vals []string) { | |
769 | var j, k, l, m, spaces int | |
770 | for i, str := range vals { | |
771 | // Trim trailing spaces | |
772 | for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- { | |
773 | } | |
774 | ||
775 | // Trim leading spaces | |
776 | for k = 0; k < j && str[k] == ' '; k++ { | |
777 | } | |
778 | str = str[k : j+1] | |
779 | ||
780 | // Strip multiple spaces. | |
781 | j = strings.Index(str, doubleSpace) | |
782 | if j < 0 { | |
783 | vals[i] = str | |
784 | continue | |
785 | } | |
786 | ||
787 | buf := []byte(str) | |
788 | for k, m, l = j, j, len(buf); k < l; k++ { | |
789 | if buf[k] == ' ' { | |
790 | if spaces == 0 { | |
791 | // First space. | |
792 | buf[m] = buf[k] | |
793 | m++ | |
794 | } | |
795 | spaces++ | |
796 | } else { | |
797 | // End of multiple spaces. | |
798 | spaces = 0 | |
799 | buf[m] = buf[k] | |
800 | m++ | |
801 | } | |
802 | } | |
803 | ||
804 | vals[i] = string(buf[:m]) | |
805 | } | |
806 | } |