slosilo 0.4.1 → 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 +4 -4
- data/lib/slosilo.rb +1 -2
- data/lib/slosilo/adapters/sequel_adapter.rb +7 -1
- data/lib/slosilo/errors.rb +12 -0
- data/lib/slosilo/key.rb +10 -0
- data/lib/slosilo/keystore.rb +3 -1
- data/lib/slosilo/version.rb +1 -1
- data/slosilo.gemspec +3 -2
- data/spec/key_spec.rb +11 -0
- data/spec/keystore_spec.rb +8 -0
- data/spec/sequel_adapter_spec.rb +94 -39
- data/spec/spec_helper.rb +4 -0
- metadata +38 -33
- data/lib/slosilo/http_request.rb +0 -59
- data/lib/slosilo/rack/middleware.rb +0 -123
- data/spec/http_request_spec.rb +0 -107
- data/spec/http_stack_spec.rb +0 -44
- data/spec/io_helper.rb +0 -18
- data/spec/rack_middleware_spec.rb +0 -109
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d243023c094f7eaec65752017e2550d57030d13a
|
4
|
+
data.tar.gz: f99b144884f5f3bf351fe4190e06c99f076b961f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b2b0a71040bedaf6e15da8c6fdf6bca34fec490fe41f8834c72ba170ab22cbe7bd8d1d1b93dcfd24855acbf4ad634aa342cdb0374d562277ac9d4df493c6e7a
|
7
|
+
data.tar.gz: 8f4741e1f0f4a00cb0b466e6776445830efc11f66947cac7533fc2d65d0060a0116f7b0239c130ff3866e24e628323170bf684ee2285dc1020e21e2a36106663
|
data/lib/slosilo.rb
CHANGED
@@ -3,8 +3,7 @@ require "slosilo/keystore"
|
|
3
3
|
require "slosilo/symmetric"
|
4
4
|
require "slosilo/attr_encrypted"
|
5
5
|
require "slosilo/random"
|
6
|
-
require "slosilo/
|
7
|
-
require "slosilo/http_request"
|
6
|
+
require "slosilo/errors"
|
8
7
|
|
9
8
|
if defined? Sequel
|
10
9
|
require 'slosilo/adapters/sequel_adapter'
|
@@ -6,15 +6,21 @@ module Slosilo
|
|
6
6
|
def model
|
7
7
|
@model ||= create_model
|
8
8
|
end
|
9
|
+
|
10
|
+
def secure?
|
11
|
+
!Slosilo.encryption_key.nil?
|
12
|
+
end
|
9
13
|
|
10
14
|
def create_model
|
11
15
|
model = Sequel::Model(:slosilo_keystore)
|
12
16
|
model.unrestrict_primary_key
|
13
|
-
model.attr_encrypted :key
|
17
|
+
model.attr_encrypted :key if secure?
|
14
18
|
model
|
15
19
|
end
|
16
20
|
|
17
21
|
def put_key id, value
|
22
|
+
fail Error::InsecureKeyStorage unless secure? || !value.private?
|
23
|
+
|
18
24
|
attrs = { id: id, key: value.to_der }
|
19
25
|
attrs[:fingerprint] = value.fingerprint if fingerprint_in_db?
|
20
26
|
model.create attrs
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Slosilo
|
2
|
+
class Error < RuntimeError
|
3
|
+
# An error thrown when attempting to store a private key in an unecrypted
|
4
|
+
# storage. Set Slosilo.encryption_key to secure the storage or make sure
|
5
|
+
# to store just the public keys (using Key#public).
|
6
|
+
class InsecureKeyStorage < Error
|
7
|
+
def initialize msg = "can't store a private key in a plaintext storage"
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/slosilo/key.rb
CHANGED
@@ -98,6 +98,16 @@ module Slosilo
|
|
98
98
|
def hash
|
99
99
|
to_der.hash
|
100
100
|
end
|
101
|
+
|
102
|
+
# return a new key with just the public part of this
|
103
|
+
def public
|
104
|
+
Key.new(@key.public_key)
|
105
|
+
end
|
106
|
+
|
107
|
+
# checks if the keypair contains a private key
|
108
|
+
def private?
|
109
|
+
@key.private?
|
110
|
+
end
|
101
111
|
|
102
112
|
private
|
103
113
|
|
data/lib/slosilo/keystore.rb
CHANGED
data/lib/slosilo/version.rb
CHANGED
data/slosilo.gemspec
CHANGED
@@ -18,9 +18,10 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.required_ruby_version = '>= 1.9.3'
|
19
19
|
|
20
20
|
gem.add_development_dependency 'rake'
|
21
|
-
gem.add_development_dependency 'rspec'
|
22
|
-
gem.add_development_dependency 'ci_reporter'
|
21
|
+
gem.add_development_dependency 'rspec', '~> 2.14'
|
22
|
+
gem.add_development_dependency 'ci_reporter', '~> 1.9'
|
23
23
|
gem.add_development_dependency 'simplecov'
|
24
|
+
gem.add_development_dependency 'io-grab', '~> 0.0.1'
|
24
25
|
gem.add_development_dependency 'sequel' # for sequel tests
|
25
26
|
gem.add_development_dependency 'sqlite3' # for sequel tests
|
26
27
|
end
|
data/spec/key_spec.rb
CHANGED
@@ -7,6 +7,7 @@ describe Slosilo::Key do
|
|
7
7
|
its(:to_der) { should == rsa.to_der }
|
8
8
|
its(:to_s) { should == rsa.public_key.to_pem }
|
9
9
|
its(:fingerprint) { should == key_fingerprint }
|
10
|
+
it { should be_private }
|
10
11
|
|
11
12
|
context "with identical key" do
|
12
13
|
let(:other) { Slosilo::Key.new rsa.to_der }
|
@@ -38,6 +39,16 @@ describe Slosilo::Key do
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
42
|
+
describe '#public' do
|
43
|
+
it "returns a key with just the public half" do
|
44
|
+
pkey = subject.public
|
45
|
+
expect(pkey).to be_a(Slosilo::Key)
|
46
|
+
expect(pkey).to_not be_private
|
47
|
+
expect(pkey.key).to_not be_private
|
48
|
+
expect(pkey.to_der).to eq(rsa.public_key.to_der)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
41
52
|
let(:plaintext) { 'quick brown fox jumped over the lazy dog' }
|
42
53
|
describe '#encrypt' do
|
43
54
|
it "generates a symmetric encryption key and encrypts the plaintext with the public key" do
|
data/spec/keystore_spec.rb
CHANGED
@@ -10,6 +10,14 @@ describe Slosilo::Keystore do
|
|
10
10
|
adapter['test'].to_der.should == rsa.to_der
|
11
11
|
end
|
12
12
|
|
13
|
+
it "refuses to store a key with a nil id" do
|
14
|
+
expect { subject.put(nil, key) }.to raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "refuses to store a key with an empty id" do
|
18
|
+
expect { subject.put('', key) }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
13
21
|
it "passes the Slosilo key to the adapter" do
|
14
22
|
adapter.should_receive(:put_key).with "test", key
|
15
23
|
subject.put :test, key
|
data/spec/sequel_adapter_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'sequel'
|
3
|
-
require '
|
3
|
+
require 'io/grab'
|
4
4
|
|
5
5
|
require 'slosilo/adapters/sequel_adapter'
|
6
6
|
|
@@ -56,65 +56,120 @@ describe Slosilo::Adapters::SequelAdapter do
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
|
59
|
+
shared_context "database" do
|
60
60
|
let(:db) { Sequel.sqlite }
|
61
61
|
before do
|
62
|
-
Slosilo::encryption_key = Slosilo::Symmetric.new.random_key
|
63
62
|
subject.unstub :create_model
|
64
|
-
|
63
|
+
begin
|
64
|
+
Sequel::Model.cache_anonymous_models = false
|
65
|
+
rescue NoMethodError # sequel 4.0 moved the method
|
66
|
+
Sequel.cache_anonymous_models = false
|
67
|
+
end
|
65
68
|
Sequel::Model.db = db
|
66
69
|
end
|
70
|
+
end
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
74
|
-
subject.put_key 'test', key
|
75
|
-
end
|
76
|
-
|
77
|
-
context "after migration" do
|
78
|
-
before { subject.migrate! }
|
72
|
+
shared_context "encryption key" do
|
73
|
+
before do
|
74
|
+
Slosilo.encryption_key = Slosilo::Symmetric.new.random_key
|
75
|
+
end
|
76
|
+
end
|
79
77
|
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
context "with old schema" do
|
79
|
+
include_context "encryption key"
|
80
|
+
include_context "database"
|
83
81
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
82
|
+
before do
|
83
|
+
db.create_table :slosilo_keystore do
|
84
|
+
String :id, primary_key: true
|
85
|
+
bytea :key, null: false
|
89
86
|
end
|
87
|
+
subject.put_key 'test', key
|
88
|
+
end
|
89
|
+
|
90
|
+
context "after migration" do
|
91
|
+
before { subject.migrate! }
|
90
92
|
|
91
93
|
it "supports look up by id" do
|
92
94
|
subject.get_key("test").should == key
|
93
95
|
end
|
94
96
|
|
95
|
-
it "supports look up by fingerprint,
|
96
|
-
|
97
|
+
it "supports look up by fingerprint, without a warning" do
|
98
|
+
$stderr.grab do
|
97
99
|
subject.get_by_fingerprint(key.fingerprint).should == [key, 'test']
|
98
|
-
end.
|
100
|
+
end.should be_empty
|
99
101
|
end
|
100
102
|
end
|
101
103
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
require 'slosilo/adapters/sequel_adapter/migration.rb'
|
106
|
-
Sequel::Migration::descendants.first.apply db, :up
|
107
|
-
subject.put_key 'test', key
|
108
|
-
end
|
104
|
+
it "supports look up by id" do
|
105
|
+
subject.get_key("test").should == key
|
106
|
+
end
|
109
107
|
|
108
|
+
it "supports look up by fingerprint, but issues a warning" do
|
109
|
+
$stderr.grab do
|
110
|
+
subject.get_by_fingerprint(key.fingerprint).should == [key, 'test']
|
111
|
+
end.should_not be_empty
|
112
|
+
end
|
113
|
+
end
|
110
114
|
|
111
|
-
|
112
|
-
|
113
|
-
|
115
|
+
shared_context "current schema" do
|
116
|
+
include_context "database"
|
117
|
+
before do
|
118
|
+
Sequel.extension :migration
|
119
|
+
require 'slosilo/adapters/sequel_adapter/migration.rb'
|
120
|
+
Sequel::Migration.descendants.first.apply db, :up
|
121
|
+
end
|
122
|
+
end
|
114
123
|
|
115
|
-
|
116
|
-
|
117
|
-
|
124
|
+
context "with current schema" do
|
125
|
+
include_context "encryption key"
|
126
|
+
include_context "current schema"
|
127
|
+
before do
|
128
|
+
subject.put_key 'test', key
|
129
|
+
end
|
130
|
+
|
131
|
+
it "supports look up by id" do
|
132
|
+
subject.get_key("test").should == key
|
133
|
+
end
|
134
|
+
|
135
|
+
it "supports look up by fingerprint" do
|
136
|
+
subject.get_by_fingerprint(key.fingerprint).should == [key, 'test']
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "with an encryption key", :wip do
|
141
|
+
include_context "encryption key"
|
142
|
+
include_context "current schema"
|
143
|
+
|
144
|
+
it { should be_secure }
|
145
|
+
|
146
|
+
it "saves the keys in encrypted form" do
|
147
|
+
subject.put_key 'test', key
|
148
|
+
|
149
|
+
expect(db[:slosilo_keystore][id: 'test'][:key]).to_not eq(key.to_der)
|
150
|
+
expect(subject.get_key 'test').to eq(key)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "without an encryption key", :wip do
|
155
|
+
before do
|
156
|
+
Slosilo.encryption_key = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
include_context "current schema"
|
160
|
+
|
161
|
+
it { should_not be_secure }
|
162
|
+
|
163
|
+
it "refuses to store a private key" do
|
164
|
+
expect { subject.put_key 'test', key }.to raise_error(Slosilo::Error::InsecureKeyStorage)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "saves the keys in plaintext form" do
|
168
|
+
pkey = key.public
|
169
|
+
subject.put_key 'test', pkey
|
170
|
+
|
171
|
+
expect(db[:slosilo_keystore][id: 'test'][:key]).to eq(pkey.to_der)
|
172
|
+
expect(subject.get_key 'test').to eq(pkey)
|
118
173
|
end
|
119
174
|
end
|
120
175
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,97 +1,111 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slosilo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rafał Rzepecki
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2.14'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '2.14'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: ci_reporter
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '1.9'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '1.9'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: simplecov
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: io-grab
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.0.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.0.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: sequel
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
|
-
- -
|
87
|
+
- - ">="
|
74
88
|
- !ruby/object:Gem::Version
|
75
89
|
version: '0'
|
76
90
|
type: :development
|
77
91
|
prerelease: false
|
78
92
|
version_requirements: !ruby/object:Gem::Requirement
|
79
93
|
requirements:
|
80
|
-
- -
|
94
|
+
- - ">="
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: sqlite3
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
|
-
- -
|
101
|
+
- - ">="
|
88
102
|
- !ruby/object:Gem::Version
|
89
103
|
version: '0'
|
90
104
|
type: :development
|
91
105
|
prerelease: false
|
92
106
|
version_requirements: !ruby/object:Gem::Requirement
|
93
107
|
requirements:
|
94
|
-
- -
|
108
|
+
- - ">="
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '0'
|
97
111
|
description: This gem provides an easy way of storing and retrieving encryption keys
|
@@ -102,8 +116,8 @@ executables: []
|
|
102
116
|
extensions: []
|
103
117
|
extra_rdoc_files: []
|
104
118
|
files:
|
105
|
-
- .gitignore
|
106
|
-
- .kateproject
|
119
|
+
- ".gitignore"
|
120
|
+
- ".kateproject"
|
107
121
|
- Gemfile
|
108
122
|
- LICENSE
|
109
123
|
- README.md
|
@@ -116,22 +130,17 @@ files:
|
|
116
130
|
- lib/slosilo/adapters/sequel_adapter.rb
|
117
131
|
- lib/slosilo/adapters/sequel_adapter/migration.rb
|
118
132
|
- lib/slosilo/attr_encrypted.rb
|
119
|
-
- lib/slosilo/
|
133
|
+
- lib/slosilo/errors.rb
|
120
134
|
- lib/slosilo/key.rb
|
121
135
|
- lib/slosilo/keystore.rb
|
122
|
-
- lib/slosilo/rack/middleware.rb
|
123
136
|
- lib/slosilo/random.rb
|
124
137
|
- lib/slosilo/symmetric.rb
|
125
138
|
- lib/slosilo/version.rb
|
126
139
|
- lib/tasks/slosilo.rake
|
127
140
|
- slosilo.gemspec
|
128
141
|
- spec/file_adapter_spec.rb
|
129
|
-
- spec/http_request_spec.rb
|
130
|
-
- spec/http_stack_spec.rb
|
131
|
-
- spec/io_helper.rb
|
132
142
|
- spec/key_spec.rb
|
133
143
|
- spec/keystore_spec.rb
|
134
|
-
- spec/rack_middleware_spec.rb
|
135
144
|
- spec/random_spec.rb
|
136
145
|
- spec/sequel_adapter_spec.rb
|
137
146
|
- spec/slosilo_spec.rb
|
@@ -147,28 +156,24 @@ require_paths:
|
|
147
156
|
- lib
|
148
157
|
required_ruby_version: !ruby/object:Gem::Requirement
|
149
158
|
requirements:
|
150
|
-
- -
|
159
|
+
- - ">="
|
151
160
|
- !ruby/object:Gem::Version
|
152
161
|
version: 1.9.3
|
153
162
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
163
|
requirements:
|
155
|
-
- -
|
164
|
+
- - ">="
|
156
165
|
- !ruby/object:Gem::Version
|
157
166
|
version: '0'
|
158
167
|
requirements: []
|
159
168
|
rubyforge_project:
|
160
|
-
rubygems_version: 2.
|
169
|
+
rubygems_version: 2.2.2
|
161
170
|
signing_key:
|
162
171
|
specification_version: 4
|
163
172
|
summary: Store SSL keys in a database
|
164
173
|
test_files:
|
165
174
|
- spec/file_adapter_spec.rb
|
166
|
-
- spec/http_request_spec.rb
|
167
|
-
- spec/http_stack_spec.rb
|
168
|
-
- spec/io_helper.rb
|
169
175
|
- spec/key_spec.rb
|
170
176
|
- spec/keystore_spec.rb
|
171
|
-
- spec/rack_middleware_spec.rb
|
172
177
|
- spec/random_spec.rb
|
173
178
|
- spec/sequel_adapter_spec.rb
|
174
179
|
- spec/slosilo_spec.rb
|
data/lib/slosilo/http_request.rb
DELETED
@@ -1,59 +0,0 @@
|
|
1
|
-
module Slosilo
|
2
|
-
# A mixin module which simplifies generating signed and encrypted requests.
|
3
|
-
# It's designed to be mixed into a standard Net::HTTPRequest object
|
4
|
-
# and ensures the request is signed and optionally encrypted before execution.
|
5
|
-
# Requests prepared this way will be recognized by Slosilo::Rack::Middleware.
|
6
|
-
#
|
7
|
-
# As an example, you can use it with RestClient like so:
|
8
|
-
# RestClient.add_before_execution_proc do |req, params|
|
9
|
-
# require 'slosilo'
|
10
|
-
# req.extend Slosilo::HTTPRequest
|
11
|
-
# req.keyname = :somekey
|
12
|
-
# end
|
13
|
-
#
|
14
|
-
# The request won't be encrypted unless you set the destination keyname.
|
15
|
-
|
16
|
-
module HTTPRequest
|
17
|
-
# Encrypt the request with key named @keyname from Slosilo::Keystore.
|
18
|
-
# If calling this manually, make sure to encrypt before signing.
|
19
|
-
def encrypt!
|
20
|
-
return unless @keyname
|
21
|
-
return unless body && !body.empty?
|
22
|
-
self.body, key = Slosilo[@keyname].encrypt body
|
23
|
-
self['X-Slosilo-Key'] = Base64::urlsafe_encode64 key
|
24
|
-
end
|
25
|
-
|
26
|
-
# Sign the request with :own key from Slosilo::Keystore.
|
27
|
-
# If calling this manually, make sure to encrypt before signing.
|
28
|
-
def sign!
|
29
|
-
token = Slosilo[:own].signed_token signed_data
|
30
|
-
self['Timestamp'] = token["timestamp"]
|
31
|
-
self['X-Slosilo-Signature'] = token["signature"]
|
32
|
-
end
|
33
|
-
|
34
|
-
# Build the data hash to sign.
|
35
|
-
def signed_data
|
36
|
-
data = { "path" => path, "body" => [body].pack('m0') }
|
37
|
-
if key = self['X-Slosilo-Key']
|
38
|
-
data["key"] = key
|
39
|
-
end
|
40
|
-
if authz = self['Authorization']
|
41
|
-
data["authorization"] = authz
|
42
|
-
end
|
43
|
-
data
|
44
|
-
end
|
45
|
-
|
46
|
-
# Encrypt, sign and execute the request.
|
47
|
-
def exec *a
|
48
|
-
# we need to hook here because the body might be set
|
49
|
-
# in several ways and here it's hopefully finalized
|
50
|
-
encrypt!
|
51
|
-
sign!
|
52
|
-
super *a
|
53
|
-
end
|
54
|
-
|
55
|
-
# Name of the key used to encrypt the request.
|
56
|
-
# Use it to establish the identity of the receiver.
|
57
|
-
attr_accessor :keyname
|
58
|
-
end
|
59
|
-
end
|
@@ -1,123 +0,0 @@
|
|
1
|
-
module Slosilo
|
2
|
-
module Rack
|
3
|
-
# Con perform verification of request signature and decryption of request body.
|
4
|
-
#
|
5
|
-
# Signature verification and body decryption are enabled with constructor switches and are
|
6
|
-
# therefore performed (or not) for all requests.
|
7
|
-
#
|
8
|
-
# When signature verification is performed, the following elements are included in the
|
9
|
-
# signature string:
|
10
|
-
#
|
11
|
-
# 1. Request path and query string
|
12
|
-
# 2. base64 encoded request body
|
13
|
-
# 3. Request timestamp from HTTP_TIMESTAMP
|
14
|
-
# 4. Body encryption key from HTTP_X_SLOSILO_KEY (if present)
|
15
|
-
#
|
16
|
-
# When body decryption is performed, an encryption key for the message body is encrypted
|
17
|
-
# with this service's public key and placed in HTTP_X_SLOSILO_KEY. This middleware
|
18
|
-
# decryps the key using our :own private key, and then decrypts the body using the decrypted key.
|
19
|
-
class Middleware
|
20
|
-
class EncryptionError < SecurityError
|
21
|
-
end
|
22
|
-
class SignatureError < SecurityError
|
23
|
-
end
|
24
|
-
|
25
|
-
def initialize app, opts = {}
|
26
|
-
@app = app
|
27
|
-
@encryption_required = opts[:encryption_required] || false
|
28
|
-
@signature_required = opts[:signature_required] || false
|
29
|
-
end
|
30
|
-
|
31
|
-
def call env
|
32
|
-
@env = env
|
33
|
-
@body = env['rack.input'].read rescue ""
|
34
|
-
|
35
|
-
begin
|
36
|
-
verify
|
37
|
-
decrypt
|
38
|
-
rescue EncryptionError
|
39
|
-
return error 403, $!.message
|
40
|
-
rescue SignatureError
|
41
|
-
return error 401, $!.message
|
42
|
-
end
|
43
|
-
|
44
|
-
@app.call env
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
def verify
|
49
|
-
if signature
|
50
|
-
raise SignatureError, "Bad signature" unless Slosilo.token_valid?(token)
|
51
|
-
else
|
52
|
-
raise SignatureError, "Signature required" if signature_required?
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
attr_reader :env
|
57
|
-
|
58
|
-
def token
|
59
|
-
return nil unless signature
|
60
|
-
t = { "data" => { "path" => path, "body" => [body].pack('m0') }, "timestamp" => timestamp, "signature" => signature }
|
61
|
-
t["data"]["key"] = encoded_key if encoded_key
|
62
|
-
t['data']['authorization'] = env['HTTP_AUTHORIZATION'] if env['HTTP_AUTHORIZATION']
|
63
|
-
t
|
64
|
-
end
|
65
|
-
|
66
|
-
def path
|
67
|
-
env['SCRIPT_NAME'] + env['PATH_INFO'] + query_string
|
68
|
-
end
|
69
|
-
|
70
|
-
def query_string
|
71
|
-
if env['QUERY_STRING'].empty?
|
72
|
-
''
|
73
|
-
else
|
74
|
-
'?' + env['QUERY_STRING']
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
attr_reader :body
|
79
|
-
|
80
|
-
def timestamp
|
81
|
-
env['HTTP_TIMESTAMP']
|
82
|
-
end
|
83
|
-
|
84
|
-
def signature
|
85
|
-
env['HTTP_X_SLOSILO_SIGNATURE']
|
86
|
-
end
|
87
|
-
|
88
|
-
def encoded_key
|
89
|
-
env['HTTP_X_SLOSILO_KEY']
|
90
|
-
end
|
91
|
-
|
92
|
-
def key
|
93
|
-
if encoded_key
|
94
|
-
Base64::urlsafe_decode64(encoded_key)
|
95
|
-
else
|
96
|
-
raise EncryptionError, "Encryption required" if encryption_required?
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def decrypt
|
101
|
-
return unless key
|
102
|
-
plaintext = Slosilo[:own].decrypt body, key
|
103
|
-
env['rack.input'] = StringIO.new plaintext
|
104
|
-
rescue EncryptionError
|
105
|
-
raise unless body.empty? || body.nil?
|
106
|
-
rescue Exception => e
|
107
|
-
raise EncryptionError, "Bad encryption", e.backtrace
|
108
|
-
end
|
109
|
-
|
110
|
-
def error status, message
|
111
|
-
[status, { 'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s }, [message] ]
|
112
|
-
end
|
113
|
-
|
114
|
-
def encryption_required?
|
115
|
-
@encryption_required
|
116
|
-
end
|
117
|
-
|
118
|
-
def signature_required?
|
119
|
-
@signature_required
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
data/spec/http_request_spec.rb
DELETED
@@ -1,107 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Slosilo::HTTPRequest do
|
4
|
-
let(:keyname) { :bacon }
|
5
|
-
let(:encrypt) { subject.encrypt! }
|
6
|
-
subject { Hash.new }
|
7
|
-
before do
|
8
|
-
subject.extend Slosilo::HTTPRequest
|
9
|
-
subject.keyname = keyname
|
10
|
-
end
|
11
|
-
|
12
|
-
describe "#sign!" do
|
13
|
-
let(:own_key) { double "own key" }
|
14
|
-
before { Slosilo.stub(:[]).with(:own).and_return own_key }
|
15
|
-
|
16
|
-
let(:signed_data) { "this is the truest truth" }
|
17
|
-
before { subject.stub signed_data: signed_data }
|
18
|
-
let(:timestamp) { "long time ago" }
|
19
|
-
let(:signature) { "seal of approval" }
|
20
|
-
let(:token) { { "data" => signed_data, "timestamp" => timestamp, "signature" => signature } }
|
21
|
-
|
22
|
-
it "makes a token out of the data to sign and inserts headers" do
|
23
|
-
own_key.stub(:signed_token).with(signed_data).and_return token
|
24
|
-
subject.should_receive(:[]=).with 'Timestamp', timestamp
|
25
|
-
subject.should_receive(:[]=).with 'X-Slosilo-Signature', signature
|
26
|
-
subject.sign!
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
describe "#signed_data" do
|
31
|
-
before { subject.stub path: :path, body: 'body' }
|
32
|
-
context "when X-Slosilo-Key not present" do
|
33
|
-
its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==" } }
|
34
|
-
end
|
35
|
-
|
36
|
-
context "when X-Slosilo-Key is present" do
|
37
|
-
before { subject.merge! 'X-Slosilo-Key' => :key }
|
38
|
-
its(:signed_data) { should == { "path" => :path, "body" => "Ym9keQ==", "key" => :key } }
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
describe "#encrypt!" do
|
43
|
-
context "when key not set" do
|
44
|
-
before { subject.keyname = nil }
|
45
|
-
it "does nothing" do
|
46
|
-
subject.should_not_receive(:body=)
|
47
|
-
encrypt
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
context "when requested key does not exist" do
|
52
|
-
before { Slosilo.stub(:[]).and_return nil }
|
53
|
-
it "raises error" do
|
54
|
-
expect{ encrypt }.to raise_error
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
context "when the key exists" do
|
59
|
-
let(:key) { double "key" }
|
60
|
-
context "when the body is not empty" do
|
61
|
-
let(:plaintext) { "Keep your solutions close, and your problems closer." }
|
62
|
-
let(:ciphertext) { "And, when you want something, all the universe conspires in helping you to achieve it." }
|
63
|
-
let(:skey) { "make me sound like a fool instead" }
|
64
|
-
before do
|
65
|
-
subject.stub body: plaintext
|
66
|
-
key.stub(:encrypt).with(plaintext).and_return([ciphertext, skey])
|
67
|
-
Slosilo.stub(:[]).with(keyname).and_return key
|
68
|
-
end
|
69
|
-
|
70
|
-
it "encrypts the message body and adds the X-Slosilo-Key header" do
|
71
|
-
subject.should_receive(:body=).with ciphertext
|
72
|
-
subject.should_receive(:[]=).with 'X-Slosilo-Key', Base64::urlsafe_encode64(skey)
|
73
|
-
encrypt
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
context "when the body is empty" do
|
78
|
-
before { subject.stub body: "" }
|
79
|
-
it "doesn't set the key header" do
|
80
|
-
subject.should_not_receive(:[]=).with 'X-Slosilo-Key'
|
81
|
-
encrypt
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
describe "#exec" do
|
88
|
-
class Subject
|
89
|
-
def exec *a
|
90
|
-
"ok, got it"
|
91
|
-
end
|
92
|
-
|
93
|
-
def initialize keyname
|
94
|
-
extend Slosilo::HTTPRequest
|
95
|
-
self.keyname = keyname
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
subject { Subject.new keyname }
|
100
|
-
|
101
|
-
it "encrypts, then signs and delegates to the superclass" do
|
102
|
-
subject.should_receive(:encrypt!).once.ordered
|
103
|
-
subject.should_receive(:sign!).once.ordered
|
104
|
-
subject.exec(:foo).should == "ok, got it"
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
data/spec/http_stack_spec.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe "http request stack" do
|
4
|
-
include_context "with example key"
|
5
|
-
include_context "with mock adapter"
|
6
|
-
before { Slosilo[:own] = key }
|
7
|
-
|
8
|
-
class MockRequest < Hash
|
9
|
-
def exec *a
|
10
|
-
end
|
11
|
-
|
12
|
-
def [] name
|
13
|
-
name = name.sub(/^HTTP_/,'').gsub('_', '-').split(/(\W)/).map(&:capitalize).join
|
14
|
-
result = super name
|
15
|
-
end
|
16
|
-
|
17
|
-
def initialize
|
18
|
-
extend Slosilo::HTTPRequest
|
19
|
-
self['Authorization'] = "Simon says it's fine"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
subject { MockRequest.new }
|
24
|
-
let(:path) { '/some/path' }
|
25
|
-
|
26
|
-
context "with authorization header" do
|
27
|
-
it "works" do
|
28
|
-
mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true
|
29
|
-
subject.stub path: path, body: ''
|
30
|
-
mw.stub path: path
|
31
|
-
subject.send :exec
|
32
|
-
mw.call(subject).should == :ok
|
33
|
-
end
|
34
|
-
|
35
|
-
it "detects tampering" do
|
36
|
-
mw = Slosilo::Rack::Middleware.new lambda{|_|:ok}, signature_required: true
|
37
|
-
subject.stub path: path, body: ''
|
38
|
-
mw.stub path: path
|
39
|
-
subject.send :exec
|
40
|
-
subject['Authorization'] = "Simon changed his mind"
|
41
|
-
mw.call(subject).should_not == :ok
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/spec/io_helper.rb
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Slosilo::Rack::Middleware do
|
4
|
-
include_context "with example key"
|
5
|
-
mock_own_key
|
6
|
-
|
7
|
-
let(:app) { double "app" }
|
8
|
-
subject { Slosilo::Rack::Middleware.new app }
|
9
|
-
|
10
|
-
describe '#path' do
|
11
|
-
context "when QUERY_STRING is empty" do
|
12
|
-
let(:env) { { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/bar', 'QUERY_STRING' => '' } }
|
13
|
-
before { subject.stub env: env }
|
14
|
-
its(:path) { should == '/foo/bar' }
|
15
|
-
end
|
16
|
-
context "when QUERY_STRING is not" do
|
17
|
-
let(:env) { { 'SCRIPT_NAME' => '/foo', 'PATH_INFO' => '/bar', 'QUERY_STRING' => 'baz' } }
|
18
|
-
before { subject.stub env: env }
|
19
|
-
its(:path) { should == '/foo/bar?baz' }
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
describe '#call' do
|
24
|
-
let(:call) { subject.call(env) }
|
25
|
-
let(:path) { "/this/is/the/path" }
|
26
|
-
before { subject.stub path: path }
|
27
|
-
context "when no X-Slosilo-Key is given" do
|
28
|
-
let(:env) { {} }
|
29
|
-
let(:result) { double "result" }
|
30
|
-
it "passes the env verbatim" do
|
31
|
-
app.should_receive(:call).with(env).and_return(result)
|
32
|
-
call.should == result
|
33
|
-
end
|
34
|
-
|
35
|
-
context "and X-Slosilo-Signature is given" do
|
36
|
-
let(:body) { "the body" }
|
37
|
-
let(:timestamp) { "long time ago" }
|
38
|
-
let(:signature) { "in blood" }
|
39
|
-
let(:env) { {'rack.input' => StringIO.new(body), 'HTTP_TIMESTAMP' => timestamp, 'HTTP_X_SLOSILO_SIGNATURE' => signature } }
|
40
|
-
let(:token) { { "data" => { "path" => path, "body" => "dGhlIGJvZHk=" }, "timestamp" => timestamp, "signature" => signature } }
|
41
|
-
context "when the signature is valid" do
|
42
|
-
before { Slosilo.stub(:token_valid?).with(token).and_return true }
|
43
|
-
it "passes the env verbatim" do
|
44
|
-
app.should_receive(:call).with(env).and_return(result)
|
45
|
-
call.should == result
|
46
|
-
end
|
47
|
-
end
|
48
|
-
context "when the signature is invalid" do
|
49
|
-
before { Slosilo.stub(:token_valid?).with(token).and_return false }
|
50
|
-
it "returns 401" do
|
51
|
-
call[0].should == 401
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
context "but encryption is required" do
|
57
|
-
subject { Slosilo::Rack::Middleware.new app, encryption_required: true }
|
58
|
-
context "and the body is not empty" do
|
59
|
-
let(:env) { {'rack.input' => StringIO.new('foo') } }
|
60
|
-
it "returns 403" do
|
61
|
-
status, headers, body = call
|
62
|
-
status.should == 403
|
63
|
-
end
|
64
|
-
end
|
65
|
-
context "but the body is empty" do
|
66
|
-
subject { Slosilo::Rack::Middleware.new app, encryption_required: true, signature_required: false }
|
67
|
-
let(:body) { "" }
|
68
|
-
let(:timestamp) { "long time ago" }
|
69
|
-
let(:env) { {'rack.input' => StringIO.new(body) } }
|
70
|
-
it "passes the env verbatim" do
|
71
|
-
app.should_receive(:call).with(env).and_return(result)
|
72
|
-
call.should == result
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
context "when no X-Slosilo-Signature is given" do
|
79
|
-
context "but signature is required" do
|
80
|
-
let(:env) {{}}
|
81
|
-
subject { Slosilo::Rack::Middleware.new app, signature_required: true }
|
82
|
-
it "returns 401" do
|
83
|
-
status, headers, body = call
|
84
|
-
status.should == 401
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
let(:plaintext) { "If you were taught that elves caused rain, every time it rained, you'd see the proof of elves." }
|
90
|
-
let(:skey) { "Eiho7xIoFj-Qwqc0swcQQJzJyM1sSv_b6VdRIoHCPRUwemB0v5MNyOirU_5dQ_bNzlmSlo8HDvfAnMgapwpIBH__uDUV_3nCkzrzQVV3-bSp6owJnqebeSQxJMoVMKEWqqek3ZCBPo0OB63A8mkYGu9955gDEDOnlxLkETGb3SmDQIVJtiMmAkUWN0fh9z1M9Ycw9FfworaHKQXRLw6z6Rl-Yoe_TDaiKVlGIYjQKpCz8h_I5lRdrhPJaP53d0yQuKMK3PBHMzE77IikZyQ3VZdoqI9XqzUJF27KehxJ_BCx0oAcPaxG6I7WWe3Xb7K7MhE4HgzqVZACDLhYfm_0XA==" }
|
91
|
-
let(:ciphertext) { "0\xDE\xE1\xBA=\x06+K\xE0\xCAD\xC6\xE3 d\xC7kx\x90\r\ni\xDCXmS!EP\xAB\xEF\xAA\x13{\x85f\x8FU,\xB3zO\x1F\x85\f\x0E\xAE\xF8\x10`\x1C\x94\xAB@\xFA\xBC\xC0/\x1F\xA6nX\xFF-m\xF4\xC3f\xBB\xCA\x05\xC82\x18l\xC3\xF0v\x96\v\x8F\xFC\xB2\xC7wX;\xF6v\xDCX:\xCC\xF8\xD7\x99\xC8\x1A\xBA\x9F\xDB\xE7\x0F\xF2\xC9f\aaGs\xEFc" }
|
92
|
-
context "when X-Slosilo-Key is given" do
|
93
|
-
context "when the key decrypts cleanly" do
|
94
|
-
let(:env) { {'HTTP_X_SLOSILO_KEY' => skey, 'rack.input' => StringIO.new(ciphertext) } }
|
95
|
-
it "passes the decrypted contents" do
|
96
|
-
app.should_receive(:call).with(rack_environment_with_input(plaintext)).and_return(:result)
|
97
|
-
call.should == :result
|
98
|
-
end
|
99
|
-
end
|
100
|
-
context "when the key is invalid" do
|
101
|
-
let(:env) { {'HTTP_X_SLOSILO_KEY' => "broken #{skey}", 'rack.input' => StringIO.new(ciphertext) } }
|
102
|
-
it "returns 403 status" do
|
103
|
-
status, headers, body = call
|
104
|
-
status.should == 403
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|