keypairs 0.1.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d5f2f484253f46b38eb0c4ee8a71e77bec208ac0aeca8a5909f20389b8de9a9
4
- data.tar.gz: bb148cb660872ca14d8ba9c3416fccff38efeac97619f581b5019f2f76b83157
3
+ metadata.gz: b147b6ede46b3ddb570fcf22a695d6cf4cb1b9d1ccba3f5b7f8afdd9419e0747
4
+ data.tar.gz: ebe56e66f398850c478b73ec5ee7cab7ff37ed660df886b5e01d43f2f154bf4e
5
5
  SHA512:
6
- metadata.gz: 6b0baa0b7ad99ed238cbd7021f81fe53a40abfc720e0a1b95bc8c83f23680b15aac0c19f551f3f4c19821423a29dc2a2e48c41e68a0af3da9e9442c094784920
7
- data.tar.gz: 53160f8c12ff02c7b3143f771b7890a50e4ad1014f6c1b1c5ec6b3d97486477b831fefa5a3b3654daeb0617ae09dcdb5494cba0465850823023abe68de8a133b
6
+ metadata.gz: 7a8c50b775aeb0786f43489f1ddc9fc9196071ce7f87d36fff7db7bc9eba63b08ea3b5c1ef97798904b3205bc3e7f9b511fed322e84227a98a5712cd863be797
7
+ data.tar.gz: b0b30cc23347d5a4c0d0add46ac6944cb1881a75aebd2577db470742e81b72613b6808bd63d36385897e4a304c328adbafe7ef196cb495fdab919defc1626410
@@ -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
@@ -6,8 +6,18 @@ require 'jwt'
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,6 +31,9 @@ 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'
26
39
  ROTATION_INTERVAL = 1.month
@@ -29,18 +42,22 @@ class Keypair < ActiveRecord::Base
29
42
 
30
43
  validates :_keypair, presence: true
31
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
32
49
 
33
50
  after_initialize :set_keypair
51
+ after_initialize :set_validity
34
52
 
35
53
  # @!method valid
36
54
  # @!scope class
37
- # The last 3 keypairs are considered valid and can be used to validate signatures and export public jwks.
38
- # It uses a subquery to make sure a +find_by+ actually searches only the valid 3 ones.
39
- 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)) }
40
57
 
41
- # @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.
42
59
  def self.current
43
- order(:created_at).where(arel_table[:created_at].gt(1.month.ago)).last || create!
60
+ order(not_before: :asc).where(arel_table[:not_before].lteq(Time.zone.now)).where(arel_table[:not_after].gteq(Time.zone.now)).last || create!
44
61
  end
45
62
 
46
63
  # The JWK Set of our valid keypairs.
@@ -66,11 +83,26 @@ class Keypair < ActiveRecord::Base
66
83
  #
67
84
  # @see https://www.imsglobal.org/spec/security/v1p0/#h_key-set-url
68
85
  def self.keyset
86
+ valid_keys = valid.order(expires_at: :asc).to_a
87
+ # 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)
88
+ if valid_keys.last.nil? || valid_keys.last.not_before <= Time.zone.now
89
+ # There is an automatic fallback to Time.zone.now if not_before is not set
90
+ valid_keys << create!(not_before: valid_keys.last&.not_after)
91
+ end
92
+
69
93
  {
70
- keys: valid.order(created_at: :desc).map(&:public_jwk_export)
94
+ keys: valid_keys.map(&:public_jwk_export)
71
95
  }
72
96
  end
73
97
 
98
+ # @return [Hash] a cached version of the keyset
99
+ # @see #keyset
100
+ def self.cached_keyset
101
+ Rails.cache.fetch('keypairs/Keypair/keyset', expires_in: 12.hours) do
102
+ keyset
103
+ end
104
+ end
105
+
74
106
  # Encodes the payload with the current keypair.
75
107
  # It forewards the call to the instance method {Keypair#jwt_encode}.
76
108
  # @return [String] Encoded JWT token with security credentials.
@@ -173,4 +205,25 @@ class Keypair < ActiveRecord::Base
173
205
  self._keypair ||= OpenSSL::PKey::RSA.new(2048).to_pem
174
206
  self.jwk_kid = public_jwk.kid
175
207
  end
208
+
209
+ # Set the validity timestamps based on the rotation interval.
210
+ def set_validity
211
+ self.not_before ||= created_at || Time.zone.now
212
+ self.not_after ||= not_before + ROTATION_INTERVAL
213
+ self.expires_at ||= not_after + ROTATION_INTERVAL
214
+ end
215
+
216
+ def not_after_after_not_before
217
+ return if not_before.nil? || not_after.nil?
218
+ return if not_after > not_before
219
+
220
+ errors.add(:not_after, 'must be after not before')
221
+ end
222
+
223
+ def expires_at_after_not_after
224
+ return if not_after.nil? || expires_at.nil?
225
+ return if expires_at > not_after
226
+
227
+ errors.add(:expires_at, 'must be after not after')
228
+ end
176
229
  end
@@ -4,6 +4,7 @@ require 'lockbox'
4
4
 
5
5
  autoload :Keypair, 'keypair.rb'
6
6
 
7
+ # The Keypairs module contains common functionality in support of the {Keypair} model.
7
8
  module Keypairs
8
9
  autoload :PublicKeysController, 'keypairs/public_keys_controller'
9
10
  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'
4
+ VERSION = '1.0.0'
5
5
  end
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
4
+ version: 1.0.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-28 00:00:00.000000000 Z
11
+ date: 2020-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -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
@@ -216,6 +230,7 @@ files:
216
230
  - LICENSE
217
231
  - README.md
218
232
  - db/migrate/20201024100500_create_keypairs.rb
233
+ - db/migrate/20201116144600_add_validity_to_keypairs.rb
219
234
  - lib/keypair.rb
220
235
  - lib/keypairs.rb
221
236
  - lib/keypairs/engine.rb