auth_signature_v4.go 27 KB

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