request_signing 0.1.0.pre1

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 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: []