v4.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. // Package v4 implements signing for AWS V4 signer
  2. package v4
  3. import (
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "sort"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/aws/aws-sdk-go/aws"
  16. "github.com/aws/aws-sdk-go/aws/credentials"
  17. "github.com/aws/aws-sdk-go/aws/request"
  18. "github.com/aws/aws-sdk-go/internal/protocol/rest"
  19. )
  20. const (
  21. authHeaderPrefix = "AWS4-HMAC-SHA256"
  22. timeFormat = "20060102T150405Z"
  23. shortTimeFormat = "20060102"
  24. )
  25. var ignoredHeaders = map[string]bool{
  26. "Authorization": true,
  27. "Content-Type": true,
  28. "Content-Length": true,
  29. "User-Agent": true,
  30. }
  31. type signer struct {
  32. Request *http.Request
  33. Time time.Time
  34. ExpireTime time.Duration
  35. ServiceName string
  36. Region string
  37. CredValues credentials.Value
  38. Credentials *credentials.Credentials
  39. Query url.Values
  40. Body io.ReadSeeker
  41. Debug aws.LogLevelType
  42. Logger aws.Logger
  43. isPresign bool
  44. formattedTime string
  45. formattedShortTime string
  46. signedHeaders string
  47. canonicalHeaders string
  48. canonicalString string
  49. credentialString string
  50. stringToSign string
  51. signature string
  52. authorization string
  53. }
  54. // Sign requests with signature version 4.
  55. //
  56. // Will sign the requests with the service config's Credentials object
  57. // Signing is skipped if the credentials is the credentials.AnonymousCredentials
  58. // object.
  59. func Sign(req *request.Request) {
  60. // If the request does not need to be signed ignore the signing of the
  61. // request if the AnonymousCredentials object is used.
  62. if req.Service.Config.Credentials == credentials.AnonymousCredentials {
  63. return
  64. }
  65. region := req.Service.SigningRegion
  66. if region == "" {
  67. region = aws.StringValue(req.Service.Config.Region)
  68. }
  69. name := req.Service.SigningName
  70. if name == "" {
  71. name = req.Service.ServiceName
  72. }
  73. s := signer{
  74. Request: req.HTTPRequest,
  75. Time: req.Time,
  76. ExpireTime: req.ExpireTime,
  77. Query: req.HTTPRequest.URL.Query(),
  78. Body: req.Body,
  79. ServiceName: name,
  80. Region: region,
  81. Credentials: req.Service.Config.Credentials,
  82. Debug: req.Service.Config.LogLevel.Value(),
  83. Logger: req.Service.Config.Logger,
  84. }
  85. req.Error = s.sign()
  86. }
  87. func (v4 *signer) sign() error {
  88. if v4.ExpireTime != 0 {
  89. v4.isPresign = true
  90. }
  91. if v4.isRequestSigned() {
  92. if !v4.Credentials.IsExpired() {
  93. // If the request is already signed, and the credentials have not
  94. // expired yet ignore the signing request.
  95. return nil
  96. }
  97. // The credentials have expired for this request. The current signing
  98. // is invalid, and needs to be request because the request will fail.
  99. if v4.isPresign {
  100. v4.removePresign()
  101. // Update the request's query string to ensure the values stays in
  102. // sync in the case retrieving the new credentials fails.
  103. v4.Request.URL.RawQuery = v4.Query.Encode()
  104. }
  105. }
  106. var err error
  107. v4.CredValues, err = v4.Credentials.Get()
  108. if err != nil {
  109. return err
  110. }
  111. if v4.isPresign {
  112. v4.Query.Set("X-Amz-Algorithm", authHeaderPrefix)
  113. if v4.CredValues.SessionToken != "" {
  114. v4.Query.Set("X-Amz-Security-Token", v4.CredValues.SessionToken)
  115. } else {
  116. v4.Query.Del("X-Amz-Security-Token")
  117. }
  118. } else if v4.CredValues.SessionToken != "" {
  119. v4.Request.Header.Set("X-Amz-Security-Token", v4.CredValues.SessionToken)
  120. }
  121. v4.build()
  122. if v4.Debug.Matches(aws.LogDebugWithSigning) {
  123. v4.logSigningInfo()
  124. }
  125. return nil
  126. }
  127. const logSignInfoMsg = `DEBUG: Request Signiture:
  128. ---[ CANONICAL STRING ]-----------------------------
  129. %s
  130. ---[ STRING TO SIGN ]--------------------------------
  131. %s%s
  132. -----------------------------------------------------`
  133. const logSignedURLMsg = `
  134. ---[ SIGNED URL ]------------------------------------
  135. %s`
  136. func (v4 *signer) logSigningInfo() {
  137. signedURLMsg := ""
  138. if v4.isPresign {
  139. signedURLMsg = fmt.Sprintf(logSignedURLMsg, v4.Request.URL.String())
  140. }
  141. msg := fmt.Sprintf(logSignInfoMsg, v4.canonicalString, v4.stringToSign, signedURLMsg)
  142. v4.Logger.Log(msg)
  143. }
  144. func (v4 *signer) build() {
  145. v4.buildTime() // no depends
  146. v4.buildCredentialString() // no depends
  147. if v4.isPresign {
  148. v4.buildQuery() // no depends
  149. }
  150. v4.buildCanonicalHeaders() // depends on cred string
  151. v4.buildCanonicalString() // depends on canon headers / signed headers
  152. v4.buildStringToSign() // depends on canon string
  153. v4.buildSignature() // depends on string to sign
  154. if v4.isPresign {
  155. v4.Request.URL.RawQuery += "&X-Amz-Signature=" + v4.signature
  156. } else {
  157. parts := []string{
  158. authHeaderPrefix + " Credential=" + v4.CredValues.AccessKeyID + "/" + v4.credentialString,
  159. "SignedHeaders=" + v4.signedHeaders,
  160. "Signature=" + v4.signature,
  161. }
  162. v4.Request.Header.Set("Authorization", strings.Join(parts, ", "))
  163. }
  164. }
  165. func (v4 *signer) buildTime() {
  166. v4.formattedTime = v4.Time.UTC().Format(timeFormat)
  167. v4.formattedShortTime = v4.Time.UTC().Format(shortTimeFormat)
  168. if v4.isPresign {
  169. duration := int64(v4.ExpireTime / time.Second)
  170. v4.Query.Set("X-Amz-Date", v4.formattedTime)
  171. v4.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
  172. } else {
  173. v4.Request.Header.Set("X-Amz-Date", v4.formattedTime)
  174. }
  175. }
  176. func (v4 *signer) buildCredentialString() {
  177. v4.credentialString = strings.Join([]string{
  178. v4.formattedShortTime,
  179. v4.Region,
  180. v4.ServiceName,
  181. "aws4_request",
  182. }, "/")
  183. if v4.isPresign {
  184. v4.Query.Set("X-Amz-Credential", v4.CredValues.AccessKeyID+"/"+v4.credentialString)
  185. }
  186. }
  187. func (v4 *signer) buildQuery() {
  188. for k, h := range v4.Request.Header {
  189. if strings.HasPrefix(http.CanonicalHeaderKey(k), "X-Amz-") {
  190. continue // never hoist x-amz-* headers, they must be signed
  191. }
  192. if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok {
  193. continue // never hoist ignored headers
  194. }
  195. v4.Request.Header.Del(k)
  196. v4.Query.Del(k)
  197. for _, v := range h {
  198. v4.Query.Add(k, v)
  199. }
  200. }
  201. }
  202. func (v4 *signer) buildCanonicalHeaders() {
  203. var headers []string
  204. headers = append(headers, "host")
  205. for k := range v4.Request.Header {
  206. if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok {
  207. continue // ignored header
  208. }
  209. headers = append(headers, strings.ToLower(k))
  210. }
  211. sort.Strings(headers)
  212. v4.signedHeaders = strings.Join(headers, ";")
  213. if v4.isPresign {
  214. v4.Query.Set("X-Amz-SignedHeaders", v4.signedHeaders)
  215. }
  216. headerValues := make([]string, len(headers))
  217. for i, k := range headers {
  218. if k == "host" {
  219. headerValues[i] = "host:" + v4.Request.URL.Host
  220. } else {
  221. headerValues[i] = k + ":" +
  222. strings.Join(v4.Request.Header[http.CanonicalHeaderKey(k)], ",")
  223. }
  224. }
  225. v4.canonicalHeaders = strings.Join(headerValues, "\n")
  226. }
  227. func (v4 *signer) buildCanonicalString() {
  228. v4.Request.URL.RawQuery = strings.Replace(v4.Query.Encode(), "+", "%20", -1)
  229. uri := v4.Request.URL.Opaque
  230. if uri != "" {
  231. uri = "/" + strings.Join(strings.Split(uri, "/")[3:], "/")
  232. } else {
  233. uri = v4.Request.URL.Path
  234. }
  235. if uri == "" {
  236. uri = "/"
  237. }
  238. if v4.ServiceName != "s3" {
  239. uri = rest.EscapePath(uri, false)
  240. }
  241. v4.canonicalString = strings.Join([]string{
  242. v4.Request.Method,
  243. uri,
  244. v4.Request.URL.RawQuery,
  245. v4.canonicalHeaders + "\n",
  246. v4.signedHeaders,
  247. v4.bodyDigest(),
  248. }, "\n")
  249. }
  250. func (v4 *signer) buildStringToSign() {
  251. v4.stringToSign = strings.Join([]string{
  252. authHeaderPrefix,
  253. v4.formattedTime,
  254. v4.credentialString,
  255. hex.EncodeToString(makeSha256([]byte(v4.canonicalString))),
  256. }, "\n")
  257. }
  258. func (v4 *signer) buildSignature() {
  259. secret := v4.CredValues.SecretAccessKey
  260. date := makeHmac([]byte("AWS4"+secret), []byte(v4.formattedShortTime))
  261. region := makeHmac(date, []byte(v4.Region))
  262. service := makeHmac(region, []byte(v4.ServiceName))
  263. credentials := makeHmac(service, []byte("aws4_request"))
  264. signature := makeHmac(credentials, []byte(v4.stringToSign))
  265. v4.signature = hex.EncodeToString(signature)
  266. }
  267. func (v4 *signer) bodyDigest() string {
  268. hash := v4.Request.Header.Get("X-Amz-Content-Sha256")
  269. if hash == "" {
  270. if v4.isPresign && v4.ServiceName == "s3" {
  271. hash = "UNSIGNED-PAYLOAD"
  272. } else if v4.Body == nil {
  273. hash = hex.EncodeToString(makeSha256([]byte{}))
  274. } else {
  275. hash = hex.EncodeToString(makeSha256Reader(v4.Body))
  276. }
  277. v4.Request.Header.Add("X-Amz-Content-Sha256", hash)
  278. }
  279. return hash
  280. }
  281. // isRequestSigned returns if the request is currently signed or presigned
  282. func (v4 *signer) isRequestSigned() bool {
  283. if v4.isPresign && v4.Query.Get("X-Amz-Signature") != "" {
  284. return true
  285. }
  286. if v4.Request.Header.Get("Authorization") != "" {
  287. return true
  288. }
  289. return false
  290. }
  291. // unsign removes signing flags for both signed and presigned requests.
  292. func (v4 *signer) removePresign() {
  293. v4.Query.Del("X-Amz-Algorithm")
  294. v4.Query.Del("X-Amz-Signature")
  295. v4.Query.Del("X-Amz-Security-Token")
  296. v4.Query.Del("X-Amz-Date")
  297. v4.Query.Del("X-Amz-Expires")
  298. v4.Query.Del("X-Amz-Credential")
  299. v4.Query.Del("X-Amz-SignedHeaders")
  300. }
  301. func makeHmac(key []byte, data []byte) []byte {
  302. hash := hmac.New(sha256.New, key)
  303. hash.Write(data)
  304. return hash.Sum(nil)
  305. }
  306. func makeSha256(data []byte) []byte {
  307. hash := sha256.New()
  308. hash.Write(data)
  309. return hash.Sum(nil)
  310. }
  311. func makeSha256Reader(reader io.ReadSeeker) []byte {
  312. hash := sha256.New()
  313. start, _ := reader.Seek(0, 1)
  314. defer reader.Seek(start, 0)
  315. io.Copy(hash, reader)
  316. return hash.Sum(nil)
  317. }