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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +10 -0
- data/.gitignore +21 -0
- data/.gitleaks.toml +221 -0
- data/.kateproject +4 -0
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +16 -0
- data/Gemfile +4 -0
- data/Jenkinsfile +132 -0
- data/LICENSE +22 -0
- data/README.md +152 -0
- data/Rakefile +17 -0
- data/SECURITY.md +42 -0
- data/dev/Dockerfile.dev +7 -0
- data/dev/docker-compose.yml +8 -0
- data/lib/slosilo/adapters/abstract_adapter.rb +23 -0
- data/lib/slosilo/adapters/file_adapter.rb +42 -0
- data/lib/slosilo/adapters/memory_adapter.rb +31 -0
- data/lib/slosilo/adapters/mock_adapter.rb +21 -0
- data/lib/slosilo/adapters/sequel_adapter/migration.rb +52 -0
- data/lib/slosilo/adapters/sequel_adapter.rb +96 -0
- data/lib/slosilo/attr_encrypted.rb +85 -0
- data/lib/slosilo/errors.rb +15 -0
- data/lib/slosilo/jwt.rb +122 -0
- data/lib/slosilo/key.rb +218 -0
- data/lib/slosilo/keystore.rb +89 -0
- data/lib/slosilo/random.rb +11 -0
- data/lib/slosilo/symmetric.rb +63 -0
- data/lib/slosilo/version.rb +22 -0
- data/lib/slosilo.rb +13 -0
- data/lib/tasks/slosilo.rake +32 -0
- data/publish.sh +5 -0
- data/secrets.yml +1 -0
- data/slosilo.gemspec +38 -0
- data/spec/encrypted_attributes_spec.rb +114 -0
- data/spec/file_adapter_spec.rb +81 -0
- data/spec/jwt_spec.rb +102 -0
- data/spec/key_spec.rb +258 -0
- data/spec/keystore_spec.rb +26 -0
- data/spec/random_spec.rb +19 -0
- data/spec/sequel_adapter_spec.rb +171 -0
- data/spec/slosilo_spec.rb +124 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/symmetric_spec.rb +94 -0
- data/test.sh +8 -0
- 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
|
data/spec/random_spec.rb
ADDED
@@ -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
|