keypairs 0.1.0.alpha.1 → 1.1.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 +4 -4
- data/README.md +20 -5
- data/db/migrate/20201024100500_create_keypairs.rb +1 -2
- data/db/migrate/20201116144600_add_validity_to_keypairs.rb +29 -0
- data/{app/models → lib}/keypair.rb +67 -10
- data/lib/keypairs/engine.rb +2 -12
- data/{app/controllers → lib}/keypairs/public_keys_controller.rb +3 -1
- data/lib/keypairs/version.rb +1 -1
- data/lib/keypairs.rb +10 -1
- metadata +28 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b27173f10b5fa527f4bc1c1ea863ee154735fa91afd45b0b050fc07e30d3d4c1
|
4
|
+
data.tar.gz: 471a038be596fec5dcac92e16923fde534348288abd000c9a235178244dff4ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7da952c440fb88a52457fa801ac78ea318cdccd7514b44187f5fd7701c36d72e83dbbc6c436789b9fb029680b6c1b4f9197c69bcca7449b74bdc09b00ded9d94
|
7
|
+
data.tar.gz: 8bb70edb0ea21824d99841414f3b02bfa467f5e420c3fb7ae1ca61ee593a491123ea7826f455f549beb42f8408399d78afb15a227a399f298f5e8d41ebae15b3
|
data/README.md
CHANGED
@@ -4,13 +4,28 @@ Applications often need to have a public/private keypair so sign messages. This
|
|
4
4
|
Note: This gem is intended to work within Rails applications. It can probably be adjusted easily to also work for non-rails / sinatra project but that's out of scope for now.
|
5
5
|
|
6
6
|
## Installation
|
7
|
-
Add
|
7
|
+
1. **Add gem**
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
```
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'keypairs'
|
13
|
+
```
|
14
|
+
|
15
|
+
The of course run `bundle install`.
|
16
|
+
|
17
|
+
2. **Copy migration**
|
18
|
+
|
19
|
+
The default migration file can be copied to your app with:
|
20
|
+
```bash
|
21
|
+
bundle exec rails keypairs:install:migrations
|
22
|
+
```
|
23
|
+
|
24
|
+
Then of course run `bundle exec rails db:migrate`
|
25
|
+
|
26
|
+
3. **Setup encryption key**
|
12
27
|
|
13
|
-
The
|
28
|
+
The private keys are encrypted with the [lockbox](https://github.com/ankane/lockbox) gem. In order for this to work, you need to set the master key as described in [the readme](https://github.com/ankane/lockbox#key-generation), but the easiest thing is to set the environment variable `LOCKBOX_MASTER_KEY` to a sufficient long string (you can generate one with `Lockbox.generate_key`).
|
14
29
|
|
15
30
|
## Usage
|
16
31
|
The central point of this gem is the `Keypair` model which is backed by the `keypairs` table. If you need to sign messages, you can get the current keypair with the `Keypair.current` method. This method performs the rotation of the keypairs if required.
|
@@ -4,8 +4,7 @@ class CreateKeypairs < ActiveRecord::Migration[6.0]
|
|
4
4
|
def change
|
5
5
|
create_table :keypairs do |t|
|
6
6
|
t.string :jwk_kid, null: false
|
7
|
-
t.
|
8
|
-
t.string :encrypted__keypair_iv, null: false
|
7
|
+
t.text :_keypair_ciphertext, null: false
|
9
8
|
t.timestamps precision: 6
|
10
9
|
# Since we are ordering on created_at, let's create an index
|
11
10
|
t.index :created_at
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AddValidityToKeypairs < ActiveRecord::Migration[6.0]
|
4
|
+
class Keypair < ActiveRecord::Base; end
|
5
|
+
|
6
|
+
def change # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
7
|
+
change_table :keypairs, bulk: true do |t|
|
8
|
+
t.datetime :not_before, precision: 6
|
9
|
+
t.datetime :not_after, precision: 6
|
10
|
+
t.datetime :expires_at, precision: 6
|
11
|
+
end
|
12
|
+
|
13
|
+
reversible do |dir|
|
14
|
+
dir.up do
|
15
|
+
Keypair.find_each do |keypair|
|
16
|
+
keypair.update!(
|
17
|
+
not_before: keypair.created_at,
|
18
|
+
not_after: keypair.created_at + 1.month,
|
19
|
+
expires_at: keypair.created_at + 2.months
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
change_column_null :keypairs, :not_before, false
|
26
|
+
change_column_null :keypairs, :not_after, false
|
27
|
+
change_column_null :keypairs, :expires_at, false
|
28
|
+
end
|
29
|
+
end
|
@@ -1,13 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'lockbox'
|
4
4
|
require 'jwt'
|
5
5
|
|
6
6
|
# This class contains functionality needed for signing messages
|
7
7
|
# and publishing JWK[s].
|
8
8
|
#
|
9
|
-
#
|
10
|
-
#
|
9
|
+
# Keypairs are considered valid based on their {#not_before}, {#not_after} and {#expires_at} attributes.
|
10
|
+
#
|
11
|
+
# A keypair can be used for signing if:
|
12
|
+
# - The current time is greater than or equal to {#not_before}
|
13
|
+
# - The current time is less than or equal to {#not_after}
|
14
|
+
#
|
15
|
+
# A keypair can be used for validation if:
|
16
|
+
# - The current time is less than {#expires_at}.
|
17
|
+
#
|
18
|
+
# By default, this means that when a key is created, it can be used for signing for 1 month and can still be used
|
19
|
+
# for signature validation 1 month after it is not used for signing (i.e. for 2 months since it started being used
|
20
|
+
# for signing).
|
11
21
|
#
|
12
22
|
# If you need to sign messages, use the {Keypair.current} keypair for this. This method
|
13
23
|
# performs the rotation of the keypairs if required.
|
@@ -21,25 +31,36 @@ require 'jwt'
|
|
21
31
|
# decoded = Keypair.jwt_decode(id_token)
|
22
32
|
#
|
23
33
|
# @attr [String] jwk_kid The public external id of the key used to find the associated key on decoding.
|
34
|
+
# @attr [Time] not_before The time before which no payloads may be signed using the keypair.
|
35
|
+
# @attr [Time] not_after The time after which no payloads may be signed using the keypair.
|
36
|
+
# @attr [Time] expires_at The time after which the keypair may not be used for signature validation.
|
24
37
|
class Keypair < ActiveRecord::Base
|
25
38
|
ALGORITHM = 'RS256'
|
39
|
+
ROTATION_INTERVAL = 1.month
|
26
40
|
|
27
|
-
|
41
|
+
encrypts :_keypair
|
28
42
|
|
29
43
|
validates :_keypair, presence: true
|
30
44
|
validates :jwk_kid, presence: true
|
45
|
+
validates :not_before, :expires_at, presence: true
|
46
|
+
|
47
|
+
validate :not_after_after_not_before
|
48
|
+
validate :expires_at_after_not_after
|
31
49
|
|
32
50
|
after_initialize :set_keypair
|
51
|
+
after_initialize :set_validity
|
33
52
|
|
34
53
|
# @!method valid
|
35
54
|
# @!scope class
|
36
|
-
#
|
37
|
-
|
38
|
-
scope :valid, -> { where(id: unscoped.order(created_at: :desc).limit(3)) }
|
55
|
+
# Non-expired keypairs are considered valid and can be used to validate signatures and export public jwks.
|
56
|
+
scope :valid, -> { where(arel_table[:expires_at].gt(Time.zone.now)) }
|
39
57
|
|
40
|
-
# @return [Keypair] the keypair used to sign messages and autorotates if it
|
58
|
+
# @return [Keypair] the keypair used to sign messages and autorotates if it has expired.
|
41
59
|
def self.current
|
42
|
-
order(:
|
60
|
+
order(not_before: :asc)
|
61
|
+
.where(arel_table[:not_before].lteq(Time.zone.now))
|
62
|
+
.where(arel_table[:not_after].gteq(Time.zone.now))
|
63
|
+
.last || create!
|
43
64
|
end
|
44
65
|
|
45
66
|
# The JWK Set of our valid keypairs.
|
@@ -65,11 +86,26 @@ class Keypair < ActiveRecord::Base
|
|
65
86
|
#
|
66
87
|
# @see https://www.imsglobal.org/spec/security/v1p0/#h_key-set-url
|
67
88
|
def self.keyset
|
89
|
+
valid_keys = valid.order(not_before: :asc).to_a
|
90
|
+
# If we don't have any keys or if we don't have a future key (i.e. the last key is the current key)
|
91
|
+
while valid_keys.last.nil? || valid_keys.last.not_before <= Time.zone.now
|
92
|
+
# There is an automatic fallback to Time.zone.now if not_before is not set
|
93
|
+
valid_keys << create!(not_before: valid_keys.last&.not_after)
|
94
|
+
end
|
95
|
+
|
68
96
|
{
|
69
|
-
keys:
|
97
|
+
keys: valid_keys.map(&:public_jwk_export)
|
70
98
|
}
|
71
99
|
end
|
72
100
|
|
101
|
+
# @return [Hash] a cached version of the keyset
|
102
|
+
# @see #keyset
|
103
|
+
def self.cached_keyset
|
104
|
+
Rails.cache.fetch('keypairs/Keypair/keyset', expires_in: 12.hours) do
|
105
|
+
keyset
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
73
109
|
# Encodes the payload with the current keypair.
|
74
110
|
# It forewards the call to the instance method {Keypair#jwt_encode}.
|
75
111
|
# @return [String] Encoded JWT token with security credentials.
|
@@ -172,4 +208,25 @@ class Keypair < ActiveRecord::Base
|
|
172
208
|
self._keypair ||= OpenSSL::PKey::RSA.new(2048).to_pem
|
173
209
|
self.jwk_kid = public_jwk.kid
|
174
210
|
end
|
211
|
+
|
212
|
+
# Set the validity timestamps based on the rotation interval.
|
213
|
+
def set_validity
|
214
|
+
self.not_before ||= created_at || Time.zone.now
|
215
|
+
self.not_after ||= not_before + ROTATION_INTERVAL
|
216
|
+
self.expires_at ||= not_after + ROTATION_INTERVAL
|
217
|
+
end
|
218
|
+
|
219
|
+
def not_after_after_not_before
|
220
|
+
return if not_before.nil? || not_after.nil?
|
221
|
+
return if not_after > not_before
|
222
|
+
|
223
|
+
errors.add(:not_after, 'must be after not before')
|
224
|
+
end
|
225
|
+
|
226
|
+
def expires_at_after_not_after
|
227
|
+
return if not_after.nil? || expires_at.nil?
|
228
|
+
return if expires_at > not_after
|
229
|
+
|
230
|
+
errors.add(:expires_at, 'must be after not after')
|
231
|
+
end
|
175
232
|
end
|
data/lib/keypairs/engine.rb
CHANGED
@@ -1,18 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Keypairs
|
4
|
-
#
|
5
|
-
# It ensures that the migrations are automatically ran in the applications.
|
4
|
+
# This engine is only needed to add the migration installation rake task.
|
6
5
|
class Engine < ::Rails::Engine
|
7
|
-
|
8
|
-
unless app.root.to_s.match? "#{root}/"
|
9
|
-
config.paths['db/migrate'].expanded.each do |expanded_path|
|
10
|
-
app.config.paths['db/migrate'] << expanded_path
|
11
|
-
end
|
12
|
-
# Apartment will modify this, but it doesn't fully support engine migrations,
|
13
|
-
# so we'll reset it here
|
14
|
-
ActiveRecord::Migrator.migrations_paths = app.paths['db/migrate'].to_a
|
15
|
-
end
|
16
|
-
end
|
6
|
+
engine_name 'keypairs'
|
17
7
|
end
|
18
8
|
end
|
@@ -18,7 +18,9 @@ module Keypairs
|
|
18
18
|
# }
|
19
19
|
class PublicKeysController < ActionController::API
|
20
20
|
def index
|
21
|
-
|
21
|
+
# Always cache for 1 week, our rotation interval is much more than a week
|
22
|
+
expires_in 1.week, public: true
|
23
|
+
render json: Keypair.cached_keyset
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
data/lib/keypairs/version.rb
CHANGED
data/lib/keypairs.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'lockbox'
|
4
|
+
|
5
|
+
autoload :Keypair, 'keypair.rb'
|
6
|
+
|
7
|
+
# The Keypairs module contains common functionality in support of the {Keypair} model.
|
8
|
+
module Keypairs
|
9
|
+
autoload :PublicKeysController, 'keypairs/public_keys_controller'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'keypairs/engine' if defined?(Rails)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keypairs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stef Schenkelaars
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -39,33 +39,33 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '6.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: jwt
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '2.1'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '2.1'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: lockbox
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0.4'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0.4'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: brakeman
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -206,6 +206,20 @@ dependencies:
|
|
206
206
|
- - ">="
|
207
207
|
- !ruby/object:Gem::Version
|
208
208
|
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: timecop
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
209
223
|
description: Manage application level keypairs with automatic rotation and JWT support
|
210
224
|
email:
|
211
225
|
- stef.schenkelaars@gmail.com
|
@@ -215,11 +229,12 @@ extra_rdoc_files: []
|
|
215
229
|
files:
|
216
230
|
- LICENSE
|
217
231
|
- README.md
|
218
|
-
- app/controllers/keypairs/public_keys_controller.rb
|
219
|
-
- app/models/keypair.rb
|
220
232
|
- db/migrate/20201024100500_create_keypairs.rb
|
233
|
+
- db/migrate/20201116144600_add_validity_to_keypairs.rb
|
234
|
+
- lib/keypair.rb
|
221
235
|
- lib/keypairs.rb
|
222
236
|
- lib/keypairs/engine.rb
|
237
|
+
- lib/keypairs/public_keys_controller.rb
|
223
238
|
- lib/keypairs/version.rb
|
224
239
|
homepage: https://drieam.github.io/keypairs
|
225
240
|
licenses:
|
@@ -235,12 +250,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
235
250
|
requirements:
|
236
251
|
- - ">="
|
237
252
|
- !ruby/object:Gem::Version
|
238
|
-
version: 2.
|
253
|
+
version: 2.6.0
|
239
254
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
240
255
|
requirements:
|
241
|
-
- - "
|
256
|
+
- - ">="
|
242
257
|
- !ruby/object:Gem::Version
|
243
|
-
version:
|
258
|
+
version: '0'
|
244
259
|
requirements: []
|
245
260
|
rubygems_version: 3.1.4
|
246
261
|
signing_key:
|