escher 0.0.2 → 0.0.5
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 +7 -0
- data/.gitignore +16 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +8 -0
- data/Rakefile +11 -0
- data/escher.gemspec +26 -0
- data/lib/escher/version.rb +3 -0
- data/lib/escher.rb +254 -110
- data/spec/aws4_testsuite/get-header-key-duplicate.authz +1 -0
- data/spec/aws4_testsuite/get-header-key-duplicate.creq +9 -0
- data/spec/aws4_testsuite/get-header-key-duplicate.req +7 -0
- data/spec/aws4_testsuite/get-header-key-duplicate.sreq +8 -0
- data/spec/aws4_testsuite/get-header-key-duplicate.sts +4 -0
- data/spec/aws4_testsuite/get-header-value-multiline.req +7 -0
- data/spec/aws4_testsuite/get-header-value-order.authz +1 -0
- data/spec/aws4_testsuite/get-header-value-order.creq +9 -0
- data/spec/aws4_testsuite/get-header-value-order.req +8 -0
- data/spec/aws4_testsuite/get-header-value-order.sreq +9 -0
- data/spec/aws4_testsuite/get-header-value-order.sts +4 -0
- data/spec/aws4_testsuite/get-header-value-trim.authz +1 -0
- data/spec/aws4_testsuite/get-header-value-trim.creq +9 -0
- data/spec/aws4_testsuite/get-header-value-trim.req +5 -0
- data/spec/aws4_testsuite/get-header-value-trim.sreq +6 -0
- data/spec/aws4_testsuite/get-header-value-trim.sts +4 -0
- data/spec/aws4_testsuite/get-relative-relative.authz +1 -0
- data/spec/aws4_testsuite/get-relative-relative.creq +8 -0
- data/spec/aws4_testsuite/get-relative-relative.req +4 -0
- data/spec/aws4_testsuite/get-relative-relative.sreq +5 -0
- data/spec/aws4_testsuite/get-relative-relative.sts +4 -0
- data/spec/aws4_testsuite/get-relative.authz +1 -0
- data/spec/aws4_testsuite/get-relative.creq +8 -0
- data/spec/aws4_testsuite/get-relative.req +4 -0
- data/spec/aws4_testsuite/get-relative.sreq +5 -0
- data/spec/aws4_testsuite/get-relative.sts +4 -0
- data/spec/aws4_testsuite/get-slash-dot-slash.authz +1 -0
- data/spec/aws4_testsuite/get-slash-dot-slash.creq +8 -0
- data/spec/aws4_testsuite/get-slash-dot-slash.req +4 -0
- data/spec/aws4_testsuite/get-slash-dot-slash.sreq +5 -0
- data/spec/aws4_testsuite/get-slash-dot-slash.sts +4 -0
- data/spec/aws4_testsuite/get-slash-pointless-dot.authz +1 -0
- data/spec/aws4_testsuite/get-slash-pointless-dot.creq +8 -0
- data/spec/aws4_testsuite/get-slash-pointless-dot.req +4 -0
- data/spec/aws4_testsuite/get-slash-pointless-dot.sreq +5 -0
- data/spec/aws4_testsuite/get-slash-pointless-dot.sts +4 -0
- data/spec/aws4_testsuite/get-slash.authz +1 -0
- data/spec/aws4_testsuite/get-slash.creq +8 -0
- data/spec/aws4_testsuite/get-slash.req +4 -0
- data/spec/aws4_testsuite/get-slash.sreq +5 -0
- data/spec/aws4_testsuite/get-slash.sts +4 -0
- data/spec/aws4_testsuite/get-slashes.authz +1 -0
- data/spec/aws4_testsuite/get-slashes.creq +8 -0
- data/spec/aws4_testsuite/get-slashes.req +4 -0
- data/spec/aws4_testsuite/get-slashes.sreq +5 -0
- data/spec/aws4_testsuite/get-slashes.sts +4 -0
- data/spec/aws4_testsuite/get-space.authz +1 -0
- data/spec/aws4_testsuite/get-space.creq +8 -0
- data/spec/aws4_testsuite/get-space.req +4 -0
- data/spec/aws4_testsuite/get-space.sreq +5 -0
- data/spec/aws4_testsuite/get-space.sts +4 -0
- data/spec/aws4_testsuite/get-unreserved.authz +1 -0
- data/spec/aws4_testsuite/get-unreserved.creq +8 -0
- data/spec/aws4_testsuite/get-unreserved.req +4 -0
- data/spec/aws4_testsuite/get-unreserved.sreq +5 -0
- data/spec/aws4_testsuite/get-unreserved.sts +4 -0
- data/spec/aws4_testsuite/get-utf8.authz +1 -0
- data/spec/aws4_testsuite/get-utf8.creq +8 -0
- data/spec/aws4_testsuite/get-utf8.req +4 -0
- data/spec/aws4_testsuite/get-utf8.sreq +5 -0
- data/spec/aws4_testsuite/get-utf8.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-empty-query-key.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-empty-query-key.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-empty-query-key.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-empty-query-key.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-empty-query-key.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key-case.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key-case.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key-case.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key-case.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key-case.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-key.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-value.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-value.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-value.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-value.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-query-order-value.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-unreserved.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-query-unreserved.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-query-unreserved.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-query-unreserved.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-query-unreserved.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-query.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-query.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-query.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-query.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-query.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla-ut8-query.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla-ut8-query.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla-ut8-query.req +4 -0
- data/spec/aws4_testsuite/get-vanilla-ut8-query.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla-ut8-query.sts +4 -0
- data/spec/aws4_testsuite/get-vanilla.authz +1 -0
- data/spec/aws4_testsuite/get-vanilla.creq +8 -0
- data/spec/aws4_testsuite/get-vanilla.req +4 -0
- data/spec/aws4_testsuite/get-vanilla.sreq +5 -0
- data/spec/aws4_testsuite/get-vanilla.sts +4 -0
- data/spec/aws4_testsuite/post-header-key-case.authz +1 -0
- data/spec/aws4_testsuite/post-header-key-case.creq +8 -0
- data/spec/aws4_testsuite/post-header-key-case.req +4 -0
- data/spec/aws4_testsuite/post-header-key-case.sreq +5 -0
- data/spec/aws4_testsuite/post-header-key-case.sts +4 -0
- data/spec/aws4_testsuite/post-header-key-sort.authz +1 -0
- data/spec/aws4_testsuite/post-header-key-sort.creq +9 -0
- data/spec/aws4_testsuite/post-header-key-sort.req +5 -0
- data/spec/aws4_testsuite/post-header-key-sort.sreq +6 -0
- data/spec/aws4_testsuite/post-header-key-sort.sts +4 -0
- data/spec/aws4_testsuite/post-header-value-case.authz +1 -0
- data/spec/aws4_testsuite/post-header-value-case.creq +9 -0
- data/spec/aws4_testsuite/post-header-value-case.req +5 -0
- data/spec/aws4_testsuite/post-header-value-case.sreq +6 -0
- data/spec/aws4_testsuite/post-header-value-case.sts +4 -0
- data/spec/aws4_testsuite/post-vanilla-empty-query-value.authz +1 -0
- data/spec/aws4_testsuite/post-vanilla-empty-query-value.creq +8 -0
- data/spec/aws4_testsuite/post-vanilla-empty-query-value.req +4 -0
- data/spec/aws4_testsuite/post-vanilla-empty-query-value.sreq +5 -0
- data/spec/aws4_testsuite/post-vanilla-empty-query-value.sts +4 -0
- data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.authz +1 -0
- data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.creq +8 -0
- data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.req +4 -0
- data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.sreq +5 -0
- data/spec/aws4_testsuite/post-vanilla-query-nonunreserved.sts +4 -0
- data/spec/aws4_testsuite/post-vanilla-query-space.authz +1 -0
- data/spec/aws4_testsuite/post-vanilla-query-space.creq +8 -0
- data/spec/aws4_testsuite/post-vanilla-query-space.req +4 -0
- data/spec/aws4_testsuite/post-vanilla-query-space.sreq +5 -0
- data/spec/aws4_testsuite/post-vanilla-query-space.sts +4 -0
- data/spec/aws4_testsuite/post-vanilla-query.authz +1 -0
- data/spec/aws4_testsuite/post-vanilla-query.creq +8 -0
- data/spec/aws4_testsuite/post-vanilla-query.req +4 -0
- data/spec/aws4_testsuite/post-vanilla-query.sreq +5 -0
- data/spec/aws4_testsuite/post-vanilla-query.sts +4 -0
- data/spec/aws4_testsuite/post-vanilla.authz +1 -0
- data/spec/aws4_testsuite/post-vanilla.creq +8 -0
- data/spec/aws4_testsuite/post-vanilla.req +4 -0
- data/spec/aws4_testsuite/post-vanilla.sreq +5 -0
- data/spec/aws4_testsuite/post-vanilla.sts +4 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.authz +1 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.creq +9 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.req +6 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.sreq +7 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded-parameters.sts +4 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded.authz +1 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded.creq +9 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded.req +6 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded.sreq +7 -0
- data/spec/aws4_testsuite/post-x-www-form-urlencoded.sts +4 -0
- data/spec/emarsys_testsuite/get-header-key-duplicate.authz +1 -0
- data/spec/emarsys_testsuite/get-header-key-duplicate.creq +9 -0
- data/spec/emarsys_testsuite/get-header-key-duplicate.req +7 -0
- data/spec/emarsys_testsuite/get-header-key-duplicate.sreq +8 -0
- data/spec/emarsys_testsuite/get-header-key-duplicate.sts +4 -0
- data/spec/emarsys_testsuite/get-header-value-order.authz +1 -0
- data/spec/emarsys_testsuite/get-header-value-order.creq +9 -0
- data/spec/emarsys_testsuite/get-header-value-order.req +8 -0
- data/spec/emarsys_testsuite/get-header-value-order.sreq +9 -0
- data/spec/emarsys_testsuite/get-header-value-order.sts +4 -0
- data/spec/emarsys_testsuite/post-header-key-order.authz +1 -0
- data/spec/emarsys_testsuite/post-header-key-order.creq +9 -0
- data/spec/emarsys_testsuite/post-header-key-order.req +6 -0
- data/spec/emarsys_testsuite/post-header-key-order.sreq +7 -0
- data/spec/emarsys_testsuite/post-header-key-order.sts +4 -0
- data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.authz +1 -0
- data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.creq +9 -0
- data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.req +5 -0
- data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.sreq +6 -0
- data/spec/emarsys_testsuite/post-header-value-spaces-within-quotes.sts +4 -0
- data/spec/emarsys_testsuite/post-header-value-spaces.authz +1 -0
- data/spec/emarsys_testsuite/post-header-value-spaces.creq +9 -0
- data/spec/emarsys_testsuite/post-header-value-spaces.req +6 -0
- data/spec/emarsys_testsuite/post-header-value-spaces.sreq +7 -0
- data/spec/emarsys_testsuite/post-header-value-spaces.sts +4 -0
- data/spec/escher_spec.rb +306 -0
- data/spec/spec_helper.rb +1 -0
- metadata +402 -31
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: ee0a2544d5cff2fa6068a42800a3730bb6bc6f2b
|
|
4
|
+
data.tar.gz: 4b005a107aaf2b1318ba6118f2561b8401c2775b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 89857d68c47ecf021465f9916e5527201848bea36cafbceff39db30dfb95bc2fbdceba251ab89309e1754f506b85f130dec989064e2b4b3283b17997ffac84e9
|
|
7
|
+
data.tar.gz: 96ebd6515e11fe2c5ce9d96a4231cf6b58d5f955fa3e36eb8cc62681e332c85dc8a4bbefc5a8e9f2a58696646b2b6f355e4d68142c6520ba09119452af06a32f
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Emarsys Technologies Kft.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
EscherRuby - HTTP request signing lib [](https://travis-ci.org/emartech/escher-ruby)
|
|
2
|
+
=====================================
|
|
3
|
+
|
|
4
|
+
Escher helps you creating secure HTTP requests (for APIs) by signing HTTP(s) requests. It's both a server side and client side implementation. The status is work in progress.
|
|
5
|
+
|
|
6
|
+
The algorithm is based on [Amazon's _AWS Signature Version 4_](http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html), but we have generalized and extended it.
|
|
7
|
+
|
|
8
|
+
More details will be available at our [documentation site](https://documentation.emarsys.com/).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "rspec/core/rake_task"
|
|
3
|
+
|
|
4
|
+
desc "Run RSpec code examples (options: RSPEC_SEED=seed)"
|
|
5
|
+
RSpec::Core::RakeTask.new :spec do |task|
|
|
6
|
+
task.verbose = false
|
|
7
|
+
task.rspec_opts = "--format progress --order random"
|
|
8
|
+
task.rspec_opts << " --seed #{ENV['RSPEC_SEED']}" if ENV['RSPEC_SEED']
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
task :default => :spec
|
data/escher.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'escher/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "escher"
|
|
8
|
+
spec.version = Escher::VERSION
|
|
9
|
+
spec.authors = ["Andras Barthazi"]
|
|
10
|
+
spec.email = ["andras.barthazi@emarsys.com"]
|
|
11
|
+
spec.summary = %q{Escher - Emarsys request signing library}
|
|
12
|
+
spec.description = %q{For Emarsys API}
|
|
13
|
+
spec.homepage = "http://emarsys.com"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
|
22
|
+
spec.add_development_dependency "rake", "~> 10"
|
|
23
|
+
spec.add_development_dependency "rspec", "~> 2"
|
|
24
|
+
|
|
25
|
+
spec.add_runtime_dependency "addressable", "~> 2.3"
|
|
26
|
+
end
|
data/lib/escher.rb
CHANGED
|
@@ -1,181 +1,325 @@
|
|
|
1
|
+
require 'escher/version'
|
|
2
|
+
|
|
1
3
|
require 'time'
|
|
2
|
-
require 'uri'
|
|
3
4
|
require 'digest'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'addressable/uri'
|
|
4
7
|
|
|
5
8
|
class EscherError < RuntimeError
|
|
6
9
|
end
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
10
23
|
|
|
11
|
-
def
|
|
12
|
-
|
|
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
|
|
13
30
|
end
|
|
14
31
|
|
|
15
|
-
def
|
|
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
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
def is_valid?(*args)
|
|
43
|
+
begin
|
|
44
|
+
validate(*args)
|
|
45
|
+
return true
|
|
46
|
+
rescue
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
21
50
|
|
|
22
|
-
|
|
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)
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
raise EscherError, 'Date header is not signed' unless signed_headers.include? options[:date_header_name].downcase
|
|
26
|
-
raise EscherError, 'Invalid request date' unless short_date(date) == short_date && within_range(current_time, date)
|
|
27
|
-
# TODO validate host header
|
|
28
|
-
raise EscherError, 'Invalid credentials' unless credential_scope == accepted_credentials
|
|
55
|
+
validate_headers(headers, signature_from_query)
|
|
29
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)
|
|
30
71
|
api_secret = key_db[api_key_id]
|
|
31
72
|
|
|
32
|
-
|
|
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}"
|
|
33
147
|
end
|
|
34
148
|
|
|
35
|
-
def
|
|
36
|
-
|
|
149
|
+
def query_key_truncate(key)
|
|
150
|
+
key[@vendor_key.length + 3..-1]
|
|
37
151
|
end
|
|
38
152
|
|
|
39
|
-
def
|
|
40
|
-
(
|
|
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
|
|
41
156
|
end
|
|
42
157
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
header[1]
|
|
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
|
|
47
161
|
end
|
|
48
162
|
|
|
49
|
-
def
|
|
50
|
-
m = /#{
|
|
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]+)$/
|
|
51
165
|
.match auth_header
|
|
52
166
|
raise EscherError, 'Malformed authorization header' unless m && m['credentials']
|
|
53
|
-
[
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
190
|
end
|
|
62
191
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
signature = generate_signature(algo, client[:api_secret], body, credential_scope_as_string(client), date, headers, method, headers_to_sign, host, request_uri, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
|
|
66
|
-
"#{algo_id(options[:vendor_prefix], algo)} Credential=#{client[:api_key_id]}/#{short_date(date)}/#{credential_scope_as_string(client)}, SignedHeaders=#{headers_to_sign.uniq.join ';'}, Signature=#{signature}"
|
|
192
|
+
def format_date_for_header
|
|
193
|
+
@date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
|
|
67
194
|
end
|
|
68
195
|
|
|
69
|
-
def
|
|
70
|
-
|
|
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
|
|
71
199
|
end
|
|
72
200
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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"
|
|
78
210
|
end
|
|
79
211
|
|
|
80
|
-
def
|
|
81
|
-
|
|
212
|
+
def prepare_headers_to_sign(headers_to_sign)
|
|
213
|
+
headers_to_sign.sort.uniq.join(';')
|
|
82
214
|
end
|
|
83
215
|
|
|
84
|
-
def
|
|
216
|
+
def parse_uri(request_uri)
|
|
85
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
|
|
86
236
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
canonicalize_path(path),
|
|
90
|
-
canonicalize_query(query),
|
|
91
|
-
] + canonicalize_headers(date, host, headers, auth_header_name, date_header_name) + [
|
|
92
|
-
'',
|
|
93
|
-
(headers_to_sign | [date_header_name.downcase, 'host']).join(';'),
|
|
94
|
-
request_body_hash(body, algo)
|
|
95
|
-
]).join "\n"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# TODO: extract algo creation
|
|
99
|
-
def self.get_string_to_sign(credential_scope, canonicalized_request, date, prefix, algo)
|
|
100
|
-
date = long_date(date)
|
|
101
|
-
lines = [
|
|
102
|
-
algo_id(prefix, algo),
|
|
103
|
-
date,
|
|
104
|
-
date[0..7] + '/' + credential_scope,
|
|
105
|
-
create_algo(algo).new.hexdigest(canonicalized_request)
|
|
106
|
-
]
|
|
107
|
-
lines.join "\n"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def self.create_algo(algo)
|
|
111
|
-
case algo.upcase
|
|
237
|
+
def create_algo
|
|
238
|
+
case @hash_algo
|
|
112
239
|
when 'SHA256'
|
|
113
|
-
return Digest::
|
|
240
|
+
return Digest::SHA2.new 256
|
|
114
241
|
when 'SHA512'
|
|
115
|
-
return Digest::
|
|
242
|
+
return Digest::SHA2.new 512
|
|
116
243
|
else
|
|
117
244
|
raise EscherError, 'Unidentified hash algorithm'
|
|
118
245
|
end
|
|
119
246
|
end
|
|
120
247
|
|
|
121
|
-
def
|
|
122
|
-
|
|
248
|
+
def long_date(date)
|
|
249
|
+
date.utc.strftime('%Y%m%dT%H%M%SZ')
|
|
123
250
|
end
|
|
124
251
|
|
|
125
|
-
def
|
|
126
|
-
|
|
252
|
+
def short_date(date)
|
|
253
|
+
date.utc.strftime('%Y%m%d')
|
|
127
254
|
end
|
|
128
255
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
276
|
signing_key
|
|
135
277
|
end
|
|
136
278
|
|
|
137
|
-
def
|
|
279
|
+
def canonicalize_path(path)
|
|
138
280
|
while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
|
|
139
281
|
path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
|
|
140
282
|
end
|
|
141
283
|
|
|
142
|
-
def
|
|
143
|
-
collect_headers(raw_headers
|
|
284
|
+
def canonicalize_headers(raw_headers, headers_to_sign)
|
|
285
|
+
collect_headers(raw_headers)
|
|
144
286
|
.sort
|
|
145
|
-
.
|
|
287
|
+
.select { |k, v| headers_to_sign.include?(k) }
|
|
288
|
+
.map { |k, v| k + ':' + v.map { |piece| normalize_white_spaces piece} .join(',') }
|
|
146
289
|
end
|
|
147
290
|
|
|
148
|
-
def
|
|
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)
|
|
149
299
|
headers = {}
|
|
150
|
-
raw_headers.each
|
|
151
|
-
if raw_header[0].downcase != auth_header_name.downcase
|
|
152
|
-
if headers[raw_header[0].downcase]
|
|
300
|
+
raw_headers.each do |raw_header|
|
|
301
|
+
if raw_header[0].downcase != @auth_header_name.downcase
|
|
302
|
+
if headers[raw_header[0].downcase]
|
|
153
303
|
headers[raw_header[0].downcase] << raw_header[1]
|
|
154
304
|
else
|
|
155
305
|
headers[raw_header[0].downcase] = [raw_header[1]]
|
|
156
306
|
end
|
|
157
307
|
end
|
|
158
|
-
|
|
308
|
+
end
|
|
159
309
|
headers
|
|
160
310
|
end
|
|
161
311
|
|
|
162
|
-
def
|
|
163
|
-
|
|
312
|
+
def canonicalize_query(query_parts)
|
|
313
|
+
query_parts
|
|
314
|
+
.map { |k, v| uri_encode(k.gsub('+', ' ')) + '=' + uri_encode(v || '') }
|
|
315
|
+
.sort.join '&'
|
|
164
316
|
end
|
|
165
317
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
else
|
|
173
|
-
[k, v]
|
|
174
|
-
end }
|
|
175
|
-
.map { |pair|
|
|
176
|
-
k, v = pair;
|
|
177
|
-
URI::encode(k.gsub('+', ' ')) + '=' + URI::encode(v || '')
|
|
178
|
-
}
|
|
179
|
-
.sort.join '&'
|
|
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)
|
|
180
324
|
end
|
|
181
325
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=54afcaaf45b331f81cd2edb974f7b824ff4dd594cbbaa945ed636b48477368ed
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
POST / http/1.1
|
|
2
|
+
DATE:Mon, 09 Sep 2011 23:36:00 GMT
|
|
3
|
+
host:host.foo.com
|
|
4
|
+
ZOO:zoobar
|
|
5
|
+
zoo:foobar
|
|
6
|
+
zoo:zoobar
|
|
7
|
+
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=54afcaaf45b331f81cd2edb974f7b824ff4dd594cbbaa945ed636b48477368ed
|
|
8
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=d2973954263943b11624a11d1c963ca81fb274169c7868b2858c04f083199e3d
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
POST / http/1.1
|
|
2
|
+
DATE:Mon, 09 Sep 2011 23:36:00 GMT
|
|
3
|
+
host:host.foo.com
|
|
4
|
+
p:z
|
|
5
|
+
p:a
|
|
6
|
+
p:p
|
|
7
|
+
p:a
|
|
8
|
+
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=d2973954263943b11624a11d1c963ca81fb274169c7868b2858c04f083199e3d
|
|
9
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592
|