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 +7 -0
- data/.circleci/config.yml +20 -0
- data/.gitignore +10 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/Rakefile +35 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/Gemfile +7 -0
- data/examples/Gemfile.lock +36 -0
- data/examples/client.rb +64 -0
- data/examples/server.rb +30 -0
- data/lib/request_signing/adapters/net_http.rb +23 -0
- data/lib/request_signing/adapters/plaintext.rb +26 -0
- data/lib/request_signing/adapters.rb +56 -0
- data/lib/request_signing/algorithms/dsa.rb +39 -0
- data/lib/request_signing/algorithms/hmac.rb +37 -0
- data/lib/request_signing/algorithms/rsa.rb +38 -0
- data/lib/request_signing/algorithms.rb +30 -0
- data/lib/request_signing/errors.rb +37 -0
- data/lib/request_signing/generic_http_request.rb +47 -0
- data/lib/request_signing/key_stores/static.rb +54 -0
- data/lib/request_signing/key_stores.rb +5 -0
- data/lib/request_signing/parameter_parser.rb +41 -0
- data/lib/request_signing/signature_parameters.rb +31 -0
- data/lib/request_signing/version.rb +3 -0
- data/lib/request_signing.rb +213 -0
- data/request_signing-faraday.gemspec +24 -0
- data/request_signing-rack.gemspec +24 -0
- data/request_signing-ssm.gemspec +24 -0
- data/request_signing.gemspec +27 -0
- metadata +76 -0
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
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
|
+
[](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
data/examples/Gemfile
ADDED
|
@@ -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
|
data/examples/client.rb
ADDED
|
@@ -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
|
+
|
data/examples/server.rb
ADDED
|
@@ -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,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,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: []
|