escher 0.0.6 → 0.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fafe8251429eca34659b05cbfd03b41806151ddb
4
- data.tar.gz: e72cc2f8a09062dad8135608f592be74aa40e257
3
+ metadata.gz: 3076cb75aeec8d2b0bed7769215425f78e772c66
4
+ data.tar.gz: 30f7f2a24e421f7ff31fb73cc0cc1c96efb337b3
5
5
  SHA512:
6
- metadata.gz: bf8f4c964e4b95519ffa8ef0c71efe76aaef857d3687fc77c487e21d46536953f050f63c7fcc1fa65100d6c0d3789004376d0ae88c9415e8da2ba2d08fc755db
7
- data.tar.gz: 592b9154e1475bddef6b956aa4875b684b1e40cd3d014a990af1282c9940522a1d88871679f83a82607bc464eea937488e901013de5cf2e3a100467f2bbc58d8
6
+ metadata.gz: 3491c84f589d3505d461610e6d23925f186f6b1cd76af3a08277a480cb974f92cc813b12d904e3d035b062435384227954a594f9aa6c81dac616dfa7a0608067
7
+ data.tar.gz: fde96a3a6e197c600e0c0c89d21137a98e01ccbf540ffd96b3589c1a8452c15418b6c9f7642c2cdaf073d7b4804c4401245015f656385c1e9fdbe45baf8dbcc9
@@ -0,0 +1,324 @@
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!(req, client)
25
+ request = EscherRequest.new(req)
26
+ auth_header = generate_auth_header(client, request.method, uri_parsed.host, uri_parsed.path, request.body || '', request.to_enum.to_a, [])
27
+
28
+ request.set_header('Host', request.host) # TODO: we shouldn't remove port from Host here
29
+ request.set_header(@date_header_name, format_date_for_header)
30
+ request.set_header(@auth_header_name, auth_header)
31
+ request
32
+ end
33
+
34
+ def is_valid?(*args)
35
+ begin
36
+ authenticate(*args)
37
+ return true
38
+ rescue
39
+ return false
40
+ end
41
+ end
42
+
43
+ def authenticate(req, key_db)
44
+ request = EscherRequest.new(req)
45
+ method = request.method
46
+ body = request.body
47
+ headers = request.headers
48
+ path = request.path
49
+ query_parts = request.query_values
50
+
51
+ signature_from_query = get_signing_param('Signature', query_parts)
52
+
53
+ validate_headers(headers, !signature_from_query)
54
+
55
+ if method == 'GET' && signature_from_query
56
+ raw_date = get_signing_param('Date', query_parts)
57
+ algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_query(query_parts)
58
+
59
+ body = 'UNSIGNED-PAYLOAD'
60
+ query_parts.delete [query_key_for('Signature'), signature]
61
+ query_parts = query_parts.map { |k, v| [uri_decode(k), uri_decode(v)] }
62
+ else
63
+ raw_date = get_header(@date_header_name, headers)
64
+ auth_header = get_header(@auth_header_name, headers)
65
+ algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_header(auth_header)
66
+ end
67
+
68
+ date = Time.parse(raw_date)
69
+ api_secret = key_db[api_key_id]
70
+
71
+ raise EscherError, 'Invalid API key' unless api_secret
72
+ raise EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed' unless %w(SHA256 SHA512).include?(algorithm)
73
+ raise EscherError, 'Invalid request date' unless short_date(date) == short_date
74
+ raise EscherError, 'The request date is not within the accepted time range' unless is_date_within_range?(date, expires)
75
+ raise EscherError, 'Invalid credentials' unless credential_scope == @credential_scope
76
+ raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
77
+ raise EscherError, 'Only the host header should be signed' if signature_from_query && signed_headers != ['host']
78
+ raise EscherError, 'Date header is not signed' if !signature_from_query && !signed_headers.include?(@date_header_name.downcase)
79
+
80
+ escher = reconfig(algorithm, credential_scope, date)
81
+ expected_signature = escher.generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
82
+ raise EscherError, 'The signatures do not match' unless signature == expected_signature
83
+ api_key_id
84
+ end
85
+
86
+ def validate_headers(headers, authenticated_by_header)
87
+ (['Host'] + (authenticated_by_header ? [@auth_header_name, @date_header_name] : [])).each do |header|
88
+ raise EscherError, 'Missing header: ' + header unless get_header(header, headers)
89
+ end
90
+ end
91
+
92
+ def reconfig(algorithm, credential_scope, date)
93
+ Escher.new(
94
+ credential_scope,
95
+ algo_prefix: @algo_prefix,
96
+ vendor_key: @vendor_key,
97
+ hash_algo: algorithm,
98
+ auth_header_name: @auth_header_name,
99
+ date_header_name: @date_header_name,
100
+ current_time: date
101
+ )
102
+ end
103
+
104
+ def generate_auth_header(client, method, host, request_uri, body, headers, headers_to_sign)
105
+ path, query_parts = parse_uri(request_uri)
106
+ headers = add_defaults_to(headers, host)
107
+ headers_to_sign |= [@date_header_name.downcase, 'host']
108
+ signature = generate_signature(client[:api_secret], body, headers, method, headers_to_sign, path, query_parts)
109
+ "#{get_algorithm_id} Credential=#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}, SignedHeaders=#{prepare_headers_to_sign headers_to_sign}, Signature=#{signature}"
110
+ end
111
+
112
+ def generate_signed_url(url_to_sign, client, expires = 86400)
113
+ uri = Addressable::URI.parse(url_to_sign)
114
+ protocol = uri.scheme
115
+ host = uri.host
116
+ path = uri.path
117
+ query_parts = parse_query(uri.query)
118
+
119
+ headers = [['host', host]]
120
+ headers_to_sign = ['host']
121
+ body = 'UNSIGNED-PAYLOAD'
122
+ query_parts += get_signing_params(client, expires, headers_to_sign)
123
+
124
+ signature = generate_signature(client[:api_secret], body, headers, 'GET', headers_to_sign, path, query_parts)
125
+ query_parts_with_signature = (query_parts.map { |k, v| [uri_encode(k), uri_encode(v)] } << query_pair('Signature', signature))
126
+
127
+ protocol + '://' + host + path + '?' + query_parts_with_signature.map { |k, v| k + '=' + v }.join('&')
128
+ end
129
+
130
+ def get_signing_params(client, expires, headers_to_sign)
131
+ [
132
+ ['Algorithm', get_algorithm_id],
133
+ ['Credentials', "#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}"],
134
+ ['Date', long_date(@current_time)],
135
+ ['Expires', expires.to_s],
136
+ ['SignedHeaders', headers_to_sign.join(';')],
137
+ ].map { |k, v| query_pair(k, v) }
138
+ end
139
+
140
+ def query_pair(k, v)
141
+ [query_key_for(k), v]
142
+ end
143
+
144
+ def query_key_for(key)
145
+ "X-#{@vendor_key}-#{key}"
146
+ end
147
+
148
+ def query_key_truncate(key)
149
+ key[@vendor_key.length + 3..-1]
150
+ end
151
+
152
+ def get_header(header_name, headers)
153
+ the_header = (headers.detect { |header| header[0].downcase == header_name.downcase })
154
+ the_header ? the_header[1] : nil
155
+ end
156
+
157
+ def get_signing_param(key, query_parts)
158
+ the_param = (query_parts.detect { |param| param[0] === query_key_for(key) })
159
+ the_param ? uri_decode(the_param[1]) : nil
160
+ end
161
+
162
+ def get_auth_parts_from_header(auth_header)
163
+ 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]+)$/
164
+ .match auth_header
165
+ raise EscherError, 'Malformed authorization header' unless m && m['credentials']
166
+ return m['algo'], m['api_key_id'], m['short_date'], m['credentials'], m['signed_headers'].split(';'), m['signature'], 0
167
+ end
168
+
169
+ def get_auth_parts_from_query(query_parts)
170
+ expires = get_signing_param('Expires', query_parts).to_i
171
+ api_key_id, short_date, credential_scope = get_signing_param('Credentials', query_parts).split('/', 3)
172
+ signed_headers = get_signing_param('SignedHeaders', query_parts).split ';'
173
+ algorithm = parse_algo(get_signing_param('Algorithm', query_parts))
174
+ signature = get_signing_param('Signature', query_parts)
175
+ return algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires
176
+ end
177
+
178
+ def generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
179
+ canonicalized_request = canonicalize(method, path, query_parts, body, headers, signed_headers.uniq)
180
+ string_to_sign = get_string_to_sign(canonicalized_request)
181
+ signing_key = calculate_signing_key(api_secret)
182
+ Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo)
183
+ end
184
+
185
+ def add_defaults_to(headers, host)
186
+ [['host', host], [@date_header_name, format_date_for_header]]
187
+ .each { |k, v| headers = add_if_missing headers, k, v }
188
+ headers
189
+ end
190
+
191
+ def format_date_for_header
192
+ @date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
193
+ end
194
+
195
+ def add_if_missing(headers, header_to_find, value)
196
+ headers += [header_to_find, value] unless headers.find { |header| header[0].downcase == header_to_find.downcase }
197
+ headers
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
@@ -0,0 +1,77 @@
1
+
2
+ class EscherRequest
3
+
4
+ def initialize(request)
5
+ @request = request
6
+ @request_uri = Addressable::URI.parse(uri)
7
+ prepare_request_headers
8
+ end
9
+
10
+ def prepare_request_headers
11
+ @request_headers = []
12
+ case @request.class.to_s
13
+ when 'Hash'
14
+ @request_headers = @request[:headers]
15
+ when 'Sinatra::Request' # TODO: not working yet
16
+ @request.env.each { |key, value|
17
+ if key.downcase[0, 5] == "http_"
18
+ @request_headers += [[ key[5..-1].gsub("_", "-"), value ]]
19
+ end
20
+ }
21
+ when 'WEBrick::HTTPRequest'
22
+ @request.header.each { |key, values|
23
+ values.each { |value|
24
+ @request_headers += [[ key, value ]]
25
+ }
26
+ }
27
+ end
28
+ end
29
+
30
+ def request
31
+ @request
32
+ end
33
+
34
+ def headers
35
+ @request_headers
36
+ end
37
+
38
+ def set_header(key, value)
39
+ @request[key] = value
40
+ end
41
+
42
+ def method
43
+ case @request.class.to_s
44
+ when 'Hash'
45
+ @request[:method]
46
+ else
47
+ @request.request_method
48
+ end
49
+ end
50
+
51
+ def uri
52
+ case @request.class.to_s
53
+ when 'Hash'
54
+ @request[:uri]
55
+ else
56
+ @request.uri
57
+ end
58
+ end
59
+
60
+ def body
61
+ case @request.class.to_s
62
+ when 'Hash'
63
+ @request[:body]
64
+ else
65
+ @request.body
66
+ end
67
+ end
68
+
69
+ def path
70
+ @request_uri.path
71
+ end
72
+
73
+ def query_values
74
+ @request_uri.query_values(Array) || []
75
+ end
76
+
77
+ end
@@ -1,3 +1,3 @@
1
1
  class Escher
2
- VERSION = "0.0.6"
2
+ VERSION = '0.1.0'
3
3
  end
data/lib/escher.rb CHANGED
@@ -1,325 +1,3 @@
1
+ require 'escher/base'
1
2
  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)
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
30
- end
31
-
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
41
-
42
- def is_valid?(*args)
43
- begin
44
- validate(*args)
45
- return true
46
- rescue
47
- return false
48
- end
49
- end
50
-
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)
54
-
55
- validate_headers(headers, signature_from_query)
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)
71
- api_secret = key_db[api_key_id]
72
-
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}"
147
- end
148
-
149
- def query_key_truncate(key)
150
- key[@vendor_key.length + 3..-1]
151
- end
152
-
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
156
- end
157
-
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
161
- end
162
-
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]+)$/
165
- .match auth_header
166
- raise EscherError, 'Malformed authorization header' unless m && m['credentials']
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
190
- end
191
-
192
- def format_date_for_header
193
- @date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
194
- end
195
-
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
199
- end
200
-
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"
210
- end
211
-
212
- def prepare_headers_to_sign(headers_to_sign)
213
- headers_to_sign.sort.uniq.join(';')
214
- end
215
-
216
- def parse_uri(request_uri)
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
236
-
237
- def create_algo
238
- case @hash_algo
239
- when 'SHA256'
240
- return Digest::SHA2.new 256
241
- when 'SHA512'
242
- return Digest::SHA2.new 512
243
- else
244
- raise EscherError, 'Unidentified hash algorithm'
245
- end
246
- end
247
-
248
- def long_date(date)
249
- date.utc.strftime('%Y%m%dT%H%M%SZ')
250
- end
251
-
252
- def short_date(date)
253
- date.utc.strftime('%Y%m%d')
254
- end
255
-
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
- }
276
- signing_key
277
- end
278
-
279
- def canonicalize_path(path)
280
- while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
281
- path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
282
- end
283
-
284
- def canonicalize_headers(raw_headers, headers_to_sign)
285
- collect_headers(raw_headers)
286
- .sort
287
- .select { |k, v| headers_to_sign.include?(k) }
288
- .map { |k, v| k + ':' + v.map { |piece| normalize_white_spaces piece} .join(',') }
289
- end
290
-
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)
299
- headers = {}
300
- raw_headers.each do |raw_header|
301
- if raw_header[0].downcase != @auth_header_name.downcase
302
- if headers[raw_header[0].downcase]
303
- headers[raw_header[0].downcase] << raw_header[1]
304
- else
305
- headers[raw_header[0].downcase] = [raw_header[1]]
306
- end
307
- end
308
- end
309
- headers
310
- end
311
-
312
- def canonicalize_query(query_parts)
313
- query_parts
314
- .map { |k, v| uri_encode(k.gsub('+', ' ')) + '=' + uri_encode(v || '') }
315
- .sort.join '&'
316
- end
317
-
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)
324
- end
325
- end
3
+ require 'escher/request'
data/spec/escher_spec.rb CHANGED
@@ -1,4 +1,4 @@
1
- require'spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  test_suites = {
4
4
  # 'get-header-key-duplicate',
@@ -134,7 +134,12 @@ describe 'Escher' do
134
134
  'X-EMS-Signature=fbc9dbb91670e84d04ad2ae7505f4f52ab3ff9e192b8233feeae57e9022c2b67'
135
135
 
136
136
  client = {:api_key_id => 'th3K3y', :api_secret => 'very_secure'}
137
- expect { escher.validate_request(key_db, 'GET', presigned_uri, 'IRRELEVANT', [%w(host example.com)]) }.not_to raise_error
137
+ expect { escher.authenticate({
138
+ :method => 'GET',
139
+ :headers => [%w(host example.com)],
140
+ :uri => presigned_uri,
141
+ :body => 'IRRELEVANT'
142
+ }, key_db) }.not_to raise_error
138
143
  end
139
144
 
140
145
  it 'should validate expiration' do
@@ -149,8 +154,12 @@ describe 'Escher' do
149
154
  'X-EMS-Signature=fbc9dbb91670e84d04ad2ae7505f4f52ab3ff9e192b8233feeae57e9022c2b67'
150
155
 
151
156
  client = {:api_key_id => 'th3K3y', :api_secret => 'very_secure'}
152
- expect { escher.validate_request(key_db, 'GET', presigned_uri, 'IRRELEVANT', [%w(host example.com)]) }
153
- .to raise_error(EscherError, 'The request date is not within the accepted time range')
157
+ expect { escher.authenticate({
158
+ :method => 'GET',
159
+ :headers => [%w(host example.com)],
160
+ :uri => presigned_uri,
161
+ :body => 'IRRELEVANT'
162
+ }, key_db) }.to raise_error(EscherError, 'The request date is not within the accepted time range')
154
163
  end
155
164
 
156
165
  it 'should validate request' do
@@ -159,7 +168,16 @@ describe 'Escher' do
159
168
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
160
169
  ['Authorization', GOOD_AUTH_HEADER],
161
170
  ]
162
- expect { call_validate_request(headers) }.not_to raise_error
171
+ expect { call_validate(headers) }.not_to raise_error
172
+ end
173
+
174
+ it 'should authenticate' do
175
+ headers = [
176
+ %w(Host host.foo.com),
177
+ ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
178
+ ['Authorization', GOOD_AUTH_HEADER],
179
+ ]
180
+ expect(call_validate(headers)).to eq 'AKIDEXAMPLE'
163
181
  end
164
182
 
165
183
  it 'should detect if signatures do not match' do
@@ -168,7 +186,7 @@ describe 'Escher' do
168
186
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
169
187
  ['Authorization', 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'],
170
188
  ]
171
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'The signatures do not match')
189
+ expect { call_validate(headers) }.to raise_error(EscherError, 'The signatures do not match')
172
190
  end
173
191
 
174
192
  it 'should detect if dates are not on the same day' do
@@ -178,7 +196,7 @@ describe 'Escher' do
178
196
  ['Date', "Mon, #{yesterday} Sep 2011 23:36:00 GMT"],
179
197
  ['Authorization', GOOD_AUTH_HEADER],
180
198
  ]
181
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Invalid request date')
199
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Invalid request date')
182
200
  end
183
201
 
184
202
  it 'should detect if date is not within the 15 minutes range' do
@@ -188,7 +206,7 @@ describe 'Escher' do
188
206
  ['Date', "Mon, 09 Sep 2011 23:#{long_ago}:00 GMT"],
189
207
  ['Authorization', GOOD_AUTH_HEADER],
190
208
  ]
191
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'The request date is not within the accepted time range')
209
+ expect { call_validate(headers) }.to raise_error(EscherError, 'The request date is not within the accepted time range')
192
210
  end
193
211
 
194
212
  it 'should detect missing host header' do
@@ -196,7 +214,7 @@ describe 'Escher' do
196
214
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
197
215
  ['Authorization', GOOD_AUTH_HEADER],
198
216
  ]
199
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Missing header: Host')
217
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Missing header: Host')
200
218
  end
201
219
 
202
220
  it 'should detect missing date header' do
@@ -204,7 +222,7 @@ describe 'Escher' do
204
222
  %w(Host host.foo.com),
205
223
  ['Authorization', GOOD_AUTH_HEADER],
206
224
  ]
207
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Missing header: Date')
225
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Missing header: Date')
208
226
  end
209
227
 
210
228
  it 'should detect missing auth header' do
@@ -212,7 +230,7 @@ describe 'Escher' do
212
230
  %w(Host host.foo.com),
213
231
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
214
232
  ]
215
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Missing header: Authorization')
233
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Missing header: Authorization')
216
234
  end
217
235
 
218
236
  it 'should detect malformed auth header' do
@@ -221,7 +239,7 @@ describe 'Escher' do
221
239
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
222
240
  ['Authorization', 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=UNPARSABLE'],
223
241
  ]
224
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Malformed authorization header')
242
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Malformed authorization header')
225
243
  end
226
244
 
227
245
  it 'should detect malformed credential scope' do
@@ -230,7 +248,7 @@ describe 'Escher' do
230
248
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
231
249
  ['Authorization', 'AWS4-HMAC-SHA256 Credential=BAD-CREDENTIAL-SCOPE, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'],
232
250
  ]
233
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Malformed authorization header')
251
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Malformed authorization header')
234
252
  end
235
253
 
236
254
  it 'should check mandatory signed headers: host' do
@@ -239,7 +257,7 @@ describe 'Escher' do
239
257
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
240
258
  ['Authorization', 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'],
241
259
  ]
242
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Host header is not signed')
260
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Host header is not signed')
243
261
  end
244
262
 
245
263
  it 'should check mandatory signed headers: date' do
@@ -248,7 +266,7 @@ describe 'Escher' do
248
266
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
249
267
  ['Authorization', 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'],
250
268
  ]
251
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Date header is not signed')
269
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Date header is not signed')
252
270
  end
253
271
 
254
272
  it 'should check algorithm' do
@@ -257,7 +275,7 @@ describe 'Escher' do
257
275
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
258
276
  ['Authorization', 'AWS4-HMAC-INVALID Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'],
259
277
  ]
260
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed')
278
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed')
261
279
  end
262
280
 
263
281
  it 'should check credential scope' do
@@ -266,7 +284,7 @@ describe 'Escher' do
266
284
  ['Date', 'Mon, 09 Sep 2011 23:36:00 GMT'],
267
285
  ['Authorization', 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/INVALID/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470'],
268
286
  ]
269
- expect { call_validate_request(headers) }.to raise_error(EscherError, 'Invalid credentials')
287
+ expect { call_validate(headers) }.to raise_error(EscherError, 'Invalid credentials')
270
288
  end
271
289
 
272
290
  it 'should convert dates' do
@@ -274,9 +292,14 @@ describe 'Escher' do
274
292
  expect(Escher.new('irrelevant', date_header_name: 'date', current_time: Time.parse(date_str)).format_date_for_header).to eq date_str
275
293
  end
276
294
 
277
- def call_validate_request(headers)
295
+ def call_validate(headers)
278
296
  escher = Escher.new('us-east-1/host/aws4_request', ESCHER_AWS4_OPTIONS.merge(current_time: Time.parse('Mon, 09 Sep 2011 23:40:00 GMT')))
279
- escher.validate_request(key_db, 'GET', '/', '', headers)
297
+ escher.authenticate({
298
+ :method => 'GET',
299
+ :headers => headers,
300
+ :uri => '/',
301
+ :body => '',
302
+ }, key_db)
280
303
  end
281
304
 
282
305
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: escher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
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-11 00:00:00.000000000 Z
11
+ date: 2014-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -82,6 +82,8 @@ files:
82
82
  - Rakefile
83
83
  - escher.gemspec
84
84
  - lib/escher.rb
85
+ - lib/escher/base.rb
86
+ - lib/escher/request.rb
85
87
  - lib/escher/version.rb
86
88
  - spec/aws4_testsuite/get-header-key-duplicate.authz
87
89
  - spec/aws4_testsuite/get-header-key-duplicate.creq