escher 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
metadata CHANGED
@@ -1,69 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: escher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andras Barthazi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-19 00:00:00.000000000 Z
11
+ date: 2014-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ~>
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.6'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ~>
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.6'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ~>
32
32
  - !ruby/object:Gem::Version
33
33
  version: '10'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ~>
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ~>
46
46
  - !ruby/object:Gem::Version
47
47
  version: '2'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ~>
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: addressable
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - "~>"
73
+ - - ~>
60
74
  - !ruby/object:Gem::Version
61
75
  version: '2.3'
62
76
  type: :runtime
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - "~>"
80
+ - - ~>
67
81
  - !ruby/object:Gem::Version
68
82
  version: '2.3'
69
83
  description: Escher helps you creating secure HTTP requests (for APIs) by signing
@@ -74,16 +88,20 @@ executables: []
74
88
  extensions: []
75
89
  extra_rdoc_files: []
76
90
  files:
77
- - ".gitignore"
78
- - ".travis.yml"
91
+ - .gitignore
92
+ - .travis.yml
79
93
  - Gemfile
80
94
  - LICENSE
81
95
  - README.md
82
96
  - Rakefile
83
97
  - escher.gemspec
84
98
  - lib/escher.rb
85
- - lib/escher/base.rb
86
- - lib/escher/request.rb
99
+ - lib/escher/auth.rb
100
+ - lib/escher/request/base.rb
101
+ - lib/escher/request/factory.rb
102
+ - lib/escher/request/hash_request.rb
103
+ - lib/escher/request/legacy_request.rb
104
+ - lib/escher/request/rack_request.rb
87
105
  - lib/escher/version.rb
88
106
  - spec/aws4_testsuite/get-header-key-duplicate.authz
89
107
  - spec/aws4_testsuite/get-header-key-duplicate.creq
@@ -261,9 +279,12 @@ files:
261
279
  - spec/emarsys_testsuite/post-header-value-spaces.req
262
280
  - spec/emarsys_testsuite/post-header-value-spaces.sreq
263
281
  - spec/emarsys_testsuite/post-header-value-spaces.sts
264
- - spec/escher_spec.rb
282
+ - spec/escher/auth_spec.rb
283
+ - spec/escher/request/factory_spec.rb
284
+ - spec/escher/request/hash_request_spec.rb
285
+ - spec/escher/request/rack_request_spec.rb
265
286
  - spec/spec_helper.rb
266
- homepage: https://github.com/emartech/escher-ruby
287
+ homepage: http://escherauth.io/
267
288
  licenses:
268
289
  - MIT
269
290
  metadata: {}
@@ -273,17 +294,17 @@ require_paths:
273
294
  - lib
274
295
  required_ruby_version: !ruby/object:Gem::Requirement
275
296
  requirements:
276
- - - ">="
297
+ - - '>='
277
298
  - !ruby/object:Gem::Version
278
- version: '0'
299
+ version: '1.9'
279
300
  required_rubygems_version: !ruby/object:Gem::Requirement
280
301
  requirements:
281
- - - ">="
302
+ - - '>='
282
303
  - !ruby/object:Gem::Version
283
304
  version: '0'
284
305
  requirements: []
285
306
  rubyforge_project:
286
- rubygems_version: 2.2.2
307
+ rubygems_version: 2.0.14
287
308
  signing_key:
288
309
  specification_version: 4
289
310
  summary: Library for HTTP request signing (Ruby implementation)
@@ -464,5 +485,8 @@ test_files:
464
485
  - spec/emarsys_testsuite/post-header-value-spaces.req
465
486
  - spec/emarsys_testsuite/post-header-value-spaces.sreq
466
487
  - spec/emarsys_testsuite/post-header-value-spaces.sts
467
- - spec/escher_spec.rb
488
+ - spec/escher/auth_spec.rb
489
+ - spec/escher/request/factory_spec.rb
490
+ - spec/escher/request/hash_request_spec.rb
491
+ - spec/escher/request/rack_request_spec.rb
468
492
  - spec/spec_helper.rb
data/lib/escher/base.rb DELETED
@@ -1,324 +0,0 @@
1
- require 'escher/version'
2
-
3
- require 'time'
4
- require 'digest'
5
- require 'pathname'
6
- require 'addressable/uri'
7
-
8
- class EscherError < RuntimeError
9
- end
10
-
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
23
-
24
- def sign!(request, client, headers_to_sign = [])
25
- uri = Addressable::URI.parse(request[:uri])
26
- body = request[:body] || ''
27
- headers = request[:headers].map {|k, v| {k.downcase => v} }.reduce({}, &:merge)
28
-
29
- host = headers['host'] || uri.host || request[:host]
30
-
31
- unless headers.has_key? 'host'
32
- headers['host'] = host
33
- end
34
- unless headers.has_key? @date_header_name
35
- headers[@date_header_name] = format_date_for_header
36
- end
37
-
38
- headers_to_sign |= [@date_header_name.downcase, 'host']
39
-
40
-
41
- auth_header = generate_auth_header(client, request[:method], uri.path + (uri.query ? '?' + uri.query : ''), body, headers.map { |k, v| [k, v] }, headers_to_sign)
42
-
43
- request[:headers] = headers.merge(@auth_header_name => auth_header)
44
- request
45
- end
46
-
47
- def is_valid?(*args)
48
- begin
49
- authenticate(*args)
50
- return true
51
- rescue
52
- return false
53
- end
54
- end
55
-
56
- def authenticate(req, key_db)
57
- request = EscherRequest.new(req)
58
- method = request.method
59
- body = request.body
60
- headers = request.headers
61
- path = request.path
62
- query_parts = request.query_values
63
-
64
- signature_from_query = get_signing_param('Signature', query_parts)
65
-
66
- validate_headers(headers, !signature_from_query)
67
-
68
- if method == 'GET' && signature_from_query
69
- raw_date = get_signing_param('Date', query_parts)
70
- algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_query(query_parts)
71
-
72
- body = 'UNSIGNED-PAYLOAD'
73
- query_parts.delete [query_key_for('Signature'), signature]
74
- query_parts = query_parts.map { |k, v| [uri_decode(k), uri_decode(v)] }
75
- else
76
- raw_date = get_header(@date_header_name, headers)
77
- auth_header = get_header(@auth_header_name, headers)
78
- algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_header(auth_header)
79
- end
80
-
81
- date = Time.parse(raw_date)
82
- api_secret = key_db[api_key_id]
83
-
84
- raise EscherError, 'Invalid API key' unless api_secret
85
- raise EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed' unless %w(SHA256 SHA512).include?(algorithm)
86
- raise EscherError, 'Invalid request date' unless short_date(date) == short_date
87
- raise EscherError, 'The request date is not within the accepted time range' unless is_date_within_range?(date, expires)
88
- raise EscherError, 'Invalid credentials' unless credential_scope == @credential_scope
89
- raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
90
- raise EscherError, 'Only the host header should be signed' if signature_from_query && signed_headers != ['host']
91
- raise EscherError, 'Date header is not signed' if !signature_from_query && !signed_headers.include?(@date_header_name.downcase)
92
-
93
- escher = reconfig(algorithm, credential_scope, date)
94
- expected_signature = escher.generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
95
- raise EscherError, 'The signatures do not match' unless signature == expected_signature
96
- api_key_id
97
- end
98
-
99
- def validate_headers(headers, authenticated_by_header)
100
- (['Host'] + (authenticated_by_header ? [@auth_header_name, @date_header_name] : [])).each do |header|
101
- raise EscherError, 'Missing header: ' + header unless get_header(header, headers)
102
- end
103
- end
104
-
105
- def reconfig(algorithm, credential_scope, date)
106
- Escher.new(
107
- credential_scope,
108
- algo_prefix: @algo_prefix,
109
- vendor_key: @vendor_key,
110
- hash_algo: algorithm,
111
- auth_header_name: @auth_header_name,
112
- date_header_name: @date_header_name,
113
- current_time: date
114
- )
115
- end
116
-
117
- def generate_auth_header(client, method, request_uri, body, headers, headers_to_sign)
118
- path, query_parts = parse_uri(request_uri)
119
- signature = generate_signature(client[:api_secret], body, headers, method, headers_to_sign, path, query_parts)
120
- "#{get_algorithm_id} Credential=#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}, SignedHeaders=#{prepare_headers_to_sign headers_to_sign}, Signature=#{signature}"
121
- end
122
-
123
- def generate_signed_url(url_to_sign, client, expires = 86400)
124
- uri = Addressable::URI.parse(url_to_sign)
125
- protocol = uri.scheme
126
- host = uri.host
127
- path = uri.path
128
- query_parts = parse_query(uri.query)
129
-
130
- headers = [['host', host]]
131
- headers_to_sign = ['host']
132
- body = 'UNSIGNED-PAYLOAD'
133
- query_parts += get_signing_params(client, expires, headers_to_sign)
134
-
135
- signature = generate_signature(client[:api_secret], body, headers, 'GET', headers_to_sign, path, query_parts)
136
- query_parts_with_signature = (query_parts.map { |k, v| [uri_encode(k), uri_encode(v)] } << query_pair('Signature', signature))
137
-
138
- protocol + '://' + host + path + '?' + query_parts_with_signature.map { |k, v| k + '=' + v }.join('&')
139
- end
140
-
141
- def get_signing_params(client, expires, headers_to_sign)
142
- [
143
- ['Algorithm', get_algorithm_id],
144
- ['Credentials', "#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}"],
145
- ['Date', long_date(@current_time)],
146
- ['Expires', expires.to_s],
147
- ['SignedHeaders', headers_to_sign.join(';')],
148
- ].map { |k, v| query_pair(k, v) }
149
- end
150
-
151
- def query_pair(k, v)
152
- [query_key_for(k), v]
153
- end
154
-
155
- def query_key_for(key)
156
- "X-#{@vendor_key}-#{key}"
157
- end
158
-
159
- def query_key_truncate(key)
160
- key[@vendor_key.length + 3..-1]
161
- end
162
-
163
- def get_header(header_name, headers)
164
- the_header = (headers.detect { |header| header[0].downcase == header_name.downcase })
165
- the_header ? the_header[1] : nil
166
- end
167
-
168
- def get_signing_param(key, query_parts)
169
- the_param = (query_parts.detect { |param| param[0] === query_key_for(key) })
170
- the_param ? uri_decode(the_param[1]) : nil
171
- end
172
-
173
- def get_auth_parts_from_header(auth_header)
174
- 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]+)$/
175
- .match auth_header
176
- raise EscherError, 'Malformed authorization header' unless m && m['credentials']
177
- return m['algo'], m['api_key_id'], m['short_date'], m['credentials'], m['signed_headers'].split(';'), m['signature'], 0
178
- end
179
-
180
- def get_auth_parts_from_query(query_parts)
181
- expires = get_signing_param('Expires', query_parts).to_i
182
- api_key_id, short_date, credential_scope = get_signing_param('Credentials', query_parts).split('/', 3)
183
- signed_headers = get_signing_param('SignedHeaders', query_parts).split ';'
184
- algorithm = parse_algo(get_signing_param('Algorithm', query_parts))
185
- signature = get_signing_param('Signature', query_parts)
186
- return algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires
187
- end
188
-
189
- def generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
190
- canonicalized_request = canonicalize(method, path, query_parts, body, headers, signed_headers.uniq)
191
- string_to_sign = get_string_to_sign(canonicalized_request)
192
- signing_key = calculate_signing_key(api_secret)
193
- Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo)
194
- end
195
-
196
- def format_date_for_header
197
- @date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
198
- end
199
-
200
- def canonicalize(method, path, query_parts, body, headers, headers_to_sign) [
201
- method,
202
- canonicalize_path(path),
203
- canonicalize_query(query_parts),
204
- canonicalize_headers(headers, headers_to_sign).join("\n"),
205
- '',
206
- prepare_headers_to_sign(headers_to_sign),
207
- create_algo.new.hexdigest(body || '') # TODO: we should set the default value at the same level at every implementation
208
- ].join "\n"
209
- end
210
-
211
- def prepare_headers_to_sign(headers_to_sign)
212
- headers_to_sign.sort.uniq.join(';')
213
- end
214
-
215
- def parse_uri(request_uri)
216
- path, query = request_uri.split '?', 2
217
- return path, parse_query(query)
218
- end
219
-
220
- def parse_query(query)
221
- (query || '')
222
- .split('&', -1)
223
- .map { |pair| pair.split('=', -1) }
224
- .map { |k, v| (k.include?' ') ? [k.str(/\S+/), ''] : [k, v] }
225
- end
226
-
227
- def get_string_to_sign(canonicalized_req)
228
- [
229
- get_algorithm_id,
230
- long_date(@current_time),
231
- short_date(@current_time) + '/' + @credential_scope,
232
- create_algo.new.hexdigest(canonicalized_req)
233
- ].join("\n")
234
- end
235
-
236
- def create_algo
237
- case @hash_algo
238
- when 'SHA256'
239
- return Digest::SHA2.new 256
240
- when 'SHA512'
241
- return Digest::SHA2.new 512
242
- else
243
- raise EscherError, 'Unidentified hash algorithm'
244
- end
245
- end
246
-
247
- def long_date(date)
248
- date.utc.strftime('%Y%m%dT%H%M%SZ')
249
- end
250
-
251
- def short_date(date)
252
- date.utc.strftime('%Y%m%d')
253
- end
254
-
255
- def is_date_within_range?(request_date, expires)
256
- (request_date - @clock_skew .. request_date + expires + @clock_skew).cover? @current_time
257
- end
258
-
259
- def get_algorithm_id
260
- @algo_prefix + '-HMAC-' + @hash_algo
261
- end
262
-
263
- def parse_algo(algorithm)
264
- m = /^#{@algo_prefix}-HMAC-(?<algo>[A-Z0-9\,]+)$/.match(algorithm)
265
- m && m['algo']
266
- end
267
-
268
- def calculate_signing_key(api_secret)
269
- algo = create_algo
270
- signing_key = @algo_prefix + api_secret
271
- key_parts = [short_date(@current_time)] + @credential_scope.split('/')
272
- key_parts.each { |data|
273
- signing_key = Digest::HMAC.digest(data, signing_key, algo)
274
- }
275
- signing_key
276
- end
277
-
278
- def canonicalize_path(path)
279
- while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
280
- path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
281
- end
282
-
283
- def canonicalize_headers(raw_headers, headers_to_sign)
284
- collect_headers(raw_headers)
285
- .sort
286
- .select { |h| headers_to_sign.include?(h[0]) }
287
- .map { |k, v| k + ':' + v.map { |piece| normalize_white_spaces piece} .join(',') }
288
- end
289
-
290
- def normalize_white_spaces(value)
291
- value.strip.split('"', -1).map.with_index { |piece, index|
292
- is_inside_of_quotes = (index % 2 === 1)
293
- is_inside_of_quotes ? piece : piece.gsub(/\s+/, ' ')
294
- }.join '"'
295
- end
296
-
297
- def collect_headers(raw_headers)
298
- headers = {}
299
- raw_headers.each do |raw_header|
300
- if raw_header[0].downcase != @auth_header_name.downcase
301
- if headers[raw_header[0].downcase]
302
- headers[raw_header[0].downcase] << raw_header[1]
303
- else
304
- headers[raw_header[0].downcase] = [raw_header[1]]
305
- end
306
- end
307
- end
308
- headers
309
- end
310
-
311
- def canonicalize_query(query_parts)
312
- query_parts
313
- .map { |k, v| uri_encode(k.gsub('+', ' ')) + '=' + uri_encode(v || '') }
314
- .sort.join '&'
315
- end
316
-
317
- def uri_encode(component)
318
- Addressable::URI.encode_component(component, Addressable::URI::CharacterClasses::UNRESERVED)
319
- end
320
-
321
- def uri_decode(component)
322
- Addressable::URI.unencode_component(component)
323
- end
324
- end
@@ -1,83 +0,0 @@
1
-
2
- class EscherRequest
3
-
4
- def initialize(request)
5
- @request = request
6
- request_uri = Addressable::URI.parse(uri)
7
- raise "Invalid request URI: #{request_uri}" unless request_uri
8
- @request_uri = request_uri
9
- prepare_request_headers
10
- end
11
-
12
- def prepare_request_headers
13
- @request_headers = []
14
- case @request.class.to_s
15
- when 'Hash'
16
- @request_headers = @request[:headers]
17
- when 'Sinatra::Request' # TODO: not working yet
18
- @request.env.each { |key, value|
19
- if key.downcase[0, 5] == "http_"
20
- @request_headers += [[ key[5..-1].gsub("_", "-"), value ]]
21
- end
22
- }
23
- when 'WEBrick::HTTPRequest'
24
- @request.header.each { |key, values|
25
- values.each { |value|
26
- @request_headers += [[ key, value ]]
27
- }
28
- }
29
- end
30
- end
31
-
32
- def request
33
- @request
34
- end
35
-
36
- def headers
37
- @request_headers
38
- end
39
-
40
- def set_header(key, value)
41
- @request[key] = value
42
- end
43
-
44
- def method
45
- case @request.class.to_s
46
- when 'Hash'
47
- @request[:method]
48
- else
49
- @request.request_method
50
- end
51
- end
52
-
53
- def uri
54
- case @request.class.to_s
55
- when 'Hash'
56
- @request[:uri]
57
- else
58
- @request.uri
59
- end
60
- end
61
-
62
- def body
63
- case @request.class.to_s
64
- when 'Hash'
65
- @request[:body]
66
- else
67
- @request.body
68
- end
69
- end
70
-
71
- def host
72
- @request_uri.host
73
- end
74
-
75
- def path
76
- @request_uri.path
77
- end
78
-
79
- def query_values
80
- @request_uri.query_values(Array) || []
81
- end
82
-
83
- end