slosilo 0.0.0 → 0.1.2
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.
- 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
|
|