hmac_authentication 0.0.0 → 1.0.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: 6b70792ff95d35d009ad4508ad0a2f023ecad063
4
- data.tar.gz: 72a5c827090f6b34193e06f27b8e220fa0df3c8b
3
+ metadata.gz: 93cb19ecbf56f6f4054b58899b122fd4dd6f0a28
4
+ data.tar.gz: 88e37b55037f15851158565db2fa11fc81175078
5
5
  SHA512:
6
- metadata.gz: e979ffd7608fb388dff2512da040c43a3811cd28b948fa6bf9ce622e5740a18cea5b6ff490473553c6dc6356cb1f4db16cad8dfa6ac6beb8c30b570e75a57c1e
7
- data.tar.gz: f93f9077f46ba1e8e22d89c6cbe286fd7a8d3eee40862565636189ec1cb084b454ea19853e8f10aba33d1c86d286530a962fd55545db2411d0ca3afb5d73c342
6
+ metadata.gz: d3ee6450b5e285164b17d74f1a54ee216e6a1dd0853cade9775951a0ec55ddd4df8b081be4ea734792ef4dbde526a25b120654787e658bf7e27394b2b3bca90d
7
+ data.tar.gz: 0d1aa098c990d41d42d9fcf93b96404700d02388cade6308c0a981504b30c51d87c3e037326b168c94f3ba2d58b1309de45caec4d1588993bafc854a7e601fa1
data/README.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # hmac_authentication RubyGem
2
2
 
3
- Signs and validates HTTP requests based on a shared-secret HMAC signature.
3
+ Signs and authenticates HTTP requests based on a shared-secret HMAC signature.
4
+
5
+ Developed in parallel with the following packages for other languages:
6
+ - Go: [github.com/18F/hmacauth](https://github.com/18F/hmacauth/)
7
+ - Node.js: [hmac-authentication](https://www.npmjs.com/package/hmac-authentication)
8
+
9
+ **Warning: Repeated HTTP headers will cause an authentication failure!**
10
+ Because of the way that Ruby's
11
+ [Net::HTTPHeader.initialize_http_header](https://github.com/rubysl/rubysl-net-http/blob/2.0/lib/net/http/header.rb)
12
+ method is implemented, it will discard all but the last of a series of
13
+ repeated headers. The packages for other languages will combine repeated
14
+ headers into one. Therefore, if your Ruby service receives an signed request
15
+ from a server using one of these other modules, and the request has repeated
16
+ headers, authentication of the request will fail.
4
17
 
5
18
  ## Installation
6
19
 
@@ -13,7 +26,7 @@ gem 'hmac_authentication'
13
26
 
14
27
  If you're not using Bundler, start.
15
28
 
16
- ## Validating incoming requests
29
+ ## Authenticating incoming requests
17
30
 
18
31
  Inject something resembling the following code fragment into your request
19
32
  handling logic as the first thing that happens before the request body is
@@ -24,10 +37,15 @@ making the request:
24
37
  ```ruby
25
38
  require 'hmac_authentication'
26
39
 
27
- def my_handler(request, headers)
28
- result, header_signature, computed_signature = (
29
- HmacAuthentication.validate_request(request, headers, secret_key))
30
- if result != HmacAuthentication::MATCH
40
+ # When only used for authentication, it doesn't matter what the first argument
41
+ # is, because the hash algorithm used for authentication will be parsed from
42
+ # the incoming request signature header.
43
+ auth = HmacAuthentication::HmacAuth.new(
44
+ 'sha1', secret_key, signature_header, headers)
45
+
46
+ def request_handler(request)
47
+ result, header_sig, computed_sig = auth.authenticate_request request
48
+ if result != HmacAuthentication::HmacAuth::MATCH
31
49
  # Cancel the request, optionally logging the values above.
32
50
  end
33
51
  end
@@ -35,8 +53,18 @@ end
35
53
 
36
54
  ## Signing outgoing requests
37
55
 
38
- Call `request_signature(request, headers, secretKey)` to sign a request before
39
- sending.
56
+ Do something similar to the following.
57
+
58
+ ```ruby
59
+ digest_name = 'sha1' # Or any other available Hash algorithm.
60
+ auth = HmacAuthentication::HmacAuth.new(
61
+ digest_name, secret_key, signature_header, headers)
62
+
63
+ def make_request(request)
64
+ // Prepare request...
65
+ auth.sign_request request
66
+ end
67
+ ```
40
68
 
41
69
  ## Public domain
42
70
 
@@ -1,4 +1,5 @@
1
1
  require 'base64'
2
+ require 'fast_secure_compare/fast_secure_compare'
2
3
  require 'openssl'
3
4
 
4
5
  module HmacAuthentication
@@ -8,20 +9,74 @@ module HmacAuthentication
8
9
  MATCH = 4
9
10
  MISMATCH = 5
10
11
 
11
- def self.signed_headers(request, headers)
12
- headers.map { |name| request[name] || '' }
13
- end
12
+ RESULT_CODE_STRINGS = %w(
13
+ NO_SIGNATURE
14
+ INVALID_FORMAT
15
+ UNSUPPORTED_ALGORITHM
16
+ MATCH
17
+ MISMATCH
18
+ )
14
19
 
15
- def self.string_to_sign(req, headers)
16
- # TODO(mbland): Test for paths of the form 'http://foo.com/bar?baz'
17
- [req.method, signed_headers(req, headers).join("\n"), req.uri.path]
18
- .join("\n")
20
+ def self.result_code_to_string(code)
21
+ index = code - 1
22
+ index >= 0 ? RESULT_CODE_STRINGS[index] : nil
19
23
  end
20
24
 
21
- def self.request_signature(request, digest, headers, secret_key)
22
- hmac = OpenSSL::HMAC.new secret_key, digest
23
- hmac << string_to_sign(request, headers) << (request.body || '')
24
- digest.name.downcase + ' ' + Base64.strict_encode64(hmac.digest)
25
+ class HmacAuth
26
+ attr_reader :digest, :secret_key, :signature_header, :headers
27
+
28
+ def initialize(digest_name, secret_key, signature_header, headers)
29
+ @digest = HmacAuthentication.parse_digest digest_name
30
+ if digest.nil?
31
+ fail "HMAC authentication digest is not supported: #{digest_name}"
32
+ end
33
+
34
+ @secret_key = secret_key
35
+ @signature_header = signature_header
36
+ @headers = headers
37
+ end
38
+
39
+ def string_to_sign(req)
40
+ [req.method,
41
+ signed_headers(req).join("\n"),
42
+ HmacAuthentication.hash_url(req)].join("\n") + "\n"
43
+ end
44
+
45
+ def sign_request(req)
46
+ req[signature_header] = request_signature req
47
+ end
48
+
49
+ def request_signature(request)
50
+ request_signature_impl request, digest
51
+ end
52
+
53
+ def signature_from_header(request)
54
+ request[signature_header]
55
+ end
56
+
57
+ def authenticate_request(request)
58
+ header = signature_from_header request
59
+ return NO_SIGNATURE unless header
60
+ components = header.split ' '
61
+ return INVALID_FORMAT, header unless components.size == 2
62
+ parsed_digest = HmacAuthentication.parse_digest components.first
63
+ return UNSUPPORTED_ALGORITHM, header unless parsed_digest
64
+ computed = request_signature_impl request, parsed_digest
65
+ [HmacAuthentication.compare_signatures(header, computed),
66
+ header, computed]
67
+ end
68
+
69
+ private
70
+
71
+ def signed_headers(request)
72
+ headers.map { |name| (request.get_fields(name) || []).join(',') }
73
+ end
74
+
75
+ def request_signature_impl(request, digest_)
76
+ hmac = OpenSSL::HMAC.new secret_key, digest_
77
+ hmac << string_to_sign(request) << (request.body || '')
78
+ digest_.name.downcase + ' ' + Base64.strict_encode64(hmac.digest)
79
+ end
25
80
  end
26
81
 
27
82
  def self.parse_digest(name)
@@ -30,14 +85,14 @@ module HmacAuthentication
30
85
  nil
31
86
  end
32
87
 
33
- def self.validate_request(request, headers, secret_key)
34
- header = request['Gap-Signature']
35
- return NO_SIGNATURE unless header
36
- components = header.split ' '
37
- return INVALID_FORMAT, header unless components.size == 2
38
- digest = parse_digest components.first
39
- return UNSUPPORTED_ALGORITHM, header unless digest
40
- computed = request_signature(request, digest, headers, secret_key)
41
- [(header == computed) ? MATCH : MISMATCH, header, computed]
88
+ def self.compare_signatures(header, computed)
89
+ FastSecureCompare.compare(computed, header) ? MATCH : MISMATCH
90
+ end
91
+
92
+ def self.hash_url(req)
93
+ result = "#{req.uri.path}"
94
+ result << '?' << req.uri.query if req.uri.query
95
+ result << '#' << req.uri.fragment if req.uri.fragment
96
+ result
42
97
  end
43
98
  end
@@ -1,3 +1,3 @@
1
1
  module HmacAuthentication
2
- VERSION = '0.0.0'
2
+ VERSION = '1.0.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hmac_authentication
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-02 00:00:00.000000000 Z
11
+ date: 2015-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fast_secure_compare
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: go_script
29
43
  requirement: !ruby/object:Gem::Requirement