v4.go 9.2 KB

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