slosilo 0.0.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -2
- data/LICENSE +2 -2
- data/README.md +8 -128
- data/lib/slosilo/adapters/abstract_adapter.rb +0 -4
- data/lib/slosilo/adapters/mock_adapter.rb +1 -14
- data/lib/slosilo/adapters/sequel_adapter/migration.rb +2 -5
- data/lib/slosilo/adapters/sequel_adapter.rb +5 -67
- data/lib/slosilo/attr_encrypted.rb +7 -33
- data/lib/slosilo/http_request.rb +59 -0
- data/lib/slosilo/key.rb +6 -129
- data/lib/slosilo/keystore.rb +12 -40
- data/lib/slosilo/rack/middleware.rb +123 -0
- data/lib/slosilo/symmetric.rb +17 -47
- data/lib/slosilo/version.rb +2 -21
- data/lib/slosilo.rb +2 -2
- data/lib/tasks/slosilo.rake +0 -10
- data/slosilo.gemspec +6 -19
- data/spec/http_request_spec.rb +107 -0
- data/spec/http_stack_spec.rb +44 -0
- data/spec/key_spec.rb +32 -175
- data/spec/keystore_spec.rb +2 -15
- data/spec/rack_middleware_spec.rb +109 -0
- data/spec/random_spec.rb +2 -12
- data/spec/sequel_adapter_spec.rb +22 -133
- data/spec/slosilo_spec.rb +12 -78
- data/spec/spec_helper.rb +15 -37
- data/spec/symmetric_spec.rb +26 -69
- metadata +51 -104
- checksums.yaml +0 -7
- data/.github/CODEOWNERS +0 -10
- data/.gitleaks.toml +0 -221
- data/.kateproject +0 -4
- data/CHANGELOG.md +0 -50
- data/CONTRIBUTING.md +0 -16
- data/Jenkinsfile +0 -132
- data/SECURITY.md +0 -42
- data/dev/Dockerfile.dev +0 -7
- data/dev/docker-compose.yml +0 -8
- data/lib/slosilo/adapters/file_adapter.rb +0 -42
- data/lib/slosilo/adapters/memory_adapter.rb +0 -31
- data/lib/slosilo/errors.rb +0 -15
- data/lib/slosilo/jwt.rb +0 -122
- data/publish.sh +0 -5
- data/secrets.yml +0 -1
- data/spec/encrypted_attributes_spec.rb +0 -114
- data/spec/file_adapter_spec.rb +0 -81
- data/spec/jwt_spec.rb +0 -102
- data/test.sh +0 -8
data/.gitignore
CHANGED
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c)
|
1
|
+
Copyright (c) 2012 Rafał Rzepecki
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
# Slosilo
|
2
2
|
|
3
|
-
Slosilo is
|
4
|
-
|
5
|
-
- a mixin for easy encryption of object attributes,
|
6
|
-
- asymmetric encryption and signing,
|
7
|
-
- a keystore in a postgres sequel db -- it allows easy storage and retrieval of keys,
|
8
|
-
- a keystore in files.
|
3
|
+
Slosilo is a keystore in the database. (Currently only works with postgres.)
|
4
|
+
It allows easy storage and retrieval of keys.
|
9
5
|
|
10
6
|
## Installation
|
11
7
|
|
@@ -17,121 +13,6 @@ And then execute:
|
|
17
13
|
|
18
14
|
$ bundle
|
19
15
|
|
20
|
-
## Compatibility
|
21
|
-
|
22
|
-
Version 3.0 introduced full transition to Ruby 3.
|
23
|
-
Consumers who use slosilo in Ruby 2 projects, shall use slosilo V2.X.X.
|
24
|
-
|
25
|
-
Version 2.0 introduced new symmetric encryption scheme using AES-256-GCM
|
26
|
-
for authenticated encryption. It allows you to provide AAD on all symmetric
|
27
|
-
encryption primitives. It's also **NOT COMPATIBLE** with CBC used in version <2.
|
28
|
-
|
29
|
-
This means you'll have to migrate all your existing data. There's no easy way to
|
30
|
-
do this currently provided; it's recommended to create a database migration and
|
31
|
-
put relevant code fragments in it directly. (This will also have the benefit of making
|
32
|
-
the migration self-contained.)
|
33
|
-
|
34
|
-
Since symmetric encryption is used in processing asymetrically encrypted messages,
|
35
|
-
this incompatibility extends to those too.
|
36
|
-
|
37
|
-
## Usage
|
38
|
-
|
39
|
-
### Symmetric encryption
|
40
|
-
|
41
|
-
```ruby
|
42
|
-
sym = Slosilo::Symmetric.new
|
43
|
-
key = sym.random_key
|
44
|
-
# additional authenticated data
|
45
|
-
message_id = "message 001"
|
46
|
-
ciphertext = sym.encrypt "secret message", key: key, aad: message_id
|
47
|
-
```
|
48
|
-
|
49
|
-
```ruby
|
50
|
-
sym = Slosilo::Symmetric.new
|
51
|
-
message = sym.decrypt ciphertext, key: key, aad: message_id
|
52
|
-
```
|
53
|
-
|
54
|
-
### Encryption mixin
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
require 'slosilo'
|
58
|
-
|
59
|
-
class Foo
|
60
|
-
attr_accessor :foo
|
61
|
-
attr_encrypted :foo, aad: :id
|
62
|
-
|
63
|
-
def raw_foo
|
64
|
-
@foo
|
65
|
-
end
|
66
|
-
|
67
|
-
def id
|
68
|
-
"unique record id"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
Slosilo::encryption_key = Slosilo::Symmetric.new.random_key
|
73
|
-
|
74
|
-
obj = Foo.new
|
75
|
-
obj.foo = "bar"
|
76
|
-
obj.raw_foo # => "\xC4\xEF\x87\xD3b\xEA\x12\xDF\xD0\xD4hk\xEDJ\v\x1Cr\xF2#\xA3\x11\xA4*k\xB7\x8F\x8F\xC2\xBD\xBB\xFF\xE3"
|
77
|
-
obj.foo # => "bar"
|
78
|
-
```
|
79
|
-
|
80
|
-
You can safely use it in ie. ActiveRecord::Base or Sequel::Model subclasses.
|
81
|
-
|
82
|
-
### Asymmetric encryption and signing
|
83
|
-
|
84
|
-
```ruby
|
85
|
-
private_key = Slosilo::Key.new
|
86
|
-
public_key = private_key.public
|
87
|
-
```
|
88
|
-
|
89
|
-
#### Key dumping
|
90
|
-
```ruby
|
91
|
-
k = public_key.to_s # => "-----BEGIN PUBLIC KEY----- ...
|
92
|
-
(Slosilo::Key.new k) == public_key # => true
|
93
|
-
```
|
94
|
-
|
95
|
-
#### Encryption
|
96
|
-
|
97
|
-
```ruby
|
98
|
-
encrypted = public_key.encrypt_message "eagle one sees many clouds"
|
99
|
-
# => "\xA3\x1A\xD2\xFC\xB0 ...
|
100
|
-
|
101
|
-
public_key.decrypt_message encrypted
|
102
|
-
# => OpenSSL::PKey::RSAError: private key needed.
|
103
|
-
|
104
|
-
private_key.decrypt_message encrypted
|
105
|
-
# => "eagle one sees many clouds"
|
106
|
-
```
|
107
|
-
|
108
|
-
#### Signing
|
109
|
-
|
110
|
-
```ruby
|
111
|
-
token = private_key.signed_token "missile launch not authorized"
|
112
|
-
# => {"data"=>"missile launch not authorized", "timestamp"=>"2014-10-13 12:41:25 UTC", "signature"=>"bSImk...DzV3o", "key"=>"455f7ac42d2d483f750b4c380761821d"}
|
113
|
-
|
114
|
-
public_key.token_valid? token # => true
|
115
|
-
|
116
|
-
token["data"] = "missile launch authorized"
|
117
|
-
public_key.token_valid? token # => false
|
118
|
-
```
|
119
|
-
|
120
|
-
### Keystore
|
121
|
-
|
122
|
-
```ruby
|
123
|
-
Slosilo::encryption_key = ENV['SLOSILO_KEY']
|
124
|
-
Slosilo.adapter = Slosilo::Adapters::FileAdapter.new "~/.keys"
|
125
|
-
|
126
|
-
Slosilo[:own] = Slosilo::Key.new
|
127
|
-
Slosilo[:their] = Slosilo::Key.new File.read("foo.pem")
|
128
|
-
|
129
|
-
msg = Slosilo[:their].encrypt_message 'bar'
|
130
|
-
p Slosilo[:own].signed_token msg
|
131
|
-
```
|
132
|
-
|
133
|
-
### Keystore in database
|
134
|
-
|
135
16
|
Add a migration to create the necessary table:
|
136
17
|
|
137
18
|
require 'slosilo/adapters/sequel_adapter/migration'
|
@@ -140,13 +21,12 @@ Remember to migrate your database
|
|
140
21
|
|
141
22
|
$ rake db:migrate
|
142
23
|
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
Slosilo.adapter = Slosilo::Adapters::SequelAdapter.new
|
146
|
-
```
|
24
|
+
## Usage
|
147
25
|
|
148
26
|
## Contributing
|
149
27
|
|
150
|
-
|
151
|
-
|
152
|
-
|
28
|
+
1. Fork it
|
29
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
30
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
31
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
32
|
+
5. Create new Pull Request
|
@@ -1,21 +1,8 @@
|
|
1
1
|
module Slosilo
|
2
2
|
module Adapters
|
3
3
|
class MockAdapter < Hash
|
4
|
-
|
5
|
-
@fp = {}
|
6
|
-
end
|
7
|
-
|
8
|
-
def put_key id, key
|
9
|
-
@fp[key.fingerprint] = id
|
10
|
-
self[id] = key
|
11
|
-
end
|
12
|
-
|
4
|
+
alias :put_key :[]=
|
13
5
|
alias :get_key :[]
|
14
|
-
|
15
|
-
def get_by_fingerprint fp
|
16
|
-
id = @fp[fp]
|
17
|
-
[self[id], id]
|
18
|
-
end
|
19
6
|
end
|
20
7
|
end
|
21
8
|
end
|
@@ -15,13 +15,10 @@ module Slosilo
|
|
15
15
|
|
16
16
|
# Create the table for holding keys
|
17
17
|
def create_keystore_table
|
18
|
-
|
19
|
-
# but we really want this to be robust in case there are any previous installs
|
20
|
-
# and we can't use table_exists? because it rolls back
|
21
|
-
create_table? keystore_table do
|
18
|
+
create_table keystore_table do
|
22
19
|
String :id, primary_key: true
|
20
|
+
# Note: currently only postgres is supported
|
23
21
|
bytea :key, null: false
|
24
|
-
String :fingerprint, unique: true, null: false
|
25
22
|
end
|
26
23
|
end
|
27
24
|
|
@@ -6,89 +6,27 @@ 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
|
13
9
|
|
14
10
|
def create_model
|
15
11
|
model = Sequel::Model(:slosilo_keystore)
|
16
12
|
model.unrestrict_primary_key
|
17
|
-
model.attr_encrypted
|
13
|
+
model.attr_encrypted :key
|
18
14
|
model
|
19
15
|
end
|
20
16
|
|
21
17
|
def put_key id, value
|
22
|
-
|
23
|
-
|
24
|
-
attrs = { id: id, key: value.to_der }
|
25
|
-
attrs[:fingerprint] = value.fingerprint if fingerprint_in_db?
|
26
|
-
model.create attrs
|
18
|
+
model.create id: id, key: value
|
27
19
|
end
|
28
20
|
|
29
21
|
def get_key id
|
30
22
|
stored = model[id]
|
31
23
|
return nil unless stored
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
def get_by_fingerprint fp
|
36
|
-
if fingerprint_in_db?
|
37
|
-
stored = model[fingerprint: fp]
|
38
|
-
return nil unless stored
|
39
|
-
[Slosilo::Key.new(stored.key), stored.id]
|
40
|
-
else
|
41
|
-
warn "Please migrate to a new database schema using rake slosilo:migrate for efficient fingerprint lookups"
|
42
|
-
find_by_fingerprint fp
|
43
|
-
end
|
24
|
+
stored.key
|
44
25
|
end
|
45
|
-
|
26
|
+
|
46
27
|
def each
|
47
28
|
model.each do |m|
|
48
|
-
yield m.id,
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def recalculate_fingerprints
|
53
|
-
# Use a transaction to ensure that all fingerprints are updated together. If any update fails,
|
54
|
-
# we want to rollback all updates.
|
55
|
-
model.db.transaction do
|
56
|
-
model.each do |m|
|
57
|
-
m.update fingerprint: Slosilo::Key.new(m.key).fingerprint
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
|
63
|
-
def migrate!
|
64
|
-
unless fingerprint_in_db?
|
65
|
-
model.db.transaction do
|
66
|
-
model.db.alter_table :slosilo_keystore do
|
67
|
-
add_column :fingerprint, String
|
68
|
-
end
|
69
|
-
|
70
|
-
# reload the schema
|
71
|
-
model.set_dataset model.dataset
|
72
|
-
|
73
|
-
recalculate_fingerprints
|
74
|
-
|
75
|
-
model.db.alter_table :slosilo_keystore do
|
76
|
-
set_column_not_null :fingerprint
|
77
|
-
add_unique_constraint :fingerprint
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
84
|
-
|
85
|
-
def fingerprint_in_db?
|
86
|
-
model.columns.include? :fingerprint
|
87
|
-
end
|
88
|
-
|
89
|
-
def find_by_fingerprint fp
|
90
|
-
each do |id, k|
|
91
|
-
return [k, id] if k.fingerprint == fp
|
29
|
+
yield m.id, m.key
|
92
30
|
end
|
93
31
|
end
|
94
32
|
end
|
@@ -5,47 +5,21 @@ module Slosilo
|
|
5
5
|
# so we encrypt sensitive attributes before storing them
|
6
6
|
module EncryptedAttributes
|
7
7
|
module ClassMethods
|
8
|
-
|
9
|
-
# @param options [Hash]
|
10
|
-
# @option :aad [#to_proc, #to_s] Provide additional authenticated data for
|
11
|
-
# encryption. This should be something unique to the instance having
|
12
|
-
# this attribute, such as a primary key; this will ensure that an attacker can't swap
|
13
|
-
# values around -- trying to decrypt value with a different auth data will fail.
|
14
|
-
# This means you have to be able to recover it in order to decrypt attributes.
|
15
|
-
# The following values are accepted:
|
16
|
-
#
|
17
|
-
# * Something proc-ish: will be called with self each time auth data is needed.
|
18
|
-
# * Something stringish: will be to_s-d and used for all instances as auth data.
|
19
|
-
# Note that this will only prevent swapping in data using another string.
|
20
|
-
#
|
21
|
-
# The recommended way to use this option is to pass a proc-ish that identifies the record.
|
22
|
-
# Note the proc-ish can be a simple method name; for example in case of a Sequel::Model:
|
23
|
-
# attr_encrypted :secret, aad: :pk
|
24
8
|
def attr_encrypted *a
|
25
|
-
options = a.last.is_a?(Hash) ? a.pop : {}
|
26
|
-
aad = options[:aad]
|
27
|
-
# note nil.to_s is "", which is exactly the right thing
|
28
|
-
auth_data = aad.respond_to?(:to_proc) ? aad.to_proc : proc{ |_| aad.to_s }
|
29
|
-
|
30
|
-
# In ruby 3 .arity for #proc returns both 1 and 2, depends on internal #proc
|
31
|
-
# This method is also being called with aad which is string, in such case the arity is 1
|
32
|
-
raise ":aad proc must take two arguments" unless (auth_data.arity.abs == 2 || auth_data.arity.abs == 1)
|
33
|
-
|
34
9
|
# push a module onto the inheritance hierarchy
|
35
10
|
# this allows calling super in classes
|
36
11
|
include(accessors = Module.new)
|
37
12
|
accessors.module_eval do
|
38
13
|
a.each do |attr|
|
39
14
|
define_method "#{attr}=" do |value|
|
40
|
-
super(EncryptedAttributes.encrypt
|
15
|
+
super(EncryptedAttributes.encrypt value)
|
41
16
|
end
|
42
17
|
define_method attr do
|
43
|
-
EncryptedAttributes.decrypt(super()
|
18
|
+
EncryptedAttributes.decrypt(super())
|
44
19
|
end
|
45
20
|
end
|
46
21
|
end
|
47
22
|
end
|
48
|
-
|
49
23
|
end
|
50
24
|
|
51
25
|
def self.included base
|
@@ -53,14 +27,14 @@ module Slosilo
|
|
53
27
|
end
|
54
28
|
|
55
29
|
class << self
|
56
|
-
def encrypt value
|
30
|
+
def encrypt value
|
57
31
|
return nil unless value
|
58
|
-
cipher.encrypt value, key: key
|
32
|
+
cipher.encrypt value, key: key
|
59
33
|
end
|
60
34
|
|
61
|
-
def decrypt ctxt
|
35
|
+
def decrypt ctxt
|
62
36
|
return nil unless ctxt
|
63
|
-
cipher.decrypt ctxt, key: key
|
37
|
+
cipher.decrypt ctxt, key: key
|
64
38
|
end
|
65
39
|
|
66
40
|
def key
|
@@ -82,4 +56,4 @@ module Slosilo
|
|
82
56
|
end
|
83
57
|
end
|
84
58
|
|
85
|
-
Object.send
|
59
|
+
Object.send:include, Slosilo::EncryptedAttributes
|
@@ -0,0 +1,59 @@
|
|
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
|
data/lib/slosilo/key.rb
CHANGED
@@ -3,8 +3,6 @@ require 'json'
|
|
3
3
|
require 'base64'
|
4
4
|
require 'time'
|
5
5
|
|
6
|
-
require 'slosilo/errors'
|
7
|
-
|
8
6
|
module Slosilo
|
9
7
|
class Key
|
10
8
|
def initialize raw_key = nil
|
@@ -15,10 +13,6 @@ module Slosilo
|
|
15
13
|
else
|
16
14
|
OpenSSL::PKey::RSA.new 2048
|
17
15
|
end
|
18
|
-
rescue OpenSSL::PKey::PKeyError => e
|
19
|
-
# old openssl versions used to report ArgumentError
|
20
|
-
# which arguably makes more sense here, so reraise as that
|
21
|
-
raise ArgumentError, e, e.backtrace
|
22
16
|
end
|
23
17
|
|
24
18
|
attr_reader :key
|
@@ -33,28 +27,18 @@ module Slosilo
|
|
33
27
|
key = @key.public_encrypt key
|
34
28
|
[ctxt, key]
|
35
29
|
end
|
36
|
-
|
37
|
-
def encrypt_message plaintext
|
38
|
-
c, k = encrypt plaintext
|
39
|
-
k + c
|
40
|
-
end
|
41
30
|
|
42
31
|
def decrypt ciphertext, skey
|
43
32
|
key = @key.private_decrypt skey
|
44
33
|
cipher.decrypt ciphertext, key: key
|
45
34
|
end
|
46
|
-
|
47
|
-
def decrypt_message ciphertext
|
48
|
-
k, c = ciphertext.unpack("A256A*")
|
49
|
-
decrypt c, k
|
50
|
-
end
|
51
35
|
|
52
36
|
def to_s
|
53
37
|
@key.public_key.to_pem
|
54
38
|
end
|
55
39
|
|
56
40
|
def to_der
|
57
|
-
@
|
41
|
+
@key.to_der
|
58
42
|
end
|
59
43
|
|
60
44
|
def sign value
|
@@ -74,119 +58,23 @@ module Slosilo
|
|
74
58
|
def signed_token data
|
75
59
|
token = { "data" => data, "timestamp" => Time.new.utc.to_s }
|
76
60
|
token["signature"] = Base64::urlsafe_encode64(sign token)
|
77
|
-
token["key"] = fingerprint
|
78
61
|
token
|
79
62
|
end
|
80
|
-
|
81
|
-
JWT_ALGORITHM = 'conjur.org/slosilo/v2'.freeze
|
82
|
-
|
83
|
-
# Issue a JWT with the given claims.
|
84
|
-
# `iat` (issued at) claim is automatically added.
|
85
|
-
# Other interesting claims you can give are:
|
86
|
-
# - `sub` - token subject, for example a user name;
|
87
|
-
# - `exp` - expiration time (absolute);
|
88
|
-
# - `cidr` (Conjur extension) - array of CIDR masks that are accepted to
|
89
|
-
# make requests that bear this token
|
90
|
-
def issue_jwt claims
|
91
|
-
token = Slosilo::JWT.new claims
|
92
|
-
token.add_signature \
|
93
|
-
alg: JWT_ALGORITHM,
|
94
|
-
kid: fingerprint,
|
95
|
-
&method(:sign)
|
96
|
-
token.freeze
|
97
|
-
end
|
98
|
-
|
99
|
-
DEFAULT_EXPIRATION = 8 * 60
|
100
63
|
|
101
|
-
def token_valid? token, expiry =
|
102
|
-
return jwt_valid? token if token.respond_to? :header
|
64
|
+
def token_valid? token, expiry = 8 * 60
|
103
65
|
token = token.clone
|
104
|
-
expected_key = token.delete "key"
|
105
|
-
return false if (expected_key and (expected_key != fingerprint))
|
106
66
|
signature = Base64::urlsafe_decode64(token.delete "signature")
|
107
67
|
(Time.parse(token["timestamp"]) + expiry > Time.now) && verify_signature(token, signature)
|
108
68
|
end
|
109
|
-
|
110
|
-
# Validate a JWT.
|
111
|
-
#
|
112
|
-
# Convenience method calling #validate_jwt and returning false if an
|
113
|
-
# exception is raised.
|
114
|
-
#
|
115
|
-
# @param token [JWT] pre-parsed token to verify
|
116
|
-
# @return [Boolean]
|
117
|
-
def jwt_valid? token
|
118
|
-
validate_jwt token
|
119
|
-
true
|
120
|
-
rescue
|
121
|
-
false
|
122
|
-
end
|
123
|
-
|
124
|
-
# Validate a JWT.
|
125
|
-
#
|
126
|
-
# First checks whether algorithm is 'conjur.org/slosilo/v2' and the key id
|
127
|
-
# matches this key's fingerprint. Then verifies if the token is not expired,
|
128
|
-
# as indicated by the `exp` claim; in its absence tokens are assumed to
|
129
|
-
# expire in `iat` + 8 minutes.
|
130
|
-
#
|
131
|
-
# If those checks pass, finally the signature is verified.
|
132
|
-
#
|
133
|
-
# @raises TokenValidationError if any of the checks fail.
|
134
|
-
#
|
135
|
-
# @note It's the responsibility of the caller to examine other claims
|
136
|
-
# included in the token; consideration needs to be given to handling
|
137
|
-
# unrecognized claims.
|
138
|
-
#
|
139
|
-
# @param token [JWT] pre-parsed token to verify
|
140
|
-
def validate_jwt token
|
141
|
-
def err msg
|
142
|
-
raise Error::TokenValidationError, msg, caller
|
143
|
-
end
|
144
|
-
|
145
|
-
header = token.header
|
146
|
-
err 'unrecognized algorithm' unless header['alg'] == JWT_ALGORITHM
|
147
|
-
err 'mismatched key' if (kid = header['kid']) && kid != fingerprint
|
148
|
-
iat = Time.at token.claims['iat'] || err('unknown issuing time')
|
149
|
-
exp = Time.at token.claims['exp'] || (iat + DEFAULT_EXPIRATION)
|
150
|
-
err 'token expired' if exp <= Time.now
|
151
|
-
err 'invalid signature' unless verify_signature token.string_to_sign, token.signature
|
152
|
-
true
|
153
|
-
end
|
154
69
|
|
155
70
|
def sign_string value
|
156
|
-
|
157
|
-
key.private_encrypt(hash_function.digest(
|
158
|
-
end
|
159
|
-
|
160
|
-
def fingerprint
|
161
|
-
@fingerprint ||= OpenSSL::Digest::SHA256.hexdigest key.public_key.to_der
|
162
|
-
end
|
163
|
-
|
164
|
-
def == other
|
165
|
-
to_der == other.to_der
|
166
|
-
end
|
167
|
-
|
168
|
-
alias_method :eql?, :==
|
169
|
-
|
170
|
-
def hash
|
171
|
-
to_der.hash
|
172
|
-
end
|
173
|
-
|
174
|
-
# return a new key with just the public part of this
|
175
|
-
def public
|
176
|
-
Key.new(@key.public_key)
|
177
|
-
end
|
178
|
-
|
179
|
-
# checks if the keypair contains a private key
|
180
|
-
def private?
|
181
|
-
@key.private?
|
71
|
+
_salt = salt
|
72
|
+
key.private_encrypt(hash_function.digest(_salt + value)) + _salt
|
182
73
|
end
|
183
74
|
|
184
75
|
private
|
185
|
-
|
186
|
-
# Note that this is currently somewhat shallow stringification --
|
187
|
-
# to implement originating tokens we may need to make it deeper.
|
188
76
|
def stringify value
|
189
|
-
|
77
|
+
case value
|
190
78
|
when Hash
|
191
79
|
value.to_a.sort.to_json
|
192
80
|
when String
|
@@ -194,20 +82,9 @@ module Slosilo
|
|
194
82
|
else
|
195
83
|
value.to_json
|
196
84
|
end
|
197
|
-
|
198
|
-
# Make sure that the string is ascii_8bit (i.e. raw bytes), and represents
|
199
|
-
# the utf-8 encoding of the string. This accomplishes two things: it normalizes
|
200
|
-
# the representation of the string at the byte level (so we don't have an error if
|
201
|
-
# one username is submitted as ISO-whatever, and the next as UTF-16), and it prevents
|
202
|
-
# an incompatible encoding error when we concatenate it with the salt.
|
203
|
-
if string.encoding != Encoding::ASCII_8BIT
|
204
|
-
string.encode(Encoding::UTF_8).force_encoding(Encoding::ASCII_8BIT)
|
205
|
-
else
|
206
|
-
string
|
207
|
-
end
|
208
85
|
end
|
209
86
|
|
210
|
-
def
|
87
|
+
def salt
|
211
88
|
Slosilo::Random::salt
|
212
89
|
end
|
213
90
|
|