bollard 1.0.0

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: 902e221383e933affc9e6e06aad20ad103e3844c
4
+ data.tar.gz: 266c40c3987a7395d7505e18b9b30a4c1abed5ee
5
+ SHA512:
6
+ metadata.gz: 7fa85f9688e7009f7ba8758162a7bf0acf04fc5a8edb728617cbf5a5003f47060276407052bdf213457aed99e0ed94eb8ec2f4e4abd56bbadc3be135cd40f8c1
7
+ data.tar.gz: b69f6bc526192d625370f9552e9ebe8793cc3dbccc6b0f7180abcc1d0da09ff88a558f4ef72a121dc8484dd97fb16f912300f0b0271a129290e42b241e54b101
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /.byebug_history
2
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bollard (1.0.0)
5
+ jwt
6
+ rest-client
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (5.2.1)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 0.7, < 2)
14
+ minitest (~> 5.1)
15
+ tzinfo (~> 1.1)
16
+ addressable (2.5.2)
17
+ public_suffix (>= 2.0.2, < 4.0)
18
+ byebug (10.0.2)
19
+ concurrent-ruby (1.0.5)
20
+ crack (0.4.3)
21
+ safe_yaml (~> 1.0.0)
22
+ diff-lcs (1.3)
23
+ domain_name (0.5.20180417)
24
+ unf (>= 0.0.5, < 1.0.0)
25
+ hashdiff (0.3.7)
26
+ http-cookie (1.0.3)
27
+ domain_name (~> 0.5)
28
+ i18n (1.1.0)
29
+ concurrent-ruby (~> 1.0)
30
+ jwt (2.1.0)
31
+ mime-types (3.2.2)
32
+ mime-types-data (~> 3.2015)
33
+ mime-types-data (3.2018.0812)
34
+ minitest (5.11.3)
35
+ netrc (0.11.0)
36
+ public_suffix (3.0.3)
37
+ rake (12.3.1)
38
+ rest-client (2.0.2)
39
+ http-cookie (>= 1.0.2, < 2.0)
40
+ mime-types (>= 1.16, < 4.0)
41
+ netrc (~> 0.8)
42
+ rspec (3.8.0)
43
+ rspec-core (~> 3.8.0)
44
+ rspec-expectations (~> 3.8.0)
45
+ rspec-mocks (~> 3.8.0)
46
+ rspec-core (3.8.0)
47
+ rspec-support (~> 3.8.0)
48
+ rspec-expectations (3.8.1)
49
+ diff-lcs (>= 1.2.0, < 2.0)
50
+ rspec-support (~> 3.8.0)
51
+ rspec-mocks (3.8.0)
52
+ diff-lcs (>= 1.2.0, < 2.0)
53
+ rspec-support (~> 3.8.0)
54
+ rspec-support (3.8.0)
55
+ safe_yaml (1.0.4)
56
+ thread_safe (0.3.6)
57
+ tzinfo (1.2.5)
58
+ thread_safe (~> 0.1)
59
+ unf (0.1.4)
60
+ unf_ext
61
+ unf_ext (0.0.7.5)
62
+ webmock (2.3.2)
63
+ addressable (>= 2.3.6)
64
+ crack (>= 0.3.2)
65
+ hashdiff
66
+
67
+ PLATFORMS
68
+ ruby
69
+
70
+ DEPENDENCIES
71
+ activesupport (>= 3.1)
72
+ bollard!
73
+ byebug
74
+ rake
75
+ rspec (~> 3.7)
76
+ webmock (~> 2.3)
77
+
78
+ BUNDLED WITH
79
+ 1.16.1
data/LICENCE.md ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright 2018 Vinomofo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Bollard
2
+
3
+ A way of securing API communications with a JWT header that verifies a payload given a known secret
4
+
5
+ ## Use
6
+ Install Bollard
7
+
8
+ ```
9
+ gem install bollard
10
+ ```
11
+
12
+ Use Bollard to :post messages
13
+ ```
14
+ require "bollard"
15
+
16
+ Bollard.secure_post("https://my.endpoint/api", '{ "key_1": "val_1" }', "shared_secret_key")
17
+ ```
18
+
19
+ Use Bollard to verify received messages
20
+ ```
21
+ require "bollard"
22
+
23
+ Bollard.verify_post('{ "key_1": "val_1" }', "", "shared_secret_key")
24
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bollard.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ # Maintain your gem's version:
4
+ require "bollard/version"
5
+
6
+ # Describe your gem and declare its dependencies:
7
+ Gem::Specification.new do |s|
8
+ s.name = "bollard"
9
+ s.version = Bollard::VERSION
10
+ s.license = "MIT"
11
+ s.authors = ["Michael Dilley"]
12
+ s.email = "mick@vinomofo.com"
13
+ s.homepage = "https://github.com/vinomofo/bollard"
14
+ s.summary = "Send a secure post somewhere"
15
+ s.description = "Send a secure post somewhere"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {spec,gemfiles}/*`.split("\n")
19
+
20
+ s.add_dependency "jwt"
21
+ s.add_dependency "rest-client"
22
+
23
+ s.add_development_dependency "activesupport", ">= 3.1"
24
+ s.add_development_dependency "rspec", "~> 3.7"
25
+ s.add_development_dependency "webmock", "~> 2.3"
26
+ s.add_development_dependency "byebug"
27
+ s.add_development_dependency "rake"
28
+ end
@@ -0,0 +1,33 @@
1
+ require 'rest-client'
2
+
3
+ module Bollard
4
+ PostError = Class.new(RuntimeError)
5
+
6
+ class Post
7
+ def initialize(url, payload, signing_secret, extra_headers, auth_header)
8
+ @url = url
9
+ @payload = payload
10
+ @signing_secret = signing_secret
11
+ @auth_header = auth_header
12
+ @extra_headers = extra_headers
13
+ end
14
+
15
+ def perform
16
+ RestClient.post(@url, @payload, headers)
17
+ rescue RestClient::ExceptionWithResponse => e
18
+ raise PostError.new(e.response.body)
19
+ rescue RestClient::Exception => e
20
+ raise PostError.new(e.message)
21
+ end
22
+
23
+ private
24
+
25
+ def headers
26
+ @extra_headers.merge({ @auth_header => signature })
27
+ end
28
+
29
+ def signature
30
+ Signature.generate(@payload, @signing_secret)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,72 @@
1
+ require 'digest'
2
+ require 'jwt'
3
+
4
+ module Bollard
5
+ class SignatureVerificationError < RuntimeError
6
+ attr_reader :message, :sig_header, :http_body
7
+
8
+ def initialize(message, sig_header, http_body: nil)
9
+ @message = message
10
+ @sig_header = sig_header
11
+ @http_body = http_body
12
+ end
13
+ end
14
+
15
+ class Signature
16
+ EXPECTED_ALGORITHM = "sig_v1".freeze
17
+
18
+
19
+ def self.generate(data, secret, ttl: 600)
20
+ iat = Time.now.to_i
21
+ payload = { iat: iat, exp: iat + ttl, sig_v1: Digest::SHA256.hexdigest(data) }
22
+ JWT.encode(payload, secret, 'HS256')
23
+ end
24
+
25
+
26
+ # Verifies the signature header for a given payload.
27
+ #
28
+ # Raises a SignatureVerificationError in the following cases:
29
+ # - the header does not match the expected format
30
+ # - no hash found with the expected algorithm
31
+ # - hash doesn't match the expected hash
32
+ #
33
+ # Returns true otherwise
34
+ def self.verify(payload, header, secret, tolerance: nil)
35
+ begin
36
+ decoded_token = JWT.decode(header, secret, true, { exp_leeway: tolerance })
37
+ rescue JWT::DecodeError => e
38
+ raise SignatureVerificationError.new(e.message, header, http_body: payload)
39
+ end
40
+
41
+ provided_hash = decoded_token[0][EXPECTED_ALGORITHM]
42
+ if provided_hash.blank?
43
+ raise SignatureVerificationError.new(
44
+ "No hash found with expected algorithm #{EXPECTED_ALGORITHM}",
45
+ header, http_body: payload
46
+ )
47
+ end
48
+
49
+ expected_hash = Digest::SHA256.hexdigest(payload)
50
+ unless secure_compare(provided_hash, expected_hash)
51
+ raise SignatureVerificationError.new("Hash mismatch for payload", header, http_body: payload)
52
+ end
53
+
54
+ true
55
+ end
56
+
57
+
58
+ # Constant time string comparison to prevent timing attacks
59
+
60
+ # Code borrowed from ActiveSupport
61
+ def self.secure_compare(a, b)
62
+ return false unless a.bytesize == b.bytesize
63
+
64
+ l = a.unpack "C#{a.bytesize}"
65
+
66
+ res = 0
67
+ b.each_byte { |byte| res |= byte ^ l.shift }
68
+ res.zero?
69
+ end
70
+ private_class_method :secure_compare
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module Bollard
2
+ VERSION = "1.0.0"
3
+ end
data/lib/bollard.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'bollard/signature'
2
+ require 'bollard/post'
3
+
4
+ module Bollard
5
+ def self.secure_post(url, payload, signing_secret, extra_headers: {}, auth_header: 'Bollard-Signature')
6
+ post = Post.new(url, payload, signing_secret, extra_headers: extra_headers, auth_header: auth_header)
7
+ post.perform
8
+ end
9
+
10
+
11
+ def self.verify_post(payload, header, signing_secret, tolerance: nil)
12
+ Signature.verify_header(payload, header, signing_secret, tolerance: tolerance)
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Bollard::Post do
4
+ it "posts the payload to the given URL" do
5
+ stub_request(:post, "https://test.localhost/")
6
+
7
+ post = Bollard::Post.new("https://test.localhost/", "{}", "secret", {}, "Bollard-Signature")
8
+ post.perform
9
+
10
+ expect(WebMock).to have_requested(:post, "https://test.localhost").with(body: "{}")
11
+ end
12
+
13
+ it "adds the correct signature header to the request" do
14
+ allow(Bollard::Signature).to receive(:generate).and_return("valid_signature")
15
+ stub_request(:post, "https://test.localhost/")
16
+
17
+ post = Bollard::Post.new("https://test.localhost/", "{}", "secret", {}, "Bollard-Signature")
18
+ post.perform
19
+
20
+ expect(WebMock).to have_requested(:post, "https://test.localhost")
21
+ .with(headers: { "Bollard-Signature" => "valid_signature" })
22
+ end
23
+
24
+ it "adds extra headers to the request" do
25
+ stub_request(:post, "https://test.localhost/")
26
+
27
+ post = Bollard::Post.new("https://test.localhost/", "{}", "secret", { content_type: :json, accept: :json }, "Bollard-Signature")
28
+ post.perform
29
+
30
+ expect(WebMock).to have_requested(:post, "https://test.localhost")
31
+ .with(headers: { content_type: 'application/json', accept: 'application/json' })
32
+ end
33
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+ require 'digest'
3
+
4
+ RSpec.describe Bollard::Signature do
5
+ describe ".generate" do
6
+ let(:jwt) { Bollard::Signature.generate("Some data", "super-secret") }
7
+ let(:jwt_payload) { JWT.decode(jwt, "super-secret", true)[0] }
8
+
9
+ it "generates a valid JWT for the given payload" do
10
+ expect { JWT.decode(jwt, "super-secret", true) }.not_to raise_error
11
+ end
12
+
13
+ it "adds an issued-at field" do
14
+ travel_to(Time.now) do
15
+ expect(jwt_payload["iat"]).to eq Time.now.to_i
16
+ end
17
+ end
18
+
19
+ it "adds an expires-at field" do
20
+ travel_to(Time.now) do
21
+ expect(jwt_payload["exp"]).to eq(Time.now.to_i + 600)
22
+ end
23
+ end
24
+
25
+ context "when given a ttl" do
26
+ let(:jwt) { Bollard::Signature.generate("Some data", "super-secret", ttl: 100) }
27
+
28
+ it "sets the expires-at field using the configurable ttl" do
29
+ travel_to(Time.now) do
30
+ expect(jwt_payload["exp"]).to eq(Time.now.to_i + 100)
31
+ end
32
+ end
33
+ end
34
+
35
+ it "adds a hash of the data" do
36
+ expect(jwt_payload["sig_v1"]).to eq(Digest::SHA256.hexdigest("Some data"))
37
+ end
38
+
39
+ it "signs the jwt with the secret so it can be verified" do
40
+ expect { JWT.decode(jwt, "not-the-same-secret", true) }.to raise_error(JWT::VerificationError, "Signature verification raised")
41
+ end
42
+ end
43
+
44
+ describe ".verify" do
45
+ it "verifies the signing secret matches the given secret" do
46
+ jwt = Bollard::Signature.generate("Some data", "super-secret")
47
+ expect { Bollard::Signature.verify("Some data", jwt, "different-super-secret") }.to raise_error(Bollard::SignatureVerificationError, "Signature verification raised")
48
+ end
49
+
50
+ it "verifies the payload matches the given hash" do
51
+ jwt = Bollard::Signature.generate("Some data", "super-secret")
52
+ expect { Bollard::Signature.verify("Some different data", jwt, "super-secret") }.to raise_error(Bollard::SignatureVerificationError, "Hash mismatch for payload")
53
+ end
54
+
55
+ it "verifies the format of the token" do
56
+ jwt = JWT.encode({ sig_v2: Digest::SHA256.hexdigest("Some data") }, "super-secret", 'HS256')
57
+
58
+ expect { Bollard::Signature.verify("Some data", jwt, "super-secret") }.to raise_error(Bollard::SignatureVerificationError, "No hash found with expected algorithm #{Bollard::Signature::EXPECTED_ALGORITHM}")
59
+ end
60
+
61
+ it "ensures that the jwt hasn't expired" do
62
+ jwt = Bollard::Signature.generate("Some data", "super-secret")
63
+ travel_to(Time.now + 1200) do
64
+ expect { Bollard::Signature.verify("Some data", jwt, "super-secret") }.to raise_error(Bollard::SignatureVerificationError, "Signature has expired")
65
+ end
66
+ end
67
+
68
+ it "allows leeway for the expiry if set" do
69
+ jwt = Bollard::Signature.generate("Some data", "super-secret")
70
+ travel_to(Time.now + 1200) do
71
+ expect { Bollard::Signature.verify("Some data", jwt, "super-secret", tolerance: 1200) }.not_to raise_error
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Bollard do
4
+ describe ".secure_post" do
5
+ let(:post) { instance_double(Bollard::Post, perform: true) }
6
+
7
+ before do
8
+ allow(Bollard::Post).to receive(:new).and_return(post)
9
+ end
10
+
11
+ it "delegates everything to Post" do
12
+ Bollard.secure_post("https://url.com", "{}", "secrets", extra_headers: { header_1: "1" }, auth_header: "Authorization")
13
+ expect(Bollard::Post).to have_received(:new).with("https://url.com", "{}", "secrets", extra_headers: { header_1: "1" }, auth_header: "Authorization")
14
+ expect(post).to have_received(:perform)
15
+ end
16
+
17
+ it "provides defaults for extra_headers" do
18
+ Bollard.secure_post("https://url.com", "{}", "secrets", auth_header: "Authorization")
19
+ expect(Bollard::Post).to have_received(:new).with("https://url.com", "{}", "secrets", extra_headers: {}, auth_header: "Authorization")
20
+ end
21
+
22
+ it "provides defaults for auth_header" do
23
+ Bollard.secure_post("https://url.com", "{}", "secrets", extra_headers: { header_1: "1" })
24
+ expect(Bollard::Post).to have_received(:new).with("https://url.com", "{}", "secrets", extra_headers: { header_1: "1" }, auth_header: "Bollard-Signature")
25
+ end
26
+ end
27
+
28
+ describe ".verify_post" do
29
+ before do
30
+ allow(Bollard::Signature).to receive(:verify_header)
31
+ end
32
+
33
+ it "delegates everything to Signature" do
34
+ Bollard.verify_post("{}", "abc123", "secrets", tolerance: 100)
35
+
36
+ expect(Bollard::Signature).to have_received(:verify_header).with("{}", "abc123", "secrets", tolerance: 100)
37
+ end
38
+
39
+ it "provides a default for tolerance" do
40
+ Bollard.verify_post("{}", "abc123", "secrets")
41
+
42
+ expect(Bollard::Signature).to have_received(:verify_header).with("{}", "abc123", "secrets", tolerance: nil)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ require 'byebug'
2
+ require 'active_support/testing/time_helpers'
3
+
4
+ require 'webmock/rspec'
5
+ require File.expand_path('../../lib/bollard', __FILE__)
6
+ Dir[File.expand_path('../spec/support/**/*.rb', __FILE__)].each { |f| require f }
7
+
8
+ RSpec.configure do |config|
9
+ config.order = 'random'
10
+
11
+ config.expect_with :rspec do |c|
12
+ c.syntax = :expect
13
+ end
14
+
15
+ config.include ActiveSupport::Testing::TimeHelpers
16
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bollard
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Dilley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rest-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Send a secure post somewhere
112
+ email: mick@vinomofo.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - ".gitignore"
118
+ - Gemfile
119
+ - Gemfile.lock
120
+ - LICENCE.md
121
+ - README.md
122
+ - Rakefile
123
+ - bollard.gemspec
124
+ - lib/bollard.rb
125
+ - lib/bollard/post.rb
126
+ - lib/bollard/signature.rb
127
+ - lib/bollard/version.rb
128
+ - spec/lib/bollard/post_spec.rb
129
+ - spec/lib/bollard/signature_spec.rb
130
+ - spec/lib/bollard_spec.rb
131
+ - spec/spec_helper.rb
132
+ homepage: https://github.com/vinomofo/bollard
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.6.8
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Send a secure post somewhere
156
+ test_files:
157
+ - spec/lib/bollard/post_spec.rb
158
+ - spec/lib/bollard/signature_spec.rb
159
+ - spec/lib/bollard_spec.rb
160
+ - spec/spec_helper.rb