slosilo 0.4.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|