You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

720 lines
22 KiB

5 years ago
5 years ago
  1. /*
  2. * The following code tries to reverse engineer the Amazon S3 APIs,
  3. * and is mostly copied from minio implementation.
  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
  14. // implied. See the License for the specific language governing
  15. // permissions and limitations under the License.
  16. package s3api
  17. import (
  18. "bytes"
  19. "crypto/hmac"
  20. "crypto/sha256"
  21. "crypto/subtle"
  22. "encoding/hex"
  23. "net/http"
  24. "net/url"
  25. "regexp"
  26. "sort"
  27. "strconv"
  28. "strings"
  29. "time"
  30. "unicode/utf8"
  31. )
  32. func (iam *IdentityAccessManagement) reqSignatureV4Verify(r *http.Request) (*Identity, ErrorCode) {
  33. sha256sum := getContentSha256Cksum(r)
  34. switch {
  35. case isRequestSignatureV4(r):
  36. return iam.doesSignatureMatch(sha256sum, r)
  37. case isRequestPresignedSignatureV4(r):
  38. return iam.doesPresignedSignatureMatch(sha256sum, r)
  39. }
  40. return nil, ErrAccessDenied
  41. }
  42. // Streaming AWS Signature Version '4' constants.
  43. const (
  44. emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
  45. streamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
  46. signV4ChunkedAlgorithm = "AWS4-HMAC-SHA256-PAYLOAD"
  47. // http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the
  48. // client did not calculate sha256 of the payload.
  49. unsignedPayload = "UNSIGNED-PAYLOAD"
  50. )
  51. // Returns SHA256 for calculating canonical-request.
  52. func getContentSha256Cksum(r *http.Request) string {
  53. var (
  54. defaultSha256Cksum string
  55. v []string
  56. ok bool
  57. )
  58. // For a presigned request we look at the query param for sha256.
  59. if isRequestPresignedSignatureV4(r) {
  60. // X-Amz-Content-Sha256, if not set in presigned requests, checksum
  61. // will default to 'UNSIGNED-PAYLOAD'.
  62. defaultSha256Cksum = unsignedPayload
  63. v, ok = r.URL.Query()["X-Amz-Content-Sha256"]
  64. if !ok {
  65. v, ok = r.Header["X-Amz-Content-Sha256"]
  66. }
  67. } else {
  68. // X-Amz-Content-Sha256, if not set in signed requests, checksum
  69. // will default to sha256([]byte("")).
  70. defaultSha256Cksum = emptySHA256
  71. v, ok = r.Header["X-Amz-Content-Sha256"]
  72. }
  73. // We found 'X-Amz-Content-Sha256' return the captured value.
  74. if ok {
  75. return v[0]
  76. }
  77. // We couldn't find 'X-Amz-Content-Sha256'.
  78. return defaultSha256Cksum
  79. }
  80. // Verify authorization header - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
  81. func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r *http.Request) (*Identity, ErrorCode) {
  82. // Copy request.
  83. req := *r
  84. // Save authorization header.
  85. v4Auth := req.Header.Get("Authorization")
  86. // Parse signature version '4' header.
  87. signV4Values, err := parseSignV4(v4Auth)
  88. if err != ErrNone {
  89. return nil, err
  90. }
  91. // Extract all the signed headers along with its values.
  92. extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
  93. if errCode != ErrNone {
  94. return nil, errCode
  95. }
  96. // Verify if the access key id matches.
  97. identity, cred, found := iam.lookupByAccessKey(signV4Values.Credential.accessKey)
  98. if !found {
  99. return nil, ErrInvalidAccessKeyID
  100. }
  101. // Extract date, if not present throw error.
  102. var date string
  103. if date = req.Header.Get(http.CanonicalHeaderKey("X-Amz-Date")); date == "" {
  104. if date = r.Header.Get("Date"); date == "" {
  105. return nil, ErrMissingDateHeader
  106. }
  107. }
  108. // Parse date header.
  109. t, e := time.Parse(iso8601Format, date)
  110. if e != nil {
  111. return nil, ErrMalformedDate
  112. }
  113. // Query string.
  114. queryStr := req.URL.Query().Encode()
  115. // Get canonical request.
  116. canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method)
  117. // Get string to sign from canonical request.
  118. stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
  119. // Get hmac signing key.
  120. signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, signV4Values.Credential.scope.region)
  121. // Calculate signature.
  122. newSignature := getSignature(signingKey, stringToSign)
  123. // Verify if signature match.
  124. if !compareSignatureV4(newSignature, signV4Values.Signature) {
  125. return nil, ErrSignatureDoesNotMatch
  126. }
  127. // Return error none.
  128. return identity, ErrNone
  129. }
  130. // credentialHeader data type represents structured form of Credential
  131. // string from authorization header.
  132. type credentialHeader struct {
  133. accessKey string
  134. scope struct {
  135. date time.Time
  136. region string
  137. service string
  138. request string
  139. }
  140. }
  141. // signValues data type represents structured form of AWS Signature V4 header.
  142. type signValues struct {
  143. Credential credentialHeader
  144. SignedHeaders []string
  145. Signature string
  146. }
  147. // Return scope string.
  148. func (c credentialHeader) getScope() string {
  149. return strings.Join([]string{
  150. c.scope.date.Format(yyyymmdd),
  151. c.scope.region,
  152. c.scope.service,
  153. c.scope.request,
  154. }, "/")
  155. }
  156. // Authorization: algorithm Credential=accessKeyID/credScope, \
  157. // SignedHeaders=signedHeaders, Signature=signature
  158. //
  159. func parseSignV4(v4Auth string) (sv signValues, aec ErrorCode) {
  160. // Replace all spaced strings, some clients can send spaced
  161. // parameters and some won't. So we pro-actively remove any spaces
  162. // to make parsing easier.
  163. v4Auth = strings.Replace(v4Auth, " ", "", -1)
  164. if v4Auth == "" {
  165. return sv, ErrAuthHeaderEmpty
  166. }
  167. // Verify if the header algorithm is supported or not.
  168. if !strings.HasPrefix(v4Auth, signV4Algorithm) {
  169. return sv, ErrSignatureVersionNotSupported
  170. }
  171. // Strip off the Algorithm prefix.
  172. v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm)
  173. authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
  174. if len(authFields) != 3 {
  175. return sv, ErrMissingFields
  176. }
  177. // Initialize signature version '4' structured header.
  178. signV4Values := signValues{}
  179. var err ErrorCode
  180. // Save credentail values.
  181. signV4Values.Credential, err = parseCredentialHeader(authFields[0])
  182. if err != ErrNone {
  183. return sv, err
  184. }
  185. // Save signed headers.
  186. signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1])
  187. if err != ErrNone {
  188. return sv, err
  189. }
  190. // Save signature.
  191. signV4Values.Signature, err = parseSignature(authFields[2])
  192. if err != ErrNone {
  193. return sv, err
  194. }
  195. // Return the structure here.
  196. return signV4Values, ErrNone
  197. }
  198. // parse credentialHeader string into its structured form.
  199. func parseCredentialHeader(credElement string) (ch credentialHeader, aec ErrorCode) {
  200. creds := strings.Split(strings.TrimSpace(credElement), "=")
  201. if len(creds) != 2 {
  202. return ch, ErrMissingFields
  203. }
  204. if creds[0] != "Credential" {
  205. return ch, ErrMissingCredTag
  206. }
  207. credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
  208. if len(credElements) != 5 {
  209. return ch, ErrCredMalformed
  210. }
  211. // Save access key id.
  212. cred := credentialHeader{
  213. accessKey: credElements[0],
  214. }
  215. var e error
  216. cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
  217. if e != nil {
  218. return ch, ErrMalformedCredentialDate
  219. }
  220. cred.scope.region = credElements[2]
  221. cred.scope.service = credElements[3] // "s3"
  222. cred.scope.request = credElements[4] // "aws4_request"
  223. return cred, ErrNone
  224. }
  225. // Parse slice of signed headers from signed headers tag.
  226. func parseSignedHeader(signedHdrElement string) ([]string, ErrorCode) {
  227. signedHdrFields := strings.Split(strings.TrimSpace(signedHdrElement), "=")
  228. if len(signedHdrFields) != 2 {
  229. return nil, ErrMissingFields
  230. }
  231. if signedHdrFields[0] != "SignedHeaders" {
  232. return nil, ErrMissingSignHeadersTag
  233. }
  234. if signedHdrFields[1] == "" {
  235. return nil, ErrMissingFields
  236. }
  237. signedHeaders := strings.Split(signedHdrFields[1], ";")
  238. return signedHeaders, ErrNone
  239. }
  240. // Parse signature from signature tag.
  241. func parseSignature(signElement string) (string, ErrorCode) {
  242. signFields := strings.Split(strings.TrimSpace(signElement), "=")
  243. if len(signFields) != 2 {
  244. return "", ErrMissingFields
  245. }
  246. if signFields[0] != "Signature" {
  247. return "", ErrMissingSignTag
  248. }
  249. if signFields[1] == "" {
  250. return "", ErrMissingFields
  251. }
  252. signature := signFields[1]
  253. return signature, ErrNone
  254. }
  255. // check query headers with presigned signature
  256. // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
  257. func (iam *IdentityAccessManagement) doesPresignedSignatureMatch(hashedPayload string, r *http.Request) (*Identity, ErrorCode) {
  258. // Copy request
  259. req := *r
  260. // Parse request query string.
  261. pSignValues, err := parsePreSignV4(req.URL.Query())
  262. if err != ErrNone {
  263. return nil, err
  264. }
  265. // Verify if the access key id matches.
  266. identity, cred, found := iam.lookupByAccessKey(pSignValues.Credential.accessKey)
  267. if !found {
  268. return nil, ErrInvalidAccessKeyID
  269. }
  270. // Extract all the signed headers along with its values.
  271. extractedSignedHeaders, errCode := extractSignedHeaders(pSignValues.SignedHeaders, r)
  272. if errCode != ErrNone {
  273. return nil, errCode
  274. }
  275. // Construct new query.
  276. query := make(url.Values)
  277. if req.URL.Query().Get("X-Amz-Content-Sha256") != "" {
  278. query.Set("X-Amz-Content-Sha256", hashedPayload)
  279. }
  280. query.Set("X-Amz-Algorithm", signV4Algorithm)
  281. now := time.Now().UTC()
  282. // If the host which signed the request is slightly ahead in time (by less than globalMaxSkewTime) the
  283. // request should still be allowed.
  284. if pSignValues.Date.After(now.Add(15 * time.Minute)) {
  285. return nil, ErrRequestNotReadyYet
  286. }
  287. if now.Sub(pSignValues.Date) > pSignValues.Expires {
  288. return nil, ErrExpiredPresignRequest
  289. }
  290. // Save the date and expires.
  291. t := pSignValues.Date
  292. expireSeconds := int(pSignValues.Expires / time.Second)
  293. // Construct the query.
  294. query.Set("X-Amz-Date", t.Format(iso8601Format))
  295. query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds))
  296. query.Set("X-Amz-SignedHeaders", getSignedHeaders(extractedSignedHeaders))
  297. query.Set("X-Amz-Credential", cred.AccessKey+"/"+getScope(t, pSignValues.Credential.scope.region))
  298. // Save other headers available in the request parameters.
  299. for k, v := range req.URL.Query() {
  300. // Handle the metadata in presigned put query string
  301. if strings.Contains(strings.ToLower(k), "x-amz-meta-") {
  302. query.Set(k, v[0])
  303. }
  304. if strings.HasPrefix(strings.ToLower(k), "x-amz") {
  305. continue
  306. }
  307. query[k] = v
  308. }
  309. // Get the encoded query.
  310. encodedQuery := query.Encode()
  311. // Verify if date query is same.
  312. if req.URL.Query().Get("X-Amz-Date") != query.Get("X-Amz-Date") {
  313. return nil, ErrSignatureDoesNotMatch
  314. }
  315. // Verify if expires query is same.
  316. if req.URL.Query().Get("X-Amz-Expires") != query.Get("X-Amz-Expires") {
  317. return nil, ErrSignatureDoesNotMatch
  318. }
  319. // Verify if signed headers query is same.
  320. if req.URL.Query().Get("X-Amz-SignedHeaders") != query.Get("X-Amz-SignedHeaders") {
  321. return nil, ErrSignatureDoesNotMatch
  322. }
  323. // Verify if credential query is same.
  324. if req.URL.Query().Get("X-Amz-Credential") != query.Get("X-Amz-Credential") {
  325. return nil, ErrSignatureDoesNotMatch
  326. }
  327. // Verify if sha256 payload query is same.
  328. if req.URL.Query().Get("X-Amz-Content-Sha256") != "" {
  329. if req.URL.Query().Get("X-Amz-Content-Sha256") != query.Get("X-Amz-Content-Sha256") {
  330. return nil, ErrContentSHA256Mismatch
  331. }
  332. }
  333. /// Verify finally if signature is same.
  334. // Get canonical request.
  335. presignedCanonicalReq := getCanonicalRequest(extractedSignedHeaders, hashedPayload, encodedQuery, req.URL.Path, req.Method)
  336. // Get string to sign from canonical request.
  337. presignedStringToSign := getStringToSign(presignedCanonicalReq, t, pSignValues.Credential.getScope())
  338. // Get hmac presigned signing key.
  339. presignedSigningKey := getSigningKey(cred.SecretKey, pSignValues.Credential.scope.date, pSignValues.Credential.scope.region)
  340. // Get new signature.
  341. newSignature := getSignature(presignedSigningKey, presignedStringToSign)
  342. // Verify signature.
  343. if !compareSignatureV4(req.URL.Query().Get("X-Amz-Signature"), newSignature) {
  344. return nil, ErrSignatureDoesNotMatch
  345. }
  346. return identity, ErrNone
  347. }
  348. func contains(list []string, elem string) bool {
  349. for _, t := range list {
  350. if t == elem {
  351. return true
  352. }
  353. }
  354. return false
  355. }
  356. // preSignValues data type represents structued form of AWS Signature V4 query string.
  357. type preSignValues struct {
  358. signValues
  359. Date time.Time
  360. Expires time.Duration
  361. }
  362. // Parses signature version '4' query string of the following form.
  363. //
  364. // querystring = X-Amz-Algorithm=algorithm
  365. // querystring += &X-Amz-Credential= urlencode(accessKey + '/' + credential_scope)
  366. // querystring += &X-Amz-Date=date
  367. // querystring += &X-Amz-Expires=timeout interval
  368. // querystring += &X-Amz-SignedHeaders=signed_headers
  369. // querystring += &X-Amz-Signature=signature
  370. //
  371. // verifies if any of the necessary query params are missing in the presigned request.
  372. func doesV4PresignParamsExist(query url.Values) ErrorCode {
  373. v4PresignQueryParams := []string{"X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Signature", "X-Amz-Date", "X-Amz-SignedHeaders", "X-Amz-Expires"}
  374. for _, v4PresignQueryParam := range v4PresignQueryParams {
  375. if _, ok := query[v4PresignQueryParam]; !ok {
  376. return ErrInvalidQueryParams
  377. }
  378. }
  379. return ErrNone
  380. }
  381. // Parses all the presigned signature values into separate elements.
  382. func parsePreSignV4(query url.Values) (psv preSignValues, aec ErrorCode) {
  383. var err ErrorCode
  384. // verify whether the required query params exist.
  385. err = doesV4PresignParamsExist(query)
  386. if err != ErrNone {
  387. return psv, err
  388. }
  389. // Verify if the query algorithm is supported or not.
  390. if query.Get("X-Amz-Algorithm") != signV4Algorithm {
  391. return psv, ErrInvalidQuerySignatureAlgo
  392. }
  393. // Initialize signature version '4' structured header.
  394. preSignV4Values := preSignValues{}
  395. // Save credential.
  396. preSignV4Values.Credential, err = parseCredentialHeader("Credential=" + query.Get("X-Amz-Credential"))
  397. if err != ErrNone {
  398. return psv, err
  399. }
  400. var e error
  401. // Save date in native time.Time.
  402. preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get("X-Amz-Date"))
  403. if e != nil {
  404. return psv, ErrMalformedPresignedDate
  405. }
  406. // Save expires in native time.Duration.
  407. preSignV4Values.Expires, e = time.ParseDuration(query.Get("X-Amz-Expires") + "s")
  408. if e != nil {
  409. return psv, ErrMalformedExpires
  410. }
  411. if preSignV4Values.Expires < 0 {
  412. return psv, ErrNegativeExpires
  413. }
  414. // Check if Expiry time is less than 7 days (value in seconds).
  415. if preSignV4Values.Expires.Seconds() > 604800 {
  416. return psv, ErrMaximumExpires
  417. }
  418. // Save signed headers.
  419. preSignV4Values.SignedHeaders, err = parseSignedHeader("SignedHeaders=" + query.Get("X-Amz-SignedHeaders"))
  420. if err != ErrNone {
  421. return psv, err
  422. }
  423. // Save signature.
  424. preSignV4Values.Signature, err = parseSignature("Signature=" + query.Get("X-Amz-Signature"))
  425. if err != ErrNone {
  426. return psv, err
  427. }
  428. // Return structed form of signature query string.
  429. return preSignV4Values, ErrNone
  430. }
  431. // extractSignedHeaders extract signed headers from Authorization header
  432. func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, ErrorCode) {
  433. reqHeaders := r.Header
  434. // find whether "host" is part of list of signed headers.
  435. // if not return ErrUnsignedHeaders. "host" is mandatory.
  436. if !contains(signedHeaders, "host") {
  437. return nil, ErrUnsignedHeaders
  438. }
  439. extractedSignedHeaders := make(http.Header)
  440. for _, header := range signedHeaders {
  441. // `host` will not be found in the headers, can be found in r.Host.
  442. // but its alway necessary that the list of signed headers containing host in it.
  443. val, ok := reqHeaders[http.CanonicalHeaderKey(header)]
  444. if ok {
  445. for _, enc := range val {
  446. extractedSignedHeaders.Add(header, enc)
  447. }
  448. continue
  449. }
  450. switch header {
  451. case "expect":
  452. // Golang http server strips off 'Expect' header, if the
  453. // client sent this as part of signed headers we need to
  454. // handle otherwise we would see a signature mismatch.
  455. // `aws-cli` sets this as part of signed headers.
  456. //
  457. // According to
  458. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20
  459. // Expect header is always of form:
  460. //
  461. // Expect = "Expect" ":" 1#expectation
  462. // expectation = "100-continue" | expectation-extension
  463. //
  464. // So it safe to assume that '100-continue' is what would
  465. // be sent, for the time being keep this work around.
  466. // Adding a *TODO* to remove this later when Golang server
  467. // doesn't filter out the 'Expect' header.
  468. extractedSignedHeaders.Set(header, "100-continue")
  469. case "host":
  470. // Go http server removes "host" from Request.Header
  471. extractedSignedHeaders.Set(header, r.Host)
  472. case "transfer-encoding":
  473. for _, enc := range r.TransferEncoding {
  474. extractedSignedHeaders.Add(header, enc)
  475. }
  476. case "content-length":
  477. // Signature-V4 spec excludes Content-Length from signed headers list for signature calculation.
  478. // But some clients deviate from this rule. Hence we consider Content-Length for signature
  479. // calculation to be compatible with such clients.
  480. extractedSignedHeaders.Set(header, strconv.FormatInt(r.ContentLength, 10))
  481. default:
  482. return nil, ErrUnsignedHeaders
  483. }
  484. }
  485. return extractedSignedHeaders, ErrNone
  486. }
  487. // getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names
  488. func getSignedHeaders(signedHeaders http.Header) string {
  489. var headers []string
  490. for k := range signedHeaders {
  491. headers = append(headers, strings.ToLower(k))
  492. }
  493. sort.Strings(headers)
  494. return strings.Join(headers, ";")
  495. }
  496. // getScope generate a string of a specific date, an AWS region, and a service.
  497. func getScope(t time.Time, region string) string {
  498. scope := strings.Join([]string{
  499. t.Format(yyyymmdd),
  500. region,
  501. "s3",
  502. "aws4_request",
  503. }, "/")
  504. return scope
  505. }
  506. // getCanonicalRequest generate a canonical request of style
  507. //
  508. // canonicalRequest =
  509. // <HTTPMethod>\n
  510. // <CanonicalURI>\n
  511. // <CanonicalQueryString>\n
  512. // <CanonicalHeaders>\n
  513. // <SignedHeaders>\n
  514. // <HashedPayload>
  515. //
  516. func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, urlPath, method string) string {
  517. rawQuery := strings.Replace(queryStr, "+", "%20", -1)
  518. encodedPath := encodePath(urlPath)
  519. canonicalRequest := strings.Join([]string{
  520. method,
  521. encodedPath,
  522. rawQuery,
  523. getCanonicalHeaders(extractedSignedHeaders),
  524. getSignedHeaders(extractedSignedHeaders),
  525. payload,
  526. }, "\n")
  527. return canonicalRequest
  528. }
  529. // getStringToSign a string based on selected query values.
  530. func getStringToSign(canonicalRequest string, t time.Time, scope string) string {
  531. stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n"
  532. stringToSign = stringToSign + scope + "\n"
  533. canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest))
  534. stringToSign = stringToSign + hex.EncodeToString(canonicalRequestBytes[:])
  535. return stringToSign
  536. }
  537. // sumHMAC calculate hmac between two input byte array.
  538. func sumHMAC(key []byte, data []byte) []byte {
  539. hash := hmac.New(sha256.New, key)
  540. hash.Write(data)
  541. return hash.Sum(nil)
  542. }
  543. // getSigningKey hmac seed to calculate final signature.
  544. func getSigningKey(secretKey string, t time.Time, region string) []byte {
  545. date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd)))
  546. regionBytes := sumHMAC(date, []byte(region))
  547. service := sumHMAC(regionBytes, []byte("s3"))
  548. signingKey := sumHMAC(service, []byte("aws4_request"))
  549. return signingKey
  550. }
  551. // getSignature final signature in hexadecimal form.
  552. func getSignature(signingKey []byte, stringToSign string) string {
  553. return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
  554. }
  555. // getCanonicalHeaders generate a list of request headers with their values
  556. func getCanonicalHeaders(signedHeaders http.Header) string {
  557. var headers []string
  558. vals := make(http.Header)
  559. for k, vv := range signedHeaders {
  560. headers = append(headers, strings.ToLower(k))
  561. vals[strings.ToLower(k)] = vv
  562. }
  563. sort.Strings(headers)
  564. var buf bytes.Buffer
  565. for _, k := range headers {
  566. buf.WriteString(k)
  567. buf.WriteByte(':')
  568. for idx, v := range vals[k] {
  569. if idx > 0 {
  570. buf.WriteByte(',')
  571. }
  572. buf.WriteString(signV4TrimAll(v))
  573. }
  574. buf.WriteByte('\n')
  575. }
  576. return buf.String()
  577. }
  578. // Trim leading and trailing spaces and replace sequential spaces with one space, following Trimall()
  579. // in http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
  580. func signV4TrimAll(input string) string {
  581. // Compress adjacent spaces (a space is determined by
  582. // unicode.IsSpace() internally here) to one space and return
  583. return strings.Join(strings.Fields(input), " ")
  584. }
  585. // if object matches reserved string, no need to encode them
  586. var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$")
  587. // EncodePath encode the strings from UTF-8 byte representations to HTML hex escape sequences
  588. //
  589. // This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8
  590. // non english characters cannot be parsed due to the nature in which url.Encode() is written
  591. //
  592. // This function on the other hand is a direct replacement for url.Encode() technique to support
  593. // pretty much every UTF-8 character.
  594. func encodePath(pathName string) string {
  595. if reservedObjectNames.MatchString(pathName) {
  596. return pathName
  597. }
  598. var encodedPathname string
  599. for _, s := range pathName {
  600. if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark)
  601. encodedPathname = encodedPathname + string(s)
  602. continue
  603. }
  604. switch s {
  605. case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark)
  606. encodedPathname = encodedPathname + string(s)
  607. continue
  608. default:
  609. len := utf8.RuneLen(s)
  610. if len < 0 {
  611. // if utf8 cannot convert return the same string as is
  612. return pathName
  613. }
  614. u := make([]byte, len)
  615. utf8.EncodeRune(u, s)
  616. for _, r := range u {
  617. hex := hex.EncodeToString([]byte{r})
  618. encodedPathname = encodedPathname + "%" + strings.ToUpper(hex)
  619. }
  620. }
  621. }
  622. return encodedPathname
  623. }
  624. // compareSignatureV4 returns true if and only if both signatures
  625. // are equal. The signatures are expected to be HEX encoded strings
  626. // according to the AWS S3 signature V4 spec.
  627. func compareSignatureV4(sig1, sig2 string) bool {
  628. // The CTC using []byte(str) works because the hex encoding
  629. // is unique for a sequence of bytes. See also compareSignatureV2.
  630. return subtle.ConstantTimeCompare([]byte(sig1), []byte(sig2)) == 1
  631. }