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.

719 lines
22 KiB

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