escher 0.0.2 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (189) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +4 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +21 -0
  6. data/README.md +8 -0
  7. data/Rakefile +11 -0
  8. data/escher.gemspec +26 -0
  9. data/lib/escher/version.rb +3 -0
  10. data/lib/escher.rb +254 -110
  11. data/spec/aws4_testsuite/get-header-key-duplicate.authz +1 -0
  12. data/spec/aws4_testsuite/get-header-key-duplicate.creq +9 -0
  13. data/spec/aws4_testsuite/get-header-key-duplicate.req +7 -0
  14. data/spec/aws4_testsuite/get-header-key-duplicate.sreq +8 -0
  15. data/spec/aws4_testsuite/get-header-key-duplicate.sts +4 -0
  16. data/spec/aws4_testsuite/get-header-value-multiline.req +7 -0
  17. data/spec/aws4_testsuite/get-header-value-order.authz +1 -0
  18. data/spec/aws4_testsuite/get-header-value-order.creq +9 -0
  19. data/spec/aws4_testsuite/get-header-value-order.req +8 -0
  20. data/spec/aws4_testsuite/get-header-value-order.sreq +9 -0
  21. data/spec/aws4_testsuite/get-header-value-order.sts +4 -0
  22. data/spec/aws4_testsuite/get-header-value-trim.authz +1 -0
  23. data/spec/aws4_testsuite/get-header-value-trim.creq +9 -0
  24. data/spec/aws4_testsuite/get-header-value-trim.req +5 -0
  25. data/spec/aws4_testsuite/get-header-value-trim.sreq +6 -0
  26. data/spec/aws4_testsuite/get-header-value-trim.sts +4 -0
  27. data/spec/aws4_testsuite/get-relative-relative.authz +1 -0
  28. data/spec/aws4_testsuite/get-relative-relative.creq +8 -0
  29. data/spec/aws4_testsuite/get-relative-relative.req +4 -0
  30. data/spec/aws4_testsuite/get-relative-relative.sreq +5 -0
  31. data/spec/aws4_testsuite/get-relative-relative.sts +4 -0
  32. data/spec/aws4_testsuite/get-relative.authz +1 -0
  33. data/spec/aws4_testsuite/get-relative.creq +8 -0
  34. data/spec/aws4_testsuite/get-relative.req +4 -0
  35. data/spec/aws4_testsuite/get-relative.sreq +5 -0
  36. data/spec/aws4_testsuite/get-relative.sts +4 -0
  37. data/spec/aws4_testsuite/get-slash-dot-slash.authz +1 -0
  38. data/spec/aws4_testsuite/get-slash-dot-slash.creq +8 -0
  39. data/spec/aws4_testsuite/get-slash-dot-slash.req +4 -0
  40. data/spec/aws4_testsuite/get-slash-dot-slash.sreq +5 -0
  41. data/spec/aws4_testsuite/get-slash-dot-slash.sts +4 -0
  42. data/spec/aws4_testsuite/get-slash-pointless-dot.authz +1 -0
  43. data/spec/aws4_testsuite/get-slash-pointless-dot.creq +8 -0
  44. data/spec/aws4_testsuite/get-slash-pointless-dot.req +4 -0
  45. data/spec/aws4_testsuite/get-slash-pointless-dot.sreq +5 -0
  46. data/spec/aws4_testsuite/get-slash-pointless-dot.sts +4 -0
  47. data/spec/aws4_testsuite/get-slash.authz +1 -0
  48. data/spec/aws4_testsuite/get-slash.creq +8 -0
  49. data/spec/aws4_testsuite/get-slash.req +4 -0
  50. data/spec/aws4_testsuite/get-slash.sreq +5 -0
  51. data/spec/aws4_testsuite/get-slash.sts +4 -0
  52. data/spec/aws4_testsuite/get-slashes.authz +1 -0
  53. data/spec/aws4_testsuite/get-slashes.creq +8 -0
  54. data/spec/aws4_testsuite/get-slashes.req +4 -0
  55. data/spec/aws4_testsuite/get-slashes.sreq +5 -0
  56. data/spec/aws4_testsuite/get-slashes.sts +4 -0
  57. data/spec/aws4_testsuite/get-space.authz +1 -0
  58. data/spec/aws4_testsuite/get-space.creq +8 -0
  59. data/spec/aws4_testsuite/get-space.req +4 -0
  60. data/spec/aws4_testsuite/get-space.sreq +5 -0
  61. data/spec/aws4_testsuite/get-space.sts +4 -0
  62. data/spec/aws4_testsuite/get-unreserved.authz +1 -0
  63. data/spec/aws4_testsuite/get-unreserved.creq +8 -0
  64. data/spec/aws4_testsuite/get-unreserved.req +4 -0
  65. data/spec/aws4_testsuite/get-unreserved.sreq +5 -0
  66. data/spec/aws4_testsuite/get-unreserved.sts +4 -0
  67. data/spec/aws4_testsuite/get-utf8.authz +1 -0
  68. data/spec/aws4_testsuite/get-utf8.creq +8 -0
  69. data/spec/aws4_testsuite/get-utf8.req +4 -0
  70. data/spec/aws4_testsuite/get-utf8.sreq +5 -0
  71. data/spec/aws4_testsuite/get-utf8.sts +4 -0
  72. data/spec/aws4_testsuite/get-vanilla-empty-query-key.authz +1 -0
  73. data/spec/aws4_testsuite/get-vanilla-empty-query-key.creq +8 -0
  74. data/spec/aws4_testsuite/get-vanilla-empty-query-key.req +4 -0
  75. data/spec/aws4_testsuite/get-vanilla-empty-query-key.sreq +5 -0
  76. data/spec/aws4_testsuite/get-vanilla-empty-query-key.sts +4 -0
  77. data/spec/aws4_testsuite/get-vanilla-query-order-key-case.authz +1 -0
  78. data/spec/aws4_testsuite/get-vanilla-query-order-key-case.creq +8 -0
  79. data/spec/aws4_testsuite/get-vanilla-query-order-key-case.req +4 -0
  80. data/spec/aws4_testsuite/get-vanilla-query-order-key-case.sreq +5 -0
  81. data/spec/aws4_testsuite/get-vanilla-query-order-key-case.sts +4 -0
  82. data/spec/aws4_testsuite/get-vanilla-query-order-key.authz +1 -0
  83. data/spec/aws4_testsuite/get-vanilla-query-order-key.creq +8 -0
  84. data/spec/aws4_testsuite/get-vanilla-query-order-key.req +4 -0
  85. data/spec/aws4_testsuite/get-vanilla-query-order-key.sreq +5 -0
  86. data/spec/aws4_testsuite/get-vanilla-query-order-key.sts +4 -0
  87. data/spec/aws4_testsuite/get-vanilla-query-order-value.authz +1 -0
  88. data/spec/aws4_testsuite/get-vanilla-query-order-value.creq +8 -0
  89. data/spec/aws4_testsuite/get-vanilla-query-order-value.req +4 -0
  90. data/spec/aws4_testsuite/get-vanilla-query-order-value.sreq +5 -0
  91. data/spec/aws4_testsuite/get-vanilla-query-order-value.sts +4 -0
  92. data/spec/aws4_testsuite/get-vanilla-query-unreserved.authz +1 -0
  93. data/spec/aws4_testsuite/get-vanilla-query-unreserved.creq +8 -0
  94. data/spec/aws4_testsuite/get-vanilla-query-unreserved.req +4 -0
  95. data/spec/aws4_testsuite/get-vanilla-query-unreserved.sreq +5 -0
  96. data/spec/aws4_testsuite/get-vanilla-query-unreserved.sts +4 -0
  97. data/spec/aws4_testsuite/get-vanilla-query.authz +1 -0
  98. data/spec/aws4_testsuite/get-vanilla-query.creq +8 -0
  99. data/spec/aws4_testsuite/get-vanilla-query.req +4 -0
  100. data/spec/aws4_testsuite/get-vanilla-query.sreq +5 -0
  101. data/spec/aws4_testsuite/get-vanilla-query.sts +4 -0
  102. data/spec/aws4_testsuite/get-vanilla-ut8-query.authz +1 -0
  103. data/spec/aws4_testsuite/get-vanilla-ut8-query.creq +8 -0
  104. data/spec/aws4_testsuite/get-vanilla-ut8-query.req +4 -0
  105. data/spec/aws4_testsuite/get-vanilla-ut8-query.sreq +5 -0
  106. data/spec/aws4_testsuite/get-vanilla-ut8-query.sts +4 -0
  107. data/spec/aws4_testsuite/get-vanilla.authz +1 -0
  108. data/spec/aws4_testsuite/get-vanilla.creq +8 -0
  109. data/spec/aws4_testsuite/get-vanilla.req +4 -0
  110. data/spec/aws4_testsuite/get-vanilla.sreq +5 -0
  111. data/spec/aws4_testsuite/get-vanilla.sts +4 -0
  112. data/spec/aws4_testsuite/post-header-key-case.authz +1 -0
  113. data/spec/aws4_testsuite/post-header-key-case.creq +8 -0
  114. data/spec/aws4_testsuite/post-header-key-case.req +4 -0
  115. data/spec/aws4_testsuite/post-header-key-case.sreq +5 -0
  116. data/spec/aws4_testsuite/post-header-key-case.sts +4 -0
  117. data/spec/aws4_testsuite/post-header-key-sort.authz +1 -0
  118. data/spec/aws4_testsuite/post-header-key-sort.creq +9 -0
  119. data/spec/aws4_testsuite/post-header-key-sort.req +5 -0
  120. data/spec/aws4_testsuite/post-header-key-sort.sreq +6 -0
  121. data/spec/aws4_testsuite/post-header-key-sort.sts +4 -0
  122. data/spec/aws4_testsuite/post-header-value-case.authz +1 -0
  123. data/spec/aws4_testsuite/post-header-value-case.creq +9 -0
  124. data/spec/aws4_testsuite/post-header-value-case.req +5 -0
  125. data/spec/aws4_testsuite/post-header-value-case.sreq +6 -0
  126. data/spec/aws4_testsuite/post-header-value-case.sts +4 -0
  127. data/spec/aws4_testsuite/post-vanilla-empty-query-value.authz +1 -0
  128. data/spec/aws4_testsuite/post-vanilla-empty-query-value.creq +8 -0
  129. data/spec/aws4_testsuite/post-vanilla-empty-query-value.req +4 -0
  130. data/spec/aws4_testsuite/post-vanilla-empty-query-value.sreq +5 -0
  131. data/spec/aws4_testsuite/post-vanilla-empty-query-value.sts +4 -0
  132. data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.authz +1 -0
  133. data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.creq +8 -0
  134. data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.req +4 -0
  135. data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.sreq +5 -0
  136. data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.sts +4 -0
  137. data/spec/aws4_testsuite/post-vanilla-query-space.authz +1 -0
  138. data/spec/aws4_testsuite/post-vanilla-query-space.creq +8 -0
  139. data/spec/aws4_testsuite/post-vanilla-query-space.req +4 -0
  140. data/spec/aws4_testsuite/post-vanilla-query-space.sreq +5 -0
  141. data/spec/aws4_testsuite/post-vanilla-query-space.sts +4 -0
  142. data/spec/aws4_testsuite/post-vanilla-query.authz +1 -0
  143. data/spec/aws4_testsuite/post-vanilla-query.creq +8 -0
  144. data/spec/aws4_testsuite/post-vanilla-query.req +4 -0
  145. data/spec/aws4_testsuite/post-vanilla-query.sreq +5 -0
  146. data/spec/aws4_testsuite/post-vanilla-query.sts +4 -0
  147. data/spec/aws4_testsuite/post-vanilla.authz +1 -0
  148. data/spec/aws4_testsuite/post-vanilla.creq +8 -0
  149. data/spec/aws4_testsuite/post-vanilla.req +4 -0
  150. data/spec/aws4_testsuite/post-vanilla.sreq +5 -0
  151. data/spec/aws4_testsuite/post-vanilla.sts +4 -0
  152. data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.authz +1 -0
  153. data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.creq +9 -0
  154. data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.req +6 -0
  155. data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.sreq +7 -0
  156. data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.sts +4 -0
  157. data/spec/aws4_testsuite/post-x-www-form-urlencoded.authz +1 -0
  158. data/spec/aws4_testsuite/post-x-www-form-urlencoded.creq +9 -0
  159. data/spec/aws4_testsuite/post-x-www-form-urlencoded.req +6 -0
  160. data/spec/aws4_testsuite/post-x-www-form-urlencoded.sreq +7 -0
  161. data/spec/aws4_testsuite/post-x-www-form-urlencoded.sts +4 -0
  162. data/spec/emarsys_testsuite/get-header-key-duplicate.authz +1 -0
  163. data/spec/emarsys_testsuite/get-header-key-duplicate.creq +9 -0
  164. data/spec/emarsys_testsuite/get-header-key-duplicate.req +7 -0
  165. data/spec/emarsys_testsuite/get-header-key-duplicate.sreq +8 -0
  166. data/spec/emarsys_testsuite/get-header-key-duplicate.sts +4 -0
  167. data/spec/emarsys_testsuite/get-header-value-order.authz +1 -0
  168. data/spec/emarsys_testsuite/get-header-value-order.creq +9 -0
  169. data/spec/emarsys_testsuite/get-header-value-order.req +8 -0
  170. data/spec/emarsys_testsuite/get-header-value-order.sreq +9 -0
  171. data/spec/emarsys_testsuite/get-header-value-order.sts +4 -0
  172. data/spec/emarsys_testsuite/post-header-key-order.authz +1 -0
  173. data/spec/emarsys_testsuite/post-header-key-order.creq +9 -0
  174. data/spec/emarsys_testsuite/post-header-key-order.req +6 -0
  175. data/spec/emarsys_testsuite/post-header-key-order.sreq +7 -0
  176. data/spec/emarsys_testsuite/post-header-key-order.sts +4 -0
  177. data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.authz +1 -0
  178. data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.creq +9 -0
  179. data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.req +5 -0
  180. data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.sreq +6 -0
  181. data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.sts +4 -0
  182. data/spec/emarsys_testsuite/post-header-value-spaces.authz +1 -0
  183. data/spec/emarsys_testsuite/post-header-value-spaces.creq +9 -0
  184. data/spec/emarsys_testsuite/post-header-value-spaces.req +6 -0
  185. data/spec/emarsys_testsuite/post-header-value-spaces.sreq +7 -0
  186. data/spec/emarsys_testsuite/post-header-value-spaces.sts +4 -0
  187. data/spec/escher_spec.rb +306 -0
  188. data/spec/spec_helper.rb +1 -0
  189. metadata +402 -31
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ee0a2544d5cff2fa6068a42800a3730bb6bc6f2b
4
+ data.tar.gz: 4b005a107aaf2b1318ba6118f2561b8401c2775b
5
+ SHA512:
6
+ metadata.gz: 89857d68c47ecf021465f9916e5527201848bea36cafbceff39db30dfb95bc2fbdceba251ab89309e1754f506b85f130dec989064e2b4b3283b17997ffac84e9
7
+ data.tar.gz: 96ebd6515e11fe2c5ce9d96a4231cf6b58d5f955fa3e36eb8cc62681e332c85dc8a4bbefc5a8e9f2a58696646b2b6f355e4d68142c6520ba09119452af06a32f
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ # Ignore bundler config.
2
+ /.bundle
3
+
4
+ .idea
5
+ *.gem
6
+ Gemfile.lock
7
+ /vendor/
8
+
9
+
10
+ # Ignore all logfiles and tempfiles.
11
+ /log/*.log
12
+ /tmp
13
+
14
+ .env
15
+ .ruby-version
16
+ .rbenv-version
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Emarsys Technologies Kft.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,8 @@
1
+ EscherRuby - HTTP request signing lib [![Build Status](https://travis-ci.org/emartech/escher-ruby.svg?branch=master)](https://travis-ci.org/emartech/escher-ruby)
2
+ =====================================
3
+
4
+ Escher helps you creating secure HTTP requests (for APIs) by signing HTTP(s) requests. It's both a server side and client side implementation. The status is work in progress.
5
+
6
+ The algorithm is based on [Amazon's _AWS Signature Version 4_](http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html), but we have generalized and extended it.
7
+
8
+ More details will be available at our [documentation site](https://documentation.emarsys.com/).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ desc "Run RSpec code examples (options: RSPEC_SEED=seed)"
5
+ RSpec::Core::RakeTask.new :spec do |task|
6
+ task.verbose = false
7
+ task.rspec_opts = "--format progress --order random"
8
+ task.rspec_opts << " --seed #{ENV['RSPEC_SEED']}" if ENV['RSPEC_SEED']
9
+ end
10
+
11
+ task :default => :spec
data/escher.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'escher/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "escher"
8
+ spec.version = Escher::VERSION
9
+ spec.authors = ["Andras Barthazi"]
10
+ spec.email = ["andras.barthazi@emarsys.com"]
11
+ spec.summary = %q{Escher - Emarsys request signing library}
12
+ spec.description = %q{For Emarsys API}
13
+ spec.homepage = "http://emarsys.com"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 10"
23
+ spec.add_development_dependency "rspec", "~> 2"
24
+
25
+ spec.add_runtime_dependency "addressable", "~> 2.3"
26
+ end
@@ -0,0 +1,3 @@
1
+ class Escher
2
+ VERSION = "0.0.5"
3
+ end
data/lib/escher.rb CHANGED
@@ -1,181 +1,325 @@
1
+ require 'escher/version'
2
+
1
3
  require 'time'
2
- require 'uri'
3
4
  require 'digest'
5
+ require 'pathname'
6
+ require 'addressable/uri'
4
7
 
5
8
  class EscherError < RuntimeError
6
9
  end
7
10
 
8
- module Escher
9
- VERSION = '0.0.2'
11
+ class Escher
12
+
13
+ def initialize(credential_scope, options)
14
+ @credential_scope = credential_scope
15
+ @algo_prefix = options[:algo_prefix] || 'ESR'
16
+ @vendor_key = options[:vendor_key] || 'Escher'
17
+ @hash_algo = options[:hash_algo] || 'SHA256'
18
+ @current_time = options[:current_time] || Time.now
19
+ @auth_header_name = options[:auth_header_name] || 'X-Escher-Auth'
20
+ @date_header_name = options[:date_header_name] || 'X-Escher-Date'
21
+ @clock_skew = options[:clock_skew] || 900
22
+ end
10
23
 
11
- def self.default_options
12
- {:auth_header_name => 'X-Ems-Auth', :date_header_name => 'X-Ems-Date', :vendor_prefix => 'EMS'}
24
+ def sign!(request, client)
25
+ uri_parsed = URI.parse(request.path)
26
+ request['Host'] = uri_parsed.host # TODO: we shouldn't remove port from Host here
27
+ request[@date_header_name] = format_date_for_header
28
+ request[@auth_header_name] = generate_auth_header(client, request.method, uri_parsed.host, uri_parsed.path, request.body || '', request.to_enum.to_a, [])
29
+ request
13
30
  end
14
31
 
15
- def self.validate_request(method, request_uri, body, headers, key_db, accepted_credentials, current_time = Time.now, options = {})
32
+ def validate(request, key_db)
33
+ headers = []
34
+ request.header.each { |key, values|
35
+ values.each { |value|
36
+ headers += [[ key, value ]]
37
+ }
38
+ }
39
+ validate_request(key_db, request.request_method, request.path, request.body, headers)
40
+ end
16
41
 
17
- options = default_options.merge(options)
18
- host = get_header('host', headers)
19
- date = get_header(options[:date_header_name], headers)
20
- auth_header = get_header(options[:auth_header_name], headers)
42
+ def is_valid?(*args)
43
+ begin
44
+ validate(*args)
45
+ return true
46
+ rescue
47
+ return false
48
+ end
49
+ end
21
50
 
22
- algo, api_key_id, short_date, credential_scope, signed_headers, signature = parse_auth_header auth_header, options[:vendor_prefix]
51
+ def validate_request(key_db, method, request_uri, body, headers)
52
+ path, query_parts = parse_uri(request_uri)
53
+ signature_from_query = get_signing_param('Signature', query_parts)
23
54
 
24
- raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
25
- raise EscherError, 'Date header is not signed' unless signed_headers.include? options[:date_header_name].downcase
26
- raise EscherError, 'Invalid request date' unless short_date(date) == short_date && within_range(current_time, date)
27
- # TODO validate host header
28
- raise EscherError, 'Invalid credentials' unless credential_scope == accepted_credentials
55
+ validate_headers(headers, signature_from_query)
29
56
 
57
+ if method == 'GET' && signature_from_query
58
+ raw_date = get_signing_param('Date', query_parts)
59
+ algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_query(query_parts)
60
+
61
+ body = 'UNSIGNED-PAYLOAD'
62
+ query_parts.delete [query_key_for('Signature'), signature]
63
+ query_parts = query_parts.map { |k, v| [uri_decode(k), uri_decode(v)] }
64
+ else
65
+ raw_date = get_header(@date_header_name, headers)
66
+ auth_header = get_header(@auth_header_name, headers)
67
+ algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_header(auth_header)
68
+ end
69
+
70
+ date = Time.parse(raw_date)
30
71
  api_secret = key_db[api_key_id]
31
72
 
32
- signature == generate_signature(algo, api_secret, body, credential_scope.join('/'), date, headers, method, signed_headers, host, request_uri, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
73
+ raise EscherError, 'Invalid API key' unless api_secret
74
+ raise EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed' unless %w(SHA256 SHA512).include?(algorithm)
75
+ raise EscherError, 'Invalid request date' unless short_date(date) == short_date
76
+ raise EscherError, 'The request date is not within the accepted time range' unless is_date_within_range?(date, expires)
77
+ raise EscherError, 'Invalid credentials' unless credential_scope == @credential_scope
78
+ raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
79
+ raise EscherError, 'Only the host header should be signed' if signature_from_query && signed_headers != ['host']
80
+ raise EscherError, 'Date header is not signed' if !signature_from_query && !signed_headers.include?(@date_header_name.downcase)
81
+
82
+ escher = reconfig(algorithm, credential_scope, date)
83
+ expected_signature = escher.generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
84
+ raise EscherError, 'The signatures do not match' unless signature == expected_signature
85
+ end
86
+
87
+ def validate_headers(headers, using_query_string_for_validation)
88
+ (['Host'] + (using_query_string_for_validation ? [] : [@auth_header_name, @date_header_name])).each do |header|
89
+ raise EscherError, 'Missing header: ' + header unless get_header(header, headers)
90
+ end
91
+ end
92
+
93
+ def reconfig(algorithm, credential_scope, date)
94
+ Escher.new(
95
+ credential_scope,
96
+ algo_prefix: @algo_prefix,
97
+ vendor_key: @vendor_key,
98
+ hash_algo: algorithm,
99
+ auth_header_name: @auth_header_name,
100
+ date_header_name: @date_header_name,
101
+ current_time: date
102
+ )
103
+ end
104
+
105
+ def generate_auth_header(client, method, host, request_uri, body, headers, headers_to_sign)
106
+ path, query_parts = parse_uri(request_uri)
107
+ headers = add_defaults_to(headers, host)
108
+ headers_to_sign |= [@date_header_name.downcase, 'host']
109
+ signature = generate_signature(client[:api_secret], body, headers, method, headers_to_sign, path, query_parts)
110
+ "#{get_algorithm_id} Credential=#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}, SignedHeaders=#{prepare_headers_to_sign headers_to_sign}, Signature=#{signature}"
111
+ end
112
+
113
+ def generate_signed_url(url_to_sign, client, expires = 86400)
114
+ uri = Addressable::URI.parse(url_to_sign)
115
+ protocol = uri.scheme
116
+ host = uri.host
117
+ path = uri.path
118
+ query_parts = parse_query(uri.query)
119
+
120
+ headers = [['host', host]]
121
+ headers_to_sign = ['host']
122
+ body = 'UNSIGNED-PAYLOAD'
123
+ query_parts += get_signing_params(client, expires, headers_to_sign)
124
+
125
+ signature = generate_signature(client[:api_secret], body, headers, 'GET', headers_to_sign, path, query_parts)
126
+ query_parts_with_signature = (query_parts.map { |k, v| [uri_encode(k), uri_encode(v)] } << query_pair('Signature', signature))
127
+
128
+ protocol + '://' + host + path + '?' + query_parts_with_signature.map { |k, v| k + '=' + v }.join('&')
129
+ end
130
+
131
+ def get_signing_params(client, expires, headers_to_sign)
132
+ [
133
+ ['Algorithm', get_algorithm_id],
134
+ ['Credentials', "#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}"],
135
+ ['Date', long_date(@current_time)],
136
+ ['Expires', expires.to_s],
137
+ ['SignedHeaders', headers_to_sign.join(';')],
138
+ ].map { |k, v| query_pair(k, v) }
139
+ end
140
+
141
+ def query_pair(k, v)
142
+ [query_key_for(k), v]
143
+ end
144
+
145
+ def query_key_for(key)
146
+ "X-#{@vendor_key}-#{key}"
33
147
  end
34
148
 
35
- def self.short_date(date)
36
- long_date(date)[0..7]
149
+ def query_key_truncate(key)
150
+ key[@vendor_key.length + 3..-1]
37
151
  end
38
152
 
39
- def self.within_range(current_time, date)
40
- (current_time - 900 .. current_time + 900).cover?(Time.parse date)
153
+ def get_header(header_name, headers)
154
+ the_header = (headers.detect { |header| header[0].downcase == header_name.downcase })
155
+ the_header ? the_header[1] : nil
41
156
  end
42
157
 
43
- def self.get_header(header_name, headers)
44
- header = (headers.detect { |header| header[0].downcase == header_name.downcase })
45
- raise EscherError, "Missing header: #{header_name.downcase}" unless header
46
- header[1]
158
+ def get_signing_param(key, query_parts)
159
+ the_param = (query_parts.detect { |param| param[0] === query_key_for(key) })
160
+ the_param ? uri_decode(the_param[1]) : nil
47
161
  end
48
162
 
49
- def self.parse_auth_header(auth_header, vendor_prefix)
50
- m = /#{vendor_prefix.upcase}-HMAC-(?<algo>[A-Z0-9\,]+) Credential=(?<api_key_id>[A-Za-z0-9\-_]+)\/(?<short_date>[0-9]{8})\/(?<credentials>[A-Za-z0-9\-_\/]+), SignedHeaders=(?<signed_headers>[A-Za-z\-;]+), Signature=(?<signature>[0-9a-f]+)$/
163
+ def get_auth_parts_from_header(auth_header)
164
+ m = /#{@algo_prefix}-HMAC-(?<algo>[A-Z0-9\,]+) Credential=(?<api_key_id>[A-Za-z0-9\-_]+)\/(?<short_date>[0-9]{8})\/(?<credentials>[A-Za-z0-9\-_\/]+), SignedHeaders=(?<signed_headers>[A-Za-z\-;]+), Signature=(?<signature>[0-9a-f]+)$/
51
165
  .match auth_header
52
166
  raise EscherError, 'Malformed authorization header' unless m && m['credentials']
53
- [
54
- m['algo'],
55
- m['api_key_id'],
56
- m['short_date'],
57
- m['credentials'].split('/'),
58
- m['signed_headers'].split(';'),
59
- m['signature'],
60
- ]
167
+ return m['algo'], m['api_key_id'], m['short_date'], m['credentials'], m['signed_headers'].split(';'), m['signature'], 0
168
+ end
169
+
170
+ def get_auth_parts_from_query(query_parts)
171
+ expires = get_signing_param('Expires', query_parts).to_i
172
+ api_key_id, short_date, credential_scope = get_signing_param('Credentials', query_parts).split('/', 3)
173
+ signed_headers = get_signing_param('SignedHeaders', query_parts).split ';'
174
+ algorithm = parse_algo(get_signing_param('Algorithm', query_parts))
175
+ signature = get_signing_param('Signature', query_parts)
176
+ return algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires
177
+ end
178
+
179
+ def generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
180
+ canonicalized_request = canonicalize(method, path, query_parts, body, headers, signed_headers.uniq)
181
+ string_to_sign = get_string_to_sign(canonicalized_request)
182
+ signing_key = calculate_signing_key(api_secret)
183
+ Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo)
184
+ end
185
+
186
+ def add_defaults_to(headers, host)
187
+ [['host', host], [@date_header_name, format_date_for_header]]
188
+ .each { |k, v| headers = add_if_missing headers, k, v }
189
+ headers
61
190
  end
62
191
 
63
- def self.generate_auth_header(client, method, host, request_uri, body, headers, headers_to_sign, date = Time.now.utc.rfc2822, algo = 'SHA256', options = {})
64
- options = default_options.merge options
65
- signature = generate_signature(algo, client[:api_secret], body, credential_scope_as_string(client), date, headers, method, headers_to_sign, host, request_uri, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
66
- "#{algo_id(options[:vendor_prefix], algo)} Credential=#{client[:api_key_id]}/#{short_date(date)}/#{credential_scope_as_string(client)}, SignedHeaders=#{headers_to_sign.uniq.join ';'}, Signature=#{signature}"
192
+ def format_date_for_header
193
+ @date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
67
194
  end
68
195
 
69
- def self.credential_scope_as_string(client)
70
- client[:credential_scope].join '/'
196
+ def add_if_missing(headers, header_to_find, value)
197
+ headers += [header_to_find, value] unless headers.find { |header| header[0].downcase == header_to_find.downcase }
198
+ headers
71
199
  end
72
200
 
73
- def self.generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, host, request_uri, vendor_prefix, auth_header_name, date_header_name)
74
- canonicalized_request = canonicalize method, host, request_uri, body, date, headers, signed_headers, algo, auth_header_name, date_header_name
75
- string_to_sign = get_string_to_sign credential_scope, canonicalized_request, date, vendor_prefix, algo
76
- signing_key = calculate_signing_key api_secret, date, vendor_prefix, credential_scope, algo
77
- calculate_signature algo, signing_key, string_to_sign
201
+ def canonicalize(method, path, query_parts, body, headers, headers_to_sign) [
202
+ method,
203
+ canonicalize_path(path),
204
+ canonicalize_query(query_parts),
205
+ canonicalize_headers(headers, headers_to_sign).join("\n"),
206
+ '',
207
+ prepare_headers_to_sign(headers_to_sign),
208
+ create_algo.new.hexdigest(body || '') # TODO: we should set the default value at the same level at every implementation
209
+ ].join "\n"
78
210
  end
79
211
 
80
- def self.calculate_signature(algo, signing_key, string_to_sign)
81
- Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo(algo))
212
+ def prepare_headers_to_sign(headers_to_sign)
213
+ headers_to_sign.sort.uniq.join(';')
82
214
  end
83
215
 
84
- def self.canonicalize(method, host, request_uri, body, date, headers, headers_to_sign, algo, auth_header_name, date_header_name)
216
+ def parse_uri(request_uri)
85
217
  path, query = request_uri.split '?', 2
218
+ return path, parse_query(query)
219
+ end
220
+
221
+ def parse_query(query)
222
+ (query || '')
223
+ .split('&', -1)
224
+ .map { |pair| pair.split('=', -1) }
225
+ .map { |k, v| (k.include?' ') ? [k.str(/\S+/), ''] : [k, v] }
226
+ end
227
+
228
+ def get_string_to_sign(canonicalized_req)
229
+ [
230
+ get_algorithm_id,
231
+ long_date(@current_time),
232
+ short_date(@current_time) + '/' + @credential_scope,
233
+ create_algo.new.hexdigest(canonicalized_req)
234
+ ].join("\n")
235
+ end
86
236
 
87
- ([
88
- method.upcase,
89
- canonicalize_path(path),
90
- canonicalize_query(query),
91
- ] + canonicalize_headers(date, host, headers, auth_header_name, date_header_name) + [
92
- '',
93
- (headers_to_sign | [date_header_name.downcase, 'host']).join(';'),
94
- request_body_hash(body, algo)
95
- ]).join "\n"
96
- end
97
-
98
- # TODO: extract algo creation
99
- def self.get_string_to_sign(credential_scope, canonicalized_request, date, prefix, algo)
100
- date = long_date(date)
101
- lines = [
102
- algo_id(prefix, algo),
103
- date,
104
- date[0..7] + '/' + credential_scope,
105
- create_algo(algo).new.hexdigest(canonicalized_request)
106
- ]
107
- lines.join "\n"
108
- end
109
-
110
- def self.create_algo(algo)
111
- case algo.upcase
237
+ def create_algo
238
+ case @hash_algo
112
239
  when 'SHA256'
113
- return Digest::SHA256
240
+ return Digest::SHA2.new 256
114
241
  when 'SHA512'
115
- return Digest::SHA512
242
+ return Digest::SHA2.new 512
116
243
  else
117
244
  raise EscherError, 'Unidentified hash algorithm'
118
245
  end
119
246
  end
120
247
 
121
- def self.long_date(date)
122
- Time.parse(date).utc.strftime("%Y%m%dT%H%M%SZ")
248
+ def long_date(date)
249
+ date.utc.strftime('%Y%m%dT%H%M%SZ')
123
250
  end
124
251
 
125
- def self.algo_id(prefix, algo)
126
- prefix + '-HMAC-' + algo
252
+ def short_date(date)
253
+ date.utc.strftime('%Y%m%d')
127
254
  end
128
255
 
129
- def self.calculate_signing_key(api_secret, date, vendor_prefix, credential_scope, algo)
130
- signing_key = vendor_prefix + api_secret
131
- for data in [short_date(date)] + credential_scope.split('/') do
132
- signing_key = Digest::HMAC.digest(data, signing_key, create_algo(algo))
133
- end
256
+ def is_date_within_range?(request_date, expires)
257
+ (request_date - @clock_skew .. request_date + expires + @clock_skew).cover? @current_time
258
+ end
259
+
260
+ def get_algorithm_id
261
+ @algo_prefix + '-HMAC-' + @hash_algo
262
+ end
263
+
264
+ def parse_algo(algorithm)
265
+ m = /^#{@algo_prefix}-HMAC-(?<algo>[A-Z0-9\,]+)$/.match(algorithm)
266
+ m && m['algo']
267
+ end
268
+
269
+ def calculate_signing_key(api_secret)
270
+ algo = create_algo
271
+ signing_key = @algo_prefix + api_secret
272
+ key_parts = [short_date(@current_time)] + @credential_scope.split('/')
273
+ key_parts.each { |data|
274
+ signing_key = Digest::HMAC.digest(data, signing_key, algo)
275
+ }
134
276
  signing_key
135
277
  end
136
278
 
137
- def self.canonicalize_path(path)
279
+ def canonicalize_path(path)
138
280
  while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
139
281
  path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
140
282
  end
141
283
 
142
- def self.canonicalize_headers(date, host, raw_headers, auth_header_name, date_header_name)
143
- collect_headers(raw_headers, auth_header_name).merge({date_header_name.downcase => [date], 'host' => [host]})
284
+ def canonicalize_headers(raw_headers, headers_to_sign)
285
+ collect_headers(raw_headers)
144
286
  .sort
145
- .map { |k, v| k + ':' + (v.sort_by { |x| x }).join(',').gsub(/\s+/, ' ').strip }
287
+ .select { |k, v| headers_to_sign.include?(k) }
288
+ .map { |k, v| k + ':' + v.map { |piece| normalize_white_spaces piece} .join(',') }
146
289
  end
147
290
 
148
- def self.collect_headers(raw_headers, auth_header_name)
291
+ def normalize_white_spaces(value)
292
+ value.strip.split('"', -1).map.with_index { |piece, index|
293
+ is_inside_of_quotes = (index % 2 === 1)
294
+ is_inside_of_quotes ? piece : piece.gsub(/\s+/, ' ')
295
+ }.join '"'
296
+ end
297
+
298
+ def collect_headers(raw_headers)
149
299
  headers = {}
150
- raw_headers.each { |raw_header|
151
- if raw_header[0].downcase != auth_header_name.downcase then
152
- if headers[raw_header[0].downcase] then
300
+ raw_headers.each do |raw_header|
301
+ if raw_header[0].downcase != @auth_header_name.downcase
302
+ if headers[raw_header[0].downcase]
153
303
  headers[raw_header[0].downcase] << raw_header[1]
154
304
  else
155
305
  headers[raw_header[0].downcase] = [raw_header[1]]
156
306
  end
157
307
  end
158
- }
308
+ end
159
309
  headers
160
310
  end
161
311
 
162
- def self.request_body_hash(body, algo)
163
- create_algo(algo).new.hexdigest body
312
+ def canonicalize_query(query_parts)
313
+ query_parts
314
+ .map { |k, v| uri_encode(k.gsub('+', ' ')) + '=' + uri_encode(v || '') }
315
+ .sort.join '&'
164
316
  end
165
317
 
166
- def self.canonicalize_query(query)
167
- query = query || ''
168
- query.split('&', -1)
169
- .map { |pair| k, v = pair.split('=', -1)
170
- if k.include? ' ' then
171
- [k.str(/\S+/), '']
172
- else
173
- [k, v]
174
- end }
175
- .map { |pair|
176
- k, v = pair;
177
- URI::encode(k.gsub('+', ' ')) + '=' + URI::encode(v || '')
178
- }
179
- .sort.join '&'
318
+ def uri_encode(component)
319
+ Addressable::URI.encode_component(component, Addressable::URI::CharacterClasses::UNRESERVED)
320
+ end
321
+
322
+ def uri_decode(component)
323
+ Addressable::URI.unencode_component(component)
180
324
  end
181
325
  end
@@ -0,0 +1 @@
1
+ AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=54afcaaf45b331f81cd2edb974f7b824ff4dd594cbbaa945ed636b48477368ed
@@ -0,0 +1,9 @@
1
+ POST
2
+ /
3
+
4
+ date:Mon, 09 Sep 2011 23:36:00 GMT
5
+ host:host.foo.com
6
+ zoo:foobar,zoobar,zoobar
7
+
8
+ date;host;zoo
9
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
@@ -0,0 +1,7 @@
1
+ POST / http/1.1
2
+ DATE:Mon, 09 Sep 2011 23:36:00 GMT
3
+ host:host.foo.com
4
+ ZOO:zoobar
5
+ zoo:foobar
6
+ zoo:zoobar
7
+
@@ -0,0 +1,8 @@
1
+ POST / http/1.1
2
+ DATE:Mon, 09 Sep 2011 23:36:00 GMT
3
+ host:host.foo.com
4
+ ZOO:zoobar
5
+ zoo:foobar
6
+ zoo:zoobar
7
+ Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=54afcaaf45b331f81cd2edb974f7b824ff4dd594cbbaa945ed636b48477368ed
8
+
@@ -0,0 +1,4 @@
1
+ AWS4-HMAC-SHA256
2
+ 20110909T233600Z
3
+ 20110909/us-east-1/host/aws4_request
4
+ 3c52f0eaae2b61329c0a332e3fa15842a37bc5812cf4d80eb64784308850e313
@@ -0,0 +1,7 @@
1
+ POST / http/1.1
2
+ DATE:Mon, 09 Sep 2011 23:36:00 GMT
3
+ host:host.foo.com
4
+ p:a
5
+ b
6
+ c
7
+
@@ -0,0 +1 @@
1
+ AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=d2973954263943b11624a11d1c963ca81fb274169c7868b2858c04f083199e3d
@@ -0,0 +1,9 @@
1
+ POST
2
+ /
3
+
4
+ date:Mon, 09 Sep 2011 23:36:00 GMT
5
+ host:host.foo.com
6
+ p:a,a,p,z
7
+
8
+ date;host;p
9
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
@@ -0,0 +1,8 @@
1
+ POST / http/1.1
2
+ DATE:Mon, 09 Sep 2011 23:36:00 GMT
3
+ host:host.foo.com
4
+ p:z
5
+ p:a
6
+ p:p
7
+ p:a
8
+
@@ -0,0 +1,9 @@
1
+ POST / http/1.1
2
+ DATE:Mon, 09 Sep 2011 23:36:00 GMT
3
+ host:host.foo.com
4
+ p:z
5
+ p:a
6
+ p:p
7
+ p:a
8
+ Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=d2973954263943b11624a11d1c963ca81fb274169c7868b2858c04f083199e3d
9
+
@@ -0,0 +1,4 @@
1
+ AWS4-HMAC-SHA256
2
+ 20110909T233600Z
3
+ 20110909/us-east-1/host/aws4_request
4
+ 94c0389fefe0988cbbedc8606f0ca0b485b48da010d09fc844b45b697c8924fe
@@ -0,0 +1 @@
1
+ AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592
@@ -0,0 +1,9 @@
1
+ POST
2
+ /
3
+
4
+ date:Mon, 09 Sep 2011 23:36:00 GMT
5
+ host:host.foo.com
6
+ p:phfft
7
+
8
+ date;host;p
9
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855