keypairs 0.1.0.alpha.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6674e2a0a6b9e2a995950c220252b13402d1410d58d2102819cf10811275a50
4
- data.tar.gz: dfd6fa3397cbce88a13b405fcf5c4f6fea0dfa20fcd94daba6d7247f0c33aca9
3
+ metadata.gz: b27173f10b5fa527f4bc1c1ea863ee154735fa91afd45b0b050fc07e30d3d4c1
4
+ data.tar.gz: 471a038be596fec5dcac92e16923fde534348288abd000c9a235178244dff4ee
5
5
  SHA512:
6
- metadata.gz: c92758e1f8552b5cc461790e7779a2526d21580a962c8de2126ed441f19a548273fe049924dbf1e80e92edb08becb84fdb00a7877142a9cfcd600bc474df1977
7
- data.tar.gz: d689d8a570e11ff872174a5163411427f9cd189fff972976741bae5c89b01624096f3646bb6194257e8698d6c3e7fe554faf20726b6992bc011e37a6094b7353
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 this line to your application's Gemfile:
7
+ 1. **Add gem**
8
8
 
9
- ```ruby
10
- gem 'keypairs'
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 of course run `bundle install` and run the migrations `bundle exec rake db:migrate`. The migrations from the gem run automatically.
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.string :encrypted__keypair, null: false
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 'attr_encrypted'
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
- # The last three created keypairs are considered valid, so creating a new Keypair
10
- # will invalidate the second to last created Keypair.
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
- attr_encrypted :_keypair, key: Rails.application.secrets.secret_key_base[0, 32]
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
- # The last 3 keypairs are considered valid and can be used to validate signatures and export public jwks.
37
- # It uses a subquery to make sure a +find_by+ actually searches only the valid 3 ones.
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 is older than 1 month.
58
+ # @return [Keypair] the keypair used to sign messages and autorotates if it has expired.
41
59
  def self.current
42
- order(:created_at).where(arel_table[:created_at].gt(1.month.ago)).last || create!
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: valid.order(created_at: :desc).map(&:public_jwk_export)
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
@@ -1,18 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Keypairs
4
- # Rails engine for this gem.
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
- initializer :append_migrations do |app|
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
- render json: Keypair.keyset
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Keypairs
4
- VERSION = '0.1.0.alpha.1'
4
+ VERSION = '1.1.0'
5
5
  end
data/lib/keypairs.rb CHANGED
@@ -1,3 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'keypairs/engine'
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: 0.1.0.alpha.1
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: 2020-10-24 00:00:00.000000000 Z
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: attr_encrypted
42
+ name: jwt
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.1'
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: '3.1'
54
+ version: '2.1'
55
55
  - !ruby/object:Gem::Dependency
56
- name: jwt
56
+ name: lockbox
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.1'
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: '2.1'
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.5.0
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: 1.3.1
258
+ version: '0'
244
259
  requirements: []
245
260
  rubygems_version: 3.1.4
246
261
  signing_key: