escher 0.0.6 → 0.1.0

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