1 // Package v4 implements signing for AWS V4 signer
3 // Provides request signing for request that need to be signed with
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
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:
17 // "//<hostname>/<path>"
20 // "//example.com/some/path"
22 // The leading "//" and hostname are required or the URL.Opaque escaping will
23 // not work correctly.
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.
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
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
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.
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 failes 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.
53 // Test `TestStandaloneSign` provides a complete example of using the signer
54 // outside of the SDK and pre-escaping the URI path.
72 "github.com/aws/aws-sdk-go/aws"
73 "github.com/aws/aws-sdk-go/aws/credentials"
74 "github.com/aws/aws-sdk-go/aws/request"
75 "github.com/aws/aws-sdk-go/private/protocol/rest"
79 authHeaderPrefix = "AWS4-HMAC-SHA256"
80 timeFormat = "20060102T150405Z"
81 shortTimeFormat = "20060102"
83 // emptyStringSHA256 is a SHA256 of an empty string
84 emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
87 var ignoredHeaders = rules{
90 "Authorization": struct{}{},
91 "User-Agent": struct{}{},
92 "X-Amzn-Trace-Id": struct{}{},
97 // requiredSignedHeaders is a whitelist for build canonical headers.
98 var requiredSignedHeaders = rules{
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{}{},
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-Website-Redirect-Location": struct{}{},
140 patterns{"X-Amz-Meta-"},
143 // allowedHoisting is a whitelist for build query headers. The boolean value
144 // represents whether or not it is a pattern.
145 var allowedQueryHoisting = inclusiveRules{
146 blacklist{requiredSignedHeaders},
150 // Signer applies AWS v4 signing to given request. Use this to sign requests
151 // that need to be signed with AWS V4 Signatures.
153 // The authentication credentials the request will be signed against.
154 // This value must be set to sign requests.
155 Credentials *credentials.Credentials
157 // Sets the log level the signer should use when reporting information to
158 // the logger. If the logger is nil nothing will be logged. See
159 // aws.LogLevelType for more information on available logging levels
161 // By default nothing will be logged.
162 Debug aws.LogLevelType
164 // The logger loging information will be written to. If there the logger
165 // is nil, nothing will be logged.
168 // Disables the Signer's moving HTTP header key/value pairs from the HTTP
169 // request header to the request's query string. This is most commonly used
170 // with pre-signed requests preventing headers from being added to the
171 // request's query string.
172 DisableHeaderHoisting bool
174 // Disables the automatic escaping of the URI path of the request for the
175 // siganture's canonical string's path. For services that do not need additional
176 // escaping then use this to disable the signer escaping the path.
178 // S3 is an example of a service that does not need additional escaping.
180 // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
181 DisableURIPathEscaping bool
183 // Disales the automatical setting of the HTTP request's Body field with the
184 // io.ReadSeeker passed in to the signer. This is useful if you're using a
185 // custom wrapper around the body for the io.ReadSeeker and want to preserve
186 // the Body value on the Request.Body.
188 // This does run the risk of signing a request with a body that will not be
189 // sent in the request. Need to ensure that the underlying data of the Body
190 // values are the same.
191 DisableRequestBodyOverwrite bool
193 // currentTimeFn returns the time value which represents the current time.
194 // This value should only be used for testing. If it is nil the default
195 // time.Now will be used.
196 currentTimeFn func() time.Time
198 // UnsignedPayload will prevent signing of the payload. This will only
199 // work for services that have support for this.
203 // NewSigner returns a Signer pointer configured with the credentials and optional
204 // option values provided. If not options are provided the Signer will use its
205 // default configuration.
206 func NewSigner(credentials *credentials.Credentials, options ...func(*Signer)) *Signer {
208 Credentials: credentials,
211 for _, option := range options {
218 type signingCtx struct {
221 Request *http.Request
225 ExpireTime time.Duration
226 SignedHeaderVals http.Header
228 DisableURIPathEscaping bool
230 credValues credentials.Value
233 formattedShortTime string
238 canonicalHeaders string
239 canonicalString string
240 credentialString string
246 // Sign signs AWS v4 requests with the provided body, service name, region the
247 // request is made to, and time the request is signed at. The signTime allows
248 // you to specify that a request is signed for the future, and cannot be
251 // Returns a list of HTTP headers that were included in the signature or an
252 // error if signing the request failed. Generally for signed requests this value
253 // is not needed as the full request context will be captured by the http.Request
254 // value. It is included for reference though.
256 // Sign will set the request's Body to be the `body` parameter passed in. If
257 // the body is not already an io.ReadCloser, it will be wrapped within one. If
258 // a `nil` body parameter passed to Sign, the request's Body field will be
259 // also set to nil. Its important to note that this functionality will not
260 // change the request's ContentLength of the request.
262 // Sign differs from Presign in that it will sign the request using HTTP
263 // header values. This type of signing is intended for http.Request values that
264 // will not be shared, or are shared in a way the header values on the request
267 // The requests body is an io.ReadSeeker so the SHA256 of the body can be
268 // generated. To bypass the signer computing the hash you can set the
269 // "X-Amz-Content-Sha256" header with a precomputed value. The signer will
270 // only compute the hash if the request header value is empty.
271 func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
272 return v4.signWithBody(r, body, service, region, 0, signTime)
275 // Presign signs AWS v4 requests with the provided body, service name, region
276 // the request is made to, and time the request is signed at. The signTime
277 // allows you to specify that a request is signed for the future, and cannot
278 // be used until then.
280 // Returns a list of HTTP headers that were included in the signature or an
281 // error if signing the request failed. For presigned requests these headers
282 // and their values must be included on the HTTP request when it is made. This
283 // is helpful to know what header values need to be shared with the party the
284 // presigned request will be distributed to.
286 // Presign differs from Sign in that it will sign the request using query string
287 // instead of header values. This allows you to share the Presigned Request's
288 // URL with third parties, or distribute it throughout your system with minimal
291 // Presign also takes an exp value which is the duration the
292 // signed request will be valid after the signing time. This is allows you to
293 // set when the request will expire.
295 // The requests body is an io.ReadSeeker so the SHA256 of the body can be
296 // generated. To bypass the signer computing the hash you can set the
297 // "X-Amz-Content-Sha256" header with a precomputed value. The signer will
298 // only compute the hash if the request header value is empty.
300 // Presigning a S3 request will not compute the body's SHA256 hash by default.
301 // This is done due to the general use case for S3 presigned URLs is to share
302 // PUT/GET capabilities. If you would like to include the body's SHA256 in the
303 // presigned request's signature you can set the "X-Amz-Content-Sha256"
304 // HTTP header and that will be included in the request's signature.
305 func (v4 Signer) Presign(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) {
306 return v4.signWithBody(r, body, service, region, exp, signTime)
309 func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) {
310 currentTimeFn := v4.currentTimeFn
311 if currentTimeFn == nil {
312 currentTimeFn = time.Now
318 Query: r.URL.Query(),
322 ServiceName: service,
324 DisableURIPathEscaping: v4.DisableURIPathEscaping,
325 unsignedPayload: v4.UnsignedPayload,
328 for key := range ctx.Query {
329 sort.Strings(ctx.Query[key])
332 if ctx.isRequestSigned() {
333 ctx.Time = currentTimeFn()
334 ctx.handlePresignRemoval()
338 ctx.credValues, err = v4.Credentials.Get()
340 return http.Header{}, err
343 ctx.assignAmzQueryValues()
344 ctx.build(v4.DisableHeaderHoisting)
346 // If the request is not presigned the body should be attached to it. This
347 // prevents the confusion of wanting to send a signed request without
348 // the body the request was signed for attached.
349 if !(v4.DisableRequestBodyOverwrite || ctx.isPresign) {
350 var reader io.ReadCloser
353 if reader, ok = body.(io.ReadCloser); !ok {
354 reader = ioutil.NopCloser(body)
360 if v4.Debug.Matches(aws.LogDebugWithSigning) {
361 v4.logSigningInfo(ctx)
364 return ctx.SignedHeaderVals, nil
367 func (ctx *signingCtx) handlePresignRemoval() {
372 // The credentials have expired for this request. The current signing
373 // is invalid, and needs to be request because the request will fail.
376 // Update the request's query string to ensure the values stays in
377 // sync in the case retrieving the new credentials fails.
378 ctx.Request.URL.RawQuery = ctx.Query.Encode()
381 func (ctx *signingCtx) assignAmzQueryValues() {
383 ctx.Query.Set("X-Amz-Algorithm", authHeaderPrefix)
384 if ctx.credValues.SessionToken != "" {
385 ctx.Query.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
387 ctx.Query.Del("X-Amz-Security-Token")
393 if ctx.credValues.SessionToken != "" {
394 ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
398 // SignRequestHandler is a named request handler the SDK will use to sign
399 // service client request with using the V4 signature.
400 var SignRequestHandler = request.NamedHandler{
401 Name: "v4.SignRequestHandler", Fn: SignSDKRequest,
404 // SignSDKRequest signs an AWS request with the V4 signature. This
405 // request handler is bested used only with the SDK's built in service client's
406 // API operation requests.
408 // This function should not be used on its on its own, but in conjunction with
409 // an AWS service client's API operation call. To sign a standalone request
410 // not created by a service client's API operation method use the "Sign" or
411 // "Presign" functions of the "Signer" type.
413 // If the credentials of the request's config are set to
414 // credentials.AnonymousCredentials the request will not be signed.
415 func SignSDKRequest(req *request.Request) {
416 signSDKRequestWithCurrTime(req, time.Now)
419 // BuildNamedHandler will build a generic handler for signing.
420 func BuildNamedHandler(name string, opts ...func(*Signer)) request.NamedHandler {
421 return request.NamedHandler{
423 Fn: func(req *request.Request) {
424 signSDKRequestWithCurrTime(req, time.Now, opts...)
429 func signSDKRequestWithCurrTime(req *request.Request, curTimeFn func() time.Time, opts ...func(*Signer)) {
430 // If the request does not need to be signed ignore the signing of the
431 // request if the AnonymousCredentials object is used.
432 if req.Config.Credentials == credentials.AnonymousCredentials {
436 region := req.ClientInfo.SigningRegion
438 region = aws.StringValue(req.Config.Region)
441 name := req.ClientInfo.SigningName
443 name = req.ClientInfo.ServiceName
446 v4 := NewSigner(req.Config.Credentials, func(v4 *Signer) {
447 v4.Debug = req.Config.LogLevel.Value()
448 v4.Logger = req.Config.Logger
449 v4.DisableHeaderHoisting = req.NotHoist
450 v4.currentTimeFn = curTimeFn
452 // S3 service should not have any escaping applied
453 v4.DisableURIPathEscaping = true
455 // Prevents setting the HTTPRequest's Body. Since the Body could be
456 // wrapped in a custom io.Closer that we do not want to be stompped
457 // on top of by the signer.
458 v4.DisableRequestBodyOverwrite = true
461 for _, opt := range opts {
465 signingTime := req.Time
466 if !req.LastSignedAt.IsZero() {
467 signingTime = req.LastSignedAt
470 signedHeaders, err := v4.signWithBody(req.HTTPRequest, req.GetBody(),
471 name, region, req.ExpireTime, signingTime,
475 req.SignedHeaderVals = nil
479 req.SignedHeaderVals = signedHeaders
480 req.LastSignedAt = curTimeFn()
483 const logSignInfoMsg = `DEBUG: Request Signature:
484 ---[ CANONICAL STRING ]-----------------------------
486 ---[ STRING TO SIGN ]--------------------------------
488 -----------------------------------------------------`
489 const logSignedURLMsg = `
490 ---[ SIGNED URL ]------------------------------------
493 func (v4 *Signer) logSigningInfo(ctx *signingCtx) {
496 signedURLMsg = fmt.Sprintf(logSignedURLMsg, ctx.Request.URL.String())
498 msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg)
502 func (ctx *signingCtx) build(disableHeaderHoisting bool) {
503 ctx.buildTime() // no depends
504 ctx.buildCredentialString() // no depends
506 unsignedHeaders := ctx.Request.Header
508 if !disableHeaderHoisting {
509 urlValues := url.Values{}
510 urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends
511 for k := range urlValues {
512 ctx.Query[k] = urlValues[k]
517 ctx.buildBodyDigest()
518 ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders)
519 ctx.buildCanonicalString() // depends on canon headers / signed headers
520 ctx.buildStringToSign() // depends on canon string
521 ctx.buildSignature() // depends on string to sign
524 ctx.Request.URL.RawQuery += "&X-Amz-Signature=" + ctx.signature
527 authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString,
528 "SignedHeaders=" + ctx.signedHeaders,
529 "Signature=" + ctx.signature,
531 ctx.Request.Header.Set("Authorization", strings.Join(parts, ", "))
535 func (ctx *signingCtx) buildTime() {
536 ctx.formattedTime = ctx.Time.UTC().Format(timeFormat)
537 ctx.formattedShortTime = ctx.Time.UTC().Format(shortTimeFormat)
540 duration := int64(ctx.ExpireTime / time.Second)
541 ctx.Query.Set("X-Amz-Date", ctx.formattedTime)
542 ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
544 ctx.Request.Header.Set("X-Amz-Date", ctx.formattedTime)
548 func (ctx *signingCtx) buildCredentialString() {
549 ctx.credentialString = strings.Join([]string{
550 ctx.formattedShortTime,
557 ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString)
561 func buildQuery(r rule, header http.Header) (url.Values, http.Header) {
562 query := url.Values{}
563 unsignedHeaders := http.Header{}
564 for k, h := range header {
568 unsignedHeaders[k] = h
572 return query, unsignedHeaders
574 func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) {
576 headers = append(headers, "host")
577 for k, v := range header {
578 canonicalKey := http.CanonicalHeaderKey(k)
579 if !r.IsValid(canonicalKey) {
580 continue // ignored header
582 if ctx.SignedHeaderVals == nil {
583 ctx.SignedHeaderVals = make(http.Header)
586 lowerCaseKey := strings.ToLower(k)
587 if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok {
588 // include additional values
589 ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...)
593 headers = append(headers, lowerCaseKey)
594 ctx.SignedHeaderVals[lowerCaseKey] = v
596 sort.Strings(headers)
598 ctx.signedHeaders = strings.Join(headers, ";")
601 ctx.Query.Set("X-Amz-SignedHeaders", ctx.signedHeaders)
604 headerValues := make([]string, len(headers))
605 for i, k := range headers {
607 headerValues[i] = "host:" + ctx.Request.URL.Host
609 headerValues[i] = k + ":" +
610 strings.Join(ctx.SignedHeaderVals[k], ",")
614 ctx.canonicalHeaders = strings.Join(stripExcessSpaces(headerValues), "\n")
617 func (ctx *signingCtx) buildCanonicalString() {
618 ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1)
620 uri := getURIPath(ctx.Request.URL)
622 if !ctx.DisableURIPathEscaping {
623 uri = rest.EscapePath(uri, false)
626 ctx.canonicalString = strings.Join([]string{
629 ctx.Request.URL.RawQuery,
630 ctx.canonicalHeaders + "\n",
636 func (ctx *signingCtx) buildStringToSign() {
637 ctx.stringToSign = strings.Join([]string{
640 ctx.credentialString,
641 hex.EncodeToString(makeSha256([]byte(ctx.canonicalString))),
645 func (ctx *signingCtx) buildSignature() {
646 secret := ctx.credValues.SecretAccessKey
647 date := makeHmac([]byte("AWS4"+secret), []byte(ctx.formattedShortTime))
648 region := makeHmac(date, []byte(ctx.Region))
649 service := makeHmac(region, []byte(ctx.ServiceName))
650 credentials := makeHmac(service, []byte("aws4_request"))
651 signature := makeHmac(credentials, []byte(ctx.stringToSign))
652 ctx.signature = hex.EncodeToString(signature)
655 func (ctx *signingCtx) buildBodyDigest() {
656 hash := ctx.Request.Header.Get("X-Amz-Content-Sha256")
658 if ctx.unsignedPayload || (ctx.isPresign && ctx.ServiceName == "s3") {
659 hash = "UNSIGNED-PAYLOAD"
660 } else if ctx.Body == nil {
661 hash = emptyStringSHA256
663 hash = hex.EncodeToString(makeSha256Reader(ctx.Body))
665 if ctx.unsignedPayload || ctx.ServiceName == "s3" || ctx.ServiceName == "glacier" {
666 ctx.Request.Header.Set("X-Amz-Content-Sha256", hash)
669 ctx.bodyDigest = hash
672 // isRequestSigned returns if the request is currently signed or presigned
673 func (ctx *signingCtx) isRequestSigned() bool {
674 if ctx.isPresign && ctx.Query.Get("X-Amz-Signature") != "" {
677 if ctx.Request.Header.Get("Authorization") != "" {
684 // unsign removes signing flags for both signed and presigned requests.
685 func (ctx *signingCtx) removePresign() {
686 ctx.Query.Del("X-Amz-Algorithm")
687 ctx.Query.Del("X-Amz-Signature")
688 ctx.Query.Del("X-Amz-Security-Token")
689 ctx.Query.Del("X-Amz-Date")
690 ctx.Query.Del("X-Amz-Expires")
691 ctx.Query.Del("X-Amz-Credential")
692 ctx.Query.Del("X-Amz-SignedHeaders")
695 func makeHmac(key []byte, data []byte) []byte {
696 hash := hmac.New(sha256.New, key)
701 func makeSha256(data []byte) []byte {
707 func makeSha256Reader(reader io.ReadSeeker) []byte {
709 start, _ := reader.Seek(0, 1)
710 defer reader.Seek(start, 0)
712 io.Copy(hash, reader)
716 const doubleSpaces = " "
718 var doubleSpaceBytes = []byte(doubleSpaces)
720 func stripExcessSpaces(headerVals []string) []string {
721 vals := make([]string, len(headerVals))
722 for i, str := range headerVals {
723 // Trim leading and trailing spaces
724 trimmed := strings.TrimSpace(str)
726 idx := strings.Index(trimmed, doubleSpaces)
729 // Multiple adjacent spaces found
731 // first time create the buffer
732 buf = []byte(trimmed)
736 for j := idx + 1; j < len(buf); j++ {
738 buf = append(buf[:idx+1], buf[j:]...)
745 idx = bytes.Index(buf[stripToIdx:], doubleSpaceBytes)
755 vals[i] = string(buf)