// Package v4 implements signing for AWS V4 signer package v4 import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "net/url" "sort" "strconv" "strings" "time" "github.com/ks3sdklib/aws-sdk-go/aws/credentials" "github.com/ks3sdklib/aws-sdk-go/internal/protocol/rest" "github.com/ks3sdklib/aws-sdk-go/aws" ) const ( authHeaderPrefix = "AWS4-HMAC-SHA256" timeFormat = "20060102T150405Z" shortTimeFormat = "20060102" ) var ignoredHeaders = map[string]bool{ "Authorization": true, "Content-Type": true, "Content-Length": true, "User-Agent": true, } type signer struct { Service *aws.Service Request *http.Request Time time.Time ExpireTime int64 ServiceName string Region string CredValues credentials.Value Credentials *credentials.Credentials Query url.Values Body io.ReadSeeker Debug uint Logger io.Writer isPresign bool isSignBody bool formattedTime string formattedShortTime string signedHeaders string canonicalHeaders string canonicalString string credentialString string stringToSign string signature string authorization string } // Sign requests with signature version 4. // // Will sign the requests with the service config's Credentials object // Signing is skipped if the credentials is the credentials.AnonymousCredentials // object. func Sign(req *aws.Request) { // If the request does not need to be signed ignore the signing of the // request if the AnonymousCredentials object is used. if req.Service.Config.Credentials == credentials.AnonymousCredentials { return } region := req.Service.SigningRegion if region == "" { region = req.Service.Config.Region } name := req.Service.SigningName if name == "" { name = req.Service.ServiceName } s := signer{ Service: req.Service, Request: req.HTTPRequest, Time: req.Time, ExpireTime: req.ExpireTime, Query: req.HTTPRequest.URL.Query(), Body: req.Body, ServiceName: name, Region: region, Credentials: req.Service.Config.Credentials, Debug: req.Service.Config.LogLevel, Logger: req.Service.Config.Logger, } if req.Service.Config.SignerVersion == "V4_UNSIGNED_PAYLOAD_SIGNER" { s.isSignBody = false } else { s.isSignBody = true } req.Error = s.sign() } func (v4 *signer) sign() error { if v4.ExpireTime != 0 { v4.isPresign = true } if v4.isRequestSigned() { if !v4.Credentials.IsExpired() { // If the request is already signed, and the credentials have not // expired yet ignore the signing request. return nil } // The credentials have expired for this request. The current signing // is invalid, and needs to be request because the request will fail. if v4.isPresign { v4.removePresign() // Update the request's query string to ensure the values stays in // sync in the case retrieving the new credentials fails. v4.Request.URL.RawQuery = v4.Query.Encode() } } var err error v4.CredValues, err = v4.Credentials.Get() if err != nil { return err } if v4.isPresign { v4.Query.Set("X-Amz-Algorithm", authHeaderPrefix) if v4.CredValues.SessionToken != "" { v4.Query.Set("X-Amz-Security-Token", v4.CredValues.SessionToken) } else { v4.Query.Del("X-Amz-Security-Token") } } else if v4.CredValues.SessionToken != "" { v4.Request.Header.Set("X-Amz-Security-Token", v4.CredValues.SessionToken) } v4.build() v4.logSigningInfo() return nil } func (v4 *signer) logSigningInfo() { cfg := v4.Service.Config cfg.LogDebug("%s", "---[ CANONICAL STRING ]-----------------------------") cfg.LogDebug("%s", v4.canonicalString) cfg.LogDebug("%s", "---[ STRING TO SIGN ]--------------------------------") cfg.LogDebug("%s", v4.stringToSign) if v4.isPresign { cfg.LogDebug("---[ SIGNED URL ]--------------------------------") cfg.LogDebug("%s", v4.Request.URL) } cfg.LogDebug("-----------------------------------------------------") } func (v4 *signer) build() { v4.buildTime() // no depends v4.buildCredentialString() // no depends if v4.isPresign { v4.buildQuery() // no depends } v4.buildCanonicalHeaders() // depends on cred string v4.buildCanonicalString() // depends on canon headers / signed headers v4.buildStringToSign() // depends on canon string v4.buildSignature() // depends on string to sign if v4.isPresign { v4.Request.URL.RawQuery += "&X-Amz-Signature=" + v4.signature } else { parts := []string{ authHeaderPrefix + " Credential=" + v4.CredValues.AccessKeyID + "/" + v4.credentialString, "SignedHeaders=" + v4.signedHeaders, "Signature=" + v4.signature, } v4.Request.Header.Set("Authorization", strings.Join(parts, ", ")) } } func (v4 *signer) buildTime() { v4.formattedTime = v4.Time.UTC().Format(timeFormat) v4.formattedShortTime = v4.Time.UTC().Format(shortTimeFormat) if v4.isPresign { duration := int64(v4.ExpireTime) v4.Query.Set("X-Amz-Date", v4.formattedTime) v4.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10)) } else { v4.Request.Header.Set("X-Amz-Date", v4.formattedTime) } } func (v4 *signer) buildCredentialString() { v4.credentialString = strings.Join([]string{ v4.formattedShortTime, v4.Region, v4.ServiceName, "aws4_request", }, "/") if v4.isPresign { v4.Query.Set("X-Amz-Credential", v4.CredValues.AccessKeyID+"/"+v4.credentialString) } } func (v4 *signer) buildQuery() { for k, h := range v4.Request.Header { if strings.HasPrefix(http.CanonicalHeaderKey(k), "X-Amz-") { continue // never hoist x-amz-* headers, they must be signed } if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok { continue // never hoist ignored headers } v4.Request.Header.Del(k) v4.Query.Del(k) for _, v := range h { v4.Query.Add(k, v) } } } func (v4 *signer) buildCanonicalHeaders() { var headers []string headers = append(headers, "host") for k := range v4.Request.Header { if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok { continue // ignored header } headers = append(headers, strings.ToLower(k)) } sort.Strings(headers) v4.signedHeaders = strings.Join(headers, ";") if v4.isPresign { v4.Query.Set("X-Amz-SignedHeaders", v4.signedHeaders) } headerValues := make([]string, len(headers)) for i, k := range headers { if k == "host" { headerValues[i] = "host:" + v4.Request.URL.Host } else { headerValues[i] = k + ":" + strings.Join(v4.Request.Header[http.CanonicalHeaderKey(k)], ",") } } v4.canonicalHeaders = strings.Join(headerValues, "\n") } func (v4 *signer) buildCanonicalString() { v4.Request.URL.RawQuery = strings.Replace(v4.Query.Encode(), "+", "%20", -1) uri := strings.Replace(v4.Request.URL.Opaque, "%2F", "/", -1) if uri != "" { uri = "/" + strings.Join(strings.Split(uri, "/")[3:], "/") } else { uri = v4.Request.URL.Path } if uri == "" { uri = "/" } if v4.ServiceName != "s3" { uri = rest.EscapePath(uri, false) } v4.canonicalString = strings.Join([]string{ v4.Request.Method, uri, v4.Request.URL.RawQuery, v4.canonicalHeaders + "\n", v4.signedHeaders, v4.bodyDigest(), }, "\n") } func (v4 *signer) buildStringToSign() { v4.stringToSign = strings.Join([]string{ authHeaderPrefix, v4.formattedTime, v4.credentialString, hex.EncodeToString(makeSha256([]byte(v4.canonicalString))), }, "\n") } func (v4 *signer) buildSignature() { secret := v4.CredValues.SecretAccessKey date := makeHmac([]byte("AWS4"+secret), []byte(v4.formattedShortTime)) region := makeHmac(date, []byte(v4.Region)) service := makeHmac(region, []byte(v4.ServiceName)) credentials := makeHmac(service, []byte("aws4_request")) signature := makeHmac(credentials, []byte(v4.stringToSign)) v4.signature = hex.EncodeToString(signature) } func (v4 *signer) bodyDigest() string { hash := v4.Request.Header.Get("X-Amz-Content-Sha256") if hash == "" { if v4.isPresign && v4.ServiceName == "s3" { hash = "UNSIGNED-PAYLOAD" } else { if v4.isSignBody { if v4.Body == nil { hash = hex.EncodeToString(makeSha256([]byte{})) } else { hash = hex.EncodeToString(makeSha256Reader(v4.Body)) } } else { hash = "UNSIGNED-PAYLOAD" } } v4.Request.Header.Add("X-Amz-Content-Sha256", hash) } return hash } // isRequestSigned returns if the request is currently signed or presigned func (v4 *signer) isRequestSigned() bool { if v4.isPresign && v4.Query.Get("X-Amz-Signature") != "" { return true } if v4.Request.Header.Get("Authorization") != "" { return true } return false } // unsign removes signing flags for both signed and presigned requests. func (v4 *signer) removePresign() { v4.Query.Del("X-Amz-Algorithm") v4.Query.Del("X-Amz-Signature") v4.Query.Del("X-Amz-Security-Token") v4.Query.Del("X-Amz-Date") v4.Query.Del("X-Amz-Expires") v4.Query.Del("X-Amz-Credential") v4.Query.Del("X-Amz-SignedHeaders") } func makeHmac(key []byte, data []byte) []byte { hash := hmac.New(sha256.New, key) hash.Write(data) return hash.Sum(nil) } func makeSha256(data []byte) []byte { hash := sha256.New() hash.Write(data) return hash.Sum(nil) } func makeSha256Reader(reader io.ReadSeeker) []byte { hash := sha256.New() start, _ := reader.Seek(0, 1) defer reader.Seek(start, 0) io.Copy(hash, reader) return hash.Sum(nil) }