slosilo 0.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +10 -0
  3. data/.gitignore +21 -0
  4. data/.gitleaks.toml +221 -0
  5. data/.kateproject +4 -0
  6. data/CHANGELOG.md +50 -0
  7. data/CONTRIBUTING.md +16 -0
  8. data/Gemfile +4 -0
  9. data/Jenkinsfile +132 -0
  10. data/LICENSE +22 -0
  11. data/README.md +152 -0
  12. data/Rakefile +17 -0
  13. data/SECURITY.md +42 -0
  14. data/dev/Dockerfile.dev +7 -0
  15. data/dev/docker-compose.yml +8 -0
  16. data/lib/slosilo/adapters/abstract_adapter.rb +23 -0
  17. data/lib/slosilo/adapters/file_adapter.rb +42 -0
  18. data/lib/slosilo/adapters/memory_adapter.rb +31 -0
  19. data/lib/slosilo/adapters/mock_adapter.rb +21 -0
  20. data/lib/slosilo/adapters/sequel_adapter/migration.rb +52 -0
  21. data/lib/slosilo/adapters/sequel_adapter.rb +96 -0
  22. data/lib/slosilo/attr_encrypted.rb +85 -0
  23. data/lib/slosilo/errors.rb +15 -0
  24. data/lib/slosilo/jwt.rb +122 -0
  25. data/lib/slosilo/key.rb +218 -0
  26. data/lib/slosilo/keystore.rb +89 -0
  27. data/lib/slosilo/random.rb +11 -0
  28. data/lib/slosilo/symmetric.rb +63 -0
  29. data/lib/slosilo/version.rb +22 -0
  30. data/lib/slosilo.rb +13 -0
  31. data/lib/tasks/slosilo.rake +32 -0
  32. data/publish.sh +5 -0
  33. data/secrets.yml +1 -0
  34. data/slosilo.gemspec +38 -0
  35. data/spec/encrypted_attributes_spec.rb +114 -0
  36. data/spec/file_adapter_spec.rb +81 -0
  37. data/spec/jwt_spec.rb +102 -0
  38. data/spec/key_spec.rb +258 -0
  39. data/spec/keystore_spec.rb +26 -0
  40. data/spec/random_spec.rb +19 -0
  41. data/spec/sequel_adapter_spec.rb +171 -0
  42. data/spec/slosilo_spec.rb +124 -0
  43. data/spec/spec_helper.rb +84 -0
  44. data/spec/symmetric_spec.rb +94 -0
  45. data/test.sh +8 -0
  46. metadata +238 -0
data/spec/jwt_spec.rb ADDED
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ # (Mostly) integration tests for JWT token format
4
+ describe Slosilo::Key do
5
+ include_context "with example key"
6
+
7
+ describe '#issue_jwt' do
8
+ it 'issues an JWT token with given claims' do
9
+ allow(Time).to receive(:now) { DateTime.parse('2014-06-04 23:22:32 -0400').to_time }
10
+
11
+ tok = key.issue_jwt sub: 'host/example', cidr: %w(fec0::/64)
12
+
13
+ expect(tok).to be_frozen
14
+
15
+ expect(tok.header).to eq \
16
+ alg: 'conjur.org/slosilo/v2',
17
+ kid: key_fingerprint
18
+ expect(tok.claims).to eq \
19
+ iat: 1401938552,
20
+ sub: 'host/example',
21
+ cidr: ['fec0::/64']
22
+
23
+ expect(key.verify_signature tok.string_to_sign, tok.signature).to be_truthy
24
+ end
25
+ end
26
+ end
27
+
28
+ describe Slosilo::JWT do
29
+ context "with a signed token" do
30
+ let(:signature) { 'very signed, such alg' }
31
+ subject(:token) { Slosilo::JWT.new test: "token" }
32
+ before do
33
+ allow(Time).to receive(:now) { DateTime.parse('2014-06-04 23:22:32 -0400').to_time }
34
+ token.add_signature(alg: 'test-sig') { signature }
35
+ end
36
+
37
+ it 'allows conversion to JSON representation with #to_json' do
38
+ json = JSON.load token.to_json
39
+ expect(JSON.load Base64.urlsafe_decode64 json['protected']).to eq \
40
+ 'alg' => 'test-sig'
41
+ expect(JSON.load Base64.urlsafe_decode64 json['payload']).to eq \
42
+ 'iat' => 1401938552, 'test' => 'token'
43
+ expect(Base64.urlsafe_decode64 json['signature']).to eq signature
44
+ end
45
+
46
+ it 'allows conversion to compact representation with #to_s' do
47
+ h, c, s = token.to_s.split '.'
48
+ expect(JSON.load Base64.urlsafe_decode64 h).to eq \
49
+ 'alg' => 'test-sig'
50
+ expect(JSON.load Base64.urlsafe_decode64 c).to eq \
51
+ 'iat' => 1401938552, 'test' => 'token'
52
+ expect(Base64.urlsafe_decode64 s).to eq signature
53
+ end
54
+ end
55
+
56
+ describe '#to_json' do
57
+ it "passes any parameters" do
58
+ token = Slosilo::JWT.new
59
+ allow(token).to receive_messages \
60
+ header: :header,
61
+ claims: :claims,
62
+ signature: :signature
63
+ expect_any_instance_of(Hash).to receive(:to_json).with :testing
64
+ expect(token.to_json :testing)
65
+ end
66
+ end
67
+
68
+ describe '()' do
69
+ include_context "with example key"
70
+
71
+ it 'understands both serializations' do
72
+ [COMPACT_TOKEN, JSON_TOKEN].each do |token|
73
+ token = Slosilo::JWT token
74
+ expect(token.header).to eq \
75
+ 'typ' => 'JWT',
76
+ 'alg' => 'conjur.org/slosilo/v2',
77
+ 'kid' => key_fingerprint
78
+ expect(token.claims).to eq \
79
+ 'sub' => 'host/example',
80
+ 'iat' => 1401938552,
81
+ 'exp' => 1401938552 + 60*60,
82
+ 'cidr' => ['fec0::/64']
83
+ expect(key.verify_signature token.string_to_sign, token.signature).to be_truthy
84
+ end
85
+ end
86
+
87
+ it 'is a noop if already parsed' do
88
+ token = Slosilo::JWT COMPACT_TOKEN
89
+ expect(Slosilo::JWT token).to eq token
90
+ end
91
+
92
+ it 'raises ArgumentError on failure to convert' do
93
+ expect { Slosilo::JWT "foo bar" }.to raise_error ArgumentError
94
+ expect { Slosilo::JWT elite: 31337 }.to raise_error ArgumentError
95
+ expect { Slosilo::JWT "foo.bar.xyzzy" }.to raise_error ArgumentError
96
+ end
97
+ end
98
+
99
+ COMPACT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=.eyJzdWIiOiJob3N0L2V4YW1wbGUiLCJjaWRyIjpbImZlYzA6Oi82NCJdLCJleHAiOjE0MDE5NDIxNTIsImlhdCI6MTQwMTkzODU1Mn0=.qSxy6gx0DbiIc-Wz_vZhBsYi1SCkHhzxfMGPnnG6MTqjlzy7ntmlU2H92GKGoqCRo6AaNLA_C3hA42PeEarV5nMoTj8XJO_kwhrt2Db2OX4u83VS0_enoztWEZG5s45V0Lv71lVR530j4LD-hpqhm_f4VuISkeH84u0zX7s1zKOlniuZP-abCAHh0htTnrVz9wKG0VywkCUmWYyNNqC2h8PRf64SvCWcQ6VleHpjO-ms8OeTw4ZzRbzKMi0mL6eTmQlbT3PeBArUaS0pNJPg9zdDQaL2XDOofvQmj6Yy_8RA4eCt9HEfTYEdriVqK-_9QCspbGzFVn9GTWf51MRi5dngV9ItsDoG9ktDtqFuMttv7TcqjftsIHZXZsAZ175E".freeze
100
+
101
+ JSON_TOKEN = "{\"protected\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiIxMDdiZGI4NTAxYzQxOWZhZDJmZGIyMGI0NjdkNGQwYTYyYTE2YTk4YzM1ZjJkYTBlYjNiMWZmOTI5Nzk1YWQ5In0=\",\"payload\":\"eyJzdWIiOiJob3N0L2V4YW1wbGUiLCJjaWRyIjpbImZlYzA6Oi82NCJdLCJleHAiOjE0MDE5NDIxNTIsImlhdCI6MTQwMTkzODU1Mn0=\",\"signature\":\"qSxy6gx0DbiIc-Wz_vZhBsYi1SCkHhzxfMGPnnG6MTqjlzy7ntmlU2H92GKGoqCRo6AaNLA_C3hA42PeEarV5nMoTj8XJO_kwhrt2Db2OX4u83VS0_enoztWEZG5s45V0Lv71lVR530j4LD-hpqhm_f4VuISkeH84u0zX7s1zKOlniuZP-abCAHh0htTnrVz9wKG0VywkCUmWYyNNqC2h8PRf64SvCWcQ6VleHpjO-ms8OeTw4ZzRbzKMi0mL6eTmQlbT3PeBArUaS0pNJPg9zdDQaL2XDOofvQmj6Yy_8RA4eCt9HEfTYEdriVqK-_9QCspbGzFVn9GTWf51MRi5dngV9ItsDoG9ktDtqFuMttv7TcqjftsIHZXZsAZ175E\"}".freeze
102
+ end
data/spec/key_spec.rb ADDED
@@ -0,0 +1,258 @@
1
+ require 'spec_helper'
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/numeric/time'
5
+
6
+ describe Slosilo::Key do
7
+ include_context "with example key"
8
+
9
+ subject { key }
10
+
11
+ describe '#to_der' do
12
+ subject { super().to_der }
13
+ it { is_expected.to eq(rsa.to_der) }
14
+ end
15
+
16
+ describe '#to_s' do
17
+ subject { super().to_s }
18
+ it { is_expected.to eq(rsa.public_key.to_pem) }
19
+ end
20
+
21
+ describe '#fingerprint' do
22
+ subject { super().fingerprint }
23
+ it { is_expected.to eq(key_fingerprint) }
24
+ end
25
+ it { is_expected.to be_private }
26
+
27
+ context "with identical key" do
28
+ let(:other) { Slosilo::Key.new rsa.to_der }
29
+ it "is equal" do
30
+ expect(subject).to eq(other)
31
+ end
32
+
33
+ it "is eql?" do
34
+ expect(subject.eql?(other)).to be_truthy
35
+ end
36
+
37
+ it "has equal hash" do
38
+ expect(subject.hash).to eq(other.hash)
39
+ end
40
+ end
41
+
42
+ context "with a different key" do
43
+ let(:other) { Slosilo::Key.new another_rsa }
44
+ it "is not equal" do
45
+ expect(subject).not_to eq(other)
46
+ end
47
+
48
+ it "is not eql?" do
49
+ expect(subject.eql?(other)).not_to be_truthy
50
+ end
51
+
52
+ it "has different hash" do
53
+ expect(subject.hash).not_to eq(other.hash)
54
+ end
55
+ end
56
+
57
+ describe '#public' do
58
+ it "returns a key with just the public half" do
59
+ pkey = subject.public
60
+ expect(pkey).to be_a(Slosilo::Key)
61
+ expect(pkey).to_not be_private
62
+ expect(pkey.key).to_not be_private
63
+ expect(pkey.to_der).to eq(rsa.public_key.to_der)
64
+ end
65
+ end
66
+
67
+ let(:plaintext) { 'quick brown fox jumped over the lazy dog' }
68
+ describe '#encrypt' do
69
+ it "generates a symmetric encryption key and encrypts the plaintext with the public key" do
70
+ ctxt, skey = subject.encrypt plaintext
71
+ pskey = rsa.private_decrypt skey
72
+ expect(Slosilo::Symmetric.new.decrypt(ctxt, key: pskey)).to eq(plaintext)
73
+ end
74
+ end
75
+
76
+ describe '#encrypt_message' do
77
+ it "#encrypts a message and then returns the result as a single string" do
78
+ expect(subject).to receive(:encrypt).with(plaintext).and_return ['fake ciphertext', 'fake key']
79
+ expect(subject.encrypt_message(plaintext)).to eq('fake keyfake ciphertext')
80
+ end
81
+ end
82
+
83
+ let(:ciphertext){ "G\xAD^\x17\x11\xBBQ9-b\x14\xF6\x92#Q0x\xF4\xAD\x1A\x92\xC3VZW\x89\x8E\x8Fg\x93\x05B\xF8\xD6O\xCFGCTp\b~\x916\xA3\x9AN\x8D\x961\x1F\xA3mSf&\xAD\xA77/]z\xA89\x01\xA7\xA9\x92\f".force_encoding('ASCII-8BIT') }
84
+ let(:skey){ "\x82\x93\xFAA\xA6wQA\xE1\xB5\xA6b\x8C.\xCF#I\x86I\x83u\x99\rTA\xEF\xC4\x91\xC5)-\xEBQ\xB1\xC0\xC6\xFF\x90L\xFE\x1E\x15\x81\x12\x16\xDD:A\xC5d\xE1B\xD2f@\xB8o\xB7+N\xB7\n\x92\xDC\x9E\xE3\x83\xB8>h\a\xC7\xCC\xCF\xD0t\x06\x8B\xA8\xBF\xEFe\xA4{\x88\f\xDD\roF\xEB.\xDA\xBF\x9D_0>\xF03c'\x1F!)*-\x19\x97\xAC\xD2\x1F(,6h\a\x93\xDB\x8E\x97\xF9\x1A\x11\x84\x11t\xD9\xB2\x85\xB0\x12\x7F\x03\x00O\x8F\xBE#\xFFb\xA5w\xF3g\xCF\xB4\xF2\xB7\xDBiA=\xA8\xFD1\xEC\xBF\xD7\x8E\xB6W>\x03\xACNBa\xBF\xFD\xC6\xB32\x8C\xE2\xF1\x87\x9C\xAE6\xD1\x12\vkl\xBB\xA0\xED\x9A\xEE6\xF2\xD9\xB4LL\xE2h/u_\xA1i=\x11x\x8DGha\x8EG\b+\x84[\x87\x8E\x01\x0E\xA5\xB0\x9F\xE9vSl\x18\xF3\xEA\xF4NH\xA8\xF1\x81\xBB\x98\x01\xE8p]\x18\x11f\xA3K\xA87c\xBB\x13X~K\xA2".force_encoding('ASCII-8BIT') }
85
+ describe '#decrypt' do
86
+ it "decrypts the symmetric key and then uses it to decrypt the ciphertext" do
87
+ expect(subject.decrypt(ciphertext, skey)).to eq(plaintext)
88
+ end
89
+ end
90
+
91
+ describe '#decrypt_message' do
92
+ it "splits the message into key and rest, then #decrypts it" do
93
+ expect(subject).to receive(:decrypt).with(ciphertext, skey).and_return plaintext
94
+ expect(subject.decrypt_message(skey + ciphertext)).to eq(plaintext)
95
+ end
96
+ end
97
+
98
+ describe '#initialize' do
99
+ context "when no argument given" do
100
+ subject { Slosilo::Key.new }
101
+ let (:rsa) { double "key" }
102
+ it "generates a new key pair" do
103
+ expect(OpenSSL::PKey::RSA).to receive(:new).with(2048).and_return(rsa)
104
+ expect(subject.key).to eq(rsa)
105
+ end
106
+ end
107
+ context "when given an armored key" do
108
+ subject { Slosilo::Key.new rsa.to_der }
109
+
110
+ describe '#to_der' do
111
+ subject { super().to_der }
112
+ it { is_expected.to eq(rsa.to_der) }
113
+ end
114
+ end
115
+ context "when given a key instance" do
116
+ subject { Slosilo::Key.new rsa }
117
+
118
+ describe '#to_der' do
119
+ subject { super().to_der }
120
+ it { is_expected.to eq(rsa.to_der) }
121
+ end
122
+ end
123
+ context "when given something else" do
124
+ subject { Slosilo::Key.new "foo" }
125
+ it "fails early" do
126
+ expect { subject }.to raise_error ArgumentError
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "#sign" do
132
+ context "when given a hash" do
133
+ it "converts to a sorted array and signs that" do
134
+ expect(key).to receive(:sign_string).with '[["a",3],["b",42]]'
135
+ key.sign b: 42, a: 3
136
+ end
137
+ end
138
+ context "when given an array" do
139
+ it "signs a JSON representation instead" do
140
+ expect(key).to receive(:sign_string).with '[2,[42,2]]'
141
+ key.sign [2, [42, 2]]
142
+ end
143
+ end
144
+ context "when given a string" do
145
+ let(:expected_signature) { "d[\xA4\x00\x02\xC5\x17\xF5P\x1AD\x91\xF9\xC1\x00P\x0EG\x14,IN\xDE\x17\xE1\xA2a\xCC\xABR\x99'\xB0A\xF5~\x93M/\x95-B\xB1\xB6\x92!\x1E\xEA\x9C\v\xC2O\xA8\x91\x1C\xF9\x11\x92a\xBFxm-\x93\x9C\xBBoM\x92%\xA9\xD06$\xC1\xBC.`\xF8\x03J\x16\xE1\xB0c\xDD\xBF\xB0\xAA\xD7\xD4\xF4\xFC\e*\xAB\x13A%-\xD3\t\xA5R\x18\x01let6\xC8\xE9\"\x7F6O\xC7p\x82\xAB\x04J(IY\xAA]b\xA4'\xD6\x873`\xAB\x13\x95g\x9C\x17\xCAB\xF8\xB9\x85B:^\xC5XY^\x03\xEA\xB6V\x17b2\xCA\xF5\xD6\xD4\xD2\xE3u\x11\xECQ\x0Fb\x14\xE2\x04\xE1<a\xC5\x01eW-\x15\x01X\x81K\x1A\xE5A\vVj\xBF\xFC\xFE#\xD5\x93y\x16\xDC\xB4\x8C\xF0\x02Y\xA8\x87i\x01qC\xA7#\xE8\f\xA5\xF0c\xDEJ\xB0\xDB BJ\x87\xA4\xB0\x92\x80\x03\x95\xEE\xE9\xB8K\xC0\xE3JbE-\xD4\xCBP\\\x13S\"\eZ\xE1\x93\xFDa pinch of salt".force_encoding("ASCII-8BIT") }
146
+ it "signs it" do
147
+ allow(key).to receive_messages shake_salt: 'a pinch of salt'
148
+ expect(key.sign("this sentence is not this sentence")).to eq(expected_signature)
149
+ end
150
+ end
151
+
152
+ context "when given a Hash containing non-ascii characters" do
153
+ let(:unicode){ "adèle.dupuis" }
154
+ let(:encoded){
155
+ unicode.dup.tap{|s| s.force_encoding Encoding::ASCII_8BIT}
156
+ }
157
+ let(:hash){ {"data" => unicode} }
158
+
159
+ it "converts the value to raw bytes before signing it" do
160
+ expect(key).to receive(:sign_string).with("[[\"data\",\"#{encoded}\"]]").and_call_original
161
+ key.sign hash
162
+ end
163
+ end
164
+ end
165
+
166
+ describe "#signed_token" do
167
+ let(:time) { Time.new(2012,1,1,1,1,1,0) }
168
+ let(:data) { { "foo" => :bar } }
169
+ let(:token_to_sign) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC" } }
170
+ let(:signature) { "signature" }
171
+ let(:salt) { 'a pinch of salt' }
172
+ let(:expected_signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt".force_encoding("ASCII-8BIT") }
173
+ let(:expected_token) { token_to_sign.merge "signature" => expected_signature, "key" => key_fingerprint }
174
+ before do
175
+ allow(key).to receive_messages shake_salt: salt
176
+ allow(Time).to receive_messages new: time
177
+ end
178
+ subject { key.signed_token data }
179
+ it { is_expected.to eq(expected_token) }
180
+ end
181
+
182
+ describe "#validate_jwt" do
183
+ let(:token) do
184
+ instance_double Slosilo::JWT,
185
+ header: { 'alg' => 'conjur.org/slosilo/v2' },
186
+ claims: { 'iat' => Time.now.to_i },
187
+ string_to_sign: double("string to sign"),
188
+ signature: double("signature")
189
+ end
190
+
191
+ before do
192
+ allow(key).to receive(:verify_signature).with(token.string_to_sign, token.signature) { true }
193
+ end
194
+
195
+ it "verifies the signature" do
196
+ expect { key.validate_jwt token }.not_to raise_error
197
+ end
198
+
199
+ it "rejects unknown algorithm" do
200
+ token.header['alg'] = 'HS256' # we're not supporting standard algorithms
201
+ expect { key.validate_jwt token }.to raise_error /algorithm/
202
+ end
203
+
204
+ it "rejects bad signature" do
205
+ allow(key).to receive(:verify_signature).with(token.string_to_sign, token.signature) { false }
206
+ expect { key.validate_jwt token }.to raise_error /signature/
207
+ end
208
+
209
+ it "rejects expired token" do
210
+ token.claims['exp'] = 1.hour.ago.to_i
211
+ expect { key.validate_jwt token }.to raise_error /expired/
212
+ end
213
+
214
+ it "accepts unexpired token with implicit expiration" do
215
+ token.claims['iat'] = 5.minutes.ago
216
+ expect { key.validate_jwt token }.to_not raise_error
217
+ end
218
+
219
+ it "rejects token expired with implicit expiration" do
220
+ token.claims['iat'] = 10.minutes.ago.to_i
221
+ expect { key.validate_jwt token }.to raise_error /expired/
222
+ end
223
+ end
224
+
225
+ describe "#token_valid?" do
226
+ let(:data) { { "foo" => :bar } }
227
+ let(:signature) { Base64::urlsafe_encode64 "\xB0\xCE{\x9FP\xEDV\x9C\xE7b\x8B[\xFAil\x87^\x96\x17Z\x97\x1D\xC2?B\x96\x9C\x8Ep-\xDF_\x8F\xC21\xD9^\xBC\n\x16\x04\x8DJ\xF6\xAF-\xEC\xAD\x03\xF9\xEE:\xDF\xB5\x8F\xF9\xF6\x81m\xAB\x9C\xAB1\x1E\x837\x8C\xFB\xA8P\xA8<\xEA\x1Dx\xCEd\xED\x84f\xA7\xB5t`\x96\xCC\x0F\xA9t\x8B\x9Fo\xBF\x92K\xFA\xFD\xC5?\x8F\xC68t\xBC\x9F\xDE\n$\xCA\xD2\x8F\x96\x0EtX2\x8Cl\x1E\x8Aa\r\x8D\xCAi\x86\x1A\xBD\x1D\xF7\xBC\x8561j\x91YlO\xFA(\x98\x10iq\xCC\xAF\x9BV\xC6\v\xBC\x10Xm\xCD\xFE\xAD=\xAA\x95,\xB4\xF7\xE8W\xB8\x83;\x81\x88\xE6\x01\xBA\xA5F\x91\x17\f\xCE\x80\x8E\v\x83\x9D<\x0E\x83\xF6\x8D\x03\xC0\xE8A\xD7\x90i\x1D\x030VA\x906D\x10\xA0\xDE\x12\xEF\x06M\xD8\x8B\xA9W\xC8\x9DTc\x8AJ\xA4\xC0\xD3!\xFA\x14\x89\xD1p\xB4J7\xA5\x04\xC2l\xDC8<\x04Y\xD8\xA4\xFB[\x89\xB1\xEC\xDA\xB8\xD7\xEA\x03Ja pinch of salt".force_encoding("ASCII-8BIT") }
228
+ let(:token) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC", "signature" => signature } }
229
+ before { allow(Time).to receive_messages now: Time.new(2012,1,1,1,2,1,0) }
230
+ subject { key.token_valid? token }
231
+ it { is_expected.to be_truthy }
232
+
233
+ it "doesn't check signature on the advisory key field" do
234
+ expect(key.token_valid?(token.merge "key" => key_fingerprint)).to be_truthy
235
+ end
236
+
237
+ it "rejects the token if the key field is present and doesn't match" do
238
+ expect(key.token_valid?(token.merge "key" => "this is not the key you are looking for")).not_to be_truthy
239
+ end
240
+
241
+ context "when token is 1 hour old" do
242
+ before { allow(Time).to receive_messages now: Time.new(2012,1,1,2,1,1,0) }
243
+ it { is_expected.to be_falsey }
244
+ context "when timestamp in the token is changed accordingly" do
245
+ let(:token) { { "data" => data, "timestamp" => "2012-01-01 02:00:01 UTC", "signature" => signature } }
246
+ it { is_expected.to be_falsey }
247
+ end
248
+ end
249
+ context "when the data is changed" do
250
+ let(:data) { { "foo" => :baz } }
251
+ it { is_expected.to be_falsey }
252
+ end
253
+ context "when RSA decrypt raises an error" do
254
+ before { expect_any_instance_of(OpenSSL::PKey::RSA).to receive(:public_decrypt).and_raise(OpenSSL::PKey::RSAError) }
255
+ it { is_expected.to be_falsey }
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Slosilo::Keystore do
4
+ include_context "with example key"
5
+ include_context "with mock adapter"
6
+
7
+ describe '#put' do
8
+ it "handles Slosilo::Keys" do
9
+ subject.put(:test, key)
10
+ expect(adapter['test'].to_der).to eq(rsa.to_der)
11
+ end
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
+
21
+ it "passes the Slosilo key to the adapter" do
22
+ expect(adapter).to receive(:put_key).with "test", key
23
+ subject.put :test, key
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Slosilo::Random do
4
+ subject { Slosilo::Random }
5
+ let(:other_salt) { Slosilo::Random::salt }
6
+
7
+ describe '#salt' do
8
+ subject { super().salt }
9
+ describe '#length' do
10
+ subject { super().length }
11
+ it { is_expected.to eq(32) }
12
+ end
13
+ end
14
+
15
+ describe '#salt' do
16
+ subject { super().salt }
17
+ it { is_expected.not_to eq(other_salt) }
18
+ end
19
+ end
@@ -0,0 +1,171 @@
1
+ require 'spec_helper'
2
+ require 'sequel'
3
+ require 'io/grab'
4
+
5
+ require 'slosilo/adapters/sequel_adapter'
6
+
7
+ describe Slosilo::Adapters::SequelAdapter do
8
+ include_context "with example key"
9
+
10
+ let(:model) { double "model" }
11
+ before { allow(subject).to receive_messages create_model: model }
12
+
13
+ describe "#get_key" do
14
+ context "when given key does not exist" do
15
+ before { allow(model).to receive_messages :[] => nil }
16
+ it "returns nil" do
17
+ expect(subject.get_key(:whatever)).not_to be
18
+ end
19
+ end
20
+
21
+ context "when it exists" do
22
+ let(:id) { "id" }
23
+ before { allow(model).to receive(:[]).with(id).and_return (double "key entry", id: id, key: rsa.to_der) }
24
+ it "returns it" do
25
+ expect(subject.get_key(id)).to eq(key)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#put_key" do
31
+ let(:id) { "id" }
32
+ it "creates the key" do
33
+ expect(model).to receive(:create).with(hash_including(:id => id, :key => key.to_der))
34
+ allow(model).to receive_messages columns: [:id, :key]
35
+ subject.put_key id, key
36
+ end
37
+
38
+ it "adds the fingerprint if feasible" do
39
+ expect(model).to receive(:create).with(hash_including(:id => id, :key => key.to_der, :fingerprint => key.fingerprint))
40
+ allow(model).to receive_messages columns: [:id, :key, :fingerprint]
41
+ subject.put_key id, key
42
+ end
43
+ end
44
+
45
+ let(:adapter) { subject }
46
+ describe "#each" do
47
+ let(:one) { double("one", id: :one, key: :onek) }
48
+ let(:two) { double("two", id: :two, key: :twok) }
49
+ before { allow(model).to receive(:each).and_yield(one).and_yield(two) }
50
+
51
+ it "iterates over each key" do
52
+ results = []
53
+ allow(Slosilo::Key).to receive(:new) {|x|x}
54
+ adapter.each { |id,k| results << { id => k } }
55
+ expect(results).to eq([ { one: :onek}, {two: :twok } ])
56
+ end
57
+ end
58
+
59
+ shared_context "database" do
60
+ let(:db) { Sequel.sqlite }
61
+ before do
62
+ allow(subject).to receive(:create_model).and_call_original
63
+ Sequel::Model.cache_anonymous_models = false
64
+ Sequel::Model.db = db
65
+ end
66
+ end
67
+
68
+ shared_context "encryption key" do
69
+ before do
70
+ Slosilo.encryption_key = Slosilo::Symmetric.new.random_key
71
+ end
72
+ end
73
+
74
+ context "with old schema" do
75
+ include_context "encryption key"
76
+ include_context "database"
77
+
78
+ before do
79
+ db.create_table :slosilo_keystore do
80
+ String :id, primary_key: true
81
+ bytea :key, null: false
82
+ end
83
+ subject.put_key 'test', key
84
+ end
85
+
86
+ context "after migration" do
87
+ before { subject.migrate! }
88
+
89
+ it "supports look up by id" do
90
+ expect(subject.get_key("test")).to eq(key)
91
+ end
92
+
93
+ it "supports look up by fingerprint, without a warning" do
94
+ expect($stderr.grab do
95
+ expect(subject.get_by_fingerprint(key.fingerprint)).to eq([key, 'test'])
96
+ end).to be_empty
97
+ end
98
+ end
99
+
100
+ it "supports look up by id" do
101
+ expect(subject.get_key("test")).to eq(key)
102
+ end
103
+
104
+ it "supports look up by fingerprint, but issues a warning" do
105
+ expect($stderr.grab do
106
+ expect(subject.get_by_fingerprint(key.fingerprint)).to eq([key, 'test'])
107
+ end).not_to be_empty
108
+ end
109
+ end
110
+
111
+ shared_context "current schema" do
112
+ include_context "database"
113
+ before do
114
+ Sequel.extension :migration
115
+ require 'slosilo/adapters/sequel_adapter/migration.rb'
116
+ Sequel::Migration.descendants.first.apply db, :up
117
+ end
118
+ end
119
+
120
+ context "with current schema" do
121
+ include_context "encryption key"
122
+ include_context "current schema"
123
+ before do
124
+ subject.put_key 'test', key
125
+ end
126
+
127
+ it "supports look up by id" do
128
+ expect(subject.get_key("test")).to eq(key)
129
+ end
130
+
131
+ it "supports look up by fingerprint" do
132
+ expect(subject.get_by_fingerprint(key.fingerprint)).to eq([key, 'test'])
133
+ end
134
+ end
135
+
136
+ context "with an encryption key", :wip do
137
+ include_context "encryption key"
138
+ include_context "current schema"
139
+
140
+ it { is_expected.to be_secure }
141
+
142
+ it "saves the keys in encrypted form" do
143
+ subject.put_key 'test', key
144
+
145
+ expect(db[:slosilo_keystore][id: 'test'][:key]).to_not eq(key.to_der)
146
+ expect(subject.get_key 'test').to eq(key)
147
+ end
148
+ end
149
+
150
+ context "without an encryption key", :wip do
151
+ before do
152
+ Slosilo.encryption_key = nil
153
+ end
154
+
155
+ include_context "current schema"
156
+
157
+ it { is_expected.not_to be_secure }
158
+
159
+ it "refuses to store a private key" do
160
+ expect { subject.put_key 'test', key }.to raise_error(Slosilo::Error::InsecureKeyStorage)
161
+ end
162
+
163
+ it "saves the keys in plaintext form" do
164
+ pkey = key.public
165
+ subject.put_key 'test', pkey
166
+
167
+ expect(db[:slosilo_keystore][id: 'test'][:key]).to eq(pkey.to_der)
168
+ expect(subject.get_key 'test').to eq(pkey)
169
+ end
170
+ end
171
+ end