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 +4 -4
- data/lib/escher/base.rb +324 -0
- data/lib/escher/request.rb +77 -0
- data/lib/escher/version.rb +1 -1
- data/lib/escher.rb +2 -324
- data/spec/escher_spec.rb +42 -19
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3076cb75aeec8d2b0bed7769215425f78e772c66
|
4
|
+
data.tar.gz: 30f7f2a24e421f7ff31fb73cc0cc1c96efb337b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3491c84f589d3505d461610e6d23925f186f6b1cd76af3a08277a480cb974f92cc813b12d904e3d035b062435384227954a594f9aa6c81dac616dfa7a0608067
|
7
|
+
data.tar.gz: fde96a3a6e197c600e0c0c89d21137a98e01ccbf540ffd96b3589c1a8452c15418b6c9f7642c2cdaf073d7b4804c4401245015f656385c1e9fdbe45baf8dbcc9
|
data/lib/escher/base.rb
ADDED
@@ -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
|
data/lib/escher/version.rb
CHANGED
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.
|
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.
|
153
|
-
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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
|
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.
|
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
|
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
|
+
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
|