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 +4 -4
- data/db/migrate/20201116144600_add_validity_to_keypairs.rb +29 -0
- data/lib/keypair.rb +61 -8
- data/lib/keypairs.rb +1 -0
- data/lib/keypairs/public_keys_controller.rb +3 -1
- data/lib/keypairs/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b147b6ede46b3ddb570fcf22a695d6cf4cb1b9d1ccba3f5b7f8afdd9419e0747
|
4
|
+
data.tar.gz: ebe56e66f398850c478b73ec5ee7cab7ff37ed660df886b5e01d43f2f154bf4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/keypair.rb
CHANGED
@@ -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
|
-
#
|
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,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
|
-
#
|
38
|
-
|
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
|
58
|
+
# @return [Keypair] the keypair used to sign messages and autorotates if it has expired.
|
42
59
|
def self.current
|
43
|
-
order(:
|
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:
|
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
|
data/lib/keypairs.rb
CHANGED
@@ -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
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.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-
|
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
|