keypairs 0.1.0 → 1.0.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: 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