escher 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. data/lib/escher.rb +55 -28
  2. metadata +20 -12
  3. checksums.yaml +0 -7
data/lib/escher.rb CHANGED
@@ -2,69 +2,95 @@ require 'time'
2
2
  require 'uri'
3
3
  require 'digest'
4
4
 
5
+ class EscherError < RuntimeError
6
+ end
7
+
5
8
  module Escher
6
- VERSION = '0.0.1'
9
+ VERSION = '0.0.2'
7
10
 
8
11
  def self.default_options
9
12
  {:auth_header_name => 'X-Ems-Auth', :date_header_name => 'X-Ems-Date', :vendor_prefix => 'EMS'}
10
13
  end
11
14
 
12
- def self.validate_request(method, url, body, headers, options = {})
15
+ def self.validate_request(method, request_uri, body, headers, key_db, accepted_credentials, current_time = Time.now, options = {})
13
16
 
14
17
  options = default_options.merge(options)
15
- auth_header = get_header(options[:auth_header_name], headers)
18
+ host = get_header('host', headers)
16
19
  date = get_header(options[:date_header_name], headers)
20
+ auth_header = get_header(options[:auth_header_name], headers)
17
21
 
18
22
  algo, api_key_id, short_date, credential_scope, signed_headers, signature = parse_auth_header auth_header, options[:vendor_prefix]
19
23
 
20
- api_secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
24
+ raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
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
29
+
30
+ api_secret = key_db[api_key_id]
21
31
 
22
- signature == generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, url, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
32
+ signature == generate_signature(algo, api_secret, body, credential_scope.join('/'), date, headers, method, signed_headers, host, request_uri, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
33
+ end
34
+
35
+ def self.short_date(date)
36
+ long_date(date)[0..7]
37
+ end
38
+
39
+ def self.within_range(current_time, date)
40
+ (current_time - 900 .. current_time + 900).cover?(Time.parse date)
23
41
  end
24
42
 
25
43
  def self.get_header(header_name, headers)
26
- (headers.detect { |header| header[0].downcase == header_name.downcase })[1]
44
+ header = (headers.detect { |header| header[0].downcase == header_name.downcase })
45
+ raise EscherError, "Missing header: #{header_name.downcase}" unless header
46
+ header[1]
27
47
  end
28
48
 
29
49
  def self.parse_auth_header(auth_header, vendor_prefix)
30
- m = /#{vendor_prefix.upcase}-HMAC-(?<algo>[A-Z0-9\,]+) Credential=(?<credentials>[A-Za-z0-9\/\-_]+), SignedHeaders=(?<signed_headers>[A-Za-z\-;]+), Signature=(?<signature>[0-9a-f]+)$/
50
+ m = /#{vendor_prefix.upcase}-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]+)$/
31
51
  .match auth_header
52
+ raise EscherError, 'Malformed authorization header' unless m && m['credentials']
32
53
  [
33
54
  m['algo'],
34
- ] + m['credentials'].split('/', 3) + [
55
+ m['api_key_id'],
56
+ m['short_date'],
57
+ m['credentials'].split('/'),
35
58
  m['signed_headers'].split(';'),
36
59
  m['signature'],
37
60
  ]
38
61
  end
39
62
 
40
- def self.get_auth_header(client, method, url, body, headers, headers_to_sign, date = Time.now.utc.rfc2822, algo = 'SHA256', options = {})
63
+ def self.generate_auth_header(client, method, host, request_uri, body, headers, headers_to_sign, date = Time.now.utc.rfc2822, algo = 'SHA256', options = {})
41
64
  options = default_options.merge options
42
- signature = generate_signature(algo, client[:api_secret], body, client[:credential_scope], date, headers, method, headers_to_sign, url, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
43
- "#{algo_id(options[:vendor_prefix], algo)} Credential=#{client[:api_key_id]}/#{long_date(date)[0..7]}/#{client[:credential_scope]}, SignedHeaders=#{headers_to_sign.uniq.join ';'}, Signature=#{signature}"
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}"
67
+ end
68
+
69
+ def self.credential_scope_as_string(client)
70
+ client[:credential_scope].join '/'
44
71
  end
45
72
 
46
- def self.generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, url, vendor_prefix, auth_header_name, date_header_name)
47
- canonicalized_request = canonicalize method, url, body, date, headers, signed_headers, algo, auth_header_name, date_header_name
73
+ def self.generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, host, request_uri, vendor_prefix, auth_header_name, date_header_name)
74
+ canonicalized_request = canonicalize method, host, request_uri, body, date, headers, signed_headers, algo, auth_header_name, date_header_name
48
75
  string_to_sign = get_string_to_sign credential_scope, canonicalized_request, date, vendor_prefix, algo
49
- signing_key = calculate_signing_key(api_secret, date, vendor_prefix, credential_scope, algo)
50
- signature = calculate_signature(algo, signing_key, string_to_sign)
76
+ signing_key = calculate_signing_key api_secret, date, vendor_prefix, credential_scope, algo
77
+ calculate_signature algo, signing_key, string_to_sign
51
78
  end
52
79
 
53
80
  def self.calculate_signature(algo, signing_key, string_to_sign)
54
81
  Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo(algo))
55
82
  end
56
83
 
57
- def self.canonicalize(method, url, body, date, headers, headers_to_sign, algo, auth_header_name, date_header_name)
58
- url, query = url.split '?', 2 # URI#parse cannot parse unicode characters in query string TODO use Adressable
59
- uri = URI.parse(url)
84
+ def self.canonicalize(method, host, request_uri, body, date, headers, headers_to_sign, algo, auth_header_name, date_header_name)
85
+ path, query = request_uri.split '?', 2
60
86
 
61
87
  ([
62
88
  method.upcase,
63
- canonicalize_path(uri),
89
+ canonicalize_path(path),
64
90
  canonicalize_query(query),
65
- ] + canonicalize_headers(date, uri, headers, auth_header_name, date_header_name) + [
91
+ ] + canonicalize_headers(date, host, headers, auth_header_name, date_header_name) + [
66
92
  '',
67
- (headers_to_sign | %w(date host)).join(';'),
93
+ (headers_to_sign | [date_header_name.downcase, 'host']).join(';'),
68
94
  request_body_hash(body, algo)
69
95
  ]).join "\n"
70
96
  end
@@ -88,7 +114,7 @@ module Escher
88
114
  when 'SHA512'
89
115
  return Digest::SHA512
90
116
  else
91
- raise('Unidentified hash algorithm')
117
+ raise EscherError, 'Unidentified hash algorithm'
92
118
  end
93
119
  end
94
120
 
@@ -102,20 +128,21 @@ module Escher
102
128
 
103
129
  def self.calculate_signing_key(api_secret, date, vendor_prefix, credential_scope, algo)
104
130
  signing_key = vendor_prefix + api_secret
105
- for data in [long_date(date)[0..7]] + credential_scope.split('/') do
131
+ for data in [short_date(date)] + credential_scope.split('/') do
106
132
  signing_key = Digest::HMAC.digest(data, signing_key, create_algo(algo))
107
133
  end
108
134
  signing_key
109
135
  end
110
136
 
111
- def self.canonicalize_path(uri)
112
- path = uri.path
137
+ def self.canonicalize_path(path)
113
138
  while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
114
- path = path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
139
+ path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
115
140
  end
116
141
 
117
- def self.canonicalize_headers(date, uri, raw_headers, auth_header_name, date_header_name)
118
- collect_headers(raw_headers, auth_header_name).merge({date_header_name.downcase => [date], 'host' => [uri.host]}).map { |k, v| k + ':' + (v.sort_by { |x| x }).join(',').gsub(/\s+/, ' ').strip }
142
+ def self.canonicalize_headers(date, host, raw_headers, auth_header_name, date_header_name)
143
+ collect_headers(raw_headers, auth_header_name).merge({date_header_name.downcase => [date], 'host' => [host]})
144
+ .sort
145
+ .map { |k, v| k + ':' + (v.sort_by { |x| x }).join(',').gsub(/\s+/, ' ').strip }
119
146
  end
120
147
 
121
148
  def self.collect_headers(raw_headers, auth_header_name)
metadata CHANGED
@@ -1,7 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: escher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Andras Barthazi
@@ -13,43 +14,49 @@ dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rspec
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
- - - '>='
19
+ - - ~>
18
20
  - !ruby/object:Gem::Version
19
21
  version: '0'
20
22
  type: :development
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
- - - '>='
27
+ - - ~>
25
28
  - !ruby/object:Gem::Version
26
29
  version: '0'
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: rake
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
- - - '>='
35
+ - - ~>
32
36
  - !ruby/object:Gem::Version
33
37
  version: '0'
34
38
  type: :development
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
- - - '>='
43
+ - - ~>
39
44
  - !ruby/object:Gem::Version
40
45
  version: '0'
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: codeclimate-test-reporter
43
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
44
50
  requirements:
45
- - - '>='
51
+ - - ~>
46
52
  - !ruby/object:Gem::Version
47
53
  version: '0'
48
54
  type: :development
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
51
58
  requirements:
52
- - - '>='
59
+ - - ~>
53
60
  - !ruby/object:Gem::Version
54
61
  version: '0'
55
62
  description: For Emarsys API
@@ -62,25 +69,26 @@ files:
62
69
  homepage: http://emarsys.com
63
70
  licenses:
64
71
  - MIT
65
- metadata: {}
66
72
  post_install_message:
67
73
  rdoc_options: []
68
74
  require_paths:
69
75
  - lib
70
76
  required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
71
78
  requirements:
72
- - - '>='
79
+ - - ! '>='
73
80
  - !ruby/object:Gem::Version
74
81
  version: '0'
75
82
  required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
76
84
  requirements:
77
- - - '>='
85
+ - - ! '>='
78
86
  - !ruby/object:Gem::Version
79
87
  version: '0'
80
88
  requirements: []
81
89
  rubyforge_project:
82
- rubygems_version: 2.0.7
90
+ rubygems_version: 1.8.23
83
91
  signing_key:
84
- specification_version: 4
92
+ specification_version: 3
85
93
  summary: Escher - Emarsys request signing library
86
94
  test_files: []
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 981338fcbb72266308f87b15a3d9bc95a26d78cd
4
- data.tar.gz: 8523d44bcaabc7165ce430ab469e0684fec5980d
5
- SHA512:
6
- metadata.gz: 6f74fc90792b502604a30b1056b2d3fd9ee13f7ec00982ec45b1aa75da387d7c69b5a51790734cbe8f124c36434f09ca407a3be1d24f14b67aa581ad3d2470ee
7
- data.tar.gz: ee4a26a105765b9ed62e5feeb0aab7e6392b8cfba21dad5765e3c376d77bb3d86c4a0b4b8915dc6b23d99d81f44ee9779f68710b3c2bb043b9a5d62bd88f02ca