slosilo 1.0.0 → 2.2.1
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 +5 -5
- data/.dockerignore +2 -0
- data/.gitleaks.toml +221 -0
- data/CHANGELOG.md +11 -0
- data/CONTRIBUTING.md +16 -0
- data/Jenkinsfile +55 -0
- data/LICENSE +2 -2
- data/README.md +125 -8
- data/lib/slosilo.rb +1 -0
- data/lib/slosilo/adapters/sequel_adapter.rb +1 -1
- data/lib/slosilo/attr_encrypted.rb +29 -6
- data/lib/slosilo/errors.rb +3 -0
- data/lib/slosilo/jwt.rb +122 -0
- data/lib/slosilo/key.rb +86 -3
- data/lib/slosilo/keystore.rb +13 -2
- data/lib/slosilo/symmetric.rb +30 -9
- data/lib/slosilo/version.rb +1 -1
- data/publish-rubygem.sh +11 -0
- data/slosilo.gemspec +11 -3
- data/spec/encrypted_attributes_spec.rb +114 -0
- data/spec/file_adapter_spec.rb +10 -10
- data/spec/jwt_spec.rb +102 -0
- data/spec/key_spec.rb +120 -41
- data/spec/keystore_spec.rb +2 -2
- data/spec/random_spec.rb +12 -2
- data/spec/sequel_adapter_spec.rb +26 -30
- data/spec/slosilo_spec.rb +47 -15
- data/spec/spec_helper.rb +2 -20
- data/spec/symmetric_spec.rb +44 -22
- data/test.sh +25 -0
- metadata +36 -11
data/slosilo.gemspec
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
2
|
+
begin
|
3
|
+
require File.expand_path('../lib/slosilo/version', __FILE__)
|
4
|
+
rescue LoadError
|
5
|
+
# so that bundle can be run without the app code
|
6
|
+
module Slosilo
|
7
|
+
VERSION = '0.0.0'
|
8
|
+
end
|
9
|
+
end
|
3
10
|
|
4
11
|
Gem::Specification.new do |gem|
|
5
12
|
gem.authors = ["Rafa\305\202 Rzepecki"]
|
@@ -18,10 +25,11 @@ Gem::Specification.new do |gem|
|
|
18
25
|
gem.required_ruby_version = '>= 1.9.3'
|
19
26
|
|
20
27
|
gem.add_development_dependency 'rake'
|
21
|
-
gem.add_development_dependency 'rspec', '~>
|
22
|
-
gem.add_development_dependency '
|
28
|
+
gem.add_development_dependency 'rspec', '~> 3.0'
|
29
|
+
gem.add_development_dependency 'ci_reporter_rspec'
|
23
30
|
gem.add_development_dependency 'simplecov'
|
24
31
|
gem.add_development_dependency 'io-grab', '~> 0.0.1'
|
25
32
|
gem.add_development_dependency 'sequel' # for sequel tests
|
26
33
|
gem.add_development_dependency 'sqlite3' # for sequel tests
|
34
|
+
gem.add_development_dependency 'activesupport' # for convenience in specs
|
27
35
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'slosilo/attr_encrypted'
|
3
|
+
|
4
|
+
describe Slosilo::EncryptedAttributes do
|
5
|
+
before(:all) do
|
6
|
+
Slosilo::encryption_key = OpenSSL::Cipher.new("aes-256-gcm").random_key
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:aad) { proc{ |_| "hithere" } }
|
10
|
+
|
11
|
+
let(:base){
|
12
|
+
Class.new do
|
13
|
+
attr_accessor :normal_ivar,:with_aad
|
14
|
+
def stupid_ivar
|
15
|
+
side_effect!
|
16
|
+
@_explicit
|
17
|
+
end
|
18
|
+
def stupid_ivar= e
|
19
|
+
side_effect!
|
20
|
+
@_explicit = e
|
21
|
+
end
|
22
|
+
def side_effect!
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
}
|
27
|
+
|
28
|
+
let(:sub){
|
29
|
+
Class.new(base) do
|
30
|
+
attr_encrypted :normal_ivar, :stupid_ivar
|
31
|
+
end
|
32
|
+
}
|
33
|
+
|
34
|
+
subject{ sub.new }
|
35
|
+
|
36
|
+
context "when setting a normal ivar" do
|
37
|
+
let(:value){ "some value" }
|
38
|
+
it "stores an encrypted value in the ivar" do
|
39
|
+
subject.normal_ivar = value
|
40
|
+
expect(subject.instance_variable_get(:"@normal_ivar")).to_not eq(value)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "recovers the value set" do
|
44
|
+
subject.normal_ivar = value
|
45
|
+
expect(subject.normal_ivar).to eq(value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when setting an attribute with an implementation" do
|
50
|
+
it "calls the base class method" do
|
51
|
+
expect(subject).to receive_messages(:side_effect! => nil)
|
52
|
+
subject.stupid_ivar = "hi"
|
53
|
+
expect(subject.stupid_ivar).to eq("hi")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "when given an :aad option" do
|
58
|
+
|
59
|
+
let(:cipher){ Slosilo::EncryptedAttributes.cipher }
|
60
|
+
let(:key){ Slosilo::EncryptedAttributes.key}
|
61
|
+
context "that is a string" do
|
62
|
+
let(:aad){ "hello there" }
|
63
|
+
before{ sub.attr_encrypted :with_aad, aad: aad }
|
64
|
+
it "encrypts the value with the given string for auth data" do
|
65
|
+
expect(cipher).to receive(:encrypt).with("hello", key: key, aad: aad)
|
66
|
+
subject.with_aad = "hello"
|
67
|
+
end
|
68
|
+
|
69
|
+
it "decrypts the encrypted value" do
|
70
|
+
subject.with_aad = "foo"
|
71
|
+
expect(subject.with_aad).to eq("foo")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "that is nil" do
|
76
|
+
let(:aad){ nil }
|
77
|
+
before{ sub.attr_encrypted :with_aad, aad: aad }
|
78
|
+
it "encrypts the value with an empty string for auth data" do
|
79
|
+
expect(cipher).to receive(:encrypt).with("hello",key: key, aad: "").and_call_original
|
80
|
+
subject.with_aad = "hello"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "decrypts the encrypted value" do
|
84
|
+
subject.with_aad = "hello"
|
85
|
+
expect(subject.with_aad).to eq("hello")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "that is a proc" do
|
90
|
+
let(:aad){
|
91
|
+
proc{ |o| "x" }
|
92
|
+
}
|
93
|
+
|
94
|
+
before{ sub.attr_encrypted :with_aad, aad: aad }
|
95
|
+
|
96
|
+
it "calls the proc with the object being encrypted" do
|
97
|
+
expect(aad).to receive(:[]).with(subject).and_call_original
|
98
|
+
subject.with_aad = "hi"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "encrypts the value with the string returned for auth data" do
|
102
|
+
expect(cipher).to receive(:encrypt).with("hello", key: key, aad: aad[subject]).and_call_original
|
103
|
+
subject.with_aad = "hello"
|
104
|
+
end
|
105
|
+
it "decrypts the encrypted value" do
|
106
|
+
subject.with_aad = "hello"
|
107
|
+
expect(subject.with_aad).to eq("hello")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
end
|
data/spec/file_adapter_spec.rb
CHANGED
@@ -13,7 +13,7 @@ describe Slosilo::Adapters::FileAdapter do
|
|
13
13
|
describe "#get_key" do
|
14
14
|
context "when given key does not exist" do
|
15
15
|
it "returns nil" do
|
16
|
-
subject.get_key(:whatever).
|
16
|
+
expect(subject.get_key(:whatever)).not_to be
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -22,7 +22,7 @@ describe Slosilo::Adapters::FileAdapter do
|
|
22
22
|
context "unacceptable id" do
|
23
23
|
let(:id) { "foo.bar" }
|
24
24
|
it "isn't accepted" do
|
25
|
-
|
25
|
+
expect { subject.put_key id, key }.to raise_error /id should not contain a period/
|
26
26
|
end
|
27
27
|
end
|
28
28
|
context "acceptable id" do
|
@@ -30,11 +30,11 @@ describe Slosilo::Adapters::FileAdapter do
|
|
30
30
|
let(:key_encrypted) { "encrypted key" }
|
31
31
|
let(:fname) { "#{dir}/#{id}.key" }
|
32
32
|
it "creates the key" do
|
33
|
-
Slosilo::EncryptedAttributes.
|
34
|
-
File.
|
35
|
-
File.
|
33
|
+
expect(Slosilo::EncryptedAttributes).to receive(:encrypt).with(key.to_der).and_return key_encrypted
|
34
|
+
expect(File).to receive(:write).with(fname, key_encrypted)
|
35
|
+
expect(File).to receive(:chmod).with(0400, fname)
|
36
36
|
subject.put_key id, key
|
37
|
-
subject.instance_variable_get("@keys")[id].
|
37
|
+
expect(subject.instance_variable_get("@keys")[id]).to eq(key)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -45,7 +45,7 @@ describe Slosilo::Adapters::FileAdapter do
|
|
45
45
|
it "iterates over each key" do
|
46
46
|
results = []
|
47
47
|
adapter.each { |id,k| results << { id => k } }
|
48
|
-
results.
|
48
|
+
expect(results).to eq([ { one: :onek}, {two: :twok } ])
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -60,13 +60,13 @@ describe Slosilo::Adapters::FileAdapter do
|
|
60
60
|
|
61
61
|
describe '#get_key' do
|
62
62
|
it "loads and decrypts the key" do
|
63
|
-
adapter.get_key(id).
|
63
|
+
expect(adapter.get_key(id)).to eq(key)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
67
|
describe '#get_by_fingerprint' do
|
68
68
|
it "can look up a key by a fingerprint" do
|
69
|
-
adapter.get_by_fingerprint(key_fingerprint).
|
69
|
+
expect(adapter.get_by_fingerprint(key_fingerprint)).to eq([key, id])
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
@@ -74,7 +74,7 @@ describe Slosilo::Adapters::FileAdapter do
|
|
74
74
|
it "enumerates the keys" do
|
75
75
|
results = []
|
76
76
|
adapter.each { |id,k| results << { id => k } }
|
77
|
-
results.
|
77
|
+
expect(results).to eq([ { id => key } ])
|
78
78
|
end
|
79
79
|
end
|
80
80
|
end
|
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
CHANGED
@@ -1,41 +1,56 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/numeric/time'
|
5
|
+
|
3
6
|
describe Slosilo::Key do
|
4
7
|
include_context "with example key"
|
5
8
|
|
6
9
|
subject { key }
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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 }
|
11
26
|
|
12
27
|
context "with identical key" do
|
13
28
|
let(:other) { Slosilo::Key.new rsa.to_der }
|
14
29
|
it "is equal" do
|
15
|
-
subject.
|
30
|
+
expect(subject).to eq(other)
|
16
31
|
end
|
17
32
|
|
18
33
|
it "is eql?" do
|
19
|
-
subject.eql?(other).
|
34
|
+
expect(subject.eql?(other)).to be_truthy
|
20
35
|
end
|
21
36
|
|
22
37
|
it "has equal hash" do
|
23
|
-
subject.hash.
|
38
|
+
expect(subject.hash).to eq(other.hash)
|
24
39
|
end
|
25
40
|
end
|
26
41
|
|
27
42
|
context "with a different key" do
|
28
43
|
let(:other) { Slosilo::Key.new another_rsa }
|
29
44
|
it "is not equal" do
|
30
|
-
subject.
|
45
|
+
expect(subject).not_to eq(other)
|
31
46
|
end
|
32
47
|
|
33
48
|
it "is not eql?" do
|
34
|
-
subject.eql?(other).
|
49
|
+
expect(subject.eql?(other)).not_to be_truthy
|
35
50
|
end
|
36
51
|
|
37
52
|
it "has different hash" do
|
38
|
-
subject.hash.
|
53
|
+
expect(subject.hash).not_to eq(other.hash)
|
39
54
|
end
|
40
55
|
end
|
41
56
|
|
@@ -54,29 +69,29 @@ describe Slosilo::Key do
|
|
54
69
|
it "generates a symmetric encryption key and encrypts the plaintext with the public key" do
|
55
70
|
ctxt, skey = subject.encrypt plaintext
|
56
71
|
pskey = rsa.private_decrypt skey
|
57
|
-
Slosilo::Symmetric.new.decrypt(ctxt, key: pskey).
|
72
|
+
expect(Slosilo::Symmetric.new.decrypt(ctxt, key: pskey)).to eq(plaintext)
|
58
73
|
end
|
59
74
|
end
|
60
75
|
|
61
76
|
describe '#encrypt_message' do
|
62
77
|
it "#encrypts a message and then returns the result as a single string" do
|
63
|
-
subject.
|
64
|
-
subject.encrypt_message(plaintext).
|
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')
|
65
80
|
end
|
66
81
|
end
|
67
82
|
|
68
|
-
let(:ciphertext)
|
69
|
-
let(:skey)
|
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') }
|
70
85
|
describe '#decrypt' do
|
71
86
|
it "decrypts the symmetric key and then uses it to decrypt the ciphertext" do
|
72
|
-
subject.decrypt(ciphertext, skey).
|
87
|
+
expect(subject.decrypt(ciphertext, skey)).to eq(plaintext)
|
73
88
|
end
|
74
89
|
end
|
75
90
|
|
76
91
|
describe '#decrypt_message' do
|
77
92
|
it "splits the message into key and rest, then #decrypts it" do
|
78
|
-
subject.
|
79
|
-
subject.decrypt_message(skey + ciphertext).
|
93
|
+
expect(subject).to receive(:decrypt).with(ciphertext, skey).and_return plaintext
|
94
|
+
expect(subject.decrypt_message(skey + ciphertext)).to eq(plaintext)
|
80
95
|
end
|
81
96
|
end
|
82
97
|
|
@@ -85,22 +100,30 @@ describe Slosilo::Key do
|
|
85
100
|
subject { Slosilo::Key.new }
|
86
101
|
let (:rsa) { double "key" }
|
87
102
|
it "generates a new key pair" do
|
88
|
-
OpenSSL::PKey::RSA.
|
89
|
-
subject.key.
|
103
|
+
expect(OpenSSL::PKey::RSA).to receive(:new).with(2048).and_return(rsa)
|
104
|
+
expect(subject.key).to eq(rsa)
|
90
105
|
end
|
91
106
|
end
|
92
107
|
context "when given an armored key" do
|
93
108
|
subject { Slosilo::Key.new rsa.to_der }
|
94
|
-
|
109
|
+
|
110
|
+
describe '#to_der' do
|
111
|
+
subject { super().to_der }
|
112
|
+
it { is_expected.to eq(rsa.to_der) }
|
113
|
+
end
|
95
114
|
end
|
96
115
|
context "when given a key instance" do
|
97
116
|
subject { Slosilo::Key.new rsa }
|
98
|
-
|
117
|
+
|
118
|
+
describe '#to_der' do
|
119
|
+
subject { super().to_der }
|
120
|
+
it { is_expected.to eq(rsa.to_der) }
|
121
|
+
end
|
99
122
|
end
|
100
123
|
context "when given something else" do
|
101
124
|
subject { Slosilo::Key.new "foo" }
|
102
125
|
it "fails early" do
|
103
|
-
expect { subject }.to raise_error
|
126
|
+
expect { subject }.to raise_error ArgumentError
|
104
127
|
end
|
105
128
|
end
|
106
129
|
end
|
@@ -108,21 +131,34 @@ describe Slosilo::Key do
|
|
108
131
|
describe "#sign" do
|
109
132
|
context "when given a hash" do
|
110
133
|
it "converts to a sorted array and signs that" do
|
111
|
-
key.
|
134
|
+
expect(key).to receive(:sign_string).with '[["a",3],["b",42]]'
|
112
135
|
key.sign b: 42, a: 3
|
113
136
|
end
|
114
137
|
end
|
115
138
|
context "when given an array" do
|
116
139
|
it "signs a JSON representation instead" do
|
117
|
-
key.
|
140
|
+
expect(key).to receive(:sign_string).with '[2,[42,2]]'
|
118
141
|
key.sign [2, [42, 2]]
|
119
142
|
end
|
120
143
|
end
|
121
144
|
context "when given a string" do
|
122
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") }
|
123
146
|
it "signs it" do
|
124
|
-
key.
|
125
|
-
key.sign("this sentence is not this sentence").
|
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
|
126
162
|
end
|
127
163
|
end
|
128
164
|
end
|
@@ -136,44 +172,87 @@ describe Slosilo::Key do
|
|
136
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") }
|
137
173
|
let(:expected_token) { token_to_sign.merge "signature" => expected_signature, "key" => key_fingerprint }
|
138
174
|
before do
|
139
|
-
key.
|
140
|
-
Time.
|
175
|
+
allow(key).to receive_messages shake_salt: salt
|
176
|
+
allow(Time).to receive_messages new: time
|
141
177
|
end
|
142
178
|
subject { key.signed_token data }
|
143
|
-
it {
|
179
|
+
it { is_expected.to eq(expected_token) }
|
144
180
|
end
|
145
|
-
|
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
|
+
|
146
225
|
describe "#token_valid?" do
|
147
226
|
let(:data) { { "foo" => :bar } }
|
148
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") }
|
149
228
|
let(:token) { { "data" => data, "timestamp" => "2012-01-01 01:01:01 UTC", "signature" => signature } }
|
150
|
-
before { Time.
|
229
|
+
before { allow(Time).to receive_messages now: Time.new(2012,1,1,1,2,1,0) }
|
151
230
|
subject { key.token_valid? token }
|
152
|
-
it {
|
231
|
+
it { is_expected.to be_truthy }
|
153
232
|
|
154
233
|
it "doesn't check signature on the advisory key field" do
|
155
|
-
key.token_valid?(token.merge "key" => key_fingerprint).
|
234
|
+
expect(key.token_valid?(token.merge "key" => key_fingerprint)).to be_truthy
|
156
235
|
end
|
157
236
|
|
158
237
|
it "rejects the token if the key field is present and doesn't match" do
|
159
|
-
key.token_valid?(token.merge "key" => "this is not the key you are looking for").
|
238
|
+
expect(key.token_valid?(token.merge "key" => "this is not the key you are looking for")).not_to be_truthy
|
160
239
|
end
|
161
240
|
|
162
241
|
context "when token is 1 hour old" do
|
163
|
-
before { Time.
|
164
|
-
it {
|
242
|
+
before { allow(Time).to receive_messages now: Time.new(2012,1,1,2,1,1,0) }
|
243
|
+
it { is_expected.to be_falsey }
|
165
244
|
context "when timestamp in the token is changed accordingly" do
|
166
245
|
let(:token) { { "data" => data, "timestamp" => "2012-01-01 02:00:01 UTC", "signature" => signature } }
|
167
|
-
it {
|
246
|
+
it { is_expected.to be_falsey }
|
168
247
|
end
|
169
248
|
end
|
170
249
|
context "when the data is changed" do
|
171
250
|
let(:data) { { "foo" => :baz } }
|
172
|
-
it {
|
251
|
+
it { is_expected.to be_falsey }
|
173
252
|
end
|
174
253
|
context "when RSA decrypt raises an error" do
|
175
|
-
before { OpenSSL::PKey::RSA.
|
176
|
-
it {
|
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 }
|
177
256
|
end
|
178
257
|
end
|
179
258
|
end
|