request_signing 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5befd0a9f3eb4b066c72fa1eb6a6cd630850bd52
4
+ data.tar.gz: 9aca1d909e3c7371023bc921d85267de50473a0f
5
+ SHA512:
6
+ metadata.gz: 1c73007fc3990490803b997d85624ddd7bc402ede54cf49a8e81ba33ab876890249e075c6ad0985698951456596a9b828c8b81e8eef0ddcb9c588cf229b77d17
7
+ data.tar.gz: 67dcd66bcf0266f5f82328fedbb3d523459b1ba4f2f6c0e3ac47457beaef0aa00521cc0427e1e18def9463475f8c3df4c10f50e00806bce676cfd0ed8d5e70e2
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ jobs:
3
+ build:
4
+ docker:
5
+ - image: circleci/ruby:2.4.1
6
+
7
+ working_directory: ~/repo
8
+
9
+ steps:
10
+ - checkout
11
+
12
+ - run:
13
+ name: install dependencies
14
+ command: |
15
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
16
+
17
+ - run:
18
+ name: run tests
19
+ command: |
20
+ bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .vimrc
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec name: "request_signing"
4
+
5
+ Dir["request_signing-*.gemspec"].each do |gemspec|
6
+ plugin = gemspec.scan(/request_signing-(.*)\.gemspec/).flatten.first
7
+ gemspec(name: "request_signing-#{plugin}", development_group: plugin)
8
+ end
9
+
10
+ group :test do
11
+ gem "rake", "~> 10.0"
12
+ gem "minitest", "~> 5.0"
13
+ gem "rack", "~> 2.0"
14
+ gem "yard", "~> 0.9"
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Vlad Yarotsky
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,62 @@
1
+ # RequestSigning
2
+
3
+ [![Build Status](https://circleci.com/gh/remind101/request_signing.png?style=shield&circle-token=b945a7d85dbfbd7ef5a1257a985dee1ff3b47015)](https://circleci.com/gh/remind101/request_signing)
4
+
5
+
6
+ An extensible implementation of [http request signing spec draft](https://tools.ietf.org/html/draft-cavage-http-signatures-08)
7
+ for Ruby HTTP clients and servers.
8
+
9
+ Supports the following algorithms:
10
+
11
+ * rsa-sha1
12
+ * rsa-sha256
13
+ * rsa-sha512
14
+ * dsa-sha1
15
+ * hmac-sha1
16
+ * hmac-sha256
17
+ * hmac-sha512
18
+
19
+ Integrates with the following libraries:
20
+
21
+ * rack
22
+ * net/http
23
+ * faraday
24
+
25
+ ## Installation
26
+
27
+ Add these lines to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'request_signing'
31
+ gem 'request_signing-rack' # for rack integration
32
+ gem 'request_signing-faraday' # for faraday integration
33
+ gem 'request_signing-ssm' # for AWS SSM integration
34
+ ```
35
+
36
+ And then execute:
37
+
38
+ $ bundle
39
+
40
+ Or install it yourself as:
41
+
42
+ $ gem install request_signing request_signing-rack request_signing-faraday request_signing-ssm
43
+
44
+ ## Usage
45
+
46
+ See [examples](./examples)
47
+
48
+ ## Development
49
+
50
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
51
+
52
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
53
+
54
+ ## Contributing
55
+
56
+ Bug reports and pull requests are welcome on GitHub at https://github.com/remind101/request_signing.
57
+
58
+
59
+ ## License
60
+
61
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
62
+
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require "rake/testtask"
2
+ require "yard"
3
+
4
+ require "bundler/gem_helper"
5
+
6
+ gems = [
7
+ :request_signing,
8
+ :"request_signing-rack",
9
+ :"request_signing-faraday",
10
+ :"request_signing-ssm",
11
+ ]
12
+
13
+ gems.each do |g|
14
+ namespace g do
15
+ Bundler::GemHelper.install_tasks :name => g.to_s
16
+ end
17
+ end
18
+
19
+ Rake::TestTask.new(:test) do |t|
20
+ t.libs << "test"
21
+ t.libs << "lib"
22
+ t.test_files = FileList['test/**/*_test.rb']
23
+ end
24
+
25
+ YARD::Rake::YardocTask.new(:doc) do |t|
26
+ t.files = ['lib/**/*.rb']
27
+ end
28
+
29
+ task :default => :test
30
+
31
+ desc "Build and install request_signing and it's plugin gems into system gems"
32
+ task :install => gems.map { |g| "#{g}:install" }
33
+
34
+ desc "Build and install request_signing and it's plugin gems into system gems without network access"
35
+ task :"install:local" => gems.map { |g| "#{g}:install:local" }
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "request_signing"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/examples/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "request_signing", "~> 0.1.0"
4
+ gem "request_signing-rack", "~> 0.1.0"
5
+ gem "request_signing-faraday", "~> 0.1.0"
6
+ gem "sinatra", "~> 2.0"
7
+ gem "faraday", "~> 0.11"
@@ -0,0 +1,36 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ faraday (0.11.0)
5
+ multipart-post (>= 1.2, < 3)
6
+ multipart-post (2.0.0)
7
+ mustermann (1.0.1)
8
+ rack (2.0.3)
9
+ rack-protection (2.0.0)
10
+ rack
11
+ request_signing (0.1.0)
12
+ request_signing-faraday (0.1.0)
13
+ faraday (~> 0.9)
14
+ request_signing (= 0.1.0)
15
+ request_signing-rack (0.1.0)
16
+ rack (~> 2.0)
17
+ request_signing (= 0.1.0)
18
+ sinatra (2.0.0)
19
+ mustermann (~> 1.0)
20
+ rack (~> 2.0)
21
+ rack-protection (= 2.0.0)
22
+ tilt (~> 2.0)
23
+ tilt (2.0.8)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ faraday (~> 0.11)
30
+ request_signing (~> 0.1.0)
31
+ request_signing-faraday (~> 0.1.0)
32
+ request_signing-rack (~> 0.1.0)
33
+ sinatra (~> 2.0)
34
+
35
+ BUNDLED WITH
36
+ 1.15.1
@@ -0,0 +1,64 @@
1
+ require 'bundler/setup'
2
+ require 'request_signing'
3
+ require 'faraday'
4
+ require 'request_signing/faraday'
5
+
6
+ require 'net/http'
7
+ require 'optparse'
8
+ require 'time'
9
+ require 'uri'
10
+
11
+ options = {
12
+ host: "localhost",
13
+ port: 4567,
14
+ sign: true,
15
+ client: :net_http
16
+ }
17
+
18
+ SUPPORTED_CLIENTS = [:net_http, :faraday]
19
+
20
+ OptionParser.new do |opts|
21
+ opts.banner = "example HTTP client"
22
+
23
+ opts.on("-h", "--host HOST", "Example server host (default: localhost)") { |v| options[:host] = v }
24
+ opts.on("-p", "--port PORT", Integer, "Example server port (default: 4567)") { |v| options[:port] = v }
25
+ opts.on("-s", "--[no-]sign", "Whether the request should be signed (default: true)") { |v| options[:sign] = v }
26
+ opts.on("-c", "--client CLIENT", SUPPORTED_CLIENTS, "which client should be used (net_http, faraday) (default: net_http)") { |v| options[:client] = v }
27
+ end.parse!
28
+
29
+ key_store = RequestSigning::KeyStores::Static.new(
30
+ "client1.v1" => "uTj1izUmomtpECEhDfFb9lVDf54luNlH"
31
+ )
32
+
33
+ case options[:client]
34
+ when :net_http
35
+ req = Net::HTTP::Get.new(URI("http://#{options[:host]}:#{options[:port]}/"))
36
+ req["Date"] = Time.now.httpdate
37
+
38
+ if options[:sign]
39
+ signer = RequestSigning::Signer.new(adapter: :net_http, key_store: key_store)
40
+ req["Signature"] =
41
+ signer.create_signature!(req, key_id: "client1.v1", algorithm: "hmac-sha256", headers: %w[(request-target) host date])
42
+ end
43
+
44
+ http = Net::HTTP.new(options[:host], options[:port])
45
+ http.set_debug_output(STDERR)
46
+ http.start do
47
+ response = http.request req
48
+ puts response.body
49
+ end
50
+ when :faraday
51
+ conn = Faraday.new(url: "http://#{options[:host]}:#{options[:port]}") do |builder|
52
+ if options[:sign]
53
+ # note Faraday does not set the Host header, it is set downstream in in Net::HTTP::GenericRequest, but we can't use it in faraday itself.
54
+ builder.request :request_signing, key_store: key_store, key_id: "client1.v1", algorithm: "hmac-sha256", headers: %w[(request-target) date]
55
+ end
56
+ builder.adapter :net_http
57
+ end
58
+
59
+ response = conn.get("/")
60
+ puts response.body
61
+ else
62
+ raise "Unsupported client: #{options[:client]}"
63
+ end
64
+
@@ -0,0 +1,30 @@
1
+ require 'bundler/setup'
2
+ require 'sinatra'
3
+ require 'request_signing/rack'
4
+
5
+ class ExceptionHandling
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ begin
12
+ @app.call env
13
+ rescue RequestSigning::Error => e
14
+ [401, { "Content-Type" => "text/plain" }, ["request signature verification error: #{e.message}"]]
15
+ rescue => e
16
+ [500, { "Content-Type" => "text/plain" }, ["unknown server error: #{e.message}"]]
17
+ end
18
+ end
19
+ end
20
+
21
+ key_store = RequestSigning::KeyStores::Static.new(
22
+ "client1.v1" => "uTj1izUmomtpECEhDfFb9lVDf54luNlH"
23
+ )
24
+
25
+ use ExceptionHandling
26
+ use RequestSigning::Rack::Middleware, key_store: key_store
27
+
28
+ get "/" do
29
+ "Request signature verified successfully!"
30
+ end
@@ -0,0 +1,23 @@
1
+ require "request_signing/generic_http_request"
2
+
3
+ module RequestSigning
4
+ module Adapters
5
+
6
+ # Registers `:net_http` adapter for user with {RequestSigning::Signer}
7
+ #
8
+ # @example
9
+ # s = RequestSigning::Signer.new(adapter: :net_http, key_store: key_store)
10
+ class NetHTTP
11
+ def call(r)
12
+ GenericHTTPRequest.new(
13
+ r.method.downcase,
14
+ r.path,
15
+ r.each_header.map do |h, v|
16
+ [h, Array(v)]
17
+ end.to_h
18
+ )
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ require "webrick/httprequest"
2
+ require "webrick/config"
3
+ require "stringio"
4
+
5
+ require "request_signing/generic_http_request"
6
+
7
+ module RequestSigning
8
+ module Adapters
9
+
10
+ # @api private
11
+ class Plaintext
12
+ def call(plaintext_http)
13
+ webrick_req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
14
+ webrick_req.parse(StringIO.new(plaintext_http))
15
+
16
+ GenericHTTPRequest.new(
17
+ webrick_req.request_method.downcase,
18
+ webrick_req.request_uri.request_uri,
19
+ webrick_req.header
20
+ )
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+
@@ -0,0 +1,56 @@
1
+ module RequestSigning
2
+
3
+ ##
4
+ # Contains adapters for various http libraries.
5
+ # Adapters are used by {RequestSigning::Signer} and {RequestSigning::Verifier} to
6
+ # convert library specific http request objects to a common format.
7
+ #
8
+ # @example Adding new adapter
9
+ #
10
+ # require 'request_signing'
11
+ #
12
+ # class MyAdapter
13
+ # def call(my_http_library_req)
14
+ # RequestSigning::GenericHTTPRequest.new(
15
+ # my_http_library_req.request_method.downcase,
16
+ # my_http_library_req.request_full_path,
17
+ # my_http_library_req.headers.map do |h, v|
18
+ # [h, Array(v)]
19
+ # end.to_h
20
+ # )
21
+ # end
22
+ # end
23
+ #
24
+ # RequestSigning.register_adapter :my_http_library, ->() { MyAdapter.new }
25
+ #
26
+ # # ...
27
+ #
28
+ # req = MyHTTPLibrary::Post("/foo?bar=baz", "Date" => "Mon, 23 Oct 2017 00:00:00 GMT")
29
+ # signer = RequestSigning::Signer.new(adapter: :my_http_library, key_store: key_store)
30
+ # req.set_header "Signature", signer.create_signature!(req, key_id: "my_key", algorithm: "rsa-sha256", headers: %w[(request-target) date])
31
+ #
32
+ # # ...
33
+ #
34
+ # @see RequestSigning::GenericHTTPRequest#initialize
35
+ ##
36
+ module Adapters
37
+ require "request_signing/adapters/plaintext"
38
+ require "request_signing/adapters/net_http"
39
+ end
40
+
41
+ @adapters = {}
42
+
43
+ def self.get_adapter(name)
44
+ @adapters.fetch(name).call
45
+ rescue KeyError
46
+ raise UnsupportedAdapter, name
47
+ end
48
+
49
+ def self.register_adapter(name, adapter_factory)
50
+ @adapters[name] = adapter_factory
51
+ end
52
+
53
+ register_adapter :plaintext, ->() { Adapters::Plaintext.new }
54
+ register_adapter :net_http, ->() { Adapters::NetHTTP.new }
55
+
56
+ end
@@ -0,0 +1,39 @@
1
+ require "openssl"
2
+
3
+ module RequestSigning
4
+ module Algorithms
5
+
6
+ class DSA
7
+ # @param digester [OpenSSL::Digest]
8
+ def initialize(digester)
9
+ @digester = digester
10
+ end
11
+
12
+ # @param raw_private_key [String] DSA private key
13
+ # @param str [String] string to sign
14
+ # @raise [InvalidKey] when invalid DSA private key is supplied
15
+ def create_signature(raw_private_key, str)
16
+ key = OpenSSL::PKey::DSA.new(raw_private_key)
17
+ key.sign(@digester, str)
18
+ rescue OpenSSL::PKey::DSAError => e
19
+ raise InvalidKey, e
20
+ end
21
+
22
+ # @param raw_public_key [String] DSA public key
23
+ # @param signature [String] signature to verify
24
+ # @param str [String] signed string
25
+ # @raise [InvalidKey] when invalid DSA public key is supplied
26
+ # @return true if signature is valid
27
+ # @return false if signature is invalid
28
+ def verify_signature(raw_public_key, signature, str)
29
+ key = OpenSSL::PKey::DSA.new(raw_public_key)
30
+ key.verify(@digester, signature, str)
31
+ rescue OpenSSL::PKey::DSAError => e
32
+ raise InvalidKey, e
33
+ rescue OpenSSL::PKey::PKeyError
34
+ false
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ require "openssl"
2
+
3
+ module RequestSigning
4
+ module Algorithms
5
+
6
+ class HMAC
7
+ # @param digester [OpenSSL::Digest]
8
+ def initialize(digester)
9
+ @digester = digester
10
+ end
11
+
12
+ # @param hmac_secret [String] HMAC signing secret; 32-byte secret is recommended
13
+ # @param str [String] string to sign
14
+ # @raise [InvalidKey] when invalid HMAC signing secret is supplied
15
+ def create_signature(hmac_secret, str)
16
+ raise InvalidKey, "HMAC secret cannot be empty" if String(hmac_secret).empty?
17
+
18
+ OpenSSL::HMAC.digest(@digester, hmac_secret, str)
19
+ end
20
+
21
+ # @param hmac_secret [String] HMAC signing secret; 32-byte secret is recommended
22
+ # @param signature [String] signature to verify
23
+ # @param str [String] signed string
24
+ # @raise [InvalidKey] when invalid HMAC signing secret is supplied
25
+ # @return true if signature is valid
26
+ # @return false if signature is invalid
27
+ def verify_signature(hmac_secret, signature, str)
28
+ raise InvalidKey, "HMAC secret cannot be empty" if String(hmac_secret).empty?
29
+
30
+ recreated_signature = OpenSSL::HMAC.digest(@digester, hmac_secret, str)
31
+ signature == recreated_signature
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+
@@ -0,0 +1,38 @@
1
+ require "openssl"
2
+
3
+ module RequestSigning
4
+ module Algorithms
5
+
6
+ class RSA
7
+ # @param digester [OpenSSL::Digest]
8
+ def initialize(digester)
9
+ @digester = digester
10
+ end
11
+
12
+ # @param raw_private_key [String] RSA private key
13
+ # @param str [String] string to sign
14
+ # @raise [InvalidKey] when invalid RSA private key is supplied
15
+ def create_signature(raw_private_key, str)
16
+ key = OpenSSL::PKey::RSA.new(raw_private_key)
17
+ key.sign(@digester, str)
18
+ rescue OpenSSL::PKey::RSAError => e
19
+ raise InvalidKey, e
20
+ end
21
+
22
+ # @param raw_public_key [String] RSA public key
23
+ # @param signature [String] signature to verify
24
+ # @param str [String] signed string
25
+ # @raise [InvalidKey] when invalid RSA public key is supplied
26
+ # @return true if signature is valid
27
+ # @return false if signature is invalid
28
+ def verify_signature(raw_public_key, signature, str)
29
+ key = OpenSSL::PKey::RSA.new(raw_public_key)
30
+ key.verify(@digester, signature, str)
31
+ rescue OpenSSL::PKey::RSAError => e
32
+ raise InvalidKey, e
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+
@@ -0,0 +1,30 @@
1
+ module RequestSigning
2
+
3
+ module Algorithms
4
+ require "request_signing/algorithms/rsa"
5
+ require "request_signing/algorithms/dsa"
6
+ require "request_signing/algorithms/hmac"
7
+ end
8
+
9
+ @algorithms = {}
10
+
11
+ def self.get_algorithm(name)
12
+ @algorithms.fetch(name).call
13
+ rescue KeyError
14
+ raise UnsupportedAlgorithm, name
15
+ end
16
+
17
+ def self.register_algorithm(name, algorithm_factory)
18
+ @algorithms[name] = algorithm_factory
19
+ end
20
+
21
+ register_algorithm "rsa-sha1", ->() { Algorithms::RSA.new(OpenSSL::Digest::SHA1.new) }
22
+ register_algorithm "rsa-sha256", ->() { Algorithms::RSA.new(OpenSSL::Digest::SHA256.new) }
23
+ register_algorithm "rsa-sha512", ->() { Algorithms::RSA.new(OpenSSL::Digest::SHA512.new ) }
24
+ register_algorithm "dsa-sha1", ->() { Algorithms::DSA.new(OpenSSL::Digest::SHA1.new) }
25
+ register_algorithm "hmac-sha1", ->() { Algorithms::HMAC.new(OpenSSL::Digest::SHA1.new) }
26
+ register_algorithm "hmac-sha256", ->() { Algorithms::HMAC.new(OpenSSL::Digest::SHA256.new) }
27
+ register_algorithm "hmac-sha512", ->() { Algorithms::HMAC.new(OpenSSL::Digest::SHA512.new) }
28
+
29
+ end
30
+
@@ -0,0 +1,37 @@
1
+ module RequestSigning
2
+
3
+ # Base class for all errors
4
+ class Error < StandardError; end
5
+
6
+ # Key with specified keyId could not be found
7
+ class KeyNotFound < Error; end
8
+
9
+ # Provided signature does not match the request
10
+ class SignatureMismatch < Error; end
11
+
12
+ # Signature/Authorization header is malformed
13
+ class BadSignatureParameters < Error; end
14
+
15
+ # Signing algorithm is not supported
16
+ class UnsupportedAlgorithm < Error; end
17
+
18
+ # Library is not supported
19
+ class UnsupportedAdapter < Error; end
20
+
21
+ # Key can not be used to create/verify the signature by the given algorithm
22
+ class InvalidKey < Error; end
23
+
24
+ # Header specified in `headers` parameter for signature is
25
+ # not present in the request
26
+ class HeaderNotInRequest < Error; end
27
+
28
+ # Signature/Authorization header are missing from the request
29
+ class MissingSignatureHeader < Error; end
30
+
31
+ # Authorization header scheme is incorrect. It must be "Signature"
32
+ class UnsupportedAuthorizationScheme < Error; end
33
+
34
+ # Keys string provided to {RequestSigning::KeyStores::Static#from_string} is not valid
35
+ class MalformedKeysString < Error; end
36
+
37
+ end
@@ -0,0 +1,47 @@
1
+ module RequestSigning
2
+
3
+ # @api private
4
+ class GenericHTTPRequest
5
+ attr_reader :method, :headers
6
+
7
+ # HTTP/1.1 request methods
8
+ # @see https://tools.ietf.org/html/rfc7231#section-4.1
9
+ HTTP_METHODS = %w(get head post put delete connect options trace).freeze
10
+
11
+ # @api public
12
+ # @param method [String] HTTP request method
13
+ # @param request_uri [URI] part of request url after host and port, e.g. "/foo?bar=baz"
14
+ # @param headers [Hash{String=>Array<String>}] hash of lowercased request headers,
15
+ # e.g. { "date" => ["Mon, 23 Oct 2017 00:00:00 GMT"] }
16
+ def initialize(method, request_uri, headers)
17
+ method = method.downcase
18
+
19
+ unless HTTP_METHODS.include?(method)
20
+ raise ArgumentError, "Invalid HTTP method"
21
+ end
22
+
23
+ @method = method
24
+ @request_uri = URI(request_uri)
25
+ @headers = Hash[headers].freeze
26
+ end
27
+
28
+
29
+ # :path pseudo-header
30
+ # @see https://tools.ietf.org/html/rfc7540#section-8.1.2.3
31
+ def path
32
+ @request_uri.to_s
33
+ end
34
+
35
+ def header?(name)
36
+ @headers.key?(name)
37
+ end
38
+
39
+ def ==(other)
40
+ return false unless self.class === other
41
+ method == other.method &&
42
+ path == other.path &&
43
+ headers == other.headers
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,54 @@
1
+ require "request_signing/errors"
2
+
3
+ module RequestSigning
4
+ module KeyStores
5
+
6
+ # Simple static key store implementation.
7
+ # @see RequestSigning::Signer
8
+ # @see RequestSigning::Verifier
9
+ class Static
10
+
11
+ ##
12
+ # Makes a new instance of {RequestSigning::KeyStores::Static} from `keys_str`
13
+ #
14
+ # @param keys_str [String] a list of keys in the form of
15
+ # `keyId:keySecret,keyId2:keySecret2`
16
+ # @raise [RequestSigning::MalformedKeysString] when the `keys_str` is malformed
17
+ #
18
+ # @note not recommended for use with anything other than HMAC secrets.
19
+ ##
20
+ def self.from_string(keys_str)
21
+ keys = keys_str.split(",").each_with_object({}) do |id_key, r|
22
+ id, key = id_key.split(":", 2).map(&:strip)
23
+ raise MalformedKeysString unless id && key
24
+ r[id] = key
25
+ end
26
+ new(keys)
27
+ end
28
+
29
+ # @param keys [Hash{String=>String}] a map from keyId to key value
30
+ # @example
31
+ # RequestSigning::KeyStores::Static.new("my_key" => "key secret")
32
+ def initialize(keys)
33
+ @keys = keys
34
+ end
35
+
36
+ # @param key_id [String] id of the key to retrieve
37
+ # @return [String] key contents
38
+ # @raise [RequestSigning::KeyNotFound] when requested key is not found
39
+ def fetch(key_id)
40
+ @keys.fetch(key_id)
41
+ rescue KeyError
42
+ raise KeyNotFound, key_id
43
+ end
44
+
45
+ # @param key_id [String] id of the key
46
+ # @return true if store knows this key
47
+ # @return false if store does not recognize the key
48
+ def key?(key_id)
49
+ @keys.key?(key_id)
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ module RequestSigning
2
+ module KeyStores
3
+ require "request_signing/key_stores/static"
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ require "webrick/httputils"
2
+ require "request_signing/errors"
3
+ require "request_signing/signature_parameters"
4
+
5
+ module RequestSigning
6
+
7
+ # @api private
8
+ class ParameterParser
9
+ def parse(signature_parameters_str)
10
+ values = values_hash(signature_parameters_str)
11
+ raise BadSignatureParameters, "keyId is required" if String(values["keyId"]).empty?
12
+ raise BadSignatureParameters, "algorithm is required" if String(values["algorithm"]).empty?
13
+ raise BadSignatureParameters, "signature is required" if String(values["signature"]).empty?
14
+
15
+ headers = String(values["headers"]).split(" ").map(&:downcase)
16
+ headers = ["date"] if headers.empty?
17
+
18
+ SignatureParameters.new(
19
+ key_id: values["keyId"],
20
+ algorithm: values["algorithm"],
21
+ headers: headers,
22
+ signature: values["signature"]
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def values_hash(signature_parameters_str)
29
+ fields = WEBrick::HTTPUtils.split_header_value(signature_parameters_str)
30
+ fields.each_with_object({}) do |f, r|
31
+ fname, quoted_value = f.split("=", 2).map { |t| String(t).strip }
32
+ unless quoted_value =~ /\A".*\"\Z/
33
+ raise BadSignatureParameters, "malformed field value"
34
+ end
35
+ value = WEBrick::HTTPUtils.dequote(quoted_value)
36
+ r[fname] = value
37
+ end
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,31 @@
1
+ require "webrick/httputils"
2
+
3
+ module RequestSigning
4
+
5
+ # @api private
6
+ class SignatureParameters
7
+ attr_reader :key_id, :algorithm, :headers, :signature
8
+
9
+ def initialize(key_id:, algorithm:, headers:, signature:)
10
+ @key_id = key_id
11
+ @algorithm = algorithm
12
+ @headers = headers
13
+ @signature = signature
14
+ end
15
+
16
+ def to_s
17
+ "keyId=#{quote(key_id)},algorithm=#{quote(algorithm)},headers=#{quote(headers_str)},signature=#{quote(signature)}"
18
+ end
19
+
20
+ private
21
+
22
+ def quote(str)
23
+ WEBrick::HTTPUtils.quote(str)
24
+ end
25
+
26
+ def headers_str
27
+ headers.join(" ")
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,3 @@
1
+ module RequestSigning
2
+ VERSION = "0.1.0.pre1"
3
+ end
@@ -0,0 +1,213 @@
1
+ require "request_signing/version"
2
+ require "uri"
3
+ require "base64"
4
+ require "request_signing/generic_http_request"
5
+ require "request_signing/parameter_parser"
6
+ require "request_signing/adapters"
7
+ require "request_signing/algorithms"
8
+ require "request_signing/errors"
9
+ require "request_signing/key_stores"
10
+ require "request_signing/signature_parameters"
11
+
12
+ # @see RequestSigning::Signer
13
+ # @see RequestSigning::Verifier
14
+ module RequestSigning
15
+
16
+ # Verifies the request signature
17
+ #
18
+ # @see RequestSigning::Rack
19
+ class Verifier
20
+
21
+ ##
22
+ # @param adapter [Symbol] name of the library adapter.
23
+ # @param key_store [#fetch, #key?] signature verification key store
24
+ #
25
+ # @raise [RequestSigning::UnsupportedAdapter] when the adapter is not registered
26
+ #
27
+ # @see RequestSigning::Adapters
28
+ # @see RequestSigning::KeyStores::Static
29
+ ##
30
+ def initialize(adapter:, key_store:)
31
+ @adapter = RequestSigning.get_adapter(adapter)
32
+ @key_store = key_store
33
+ end
34
+
35
+ ##
36
+ # Verifies request signature
37
+ #
38
+ # @param req - an http request object from the library specified via :adapter
39
+ #
40
+ # @raise [RequestSigning::SignatureMismatch] when the signature is invalid
41
+ # @raise [RequestSigning::KeyNotFound] when the key store does not contain the key
42
+ # referenced in the request
43
+ # @raise [RequestSigning::BadSignatureParameters] when the signature is malformed
44
+ # @raise [RequestSigning::UnsupportedAlgorithm] when the algorithm referenced
45
+ # in the request is not supported
46
+ # @raise [RequestSigning::InvalidKey] when the key in key store can not be used
47
+ # to verify the signature
48
+ # @raise [RequestSigning::HeaderNotInRequest] when one of the headers specified
49
+ # in `headers` signature component is not present in the request
50
+ # @raise [RequestSigning::MissingSignatureHeader] when neither `Signature` nor
51
+ # `Authorization` headers are present
52
+ # @raise [RequestSigning::UnsupportedAuthorizationScheme] when the scheme
53
+ # specified in the `Authorization` header is not `Signature` and the `Signature`
54
+ # header is absent
55
+ ##
56
+ def verify!(req)
57
+ verifiable_req = @adapter.call(req)
58
+ signature_parameters = get_signature_parameters(verifiable_req)
59
+
60
+ key = get_key(signature_parameters.key_id)
61
+ alg = get_algorithm(signature_parameters.algorithm)
62
+ string_for_signing = RequestSigning.make_string_for_signing(signature_parameters.headers, verifiable_req)
63
+ signature = decode_signature(signature_parameters.signature)
64
+ unless alg.verify_signature(key, signature, string_for_signing)
65
+ raise SignatureMismatch
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def get_signature_parameters(req)
72
+ if req.header?("signature")
73
+ parameters_str = req.headers["signature"].first
74
+ ParameterParser.new.parse(parameters_str)
75
+ elsif req.header?("authorization")
76
+ auth_header = req.headers["authorization"].first
77
+ auth_scheme, parameters_str = auth_header.split(" ", 2).map(&:strip)
78
+ unless auth_scheme == "Signature"
79
+ raise UnsupportedAuthorizationScheme, "Authorization header scheme must be 'Signature'"
80
+ end
81
+ ParameterParser.new.parse(parameters_str)
82
+ else
83
+ raise MissingSignatureHeader, "request must contain either Authorization or Signature header"
84
+ end
85
+ end
86
+
87
+ def get_algorithm(name)
88
+ RequestSigning.get_algorithm(name)
89
+ end
90
+
91
+ def get_key(id)
92
+ @key_store.fetch(id)
93
+ end
94
+
95
+ def decode_signature(signature_base64)
96
+ Base64.strict_decode64(signature_base64)
97
+ rescue ArgumentError
98
+ raise BadSignatureParameters, "malformed signature"
99
+ end
100
+ end
101
+
102
+ ##
103
+ # Creates request signature string
104
+ #
105
+ # @example
106
+ # key_store = RequestSigning::KeyStores::Static.new(
107
+ # "app_1.v1" => ENV["APP_1_PRIVATE_KEY"],
108
+ # "app_2.v1" => ENV["APP_2_PRIVATE_KEY"],
109
+ # )
110
+ # req = Net::HTTP::Get.new("/foo?bar=baz")
111
+ # req["Date"] = "Thu, 05 Jan 2014 21:31:40 GMT"
112
+ # req["Signature"] =
113
+ # @signer.create_signature!(req, key_id: "app_1.v1", algorithm: "rsa-sha256", headers: %w[(request-target) date host])
114
+ # Net::HTTP.start("http://example.com", 80) do |http|
115
+ # response = http.request(req)
116
+ # end
117
+ ##
118
+ class Signer
119
+ ##
120
+ # @param adapter [Symbol] name of the library adapter.
121
+ # @param key_store [#fetch, #key?] signature verification key store
122
+ #
123
+ # @raise [RequestSigning::UnsupportedAdapter] when the adapter is not registered
124
+ #
125
+ # @see RequestSigning::Adapters
126
+ # @see RequestSigning::KeyStores::Static
127
+ ##
128
+ def initialize(adapter:, key_store:)
129
+ @adapter = RequestSigning.get_adapter(adapter)
130
+ @key_store = key_store
131
+ end
132
+
133
+ ##
134
+ # Creates a signature string
135
+ #
136
+ # @example
137
+ # keyId="hmac",algorithm="hmac-sha256",headers="date",signature="id0KmonZJTY53n+fk27Q5CtroeQ5UyRY/tbotiuhob4="
138
+ #
139
+ # @param req an http request object from the library specified via :adapter; see {RequestSigning::Adapters}
140
+ # @param key_id [String] key id to use for signing
141
+ # @param algorithm [String] algorithm to use for signing, e.g. `"rsa-sha256"`
142
+ # @param headers [Array<String>] headers to sign
143
+ #
144
+ # May include special `(request-target)` header.
145
+ #
146
+ # The recommendation is to sign:
147
+ # - for HTTPS requests - `["(request-target"), "host", "date"]`
148
+ # - for HTTP requests - all headers
149
+ #
150
+ # See {https://tools.ietf.org/html/draft-cavage-http-signatures-08#section-2.3}
151
+ #
152
+ # @return [String] signature components string. See example above.
153
+ #
154
+ # @raise [RequestSigning::KeyNotFound] when the key store does not contain the key
155
+ # referenced in the :key_id parameter
156
+ # @raise [RequestSigning::UnsupportedAlgorithm] when the algorithm referenced
157
+ # in :algorithm parameter is not supported
158
+ # @raise [RequestSigning::InvalidKey] when the key in key store can not be used
159
+ # to create the signature
160
+ # @raise [RequestSigning::HeaderNotInRequest] when one of the headers specified
161
+ # in `headers` signature component is not present in the request
162
+ ##
163
+ def create_signature!(req, key_id:, algorithm:, headers: %w[date])
164
+ signable_req = @adapter.call(req)
165
+
166
+ headers = normalize_headers(headers)
167
+ key = get_key(key_id)
168
+ alg = get_algorithm(algorithm)
169
+ string_for_signing = RequestSigning.make_string_for_signing(headers, signable_req)
170
+ signature = alg.create_signature(key, string_for_signing)
171
+ SignatureParameters.new(
172
+ key_id: key_id,
173
+ algorithm: algorithm,
174
+ headers: headers,
175
+ signature: encode_signature(signature)
176
+ )
177
+ end
178
+
179
+ private
180
+
181
+ def get_algorithm(name)
182
+ RequestSigning.get_algorithm(name)
183
+ end
184
+
185
+ def get_key(id)
186
+ @key_store.fetch(id)
187
+ end
188
+
189
+ def encode_signature(signature)
190
+ Base64.strict_encode64(signature).chomp
191
+ end
192
+
193
+ def normalize_headers(headers)
194
+ headers.map(&:downcase)
195
+ end
196
+ end
197
+
198
+ # @api private
199
+ def self.make_string_for_signing(headers_list, verifiable_req)
200
+ headers_list.each_with_object([]) do |h, a|
201
+ case h
202
+ when "(request-target)"
203
+ a << "(request-target): #{verifiable_req.method.downcase} #{verifiable_req.path}"
204
+ else
205
+ vs = Array(verifiable_req.headers[h])
206
+ if vs.empty?
207
+ raise HeaderNotInRequest, h
208
+ end
209
+ a << "#{h}: #{vs.join(", ").strip}"
210
+ end
211
+ end.join("\n")
212
+ end
213
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'request_signing/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "request_signing-faraday"
8
+ spec.version = "0.1.0.pre1"
9
+ spec.authors = ["Vlad Yarotsky"]
10
+ spec.email = ["vlad@remind101.com"]
11
+
12
+ spec.summary = %q{Faraday middleware for request signing}
13
+ spec.description = %q{Faraday middleware for request signing}
14
+ spec.homepage = "https://github.com/remind101/request_signing"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = ["lib/request_signing/faraday.rb"]
18
+
19
+ spec.require_paths = ["lib"]
20
+ spec.metadata["yard.run"] = "yri"
21
+
22
+ spec.add_dependency "request_signing", RequestSigning::VERSION
23
+ spec.add_dependency "faraday", "~> 0.9"
24
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'request_signing/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "request_signing-rack"
8
+ spec.version = "0.1.0.pre1"
9
+ spec.authors = ["Vlad Yarotsky"]
10
+ spec.email = ["vlad@remind101.com"]
11
+
12
+ spec.summary = %q{Rack middleware for request signature verification}
13
+ spec.description = %q{Rack middleware for request signature verification based on request_signing}
14
+ spec.homepage = "https://github.com/remind101/request_signing"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = ["lib/request_signing/rack.rb"]
18
+
19
+ spec.require_paths = ["lib"]
20
+ spec.metadata["yard.run"] = "yri"
21
+
22
+ spec.add_dependency "request_signing", RequestSigning::VERSION
23
+ spec.add_dependency "rack", "~> 2.0"
24
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'request_signing/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "request_signing-ssm"
8
+ spec.version = "0.1.0.pre1"
9
+ spec.authors = ["Vlad Yarotsky"]
10
+ spec.email = ["vlad@remind101.com"]
11
+
12
+ spec.summary = %q{AWS SSM key store for request_signing gem}
13
+ spec.description = %q{AWS SSM key store for request_signing gem}
14
+ spec.homepage = "https://github.com/remind101/request_signing"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = ["lib/request_signing/ssm.rb"]
18
+
19
+ spec.require_paths = ["lib"]
20
+ spec.metadata["yard.run"] = "yri"
21
+
22
+ spec.add_dependency "request_signing", RequestSigning::VERSION
23
+ spec.add_dependency "aws-sdk-ssm", "~> 1"
24
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'request_signing/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "request_signing"
8
+ spec.version = RequestSigning::VERSION
9
+ spec.authors = ["Vlad Yarotsky"]
10
+ spec.email = ["vlad@remind101.com"]
11
+
12
+ spec.summary = %q{Implementation of http request signing draft https://tools.ietf.org/html/draft-cavage-http-signatures-08}
13
+ spec.description = %q{Implementation of http request signing draft https://tools.ietf.org/html/draft-cavage-http-signatures-08}
14
+ spec.homepage = "https://github.com/remind101/request_signing"
15
+ spec.license = "MIT"
16
+
17
+ plugin_files = Dir["request_signing-*.gemspec"].map { |gemspec|
18
+ eval(File.read(gemspec)).files
19
+ }.flatten.uniq
20
+
21
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
22
+ f.match(%r{^(test|spec|features)/})
23
+ end - plugin_files
24
+
25
+ spec.require_paths = ["lib"]
26
+ spec.metadata["yard.run"] = "yri"
27
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: request_signing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Vlad Yarotsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Implementation of http request signing draft https://tools.ietf.org/html/draft-cavage-http-signatures-08
14
+ email:
15
+ - vlad@remind101.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".circleci/config.yml"
21
+ - ".gitignore"
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - bin/console
27
+ - bin/setup
28
+ - examples/Gemfile
29
+ - examples/Gemfile.lock
30
+ - examples/client.rb
31
+ - examples/server.rb
32
+ - lib/request_signing.rb
33
+ - lib/request_signing/adapters.rb
34
+ - lib/request_signing/adapters/net_http.rb
35
+ - lib/request_signing/adapters/plaintext.rb
36
+ - lib/request_signing/algorithms.rb
37
+ - lib/request_signing/algorithms/dsa.rb
38
+ - lib/request_signing/algorithms/hmac.rb
39
+ - lib/request_signing/algorithms/rsa.rb
40
+ - lib/request_signing/errors.rb
41
+ - lib/request_signing/generic_http_request.rb
42
+ - lib/request_signing/key_stores.rb
43
+ - lib/request_signing/key_stores/static.rb
44
+ - lib/request_signing/parameter_parser.rb
45
+ - lib/request_signing/signature_parameters.rb
46
+ - lib/request_signing/version.rb
47
+ - request_signing-faraday.gemspec
48
+ - request_signing-rack.gemspec
49
+ - request_signing-ssm.gemspec
50
+ - request_signing.gemspec
51
+ homepage: https://github.com/remind101/request_signing
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ yard.run: yri
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">"
68
+ - !ruby/object:Gem::Version
69
+ version: 1.3.1
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.6.8
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Implementation of http request signing draft https://tools.ietf.org/html/draft-cavage-http-signatures-08
76
+ test_files: []