escher 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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