request-signature-v2.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /*
  2. * MinIO Go Library for Amazon S3 Compatible Cloud Storage
  3. * Copyright 2015-2017 MinIO, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package signer
  18. import (
  19. "bytes"
  20. "crypto/hmac"
  21. "crypto/sha1"
  22. "encoding/base64"
  23. "fmt"
  24. "net/http"
  25. "net/url"
  26. "sort"
  27. "strconv"
  28. "strings"
  29. "time"
  30. "github.com/minio/minio-go/v7/pkg/s3utils"
  31. )
  32. // Signature and API related constants.
  33. const (
  34. signV2Algorithm = "AWS"
  35. )
  36. // Encode input URL path to URL encoded path.
  37. func encodeURL2Path(req *http.Request, virtualHost bool) (path string) {
  38. if virtualHost {
  39. reqHost := getHostAddr(req)
  40. dotPos := strings.Index(reqHost, ".")
  41. if dotPos > -1 {
  42. bucketName := reqHost[:dotPos]
  43. path = "/" + bucketName
  44. path += req.URL.Path
  45. path = s3utils.EncodePath(path)
  46. return
  47. }
  48. }
  49. path = s3utils.EncodePath(req.URL.Path)
  50. return
  51. }
  52. // PreSignV2 - presign the request in following style.
  53. // https://${S3_BUCKET}.s3.amazonaws.com/${S3_OBJECT}?AWSAccessKeyId=${S3_ACCESS_KEY}&Expires=${TIMESTAMP}&Signature=${SIGNATURE}.
  54. func PreSignV2(req http.Request, accessKeyID, secretAccessKey string, expires int64, virtualHost bool) *http.Request {
  55. // Presign is not needed for anonymous credentials.
  56. if accessKeyID == "" || secretAccessKey == "" {
  57. return &req
  58. }
  59. d := time.Now().UTC()
  60. // Find epoch expires when the request will expire.
  61. epochExpires := d.Unix() + expires
  62. // Add expires header if not present.
  63. if expiresStr := req.Header.Get("Expires"); expiresStr == "" {
  64. req.Header.Set("Expires", strconv.FormatInt(epochExpires, 10))
  65. }
  66. // Get presigned string to sign.
  67. stringToSign := preStringToSignV2(req, virtualHost)
  68. hm := hmac.New(sha1.New, []byte(secretAccessKey))
  69. hm.Write([]byte(stringToSign))
  70. // Calculate signature.
  71. signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
  72. query := req.URL.Query()
  73. // Handle specially for Google Cloud Storage.
  74. if strings.Contains(getHostAddr(&req), ".storage.googleapis.com") {
  75. query.Set("GoogleAccessId", accessKeyID)
  76. } else {
  77. query.Set("AWSAccessKeyId", accessKeyID)
  78. }
  79. // Fill in Expires for presigned query.
  80. query.Set("Expires", strconv.FormatInt(epochExpires, 10))
  81. // Encode query and save.
  82. req.URL.RawQuery = s3utils.QueryEncode(query)
  83. // Save signature finally.
  84. req.URL.RawQuery += "&Signature=" + s3utils.EncodePath(signature)
  85. // Return.
  86. return &req
  87. }
  88. // PostPresignSignatureV2 - presigned signature for PostPolicy
  89. // request.
  90. func PostPresignSignatureV2(policyBase64, secretAccessKey string) string {
  91. hm := hmac.New(sha1.New, []byte(secretAccessKey))
  92. hm.Write([]byte(policyBase64))
  93. signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
  94. return signature
  95. }
  96. // Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;
  97. // Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) );
  98. //
  99. // StringToSign = HTTP-Verb + "\n" +
  100. // Content-Md5 + "\n" +
  101. // Content-Type + "\n" +
  102. // Date + "\n" +
  103. // CanonicalizedProtocolHeaders +
  104. // CanonicalizedResource;
  105. //
  106. // CanonicalizedResource = [ "/" + Bucket ] +
  107. // <HTTP-Request-URI, from the protocol name up to the query string> +
  108. // [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
  109. //
  110. // CanonicalizedProtocolHeaders = <described below>
  111. // SignV2 sign the request before Do() (AWS Signature Version 2).
  112. func SignV2(req http.Request, accessKeyID, secretAccessKey string, virtualHost bool) *http.Request {
  113. // Signature calculation is not needed for anonymous credentials.
  114. if accessKeyID == "" || secretAccessKey == "" {
  115. return &req
  116. }
  117. // Initial time.
  118. d := time.Now().UTC()
  119. // Add date if not present.
  120. if date := req.Header.Get("Date"); date == "" {
  121. req.Header.Set("Date", d.Format(http.TimeFormat))
  122. }
  123. // Calculate HMAC for secretAccessKey.
  124. stringToSign := stringToSignV2(req, virtualHost)
  125. hm := hmac.New(sha1.New, []byte(secretAccessKey))
  126. hm.Write([]byte(stringToSign))
  127. // Prepare auth header.
  128. authHeader := new(bytes.Buffer)
  129. authHeader.WriteString(fmt.Sprintf("%s %s:", signV2Algorithm, accessKeyID))
  130. encoder := base64.NewEncoder(base64.StdEncoding, authHeader)
  131. encoder.Write(hm.Sum(nil))
  132. encoder.Close()
  133. // Set Authorization header.
  134. req.Header.Set("Authorization", authHeader.String())
  135. return &req
  136. }
  137. // From the Amazon docs:
  138. //
  139. // StringToSign = HTTP-Verb + "\n" +
  140. // Content-Md5 + "\n" +
  141. // Content-Type + "\n" +
  142. // Expires + "\n" +
  143. // CanonicalizedProtocolHeaders +
  144. // CanonicalizedResource;
  145. func preStringToSignV2(req http.Request, virtualHost bool) string {
  146. buf := new(bytes.Buffer)
  147. // Write standard headers.
  148. writePreSignV2Headers(buf, req)
  149. // Write canonicalized protocol headers if any.
  150. writeCanonicalizedHeaders(buf, req)
  151. // Write canonicalized Query resources if any.
  152. writeCanonicalizedResource(buf, req, virtualHost)
  153. return buf.String()
  154. }
  155. // writePreSignV2Headers - write preSign v2 required headers.
  156. func writePreSignV2Headers(buf *bytes.Buffer, req http.Request) {
  157. buf.WriteString(req.Method + "\n")
  158. buf.WriteString(req.Header.Get("Content-Md5") + "\n")
  159. buf.WriteString(req.Header.Get("Content-Type") + "\n")
  160. buf.WriteString(req.Header.Get("Expires") + "\n")
  161. }
  162. // From the Amazon docs:
  163. //
  164. // StringToSign = HTTP-Verb + "\n" +
  165. // Content-Md5 + "\n" +
  166. // Content-Type + "\n" +
  167. // Date + "\n" +
  168. // CanonicalizedProtocolHeaders +
  169. // CanonicalizedResource;
  170. func stringToSignV2(req http.Request, virtualHost bool) string {
  171. buf := new(bytes.Buffer)
  172. // Write standard headers.
  173. writeSignV2Headers(buf, req)
  174. // Write canonicalized protocol headers if any.
  175. writeCanonicalizedHeaders(buf, req)
  176. // Write canonicalized Query resources if any.
  177. writeCanonicalizedResource(buf, req, virtualHost)
  178. return buf.String()
  179. }
  180. // writeSignV2Headers - write signV2 required headers.
  181. func writeSignV2Headers(buf *bytes.Buffer, req http.Request) {
  182. buf.WriteString(req.Method + "\n")
  183. buf.WriteString(req.Header.Get("Content-Md5") + "\n")
  184. buf.WriteString(req.Header.Get("Content-Type") + "\n")
  185. buf.WriteString(req.Header.Get("Date") + "\n")
  186. }
  187. // writeCanonicalizedHeaders - write canonicalized headers.
  188. func writeCanonicalizedHeaders(buf *bytes.Buffer, req http.Request) {
  189. var protoHeaders []string
  190. vals := make(map[string][]string)
  191. for k, vv := range req.Header {
  192. // All the AMZ headers should be lowercase
  193. lk := strings.ToLower(k)
  194. if strings.HasPrefix(lk, "x-amz") {
  195. protoHeaders = append(protoHeaders, lk)
  196. vals[lk] = vv
  197. }
  198. }
  199. sort.Strings(protoHeaders)
  200. for _, k := range protoHeaders {
  201. buf.WriteString(k)
  202. buf.WriteByte(':')
  203. for idx, v := range vals[k] {
  204. if idx > 0 {
  205. buf.WriteByte(',')
  206. }
  207. if strings.Contains(v, "\n") {
  208. // TODO: "Unfold" long headers that
  209. // span multiple lines (as allowed by
  210. // RFC 2616, section 4.2) by replacing
  211. // the folding white-space (including
  212. // new-line) by a single space.
  213. buf.WriteString(v)
  214. } else {
  215. buf.WriteString(v)
  216. }
  217. }
  218. buf.WriteByte('\n')
  219. }
  220. }
  221. // AWS S3 Signature V2 calculation rule is give here:
  222. // http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign
  223. // Whitelist resource list that will be used in query string for signature-V2 calculation.
  224. // The list should be alphabetically sorted
  225. var resourceList = []string{
  226. "acl",
  227. "delete",
  228. "lifecycle",
  229. "location",
  230. "logging",
  231. "notification",
  232. "partNumber",
  233. "policy",
  234. "replication",
  235. "requestPayment",
  236. "response-cache-control",
  237. "response-content-disposition",
  238. "response-content-encoding",
  239. "response-content-language",
  240. "response-content-type",
  241. "response-expires",
  242. "torrent",
  243. "uploadId",
  244. "uploads",
  245. "versionId",
  246. "versioning",
  247. "versions",
  248. "website",
  249. }
  250. // From the Amazon docs:
  251. //
  252. // CanonicalizedResource = [ "/" + Bucket ] +
  253. // <HTTP-Request-URI, from the protocol name up to the query string> +
  254. // [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
  255. func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request, virtualHost bool) {
  256. // Save request URL.
  257. requestURL := req.URL
  258. // Get encoded URL path.
  259. buf.WriteString(encodeURL2Path(&req, virtualHost))
  260. if requestURL.RawQuery != "" {
  261. var n int
  262. vals, _ := url.ParseQuery(requestURL.RawQuery)
  263. // Verify if any sub resource queries are present, if yes
  264. // canonicallize them.
  265. for _, resource := range resourceList {
  266. if vv, ok := vals[resource]; ok && len(vv) > 0 {
  267. n++
  268. // First element
  269. switch n {
  270. case 1:
  271. buf.WriteByte('?')
  272. // The rest
  273. default:
  274. buf.WriteByte('&')
  275. }
  276. buf.WriteString(resource)
  277. // Request parameters
  278. if len(vv[0]) > 0 {
  279. buf.WriteByte('=')
  280. buf.WriteString(vv[0])
  281. }
  282. }
  283. }
  284. }
  285. }