jwt-multi-signatures 1.0.9

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 27579f8d71bb71562ce2fb17902a16b2b798bc029570b343440724c2796cf1c1
4
+ data.tar.gz: d59ae7032ae3ec97240eaed9169aedddeb64a6d45a603fc9cb6f8346d72839df
5
+ SHA512:
6
+ metadata.gz: 658ae4a2a24b0a6bae1ea169d171ba44316756eea38094cbc8862a581d378abb942f9d7b8d76fdd426eb0bff5b040dfbd6725d4f157e172d46cff4c530e94245
7
+ data.tar.gz: 7bcf2734bebeb517353b318a91295e3439d88fc4cefe54f8189390907c94136886b04d8b1c3acc339c33f82c9d0413cf71ccc172bf920706e73027a2b330be63
data/.drone.yml ADDED
@@ -0,0 +1,30 @@
1
+ ---
2
+ kind: pipeline
3
+ name: default
4
+
5
+ steps:
6
+ - name: Run tests
7
+ image: ruby:2.6
8
+ commands:
9
+ - gem install bundler:2.4.7
10
+ - bundle install
11
+ - bundle exec rake test
12
+
13
+ - name: Release gems
14
+ image: ruby:2.6
15
+ environment:
16
+ GEM_CREDENTIALS:
17
+ from_secret: gem_credentials
18
+ commands:
19
+ - mkdir -p ~/.gem
20
+ - echo $GEM_CREDENTIALS | base64 -d > ~/.gem/credentials
21
+ - chmod 0600 ~/.gem/credentials
22
+ - gem build jwt-multi-signatures.gemspec
23
+ - gem push jwt-multi-signatures-*.gem
24
+ when:
25
+ branch:
26
+ - master
27
+
28
+ trigger:
29
+ event:
30
+ - push
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .rubocop-*
3
+ .bundle
4
+ /pkg
5
+ /tmp
6
+ /.idea
data/.rubocop.yml ADDED
@@ -0,0 +1,107 @@
1
+ Style/StringLiterals:
2
+ EnforcedStyle: double_quotes
3
+
4
+ Naming/FileName:
5
+ Regex: !ruby/regexp /\A[-a-z0-9]+\z/
6
+
7
+ Style/Encoding:
8
+ Enabled: false
9
+
10
+ Layout/CaseIndentation:
11
+ EnforcedStyle: end
12
+ IndentOneStep: true
13
+
14
+ Layout/AccessModifierIndentation:
15
+ EnforcedStyle: outdent
16
+
17
+ Layout/EmptyLinesAroundClassBody:
18
+ Enabled: false
19
+
20
+ Metrics/ModuleLength:
21
+ Enabled: false
22
+
23
+ Metrics/MethodLength:
24
+ Enabled: false
25
+
26
+ Metrics/PerceivedComplexity:
27
+ Enabled: false
28
+
29
+ Style/PerlBackrefs:
30
+ Enabled: false
31
+
32
+ Metrics/BlockLength:
33
+ Enabled: false
34
+
35
+ Metrics/LineLength:
36
+ Enabled: true
37
+ Max: 120
38
+ Exclude:
39
+ - Rakefile
40
+ - test/**/*
41
+ IgnoredPatterns: ['\A *#']
42
+
43
+ Metrics/AbcSize:
44
+ Enabled: false
45
+
46
+ Metrics/CyclomaticComplexity:
47
+ Enabled: false
48
+
49
+ Bundler/OrderedGems:
50
+ Enabled: false
51
+
52
+ Style/EmptyMethod:
53
+ Enabled: false
54
+
55
+ Style/GuardClause:
56
+ Enabled: false
57
+
58
+ Style/PercentLiteralDelimiters:
59
+ PreferredDelimiters:
60
+ default: '[]'
61
+ '%i': '[]'
62
+ '%': '{}'
63
+
64
+ Layout/AlignParameters:
65
+ EnforcedStyle: with_fixed_indentation
66
+
67
+ Lint/UnusedMethodArgument:
68
+ Enabled: false
69
+
70
+ Lint/UnusedBlockArgument:
71
+ Enabled: false
72
+
73
+ Lint/UselessAssignment:
74
+ Enabled: false
75
+
76
+ Style/StringLiteralsInInterpolation:
77
+ EnforcedStyle: double_quotes
78
+
79
+ Layout/SpaceBeforeBlockBraces:
80
+ Enabled: true
81
+
82
+ Layout/SpaceInsideBlockBraces:
83
+ Enabled: true
84
+
85
+ Layout/SpaceInsideHashLiteralBraces:
86
+ Enabled: true
87
+
88
+ Style/DoubleNegation:
89
+ Enabled: false
90
+
91
+ Style/CaseEquality:
92
+ Enabled: false
93
+
94
+ Gemspec/OrderedDependencies:
95
+ Enabled: false
96
+
97
+ Layout/SpaceInsideStringInterpolation:
98
+ EnforcedStyle: space
99
+
100
+ Layout/MultilineArrayBraceLayout:
101
+ Enabled: false
102
+
103
+ Layout/MultilineHashBraceLayout:
104
+ EnforcedStyle: same_line
105
+
106
+ Style/AsciiComments:
107
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.3
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - 2.5
7
+ - 2.6
8
+
9
+ env:
10
+ - RAKE_ENV=test BUNDLE_PATH=vendor/bundle
11
+
12
+ before_install:
13
+ - gem install bundler -v 1.17.3
14
+
15
+ install:
16
+ - bundle install
17
+
18
+ script:
19
+ - bundle exec rake test
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ source "https://rubygems.org"
5
+
6
+ gemspec
7
+
8
+ gem "rake", "~> 12.3"
9
+ gem "test-unit", "~> 3.1"
10
+ gem "memoist", "~> 0.16"
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jwt-multi-signatures (1.0.9)
5
+ activesupport (>= 4.0)
6
+ jwt (~> 2.2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (6.1.7.2)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+ zeitwerk (~> 2.3)
17
+ concurrent-ruby (1.2.2)
18
+ i18n (1.12.0)
19
+ concurrent-ruby (~> 1.0)
20
+ jwt (2.7.0)
21
+ memoist (0.16.2)
22
+ minitest (5.17.0)
23
+ power_assert (2.0.3)
24
+ rake (12.3.3)
25
+ test-unit (3.5.7)
26
+ power_assert
27
+ tzinfo (2.0.6)
28
+ concurrent-ruby (~> 1.0)
29
+ zeitwerk (2.6.7)
30
+
31
+ PLATFORMS
32
+ x86_64-linux
33
+
34
+ DEPENDENCIES
35
+ bundler (~> 2.4.7)
36
+ jwt-multi-signatures!
37
+ memoist (~> 0.16)
38
+ rake (~> 12.3)
39
+ test-unit (~> 3.1)
40
+
41
+ BUNDLED WITH
42
+ 2.4.7
data/LICENSE.md ADDED
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © `2018` `Helios Technologies`
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ ## Usage
2
+
3
+ `JWT::Multisig.generate_jwt(payload, private_keychain, algorithms)`
4
+
5
+ `JWT::Multisig.generate_jws(payload, key_id, key_value, algorithm)`
6
+
7
+ `JWT::Multisig.verify_jwt(jwt, public_keychain, options)`
8
+
9
+ `JWT::Multisig.verify_jws(jws, payload, public_keychain, options)`
10
+
11
+ `JWT::Multisig.add_jws(jwt, key_id, key_value, algorithm)`
12
+
13
+ `JWT::Multisig.remove_jws(jwt, key_id)`
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new { |t| t.libs << "test" }
7
+
8
+ task(:release) { Kernel.system "gem build *.gemspec && gem push *.gem && rm *.gem" }
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "lib/jwt-multi-signatures/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "jwt-multi-signatures"
8
+ s.version = JWT::Multisig::VERSION
9
+ s.author = "Vitalspec."
10
+ s.summary = "The tool for working with multi-signature JWT."
11
+ s.description = "The tool for working with JWT signed by multiple " \
12
+ "verificators as per RFC 7515. Based on the RubyGem «jwt» under the hood."
13
+ s.homepage = "https://github.com/vitalspec/jwt-multi-signatures"
14
+ s.license = "MIT"
15
+ s.files = `git ls-files -z`.split("\x0")
16
+ s.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\x0")
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency "jwt", "~> 2.2"
20
+ s.add_dependency "activesupport", ">= 4.0"
21
+ s.add_development_dependency "bundler", "~> 2.4.7"
22
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module JWT
5
+ module Multisig
6
+ VERSION = "1.0.9"
7
+ end
8
+ end
@@ -0,0 +1,255 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "jwt"
5
+ require "openssl"
6
+ require "active_support/core_ext/hash/keys"
7
+ require "active_support/core_ext/hash/slice"
8
+ require "active_support/core_ext/hash/indifferent_access"
9
+
10
+ module JWT
11
+ #
12
+ # The module provides tools for encoding/decoding JWT with multiple signatures.
13
+ #
14
+ module Multisig
15
+ class << self
16
+ #
17
+ # Generates new JWT based on payload, keys, and algorithms.
18
+ #
19
+ # @param payload [Hash]
20
+ # @param private_keychain [Hash]
21
+ # The hash which consists of pairs: key ID => private key.
22
+ # The key may be presented as string in PEM format or as instance of {OpenSSL::PKey::PKey}.
23
+ # @param algorithms
24
+ # The hash which consists of pairs: key ID => signature algorithm.
25
+ # @return [Hash]
26
+ # The JWT in the format as defined in RFC 7515.
27
+ # Example:
28
+ # { payload: "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
29
+ # signatures: [
30
+ # { protected: "eyJhbGciOiJSUzI1NiJ9",
31
+ # header: { kid: "2010-12-29" },
32
+ # signature: "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"
33
+ # },
34
+ # { protected: "eyJhbGciOiJFUzI1NiJ9",
35
+ # header: { kid: "e9bc097a-ce51-4036-9562-d2ade882db0d" },
36
+ # signature: "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
37
+ # }
38
+ # ]
39
+ # }
40
+ # @raise [JWT::EncodeError]
41
+ def generate_jwt(payload, private_keychain, algorithms)
42
+ proxy_exception JWT::EncodeError do
43
+ algorithms_mapping = algorithms.with_indifferent_access
44
+ { payload: base64_encode(::JSON.dump(payload)),
45
+ signatures: private_keychain.map do |id, value|
46
+ generate_jws(payload, id, value, algorithms_mapping.fetch(id))
47
+ end }
48
+ end
49
+ end
50
+
51
+ #
52
+ # Generates and adds new JWS to existing JWT.
53
+ #
54
+ # @param jwt [Hash]
55
+ # The existing JWT.
56
+ # @param key_id [String]
57
+ # The JWS key ID.
58
+ # @param key_value [String, OpenSSL::PKey::PKey]
59
+ # The private key in PEM format or as instance of {OpenSSL::PKey::PKey}.
60
+ # @param algorithm [String]
61
+ # The signature algorithm.
62
+ # @return [Hash]
63
+ # The JWT with added JWS.
64
+ # @raise [JWT::EncodeError]
65
+ def add_jws(jwt, key_id, key_value, algorithm)
66
+ proxy_exception JWT::EncodeError do
67
+ remove_jws(jwt, key_id).tap do |new_jwt|
68
+ payload = JSON.parse(base64_decode(new_jwt.fetch(:payload)))
69
+ new_jwt.fetch(:signatures) << generate_jws(payload, key_id, key_value, algorithm)
70
+ end
71
+ end
72
+ end
73
+
74
+ #
75
+ # Removes all JWS associated with given key ID.
76
+ #
77
+ # @param jwt [Hash]
78
+ # The existing JWT.
79
+ # @param key_id [String]
80
+ # The key ID to match JWS by.
81
+ # @return [Hash]
82
+ # The JWT with all matched JWS removed.
83
+ def remove_jws(jwt, key_id)
84
+ jwt.deep_symbolize_keys.tap do |new_jwt|
85
+ new_jwt[:signatures] = new_jwt.fetch(:signatures, []).reject do |jws|
86
+ jws.fetch(:header).fetch(:kid) == key_id
87
+ end
88
+ end
89
+ end
90
+
91
+ #
92
+ # Verifies JWT.
93
+ #
94
+ # @param jwt [Hash]
95
+ # The JWT in the format as defined in RFC 7515.
96
+ # Example:
97
+ # { "payload" => "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
98
+ # "signatures" => [
99
+ # { "protected" => "eyJhbGciOiJSUzI1NiJ9",
100
+ # "header" => { "kid" => "2010-12-29" },
101
+ # "signature" => "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"
102
+ # },
103
+ # { "protected" => "eyJhbGciOiJFUzI1NiJ9",
104
+ # "header" => { "kid" => "e9bc097a-ce51-4036-9562-d2ade882db0d" },
105
+ # "signature" => "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
106
+ # }
107
+ # ]
108
+ # }
109
+ # @param public_keychain [Hash]
110
+ # The hash which consists of pairs: key ID => public key.
111
+ # The key may be presented as string in PEM format or as instance of {OpenSSL::PKey::PKey}.
112
+ # The implementation only verifies signatures for which public key exists in keychain.
113
+ # @param options [Hash]
114
+ # The rules for verifying JWT. The variable «algorithms» is always overwritten by the value from JWS header.
115
+ # @return [Hash]
116
+ # The returning value contains payload, list of verified, and unverified signatures (key ID).
117
+ # Example:
118
+ # { payload: { sub: "session", profile: { email: "username@mailbox.example" },
119
+ # verified: [:"backend-1.mycompany.example", :"backend-3.mycompany.example"],
120
+ # unverified: [:"backend-2.mycompany.example"] }
121
+ # }
122
+ # @raise [JWT::DecodeError]
123
+ def verify_jwt(jwt, public_keychain, options = {})
124
+ proxy_exception JWT::DecodeError do
125
+ keychain = public_keychain.with_indifferent_access
126
+ encoded_payload = jwt.fetch("payload")
127
+ serialized_payload = base64_decode(jwt.fetch("payload"))
128
+ payload = JSON.parse(serialized_payload)
129
+ verified = []
130
+ unverified = []
131
+
132
+ jwt.fetch("signatures").each do |jws|
133
+ key_id = jws.fetch("header").fetch("kid")
134
+ if keychain.key?(key_id)
135
+ verify_jws(jws, encoded_payload, public_keychain, options)
136
+ verified << key_id
137
+ else
138
+ unverified << key_id
139
+ end
140
+ end
141
+ { payload: payload.deep_symbolize_keys,
142
+ verified: verified.uniq.map(&:to_sym),
143
+ unverified: unverified.uniq.map(&:to_sym) }
144
+ end
145
+ end
146
+
147
+ #
148
+ # Generates new JWS based on payload, key, and algorithm.
149
+ #
150
+ # @param payload [Hash]
151
+ # @param key_id [String]
152
+ # The value which is used as «kid» in JWS header.
153
+ # @param key_value [String, OpenSSL::PKey::PKey]
154
+ # The private key.
155
+ # @param algorithm [String]
156
+ # The signature algorithm.
157
+ # @return [Hash]
158
+ # The JWS in the format as defined in RFC 7515.
159
+ # Example:
160
+ # { protected: "eyJhbGciOiJFUzI1NiJ9",
161
+ # header: {
162
+ # kid: "e9bc097a-ce51-4036-9562-d2ade882db0d"
163
+ # },
164
+ # signature: "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
165
+ # }
166
+ # @raise [JWT::EncodeError]
167
+ def generate_jws(payload, key_id, key_value, algorithm)
168
+ proxy_exception JWT::EncodeError do
169
+ protected, _, signature = JWT.encode(payload, to_pem_or_key(key_value, algorithm), algorithm).split(".")
170
+ { protected: protected,
171
+ header: { kid: key_id },
172
+ signature: signature }
173
+ end
174
+ end
175
+
176
+ #
177
+ # Verifies JWS.
178
+ #
179
+ # @param jws [Hash]
180
+ # The JWS in the format as defined in RFC 7515.
181
+ # Example:
182
+ # { "protected" => "eyJhbGciOiJFUzI1NiJ9",
183
+ # "header" => {
184
+ # "kid" => "e9bc097a-ce51-4036-9562-d2ade882db0d"
185
+ # },
186
+ # "signature" => "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
187
+ # }
188
+ # @param payload [Hash]
189
+ # @param public_keychain [Hash]
190
+ # The hash which consists of pairs: key ID => public key.
191
+ # The key may be presented as string in PEM format or as instance of {OpenSSL::PKey::PKey}.
192
+ # @param options [Hash]
193
+ # The rules for verifying JWT. The variable «algorithms» is always overwritten by the value from JWS header.
194
+ # @return [Hash]
195
+ # Returns payload if signature is valid.
196
+ # @raise [JWT::DecodeError]
197
+ def verify_jws(jws, encoded_payload, public_keychain, options = {})
198
+ proxy_exception JWT::DecodeError do
199
+ encoded_header = jws.fetch("protected")
200
+ serialized_header = base64_decode(encoded_header)
201
+ signature = jws.fetch("signature")
202
+ public_key = public_keychain.with_indifferent_access.fetch(jws.fetch("header").fetch("kid"))
203
+ jwt = [encoded_header, encoded_payload, signature].join(".")
204
+ algorithm = JSON.parse(serialized_header).fetch("alg")
205
+ JWT.decode(jwt, to_pem_or_key(public_key, algorithm), true, options.merge(algorithms: [algorithm])).first
206
+ end
207
+ end
208
+
209
+ private
210
+
211
+ #
212
+ # Masks all caught exceptions as different exception class.
213
+ # @param exception_class [Class]
214
+ def proxy_exception(exception_class)
215
+ yield
216
+ rescue StandardError => e
217
+ exception_class === e ? raise(e) : raise(exception_class, e.inspect)
218
+ end
219
+
220
+ #
221
+ # Transforms key into string (PEM format) or returns as {OpenSSL::PKey::PKey} depending on given algorithm.
222
+ # This operation is needed to satisfy {JWT#encode} and {JWT#decode} APIs.
223
+ #
224
+ # @param key [String, OpenSSL::PKey::PKey]
225
+ # @param algorithm [String]
226
+ # @return [String, OpenSSL::PKey::PKey]
227
+ # Returns PEM for HMAC algorithms, {OpenSSL::PKey::PKey} in other cases.
228
+ def to_pem_or_key(key, algorithm)
229
+ if algorithm.start_with?("HS")
230
+ OpenSSL::PKey::PKey === key ? key.to_pem : key
231
+ else
232
+ OpenSSL::PKey::PKey === key ? key : OpenSSL::PKey.read(key)
233
+ end
234
+ end
235
+
236
+ #
237
+ # Encodes string in Base64 format (URL-safe).
238
+ #
239
+ # @param string [String]
240
+ # @return [String]
241
+ def base64_encode(string)
242
+ JWT::Base64.url_encode(string)
243
+ end
244
+
245
+ #
246
+ # Decodes string from Base64 format (URL-safe).
247
+ #
248
+ # @param string [String]
249
+ # @return [String]
250
+ def base64_decode(string)
251
+ JWT::Base64.url_decode(string)
252
+ end
253
+ end
254
+ end
255
+ end
data/metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwt-multi-signatures
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.9
5
+ platform: ruby
6
+ authors:
7
+ - vitalspec
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.4.7
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.4.7
55
+ description: The tool for working with JWT signed by multiple verificators as per
56
+ RFC 7515. Based on the RubyGem «jwt» under the hood.
57
+ email: support@vitalspec.io
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".drone.yml"
63
+ - ".gitignore"
64
+ - ".rubocop.yml"
65
+ - ".ruby-version"
66
+ - ".travis.yml"
67
+ - Gemfile
68
+ - Gemfile.lock
69
+ - LICENSE.md
70
+ - README.md
71
+ - Rakefile
72
+ - jwt-multi-signatures.gemspec
73
+ - lib/jwt-multi-signatures.rb
74
+ - lib/jwt-multi-signatures/version.rb
75
+ - test/test-helper.rb
76
+ - test/test-jws-generator.rb
77
+ - test/test-jws-verificator.rb
78
+ - test/test-jwt-editor.rb
79
+ - test/test-jwt-generator.rb
80
+ - test/test-jwt-verificator.rb
81
+ homepage: https://github.com/vitalspec/jwt-multi-signatures
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.0.3.1
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: GEM for working with multi-signature JWT.
104
+ test_files: []
@@ -0,0 +1,51 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ Bundler.require :default, :development
5
+
6
+ module TestHelper
7
+ extend Memoist
8
+
9
+ def keys
10
+ { "wisoky.co" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBblFWWXUrb3pCTytGTlg1UUNwMitLWUZ5S1JGUnd4OHVFNFRkR3dWZHRRUllPVjBDCmRMaGQxQzNXYlFKTjZveWx2YXFmSGpJdDJoNG1vWkF0N00zR1crcWtXelc0SzgzQm52aUVFRHFZdGsxUTkxcmsKNGFrWHM3MlFIWDMxaDJlNFExUWV6NUg4UHpVdmFkVEhDWXdPK2QzbFFZUkZtRUZESG9aazZkajVJcGNQVEg1SApSTUtxODNWM0pKMjZoR2FNMUpSSEtOd0F1YytzbkhnTS8zdTZEcVFuZkR3Z3J0eElTZlhucXpwUmhpZk5oUFdSCmZsWGVsOExTWC9pd3FMTWxtMUMydzhmNkI5ZzF3M2dZeElHQXArYVRSRjErSzYwOUNBbmhLajlZeCs5cTFKMTIKSlZaeXU0WHVCOGZleElTdFozTU1XR29qMWFlcnZmUUJRaGEwZDNiSzY3RXR5a0FlNUMyb0FqaHFZbzg5ZGhsUQpZdndlT2pLWGQ2VEpBODRwcUtVYjRaMTkyU3kvanJNNGorcHNMZ290WkM2bHZvbnkzODVwRk9RTjh0QVZMcUF1Cm0vRkNJL0F3OXZ5MW1ER3dpSWpXOEREa0s5Q1lrb21JZHhrdG5FR0h2VG1hRzczZEMzUkxhSmxKamN5cUMvNWQKT2p2V2Y3SEhTZFM5OW1JMlVjeldpUXZianhXM2VhSGFmMGlFL3krNUl0YUEvNHNZdjl1TFlPS1BHRHlkUlJZcAp5enpsWjc3VWVtZXhDcG45dlNNVVkwdTdzT1cwRk4zK0RiL2dTL0VTa0VMaWFxMGQ0L0k3TEZsM3duRG9wbG91CmNxMEF6d0pjdi9ncjMzU2xxWE9PbHBNWW5UODJ5TlJoTWt1NDUrakQ2aG5BS0tmbE13WXZ4RllQMldjQ0F3RUEKQVFLQ0FnQStSYXVPUXZCZTZicnpueGVSVGtQblpBM3BXWlFLaFNnWjE1eDBwZWttN0FVdElzVGhrMmlxeUU3OAp4bWd1Ti85WE8vNkUxRE81Q0RJYjZ2azdxOVFhQ2ZHS3RzQkdwd0E5MHFOVmFGZStIT1dhWTdMWUI5NTlpeFZICmpQZTk3cFYySmp0ZDZMQ1lSTGg4Q1VXeWRKaFA0ZitVdnlkMm5aTkgzTmJTb3hrUzdjUEVlMlE2VWRYSVhmS1YKVS9Sdm85Z0FTcG42QzE1Q1VxbExHSlZYRVRPVnNPWno3OGlxY0hRKzJNWTY4eEwzMkhzNldzV0x5L1JPVFpadgpOMHFnYlFQaUY5MlR3WkJZWWhmWVlKMjUrUDRVR0c2Wk0xYmhiWUFCMnlFd1J4VW5uYnpKZTNVcWs2RkcyM091CkpFY2x1dFNtYlVzZEdXTUN6Yzlmc3hCNHJGWi9WMWEwQWJzRGtxRGFiVnhNVXNzb3VGN2RnZjhqSmZxY29JaGUKeWJDNXNtUDdQY1dxTWk3VDh4RXNtMTJBNnRmTHpWdllOTzI0bGwvcFBWWlRKYUs3UnFsSTRJNTNTNGlKNE9WbwpmaHVMK0orOEZjUWFpWkIza1dUWnJ4aVVGZmpXU25IcHNHNXlVb0xjYTluWGZaTFB3eUlHMGxxWnM3N3BHTWc2CmMwVncySlg3NUMrSWVFeFdoTG9SUXplUHpqTmk4TWI1cHBEK0R0d3Y2d2VxUG9TWUxIMXJVeStPN2lKTVBFZlEKOUtINy8rUk1vSnZ1V2hOUEtBOWRuVmRLT01laW9XL0hpZlkvM0R3ekpNTkQ4WHB3NmQwblVyTDBZSlprQWsrdgoyRERSMnFjSDRYUXNMeHI3NkZIYlhGclJzdUZtSXV5bzdJS1NYK2NQYUdKWmpRUERVUUtDQVFFQXlhUXFnM2FrCjdGbGVpd3FrNzVOZlN1aW1oTGZMenEzY1k4dU4yaG1rQ0M5Sjd3Qmh5a1lEYVdaU3hJdmRSbDlFaGg4Uld5OSsKYWQ2VUozMGlNc09nUlIxRWt0R0dOeGpRV3orRE9aNkNZYmNoYSs4c01sZEx2cGFxdVUrN0lXWEFvLy9EUzFpVQpmSUhqSlFlcUgzbTEyeHVIOTI4U0VzS2gzMFJlZGFqRnpVTmcrK0tkbTFJeU85aHk4REFNZWw5OUVMWGtWSWhmCnkrMzZHTXJRV082LzVBak9WVXFqd1NNaUwzZVpmRjNvRHVVcnNrRm5WeUxibStvQVlZSnlidVl6UmtrWUJiRnkKbkt2dHcxSldSeHduZzAxTGpUMDU5dThoL2FwNDBZMFhTcVlqcEJLZ1UzY3FscXZ3bzFzUG1aWllZU0FnS3VwbApjWUVGWUQwaTlWbktLUUtDQVFFQXgxbk5MbERuZzlCRWNGaGhrL282MERCM21IMTVKNlpXQ1BwWnpPVnFNL0VUCld0S1ZWQUxCSVJoeVo2cGhZT1lneWtIY1dRMWdpZzVBYmxJTnZXV0pveTdYQ01VMHdObldJaXhaU0FlREpCQVcKMVlPTEZqcUZrdXljS001V1pNNW52Z2hmdG5KdkVMQUZuMDgzSmduVEdSL2R2alR4OUhTSUdzWkNNME5kQmRZbApUWWtuWURlOXZaWmJMa1k5SUtKODdjYUpmemVCMzkxK3VUdFdNbVB1VUl2MHA0ZW96NnpHdmI1dnFoOEl6RHp2Ci9OSUNiVExLRmUwZ3d2OHVkVkE2QyszZnVKazZkeGpDb1lqaWVZN0tIVW0xdHFaa3hkY3AzOVVkUXdvZk9zTmkKeDltc21jTXByaEgrVmpiU1J2RDFSY3duWDhQbGhnOW1hcTl5ZUhjWkR3S0NBUUVBbUxmQml6Zjh5UlVXeWZBUgo0M0dXcHNGMS9PYkhjWTIwY2REbGF0NG9vaHBPd0xsbFZ6R1h1K2hIbjV6ZXhrRzVRR3VmVlpTdkJiZ1NOYVpNCmxHNGRvTHIrQ01Td0JtTEF5NXRhNC9UdGd0eVViNDhCeGs3ZmkwWEpuL2lISGxCV2l0OVhKbVc4Y0dCZmpOZzEKUFFtTmRwbHZiVE91V0k4WTBtU1J0a05STEpsdmh0YW56ODk5UkY0M0R6c1UrRW9DQ3ZuNEtSM3drQjk1WC9XYgp2djkwVGwxdENLUXpTa0ExMEFXaE5kUlp3WTVJZmdXVEl5ZS9kR0xTVHdmaGE2VG1DTUdyZEFSbGJjdTVsRWwwCkZ2OTMzYlpaRm12Y3p1MW1yUnpEek5JelpkSlhCQmtuWEkvUXJiVWoyRlZMaDJPYkpGU1VpR3htMElTTGNjeGMKQWI4em9RS0NBUUF2ZTRwTnIrT1ZGL1JWTmhmMzRUQkZDbVpTSWdETG11ai9ObkpSUll1b1Y1R2VubTRIRnFqZApzeTc4MWk1Zm9ERExQQ2k1NVYvTFFsM0NhVFR3bWREUTE0Vk1oM3hyT3Zld0tCUVQvZ1lVZnVpUmJzV2dROHd4CkZMNlZVYUJ1WG1PRGRnY21NOWVVaC9pdTIzVnRVQVhDQkQ4UzRSV0lmb0UwcjJoeFFXaFV6WThSQ3N3Z05PYXkKMDY5Z05FYTNFVHprZmRlZVA2QmxyQ0pWQ0hjZGhZUHNGNG5zcFhsbURlZEFwcTErUGVvZ2k1czJBdWVsRHVYbgpseFdvbkpONlNlT3BsNzBrQVF0VjlzWFZKLytacUpNbnFyam5pbmFTVVErZVN1cXZYeWZWSFZqWDlWY1JRTlVhCnF4cURlb2RYY21sWmVLa2dQRTdkUWFuSlc0VE9nTCt0QW9JQkFRREc0Tlp6KzlaRFdOOWtOVFBYT0dGTFkzTVUKOG9kUkNjZGVEQVlGa1RSbUF5UVMxVTFWQTgwcU1CVkJ0OTIxZFdmUFV2SlM2dkRXMWhqQU5lMFM1a1hzYzVUNQpLOVJOZTJ3elFLbHpwV2JCaWhSYlUydnR4cVVaQ0dadE5lU1RINFBlUS9rcUxIQVdUbzVmYjlPRU55Zk8vU0hPCmJabTUranFRTVVkTkRsTlYrTkRESFI1eHhQcXg0Y1JZTVRqb0xrVlkxOFduZTlsUUpIY3g3blEwMm1qMW4rc1MKeGFlMGpxWThVWDQwRkdBZHcrdzJzUmpMdW45YkVLTEc0cEM4aDFLeXJsOHJCUVV5Mlk0T3ZLVHljc2xXWDdTegpiZkQ2R2ZJL2E1Q1MzVWU3M2VBTjFpZWlPSC9jSTE4RXFrUkwxOUNRMzd6Q2FXUEJ5dEd3SmtvU1RlcG0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K")),
11
+ "powlowski.info" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBdHg5R05SOGhnSmJtS0JPbGw1Ym10K0VPNWNIRDVJRTMreitobldOSG84eW9CakkvCmVIaVRYdXprTG8zSFBISUZvby91czZHbDQzOGdRRVhGTXpCalVGNU13bEZ2dTRBK256WVNDWWpDREN5d2F4aWEKSm0zcVNXM21HVWhER013b3R6d1FYNWN3Qjd3MExsT2QzV0hkd3paZGk3bXR5L2tpY3VsVUhYakFzTU9GV2gxRwpxSnY5d3crL0lvVWR1clZubnh5dTNQTEwxTnZqM3NCaDlQOHlIZW1vRTBRQlNIRXN2dFBxVWI1WktFTlI3MVBTCnVTWmVIYnVIcXVja2k2RWM4R3NSMTEzUTkrK1JWbmFSS2ZLMUxKSkRnNVMwSUZiS2JKT05vMXdCVUdsUlZ2VXMKbDVha2dnbiszYk1XT05tclFyTG5aVDRhVmJOOUZMWERXQ2VPTnZSc3NLUlAvZTdzbVZSUWRWYTdKNk1VcnFRbwpiYzNFbUxPbUkvb0hhRmhCUTZ0U0tSRGVjdW9IREoxbVpqM0tmeXNvWThNWjl1WXpPcm5XVkZONExSQ2E3S0pMCktSVGFJZERldm1RLzZQU1RFOFAxcVg4Y2RxTi8rSElLWVJkUWsxaExSZUNpTzBaeWVxejNSZW10ZEh4Tk9uSUMKZ1NhR2JMZnRMWmtYTWJLNXd1QS84emJLSjJ5RDQ4bWkvSGpRbDhZaTRNTWtnL2ltM2VPbGwwNmxFa043Q3FMVgppdWR6U1pLSnluWldtV0VCUWV2UWxBc0ZDZk1ZK2h5Snp1dHp5VU9QU2hVTHlFWjJsSllDS0NKaVE0VUs4Q0RvCkdmWFA3ZFhqenRmbFVmMTFkTzEvbFVGNDdVNjBFWUtMV01yWmZGWmhYQTlhSlhFT3MzU0g4Y0lWcVdFQ0F3RUEKQVFLQ0FnQnpMUlIzYlBFaGM4ZW5CVlJ0bDlmZFo0eDdMZmdMek1wdEdJU0ovVnVkeHFjWDNwclZKdUZxSHcwVgp5czY1VWU0QlpRMzVwWDQxTEV3WW9NbDdmTCs2V05WbWt0bjMwSjJTZmV1eVczWFJPbnByb2JteTJnYzEwQTJkCmNUbmlhdVpnK1VKREhWQjBUUWQwNjlxcTExY241UlhKUUN2ejB1cTc0ODJvQzc4R2JyTjlEbFRXeitZM3ZidTEKOW92UVZ6Q3BmdHpzMHprbzFIVHFNWTVyRGVkenNQYXB4MmdYTERlOGZvVXVqTTUrNkhpc1VzaUM4NExXcUpDWQpDWEdPOFBMR3RGRXdhQzE3QkE4aGxzbU8zTHpmSDgrZS92U2NNbnAyK0FkcDdBQlhseVkxejFjUXNRc2ZUeklpCk52V1BKRGozWnBicnNyZlZsMkxnbDhJWnZDZFJrQitoUHk1WjNYWGN2bkVJOU1WZDRLaUVEdHdtMm0wUWNFSWYKVlUrblhPZGdNaUZvTTN0R24xeVJ0MG5oR3ZVNW5tTWh2RDhmNzRwaHFnTlRBUmN3NDF6aUJTR1JaOGhPVkExcApNUmcyZUh0SU1KeTI4RE5PdDdtcnpQb21TOUpqblBkR2dsc2ZTcnRWREtWeDBpbEpXQ3FReGZEWExBTUFTdjc2CjRNMFNnMTk2NW13WTdNUVF0eFdrWkpjcU5MU0dCdGVSeUtzUHBuWjBzZ2FvV1JiRjA0cFk3b2ZwOEFDYUJLRVQKbUFXdmgxZlVPcFdBaUllMHI2QjFSRGQ0YzlBZWtUV2JObmViUmRVeldLL1NlSDAyZm5jTEo5emdGUVU5TitKZAp1aE5YK2lIKy9iaHRIL0ZxaEVwTC9id1A5bDFOckxKQThDa2orb1N2UUVzVGJaYWt3UUtDQVFFQTZYV2VsdzI1CmU1VFdGcTlldnV1N0s4Y21TT2srMUd0cW1wd0ZZTURPdHZKSWJjU1ZMVy9PQU1yYXhxaTBtNFlLUFphbWpnUjEKdHJvU1IxWi8yVHo4cytJTU5kdE52SUp0THNyNjlpamwvN2Z1R0ZuVXFPK2FUQXJieG1LRmR5VDlBNGVaUnMycQpMRkY4aHNSVUgwZkVKaXZTZVNCeWxSSEwvRS8yd3Jqalp4R1RWdTQ2UGgrWUxUcUdudDZ0MitNNnZ0YjJzOUl4CkNCSlV5T1lyWTFGdFNEbzBLR3R5eTNWdkZiZTZGcFNaMUVQK0liRU1laXErN1VWeUgxV1l6QTRLa1Q5N2dGTTQKcEw3S0J5aUo1amdEL3N5b1ZHV1ZkL0xWWW5HM3hFNTBMN29RaUFaa3hyRnN6dU85QTl0VEdqV1ZyNW40eUFRNApDUFJsMG0vdXNObUdQUUtDQVFFQXlNMTUyYkJQOUNiaVYyZU9wUWxwL25yWEwxbGhxVUhjTlplZnRTNFRtalloCjNHS0M4TUE4OUZnOEpqYk1OYjNoNDRkQlA0TXVxdVo3Uk9INXcyRFFEcnN0cTdBaUNiVEFkTEVodDBSQWlVY28KUjRuTWRZWlRHQWhhOVdhQWwxalVIakZVMlFUZjl3S3BZeXZmdTVKb2RXaFRPY2sxS3ZleHFiVVRkcG9CbEdoZwpZN0I1cEpmK1AzaCtOK0xsTzVQOUYyakc5TWprOUJIbEpHcFl4SFE0THNBVHdqcHhsNFJEUFcyNWVBVlMzVTZSCjlBdlZ5Qm03bG1Cb0NhVTNCWHU5aTZmeDRhUGJTdDNFankyZGZPR1hPRUQwRCs2QjVYS29PelNMdFZzVlJIRHUKeTI0SzExbk8wUVNyeW1HN3l6ME50MHlvR2drRHhGQXpPRUJJZVAwRjlRS0NBUUVBM0pNa285Tnp6Qzl6bHp0YQphVWlRTDJ5WjM0bUFzM0pKNW9wRENvY2d4L2xpTlZQbkhtYmtYQnROV1NWTWZ5VEZ5Q3J1Y29BRU9BRFdCRkRWCnVvckV0N0I3bU9iN0s0Q1BhQWFmMXJRTm11NU5KdlM0MkdTSmhBOCtWdEgvQi9NS21xc2pScUpLaGxUM010Mk0KSFlIUThiKzF2SHZMeHN4cHpwbytxdnZFM3p6YjJPWjhZUFc0OGdLNTdxQzE0MnR0dGFHa3RZR0NrZjIvM1pDYQpyZHZoUkx5NVN2YzZIc0Yxa3k5andySGtKWW1ZTW56MUxQZjJMSGZRdTRwRU00ZVF0R3NtWkxnOGJHdFd0aXkzCkhhMFBHVTZFUERrK1gzWXY1ak5MVFU1U3VFVTBHVkR4SmttOFpEMEgrUHpnSjRNNVNoQlAzYXNleGxjalhSQWsKbFRMd2dRS0NBUUJXK1RqSDR5Z2VWaUUvUG1sNGJrVnNwZ1JDUy9LUy95WEVTTEl5Sll6MEJISlNKSkVXZWcxcwp3REw5VWtyTkZEdWM4MTU5aGZKV3I1SEEyaWYyU2g2VDR0cjdQRVRoODFwUXNOQXJzdkpKQTNzYzBVQ3Z2c2lLCjVrT1BleUJEYllRaXQ3ZEtjR1FaZHh1ckNydlRZS3pCL2JmZWxabmp6SGsxU21ydHVmTHBOdlJZK1gwV240Yk8KTXdCb2NHeGRpOUhacTlaUS9CcSs1R2xkaG5xQVRONXcwVjA5aVZiZUM3bWNCOFNIaWJiRWlGMkxXUHoxdUwrWgplSlJYYVNvVncrenJhb2pIOU5MczhIVk1sck5aL2RRajEyNWU0QzEvRmxScm9HeksxbksxdkR5Ui9FM1J6T0paCmdpNXVjRHFJNHg3bnY5b252TFBXK2UzVmRYSnVoUmxwQW9JQkFRQzNoWW9MVGtLY2ViNmVURHRmZDdpSGdlUTAKdUp0L2sxME8zbWFjSUlzMGd5ZDQ4cDBmcmpteDdBWFFaSWhFZ2pNcGtHK3hFRGRWMTh4d00zYkV1T2RjSm83NApwSlZYMDhVaURPT05BckxkY1hvL1gxYysxZXNVeDYxc2Uwbm5lOEZxd3Y5b091aXM4Z1ZGWS9XN01tcEY5dnNlCkRMbHZadnpWelNxcXR6OTJpTEM2enJ5cTdrK0pCL3hwRU1XemRUM1lKYXlGK1NwSjJveDRXVTIrTjI1V0NiL1gKc2NqZHkyNUZzSW5teXpSKzRxMnZHb3grR2NjU3ZCcCtrZVhHN0dNNmMwbTVXUCttZnExSUtWRFh1WlZSTUR5cQptSEY4cmxBYU9ES1c4SlI1Mnp1Wi8zTXpzWkNJS2NqYjA1V09KdDdKM1NXVWQyOWU5WDJQdStkdjBXa0wKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K")),
12
+ "mcglynn.org" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlHNVFJQkFBS0NBWUVBNlZ4UExhYVN4QTRrdUx0SHhiNVI2YW0wVVF4L3BTOEhjZDAyQy9YS29GSEk3QnJGCk5VUzlkSDBIWGpBc3NIWkhRa1BkamlRUUh2OW9aR0QxNGZXelhPWTRlbUhJVXFPWmljSk1mRnFXVlJYSEx3SVUKOUxITUlQeDFabHJOVEFjOEN1aG5FZUtoWTBwc1NMc09GeitDL0QvSVNvM2R0aVI5Tk5TenlqdUdOKzdxUnRUbgpYcHdIQlJHbFdCT1VneUI0TDF2QTFKUE0xY2UwODhOMmhZeDIwOTVtUTZpM3pvY2VJSTFrb0FoQjFVbm5YUEN4Ck1oaGlScjV5Q3N4d0ZEQzkwSlQ1MXNOaWFoVEdoRFZLUkg1UUdWd2xyOVo3L3Y2d1dKZ0I2TUw2VWFEbEw3SEkKVDRXRWNJYjBrOVNzMGY3MlcxRFJDdXlhdHltWnhPaCt0TXBIN3J3cmtvSXlldnAwT1RUQ29GT0hUWWlNaGRrRgowa2t1L05nclNVdWxRSnFlaFMrNmtzZTZveVZFU2E4enNyVGhLL1gyYmJvSUhwOU0vRU9GVFVTSXVhZmY5b3MrCk9LUlhOOWI5Q0tnUTk5NFA4S1V1UHR5bjZ1M0UvNUtMVnAvbkpHM2d5anRvZWZIdktwdXRtNWk2TUlRSi85eVMKcUwvRVlYL04wTzBXVlRkVkFnTUJBQUVDZ2dHQkFLaGZtQ21DQkdjOUpUVzh1djVzWWNITVZuUWNKb1ZTdDNacQplN0tKZDlmUTZyMmdXeVlpSU9oSnhlVXBzVFRwUW1VSGZuWXVnd3M2a1dITHE5MkxZQXpwZDFxbDd0bmhmTWl1CnptenpGNER3bzdUQk5jbVA5NDdkV1ArdkNHMlEwcnUwRDVvU0FRd1pDS1E1Z3VNM1NoVWpHQ3JpelZPOFpES1kKUGRqdXRkcnBvVlBXRGRKdmxZa013RllhV285NS8vMTdvRmhCQkF4RGVjWmdBOFk4SVFpaGNQdmtZaXE3eHZzSgp5YzdGNW1vMFZxRHljWWVKbkc4YXZrSVlXODNWazk2VHdyZVJWSkRjTldYS2pWRjBJeXQzK2FEU3VZM05JNkpyCkZVamVNZnoyYndYZUhBV1pSYng2L25temxybjRHVEV0TzI1Q0VZVVhNNkNMVFRHYWdWS2dpL2VJWGdiWDB0bmsKcDFCSWhBRWFxdFZLMjlkdHd1aWdWNjhxdU1ZRU5wNjh5ZkplV0s3VmczTUsrbVI2cTJ5NmNZdFhjUVJTbk1GagpXMmRzTVliNlZxQ21XRHFBNld2Vis4TTNhRFJWaHhBM2t0dEtBdXcwTUw5THl5dlNWeHdYL2luOGNtUXgwL0pPCjk0enlDMnAwdjFmc0xkeEJHMm1ZYytWZHBSblR3UUtCd1FEMUF3RkhPM1dqa0lrWDhLQjVtVXY2VXl3MWQ1c2sKWDVkdjNXYVZmNGhTUGxPSHRSaGJHNWJ6eFJXaG50ZHg1MTVIT2tEZHo0dEtHZXRyWjlVUWo5WjlrUXMyNFFLVApCaitNRlVjZ3F6dGtYRjVHdy9QMnNzN2M1dVhjdkkyanFlU3Uxd2xGOTlZQ1owUzhEVUNWWHdQU1Rla3pTL0Q2CkhaaE5lT0xHSTlzVmlGMkl5RnMyL2cvaXpYbDRlTGwyV2swRkYzMDNhZjk2ME5LSjJITFhEbUE0MHRITzVtQnAKUXZxQUMzQmVRTFR5NWIzckVWcHVJaE0wOUJsbW8xdEFCRjBDZ2NFQTg5T0pZcHRBYmZFSkduUkVTeHZTbTdCQgp5c2FDTEdrUEpSYnlmUjJOOEF6NTBUd05Ra1RiYlpLb0pLcksxT0FtV1JHOTF1cXJVVUJ4L0lVVThIMVh1RmIvClJKamxQZ2tCNnRyRFczTk4xeGp6Y1BUY3FOblBDcndvMUtlUUw4ZlpDYVArNkMxMTZ6YllVa1hQQ2g4TFhFbk8KUHBmbnQrclpHSytsMjhxMk9KMkRqM0RxaEN2dndPSnpIQUlGSkFwVTZXcStuOEhBM1RmVHhCc2UxYW1TQkROSQp2M0RRTytLSEJpRHJpMzFmeWxBZlFEUnNUQk5zZldzejRWU2U5MDlaQW9IQUlhMDREOEpzZVA3MDJRV0tDU3k0CjlMOVo1RDk4WTVPQURUQXhXWHNlRWEvZmExZkk4VHpwa3JnVU1STFVLaVBUSVpjd00wekRxSHZIa0F2RmpYRTMKMmlxRmtCVjlkUmYyeEJwb25HVHMxTzZkUnJ6SVc3QllIcVRlRTJrWFR0ZWJSeXpuYVdhWFU5MDk1VnNzOVZzSgorMjRhRDZMd2pIQms3c0VlNm4wakwrSitlTDZSU3czQXdUdmM1bUl4bThMdHN6VjNVSmFSTnlCY3ovV2dVMDcyCml0anZYYkRzcjRzMVEwUlBQYVZIT2R1Nkx3VkRtTCsyUkNFSkhNSjNXR1ZCQW9IQkFNQnVwQkFSclhEWGViTEsKTGhnRkZsdlBhSzFycTlMMiszL3ZNMlB4VGxNMU9uaWE1Mi8wdmlVbFNOVGZnb010Z0xadEhTR2dSYU16dElKeQpXY3RQY1VySVJtRFNOcUtXSTFCQ1pVb29uemR5dHJiZ1djSmRYRjBCa1V2OER2eld3Z0VzMEFKWDFxZlR1amg4ClplRjhETkJDWTZiYzVvRXR0VGNaY1ZJZEUyRnRWeVovSEdkQjhjK09LUURpeTBIZGNaUmlyWjJTSWUrMW5zazQKQ2tiZ3RKL2lCYmtwaFA2dVVwaFFwUFdLOW0zS2ZFK0UwQy9lYUpJM2FGT2ZJSExZeVFLQndRRGF5SFQzZWExaAo0WWtaRWF3OHl3cCtrTDRDSVliYnhqc3l3Ly9tOTVJSXJUdHhkeDVHTUZPOUpuVUp2NUhQSEVJdjhvb0dyL2s2Cmo4NkdxUkVDWC9UVHpJYlppcFRGZzJQd3h5OWY1eThlMlZOTHZqa0JSd1Y1SW1jbEhEZCtrWTI2ZlI4VmkwQlAKNkpNN1lkUExIMXpJZmRGQkxuRitOcE5pbE5zdk5GT0Q0NEdiOVZVemd0b0FlUzJRWjhpcUZxcWRZbSs3STlPRgpDYjl0ekEvY1Y4ekFUdmZwZFk0REJnMXNzeFlZSXlYZngwSHpoNHRuR2xZczJpQXl0aFk4dzdzPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=")),
13
+ "okon.info" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlHNHdJQkFBS0NBWUVBcytORWZGbkd5MFFWZFhqVm1IbDZvUXZ4clM5M1ZrdjFkcVYwMXBpQnJBYTRjaThLCk1IVzYxNHZ5MEhsdy8rVjZEN3RwZmxwMm9WbkRjY1lkeitVSVBUL21SbWEyMHFrajVBbTNEdEJyVDhkMUdabWkKaG9nU28waTRCVzBad0ZYdCtWQnRMYjZLbWtyZkN4RytUcUdsN1hsQmFYZzNqVkVSNWMrcFdoeDlqZkNtREpULwpVZE9FRUQwVFp5WU9VbmIyZWFiMzE1Ym9XOFR6Q0dNVUJta3JGd2ZtOGp0M0t1c0FhRkF1MWpZb0NUSEU0TnZqCi9BeFREdnVvN0NIWlVnUStrMWhUM2tQK1ZXQWVYZ29hczVpdHZaKzJsRWcrRU04clBoQnJzbTU4cGpJWG9hL3UKY0pNU0M5NnhGUytNcm1GVHU5ZFVaS0c5ZVhKZHRoWjVWR3Z5cTk3WFJoQkJENk9KcVUyUy85QzRrZi9FYlZ1VgpFWlYzWnpSNEF3dXo2NjBsWmpnOHIxRmYwT2t5cDJiUlRSZVlzRG9adUxTMEVPWWEveXdhNWQyb3FhRSsvd2xWCjZBbU13L3RaTE8rS29zVWpDbWdxTnkwNGtrWFNqUitUOUpRMzYzRmd0cEJzYlNGRzJiVFhndFRkSWRUdE9YMlMKN1NYZzBCdlF4OGdkM2gxSkFnTUJBQUVDZ2dHQkFKelFrd1JBRXY5aGM3OTdQZUIwamNWVXJ6TEZQU2Y2Z1pvVApkSDRhWm5nN1I1RXFscHhXRlRJUDZ2VjMyRjBMZzlPeEVhNjNWOUVpZWpGMWZzbWJwQW9ZNlRvWUVtb2tUb2hkCk83cHJVQkUxRkV6ajFIMm9vMjY2VnNsTUtYVzBzd1p5NlNwR2YzY3ZxNjV2b2xIVHN0MElwbEEySDE5TysrWTEKbURGWXdzNzRxUmd3eno1YXdEYVR0NVZrNUhsWkFWWmRpcExqRjkzZ0cra0V6aEVrbnc2dHNkTWFxYkM5T2hhMQpSNDh2bU1rakhMalVsN05DWHltaGtzbCtYSCtWK0N5T3Y1WFRpZ0c2VFJoNWN1YjloSitYWFhlZU01MGdOWmhPCnRkcWtLZzAyczBhekh3WUs0eU54bVZOcUdLMjA3T3BCZlpCRnYvV2hqaHRoV3BUSWhHUDl1alV3VG12Z3FaaXQKVmRzYzk4VG9ETUhrSGZLU2VqRUhkYlhWM1drMksxYStNYy9xenBTWU9FWUhoQU0xWmo2Sy9GWGc3R1VZWFhOSwprWkF2dW4wSXJYbWxCVGFsRU1MZTVGaytoNlNoRnBsTFYwUU01am1ySHNFd09QUU00bG5GUWl1czlBRjFqMlhVCm1ZWGsrMWVnanh5Uk1zcDVtZTJOd0hzWFc1OGE3UUtCd1FEZGdFWEJBVEZrM3IvWE9tUE5uS0NIZlJWdkREZGwKRVNDVlIwM2VKVU0ycFhmRHpIY25rRlFCN1dSb0dkRGlIbHhzSldsRklnM1U5UTU4LzlJcmU0Um9Ba05zd3FucQpGZVF1OVZ5SUxMandicnM5ZzA2Qk1XelFTanBHZHZwbmVUY3NGeitOM2JvWnR3ejA1akhSZjlKc0Z0bmVhbEVsCms2VC8zejd4NjNWOFFYZU5VbnFnd3FRYzFqdUh5ZzdnQU1hdzN1eXFkQkl4NTJ0eFV6OGxKWjVsMStJWExxZTQKeWxmbTV6ZnlxVmc0blo1Yjc1U1FMQkZyNVhTMm1mZ2NkS3NDZ2NFQXorZklWYTRGWHdEeVU1d2VqSjF5alRzcwovYWtQT2x5M2szbzRWanVnU09wMUdudDh1cEpCdWpidUFlZ3hoOTNQamtHRDMrQ0xlczlmMTlMSHpxOFJEaE9WCnBEOGZqTU1lcEFiSWJWbEZSMTNCOUMxbFp6YXZ1MTV5UDFHSERVV2ErZXg0UlVZbFR1dks4c2hGMmh0MlpmamYKc25ERTBZa1NjaXhhN1BhbnlSbGhvdEt6b2ZYMnBhcjI5TVhwQW5nWlNWUXZCNXlzSUFzZ2xQUWlmMGNqaW1ERgpVcStONUZHZnh4TG12OTRDOEZ2dGFlTVVrNTFSUkJwNnFMd29qTzNiQW9IQWV5RUdKWThUTlI1NkNCdkdSUk1QClRhSGoyMUl3TFBlRFpGZzUyZ1pld2E3ano2MEdnN0RBY2ozVHRlYTc3aWF3ZTlHa0hqRWEvVW1vWHlZYVg5K08Kci82cUduaHYyZGVIZSs4YkcvdTRacmMzVUsrQVlXTG5PVFk2Qk5lNHhQSm1FQlZ3Vjkxc3lVU3ZhQ2ZhZzdvSgpiOXFZREFLUHoxS3V3eW9IcEpXZVBvOXA5TjVubXB2NGZLcytkbktGS0ZKbUlRWWJDM253YjF0VXA2OStCNWxNCjN4Sk45Vk1UR3k4b0JBeCtWbDk2MGlZVVZNanVqZUpoWU5neHRCd05CMHgzQW9IQUdOQmpPc2F3WVd4dGY4a3MKWkVBT2dnakVEK3B6cE5XWUc0UUU0Vlh3aFlObVFxam1kQ1lzcmhzTVFUSURaMkh6K2RpYjhzYU1IelpORENkZQpMYTc3YkNDdVJaSTdJOFBPRG1tNDFrUkhYb00wT1A2S0VjMlhIOWZmN3VxK0libGpDOTFMWllrL3ZyR3A0VnhCCjZneEpEMWFxN0ZORlNuVC92SnpLcFdtekVPOTBsY2hzSkRLRkk3VEtFT0RtTktNODhXR1kwMkhCc1hsaWhDUzMKVVZXZVNrL09mVlh5cTRPS2ZHb25IRk5WS25idVdTci9NN2NkRWZIUEhnQ0hIbnJ0QW9IQUxsSjdnbFBic3JEdQpnV1N4c3RoWFRnWmFzRFJPNi92M0ZsVTAwRjZ0cnJBM1YxMkZxQzduYmxLL3F2b1BBK3RxT2gyR2h4Szk4Tkw2ClZldWpSdjhVVDZZZ2tuQUF6WDZDWEl2SkpSUERYaGY0b1FNbFVZZk1BZTRYWjV0UTBuWVI3eFlpYkFDYXZ5ZEoKVDFVOEtEZ2E2YVYvd0NBeGx6Q29TNXpNQ2t1RjdvT1czTzZIdUEvQzdnazFrVHhrd29KUEhuQ2lWNTIxeld1SQo2dURvem9IVkwxRS9MUmlidDFiS25sQktCSGVReGZxekxhcFBYWmVqNGlDdi9acll6S0RTCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==")),
14
+ "ebert.biz" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlHNHdJQkFBS0NBWUVBeVAycEt2eVRORjVXMHdaa0RtNUVVYkhTekRJaklwTEJlSld0V1ptaVFhTDR5QzkyCnFDMWd3Zm1XbERIQjRYbUhLNWhTZVVRN1RpZGdBVUxsTG5RRkRmbnVRbjlvdkhzeDNGaUlUZWJtRmNRdDh0NEQKU1NmTVN0ekNWRFhJaDlwblJoYndhdTB6UTFWK1RQT0NwMDFiVGYrSHE4bWY0Q3h5Ti9veTh2UHRTcFJXUGZ4cApuY0ZoL2ZqM3J5WEk2MEZZSGYvZWxZRGRQTVZvUWMyQXM2Zkt4dVlxUnRpQndERDJDMDZISlp0MTBQNGM5dDFMCk93cURMa1NxZ3BkRGV2VE5VelE0UTlTWWxLMkYxbURMR3dpTVpLUWZhRXRvYUNwMUs1RklpZkcya0RmUkNkSTcKMzczM01PWjVqR2JmcFpiaGFiYnRqMDhna3pYZGM5Z2czTzFIM0xuMUppazN2RCtia2N3a0lrdTVKMm15MXoxWgo4RUY0SHZoQjg0aWpKZStiRFF0eUVNb1Rqdm1ZWC9jSVoxU1VMTmV2UjVCOWJGVVI3ZldSb3grM1NNL0lEZ0ZYCnZlTnNZQ1dSdzR0SWVYOEJ1WnFpNVZ4YnIrbVR0TVR2YnhmMEdwcEtKY1hENHg3aXczL1ROKzUySGN0bDVtVTAKWk5WNmNsQkZwQWwrTDdLL0FnTUJBQUVDZ2dHQU9XZXBqMnVBSjY3aUlYZHIwR3RSKy90TDk2SkNRcmVqcG1zcApqYlBCa2ZtWUVLVHR3TzdrK2NIdGJmb2dJK1B2NVZXbUNKaWlUNW9UWTRqVnFFVGV4TFVqaGI2YURXc3FQSUxVCnUxczlUKzR1S1hXYmZxTnRSOXh4YkZmSUpIVU9sZ2dyTm43MDYwQlp5R1NzWmxoRHdhMC85S0tybFAxY3lmd2QKM1NJcUhlanNFTndzMWkvTGF4eFdzYUdiRndZY3dzUzNyLytVTUswNUw4SWdCaS9nVEpxa2JJT2QyMlNnZ1c0MgpUMWx0ZHZsOUVFejRGYTdVOUx3TDd5eUF5M2RyUTJVeG9lTU9vb00vcE1tL1NmdkQrVFdRZWVjQy9pcEpxVEhiCmlOK2tEbVY0d1ZzdGNqRUNxcmF2ajBqRUY5L0NTbnVDeEZvSXJnWmc3SkQwRXhkU0RaRHFCU01QZGo0ZVJOeDkKazN5aytnU3hMSlBPU00rOHZLZ0lIRGxaZU01dUhEUmZIdlNXWWd3cUdST05yNHFUZ3M4TUhMUkh0YTNZRUx2TgpTV0E5RVgzSHZEOXFETHh0QzRwNURaeTd2Q3pZS2oxOFZNdFJTR1RyamVCT2xlZjU3TE1zUFlyRnEvVVNrUHQvCkhqZTJVbC83T1VhL1ZzQUZ0T3oxUmlqcG9xR3hBb0hCQVBWYnFsdlBoaVBHVVpnK3IvZGpCWTdtVU5UMHpWckwKZWdaRVBvMlZLWC9DanZ6SEhCTjZQYzlFTFJ1bk9CQllkQzlVS1dRZUpNM3ZEVkF5SFZPNjM3cUtzS3FxNWZ2OQpRQXZYdHBsdnY1NGloMVlUK0wvYXVlUm16MXFPdHVNMUl3c0FvSmlpQjdPWUptRCt5Y0JQRTdMSEczcVoxTzk3Cm9mN2dNVFJiYUJWYWQ1dWVDZGgrNjFvWlc5UDRzaDlrdWwxUGo4STJNbTRPaXFCQ2E5Snlsa0RLQmhVTldCMG8KTmFOZTNINXY1NEZQQW14dENMcXVXR0U3MFV6N3cvUVR3d0tCd1FEUnRWMGwyemJhcmgyUTk2NE95ZlpDVnk1MAowYWY1YWJTM3lPS1BRcUo3UUlGSDA2UUJXZVZlalVtWW1SaE1JV0FLV3lVY1lkbU0vdzFlMThMRE9MeXlyZDFPCkMzUlZ6a05OV2E4WjFEbzN4UUdpOVB4M01RYjd1aHlSV2lEMzZ4Y3JIdEtnbmJPK1BlbExXRk9lRXNmOHBGcmEKdDRWZDFqYUo1OEJEUVEvYXRsNy9tUGV0SHNpR2xuSkdzaWw1SkFvTC9SVWhXVHFUYTZkQTd2MjkzMktuNFBCRgpzS0xUVzBaVXMzZUpFVXpvUnlNWXlJbzdhR3RWV0VLN3J4MXpJVlVDZ2NCUlRlZmM5cDYzdWg4TnVUQXNaU2JSClhLYktlcmlWN3JsbjNETnlUVXhzSnJlbE1nR3V2cUkrelpPNUJ5ZC8yeC9kRXlHSUtLai9pTWk3bTIrMmNFVjEKRmtKR3U4enNQTlo5VmlVUElVVzVEQzRXcXhXUjFkUWx5Si9MbldFalYxZGViUDNLdGw2Zzk3azRDUlluNE14aApRTE50WkE1NHNWcFVFRXlkMGZCaXF4RFpnM3cxdnBFVTBUUnB3STZkOG80REg3cytteUVJOFU2a25uNEdSYXhlCm1kTjhKR2pmZUpTVnAzaWZlVXVZd09ySHJUWG9UcC9BME1haG9RZ2xHR3NDZ2NFQWpLSWgyM2ROTEEzRUNpbnYKY2orQ2hDN1BHc3hXNTI0NklWMzRnYlpnSEdPL3p4bGhUUDZxVVdSU3pLRXVxQzlocHRCRTdPbTU2VkpKOXlhZQptQ2orY3AwaVltcFFGQk1GRTJSbmh0ektSZ2c1OXJ4d2FzWllOb3d6Q0U3MitRdVJOL3V0cU1UWmUyVXRoTGV4Cks3clFudDdoaHZlMzJwd0RheXdkeFh4bVUzZ2w4U2IyUWdUNDJUa2ZYY1d4Qm1waXluTm9yanFXaTdLUU1YMlUKY0xiVUJGMVhCSVZXVElOVGdRM3h4ZHZ5UlhzZlVFbDhXaWJHaEM5eVlVY29hc01aQW9IQVJydVRaRE5yY1F2Swp3YnRJMWpxYTNLQ3paTjVGSkZyNEFLek91eVhKZmMyVnMyR2NKQVFqYTdUZlA3dHdiYUxRdWEyVTFBcEtwampmClh5ZnVib2pzSFhvQ0pWYk4wQktVazR2c3BKU0pVdzJZZkRqNjJ3NVNCbCthMHJYYjhOWS9MdTU2QWZqUEpwYWgKdm9yUGs5SXlhQUx0cjNETjZtS21HZVBWK0wrZVVDRXVCb3VhTTNmYnB4OXZCOGRnQW9sR1hGeG9ua1dmWFBxcgovSmxjWXF4WjJ5YUZNem5jR2JWTTJYaW04SkVkTlZEZXFMMWVOZDd0bHlyRzR2eWFPSUhICi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==")),
15
+ "olsonjacobi.name" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBOWZpZXBwUHpCQkl3cUR3bU56RDZqZFVxL2lSSitoZ3F4WHF1akZxZ25NQnF4aHFuCmR4elJhbEdGT2tXeXdLTTJPS0RuSHY5akNReGVOY0xUcHdQb0pEaEwrLzlWcE9mTXliQmZPazJhVk81TUJRakUKUnROM05WeUNobnpKZVl2cjczTk51NU01aTJ2emw4elh6RUFLS2R5bm0zVXFDbE1tbGtyRUM2VXFWdFdJR3dPLwpMb2NBQXFxa2F5TGJBVzFMSlkxdE94VklzYkJDSHNRYXlhRUFMYTJBVE8wTlJvRSs1ZXRoRkQ4RG10UEZwckFNCnNrWkxicm1iQlQ5bC9pWkNRUDU0N0FFUkxOcG5haC95VlRXZ2pFdHVWK1dHeklKZTN3c0JkVHZ1TkxpaFh4U20KVm9uUXo4TXpvbVBoOU5INUF4SnRyaCtjeHpZWWRocGFZTkdZYndJREFRQUJBb0lCQVFDSTl0ajQ3dGRhUS9xKwpJMGd3WVdDVFM4ajEzU1VvVXY2MkdodEo0a2tmSC9JVXY5RFNmY1NLakR4QWQ0RVN6WThxdDBZYk42Qmc4SGNoClBveDJxckZBUWV6bHRJZHZIUGdtc3NSRUJlUlRPS0l5QjNDcjg2S2tudys3Ylk4TzFJQWJSTHhiSDU3aWFNa3EKbFJEeEZoUFN2YURDNnRudkIrQXJ2aFF1VzlrWW9oVUJOcmtJdk90aHhmMDJTaDBCcXF4ckQxSUVPaVpFUmJ2Lwp2cUcydUdKYy9reEVKcUNqK3IwQUlIYXdKSDJ5Sk9iMnVoWHB2L2czbjJjdjFhc3l3WGV6ZmlmbUYxbmtkZVV0CmxQclIrU0ppVnk2M3ZoTDZGa0dxMG05REdEeFZLN201MGluOGVlZTZ0L2JvV2ZXTVovaURyR1B5TjdHMFBZRDQKK2dkTUFPWjVBb0dCQVB5WUxyV1ZJczh2SEUwVk13aXRoQWQxN3Z1RGJKV1Y4OER1WThGWXlsTWZQSHBEejNRYgpLOElWbmhaMlBtNEQ2WFhZWG9Lc1BDVExWRE5hTDg1VzBUaVFhT09rWE1Hc0xDM2xjZlRTQ2NqbkFESDVtTWlHCm1nUUpYdTVFbmFIajJVbERnUE9kYkRlcjUvem9xdjVzKys0NDdrRUo2N2NWSGZ0MVB5aVdhNFNkQW9HQkFQbEoKazhueEFuVFBzOENpOWE4Z3VZTXNqVHFQc1cvYUp4YmRtQ3dBZWxtMVFTM2RKekI0OWJLQU1hSG9mSnlIcXRwWApkSXYzeVRFZFZaTEdJOVpyMk5HN0ptUlZpejBvM2lQVTJMSHV3UmxVeXRNMDFBdGNPbFdZQjc4NUg4V1RPakpjCjZuUmNwT0hETHlNcVVsWXBMM3dKTlF4WXpydmRqUW5ldFAwWDZCVjdBb0dBWHBNMFdmU2U5ZWZ6dHNETFBPS0sKM3FnL2RKaCtuWHRwcXNFWFJKdFVGYzlLTzVVTUpiTE9yWHFlbUZacGhaT2RZK3hCWnJmS1JSU0VVRDNpVEdXaApMSWFWWHpaNUxHS2tvQUthcWtuQ05DQ1pxQnlHSWY2VHlCTWlJaUE2elJTY2xKdmJ1bHNrMjZ0WHp3L21oaUNVCksxdlJpVFNIdHlNRytOR3JkaXpyME5rQ2dZRUFoeW03dGd5MU5qeS95NzBQMVFxN1MwSkd1Ty9jVnpkRFpvUnoKMDdmV252bEdBK1liTDQ4R09PaUZBTEtiamd1Sk9hV2RqWjZtT2JrY0F0N240NFRLSkwyQ2pYaE1iTGJSNnorbQoxcU1May82RGtvemNRK1NYeEgwUUJrQ2sxMjJDYW5neXJ6RGtQWHlrL0Q0Z01wTldLYnljUkx1S0xCWnVPR0hHClpFd29EQzBDZ1lFQW1KYVRXSUtMeTAwLzkwVWJSN1N5QUlEQ1dLKzYzM3hnQ1hUOE1nOHNxZjRjWXA2dEJvVHAKWWdYVWtOcUtwRGowQUcvZUxQZ29vaDQrS0Q0K0pEbU5acWlTdkQ2SnU2dnhHcUlpOFZ1NlN5Wm9sWmYzT1cwSgoxQWZwODBUSDRlc29YTFJkMk1FMnQrMURwMlJCRDNoMmd4K09POW5UR3pYS2ptR3M4K3hsQnlrPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=")),
16
+ "okunevabednar.io" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdXpzSUI4Sk96aXVRa01TUWxtODJBRm9SVExwRWJiekVxL3hBbEJiSjJvb0RqaEZtCm1FaUZLSWQxZERmNjdreWZsYUtoeGhSaTZoQ0VtRHJVQ0FheG1DWjdiOXRPZS9YaTdBWm8vYlRJVHlEY2Q1b2kKbmk5RXBPZEpDM1RscGZoeXN2TDlpV1dzTkNmSjI1aFZjUWQxN081RVFRWmJGc2N4MklwYUkzdU9kblVISmYyUQpNa24vbmpSUGNqeVZ0bE80RnFCbWw5bjMrRXdTTHBiQ0RnZk0wSmxidTE4WE9kNnZvYmFaaW92WjFpMCtwMHkwCkgwYTBzWHp6RmhPMXRIaFNreWtZS3plSFJGTXUzTzh1MFFGQVgyV0tpQjVNemVxQk5FQTFZRzNDRXdmZjByMEQKREIrWVZMMzZaaE5YenBKT1oyTEtRQkFXU3ZyY1Nyc3ppZ0RjUFFJREFRQUJBb0lCQUE5MHBnc043VGR6dlRGVwpLS0ZpZU5DNm5xYjQwV0ZGcmU2TW1rQWZTWFp5NGl3K0gzditzSTlSNzA0eXVOSW5IUjFiR1lPaWR5L2ZRVExYCjJGejVRSHZRNFd1d2JPQXF3aHE5eExqOHpYUkt3Q2hYWHZnejZyUzZLdnQ1SU9QOGlHdGhSN0NwNWZkQU9aZVoKRWFTTSt4MGQ1aUNBQjlEdmpKdlZmKzloNmJhNWVqYnJMUkNoMzZZM0UyZEYweTFraFdCdFhKNUxpQmdkMytqKwo4d1dqelZQMnZiZVBsYU5XM3NRdG8xdG1QSjlnSUtyTkorSE5aejdyMVVFcDhYNUhwNlpkUGMxN1ZRaHZRUVkwCmZvd0M0SWlzWC9RZlBZUlZPdmxpQ1h5dkpKUk43RTFwN1U1SWNhcmpGY0p5TmxGTDFUQmRreXo0dE11QkhFZXQKSG8xNzJBRUNnWUVBNWhFelBTVDhsNzhZazZtNFNQdHBNY3pqUHBZVVAvckk2SDdaWnZZNFVJQ21CUVBINitkVQpDVk5IaHB4OGVyV2tENXlKdzI0NlNyTFl1NmNBL2ZnaFpWc3R3b0FKakRuZlRwdnFOR0QzbjFKcWU2alkzMzJrClJhTXVIUWJ4US9JakV5N251N1JKQlkzaDc5bEpBMEJ0NXVVeWNQRHB6bXNQeVFyMW5ra1FpejBDZ1lFQTBGVzkKN1dzTHRtc0x3ZXNZeEpTeUxYaWxLYm9IU1JiZ2tpQ2FuMHpkUWRZTk1NQ2RRNW5nTXVRaWNCbk5nRjBxRWFqcApuWTEvY2phRnUvb0t2S1Y2SXNpa3UwYi9USWxaeC9VTDhpNFo0WjI4ZU1BVUN0QmhoWXU1Rjc5aUg0UGM2bU5mCmF4dGdGa05HWXE1N3ZrcDN3a0l0SngvVjJTTHRjNDRSTUc4YXBRRUNnWUJ1aHlManBDcEp5TC9JNlFlazRFdTQKWmlOaVJQMnpnd3NVVHlTb3gyOWtsWG1zL1JVRjYxdS9JeWhBcmx0TEpJcU9DWGxSejFubjJ5WXVlTndNSnpIOApIS0xPUjI3TzFGckl6RFRuTnhLZmt4dWZEdzRweUpXcjh1cExmYk5aSGpIbG5Hb3VEajNxa2pCU1owUWhjTW1iCjNNNnYzYjJsc2wraUNVYlk1V2N5VFFLQmdRQ0VJOTR6bUpIMVFqQlM3eXJtaE9uK3JXY1U4RWx1c25QK08yL1gKV29sOEdLaUZJNmFjR2gxNktma3Q0Uy9YRzBCenN3OTZQeVYyNjk0blBKRlMxaUtCcllIT3gxbG0wamQvL0kxMwpMb1o3OC9CM0psMlAvbHZjdUtMTnpUWVBoek8zOXcrdWY4NlNVRmwwZmZjKzJ6emFtZDdKMGdkeGtoWEtGWElSCnBtYmdBUUtCZ1FEWGllcjZScHFDczdXNUNma1p5VmhabDNEQThhUVVzOHEyYUIvd1B6NzU2Tm0yZVB4R0VaMHgKbSsxcUVFbmhZWEdybVNyMzl4RHBYd3Y0VVNCOFNhMnFENzVaMVh0OFRFNnZBZDRMNGdwVGt4QS9MOHNMLzlnKwo5SjhYcmxaWWQ5ZUMwWjQvd0duQjNmbkkxckFPTGFnaG9ES2VUKzdXakVnMnprV25GUmtNM2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=")),
17
+ "gerhold.co" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNTRGWm15UEE1NlVneFIySlJ6WXJ5d2ZrUHV6ODc4NGp6cWNKbTduemJGLzdqaGVpCmFHeVI2WXFZRmExU2NyaGxzMXVSNGMxNFdJL2YvY2NTWVN4U0ZkNkVNV0NwQm5HRERGZ0pYcjhLRk1nNkNYRnEKM2lsbEVncm44V0VrOGFDTEtzQmpzd1BQZFNySHdjZGw3MmYrMHNhN1d0YzNUNVJkaTdGZCtIcGR6UW9UWHVwYgpaVXN5RU1qaG9QQjh6SDFiS1A1OE00UXhaU0oxdThaWkk0SUJiQ0Z0QVNNNlZ5alZESTAzSGV2cXAxZXFPanJwCkZJYWhWQStWdFd3ZGE2TzFuR3RjWXNKUWhGMXJ3OWgxNHFtU0tTbm1LQWZWWW83VWpzd01kRXhMMjhHZThvaVUKd0liSWdhSUpYcVRldE5ZUVZDWTBLNTl6Q21VS0FmVktwb2RqZ3dJREFRQUJBb0lCQUYzY1FNTTRwTDZHWVpucApscjNyaGFma2hETExET1lCTXQxWE5mc1FVbFJQT2dOcks2cWcwaXZZeUQ2SnJoTGJGa2k0eUpXL0k1cnNna2szCkRBbWYyWXdLVXBoZWMwa3NmcEJqcFREbnphT05acEpyaklPVVR1a1l5TjlCbnFQa2ptZi81cXd1MEU3VjBIV04KYlpPNkcwUEQxVFJJYTZGMUt0UTNUajB2QjViWVNJaU5LcS9WMEtWbWtucjU1Q1pxUFBkVDl5dytITjNrNG12MQpzQmpTQTNxZGFDMG56UkgwTEUvZ1AwQXFYNWswWkhiYzdKNFdyRXd0dUlia2dRQlRzNU9PaERmY2t2NFlycDloCnhHbUxYQzJYaC9nUzhGc2ZpN1F2b21acS9mbXJqSG9qTlNSR2hPNGNIT3ZuU1FyUXAwemVEaXJlUjB0MDlWaDMKNnM2NmJPRUNnWUVBODVCV0RIeWs0RmQzQUxVeGN5VHptRmRQRk51S2lPZnFWanlVZHM5VjZKeURHQzIxald3RQpCcXFISnRrUHdWUlVmUHI5ckJ6VzhSaEhaV29DSlI2K0pUVFJRQkJuNmFLditRaFlOOVJReWR0VGFFaktSWkZoCjl0cjJhYm5DeVBkZXJzNm45VENHdjlJVkZsZnF4YzZmMmIwdmVDWnFKN0MyOFFqelhwQnpidEVDZ1lFQTgxTmwKQ2V6ZjJpcWhQVmwvNXJUNWY3OVhkRjRablcyNE9KMmNNUHNBMkMrWlh4NnNzV2FsbTZ6OEhwVDhFc2lRVmYrdgpsZzduSE1JNDlzeWFnQkFKdmlnZzYyd3g3YkVKSnhUS1V4WFZ4amswRTVGc1NRZ0dtV1ZnZ3lMc2d5Vlk2RFJNCk9xTERDNDkza2VacmxmSC9iaFJyOHI3U3lmcmRKNWdHQVM0Q0NoTUNnWUVBak9hdDhQRldqSFhzNFJyeExYUnQKKzI1ZTBHa2xIb2hUbDJuYVZWVWlsTHVlVnlseVF3cisxVUJuaVVDL0RZK1VoT3pLUFh3OW1DSDhnNTJzK2Y1cgo3Nmc3ZVQxRWIvTnVxN2w1RjJzYkJYdDlKL0ljR2R5OStJbTVUWFpxU2NwWkd2VndVcmFzN1dGQ1U3ZXVtSm9zCi9WQ2xtbk5XcS9sZUM5aXF1Y1VGRWxFQ2dZRUE3MGxHcHFFVWJwYlhvOTVkQWtOY3pQMGRBdW43Sks4ZXFFYU8Kc0RoVzEwTFFBQlBKWGxnRWFuaU9JNEQ5OTNiWFFrdEVvRHdkbVZHQzlXbTJVbFB6VU5aanNVRGdSTkNCb0xZNApWY2EyU000K1lUUDBta2xUUEF6UEFZY1pzY3JMaU9iTlJDaUZ5TnVZaVpsZ21iKzNJc2pnYzRLbkJrdzJxbFk5CktYSFdQWk1DZ1lBaGR3eVdFK3pFWEpnUUFrVGVQdXhhR3V1YlFtKzdDYWpOa0thRkhPZzFFSXNkZllwRlNpc00KUW5RQlhydW02bWxJZjdXaGNBaDN0MjJBMWRwc1dhckZiN1F1cVRaRTlBbVcvTDRaMmNSc1hLd1A4L3Z6bzJoSApQbXVJK3loc083UlRwdENUSkxWVENJWXQ4VkhZNTB6eHdxY2NHNFEzZVVialh5TmFlTVpWd3c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=")),
18
+ "hoegerrenner.info" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlHNGdJQkFBS0NBWUVBbHJOKzFhTjVVSkFaSlFNWVZRUnFUZ1RvM2pTdWEyUHpmSmVoV05jZ1NvS0J5UU5NCnVZYzFGYUp6VUJnNlBQWGhzYUt6b0owZFc1S0UwZEdYUEk2eDhmZW9SdzVrdjd1aFVkMDROK24wK0hMMzViMzgKQ2dBTm1neldHV3RZaWE1cmlLVDlHbFpUZHExMC9YQVlGT3AxNkFscGdqN1Rxb3ZYZ3F1VDRGVUQ1RXIyUUVHTQphK0NvU0RsMFJ1cXpLSDZwV1l1d21yZVZkYmVwTWQ2UUNSUmI2MWRhOXM4dkozcHJxTnpoSk9Zc3UvMlpwTzVnCjNUQ3FQNUtWOG95SFhWSktSSmUwcnRUdThyemRoeHhjVlk0Z0kvaXhQTGpEcXFNZVBqU0xBQnFrSGNab3l5QTAKeW1Mc3kzdnNZY2NQald2TlN4WUowakg4KzRhU1JleS94RU5meFlWV1c3YlJOOFVYSUNMZS9HRUxEVytteVR5RApxR0VEM0l0dE56bWhyY2YxYUI5TDZQSkZ4UWplMlpFeURiM29McG9OdGM0aSs1eXQvbDRkRVZBNDRmai9DRVYvClIyYm1LTlB6ckJZckhBK1c3TkV5eWNRSGhacWdMZEZ3a0gvOWhuMlhHRTIwS2hwK0taaHk4aHVpMFRhSzFNZ2IKajFqQjRFWEFGWGoxYjdMQkFnTUJBQUVDZ2dHQVU3Q00wRUcvZmxEMzFja1pPeVYvajZKRVhCb3ZmcTM4S3dYZAo4WU5PaUhKZmR1MGhMNnI1ZlBGQlRvcVYxUUxMZXFXYlVhZlBCT3FpWGc4aUNOeEp6OUUwSDNuTDAzcDBoUXp3ClNvVGZxUlhYdXpzOWU2UTU2WUlWWi9wb0tkVzJIQ1ZiOWNOWkNJQWRoeDA0RW0xK1d3VFhGaUNqMVlOaGhFeWEKaTZ0S3hQNG9NTmoyRFhhdW5hVmlnSHVZVVBXK1FGOUdEVFhFaDZJZUVQYkRVSGVBOEhvTHB5SzUwaGUzTUFpeAo4NWJyNHQ1YjgrNzUxZnhQbjR4ZFAxeHhWQTc5RzFSVlNDd1Q3bHc1VjJubFBKM2lUd1VQWGY4UmlDODRWOGtUCi9QMVhuWk5WMlpvNjY3ZWtNbnNvN3lFSk90enBaSEdtTXJDeWpTdmNQZ2ZwblVBVUh1ay91MVc2NzlHZ3M2V2wKY2MyRER4ZDQxMFlHSFJQTjN4SG9Pd0pSTzNZM2gzWERMckdYUTk2K3VORTRlSENYR3YwM3NhV25TWnpJSEpZbgpTUnZwNnJOMnhXcFlZT0pDa0NlcXUrMDAzeXI1dlE2ekNUR3dqcGljbDlmbnJYWEdNd0JpeEhwcGFlazYxR3AvCkxaakhMWnE0aXdSYkdGZUdKclkrcHVDZzE2M3RBb0hCQU1pRWJCbmQreSs0bEpXV1hHMVNlaU9RMFhtYTYyeE0KRSttcWFVNnVqNzA0NG4wSVJrZHptM3UzZ0hhV3p3c2haaFh0alovL0lwZXFJTTRWekhRSUhvVWVpaEVRWXpYNApwM29TUzI1dkJxbE9PcnRpUmRwOEVOMEJWUFlUUE1FUnMvT0NZdVk0SEN0bWEzbVBWcXVtK0szc1pFaEV3WEpaCjVxcXM0M3hQdkxoZ0lpMi9lL1l6UnpQeEppc2d0dnZjRzZoZ0FqYjdkQklLVUZEQk1yYlplbjdDU0tPQWRjNFAKUlBDN1BmQ1VDbGlPdEJFbFh1WXpJYXNaTEh3QyticnlGd0tCd1FEQVpsMHgzam9wVFpnOVVjWnZzdHd0ZURZUApwSWlsVDc0UmUyemRQY09RbStmeGlsbFgzTE1NRjE2citZaFJyMmJQcmVaNEtubFdPQ3Q3TTJaaFlCVDY1WXVECkVyVkt4WXZXdVJvTUVqWmlVMzEzQXNFUVZKWEszU1k1ZHc3RWZ1dDN6QmlwajBOSTZiL2ZRL3pzWUZTd1VTSDIKV2VxZVhvUHVwZUZONkpWaHdOeUNnYzdncXhVRXF5RkVSVHZoUEFIZVU5UldrV08xeVhtMGg4bGpCUjBVK3I5RQpkcG14WWN5TEZxWWxhUHA4aWYvOElnbEtYOXMwNlR6Myt4d0N3T2NDZ2NCSUxxTmJqSFZuOEdKTWx4d2VucG9wClEzQ2svZ2ZSckhGZXBHSFVXVEtWUTIwRTVYMm5LdzdGc29Fa0w1WEl3L1VqMzZnaitJeFRYSU1DclFZMG50ZWQKeENpZmkrNnE5eUFTNlpNTjVoblh5TG1MeXd5cVlnOFAvL0s5d3A4VVFYTXVMYm04ZG1adG1Ta0hVWG81d0ptMAp3bXczTjhrTGlTRm9QMlNFMDQ5MEwrY2Q0TmlYQUU2WmZDM3BTSldXaE4zUDl2L1ZHeC9sZnFENjhSRjRrVUZ2CmNERUY2ckI5eFRGa0Y0TnNuMTQ2RXVUdlp5eUtZYzIwOGhMNWNYakV1M0VDZ2NCTEhTdXhObU5ha0xLbzdlNGMKMmFWZ0V4aDREdkpTSjhtNnBZY2c4T1lTNU9zdXY2YVZ5TklXSEdHWG5ubjcraENYYi9zVVd2QzRHb0hQUFlmdwo2RVFJbCtsWnFNb2lnUEZSU1Q3RUM3QXp2d2l5bDk2cjgzbnZrMXREQUJwQjJKTXhWL3NnNTQrTFBjYnM4V3dqCkZKQzdyVkVuRG4rc2lKWFZhK21FTXhOdThJNm1YT3RaaHpGVGUwUW5sU2dGalJubHBMQzNnMWQ4TjBaT2x0eW0KemU1R3JJWlR3a0hLb0xYc2IxRTZOYnZsTnpNN1NrWjZST1lkeUJsSGJ6L0dTMjBDZ2NBZkcxK204cHpNRUZ5cQpUemFtb2VSUzluanVjVUErSFdaa2NKUlBxTThwU3pBQVN0Y1I5cTNrcDg4U1RlM3dKQm9qd1QzeXp2Yk5ySno0Cnd4YUV0bmw4b05La2N3SFBya1JCUGxQRXZxTHA1RnF2WDF3akNFVVhlbENPaDBGelV5RThxZkQ2V0J5U3NaYVMKVmxxQkpXWTRBeFNjWW9wd2MvbG5QWksyK3VyQWxNcDFPRGtpYmpveGs0V29FSitEQURQdGt3eUZYc0hqbWVDTgpvVnE0dUIrU0FBRVprc291aWU2eitPRitnelJOc2ZaUEdseGJKUW5uVTg2NTY5YnpnWHc9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==")),
19
+ "rice.com" => OpenSSL::PKey.read(Base64.urlsafe_decode64("LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBelNSeHpxZkhpVFg1bzl5N0JBdE1NM0lxcmtrSWNMZmhia3FHS0V3VFJXYkMyam5ZCmRDaXQ5SC9BV25zYTdpcnlOQ0hwS1lhUVozMkJ4MVVycnQrVk9kMm1YSDEwZHJ5VUtQcTZDdk1rSnBqYitNTncKTXlmd0dxKzdNdmMrUDBWcXp3dE5oNnplVThubVRzMTY2eWd0SVdzREEydWprc1R3ZHY3bEVFK0xMY1djbC90Ugp2dCtKcWtqeVNDYm1UOWl2bXUyeWh4UGFWbmU1TGxLQ2JnOWVJZEZTWEV1R2JFSnBpVGNhZ0lsSUh3VmJ6VnpSCllCbzlobjRXbHhXSmVUSkNQYmxIN0U0cmtrNUdJUDJqUnlvYmdUb2pTNERHS1hqc1ZwamJtc0l1Vk15OE5qQjUKWTJVcU54MVRaWFcvZTV2Nk9jUk9mOFFXSmxhQW1jNTd3S09La3Y0QXdoZ3QzWnluTXpnaVZaQ0Q2MzFDMmszYgpIUitTeHNneXFocHFZSEhUMThxM1hlVnI3d2lsR1Q1WnBremlIbk9SbjlFRjIxa21jczc2MWNBS3hIU3lENkNwCmdTVERObkc0Sm5sWjVnM1BpSk9YR1IvajgvZzNmZ3YydnAzL05nVm55Q2IrTUhCT0ZSaStXOXM1ZytMRU9XdTEKNmp1b2lWOHpaNlk5UmZFdHhVZjRzZ0ErNFJjUGZxQkJ5U2JPRW9neDJ5dDB3aVVJeUwyTEpZMmpBQUNxTVpSWApVUnB0bEtsVjAzbWZuL0I2aHkzNWR5VmN4ME9WRlpXK01tZUgzaHNHSWQwZE92UWdQaHpHOVBkcTVCUWVoQkp5ClpvdlU4RE54U0p5dnA1N0tqYlJWY2VIeklzUUw0MGZXdW81VlU0bHdnLzZueDdmYWlsdkRKSS9hZXQwQ0F3RUEKQVFLQ0FnQU1YRzdUSWY3L0FKYWJUaGlpeEwrQnRoWm1UQlpMSEhsaitPK2VpLzc1UnBqbEoya29qcTcwdGFIMAprY2hzbzMvV3JsaHJYU1ZrWndhajZUanBuNlZSU0U3VzhlUkxwMDlTTE5GN0NXMmJPY2kvYzU5V0pjanRBcnZICjlXZjF6Z3dDajg3TEp4cDZlQWI5cHBvS2czQTh2RU1CT01JeGZOWjBoU1Z1Vnl5dXhHS01NZU9hR2NRazA2SnQKd0pKT0syTmhkWU0xYW5mVWtBQkRqMHMyc0l4ZWcwdHdMa2phU3lJcTEzd3NWSmxZN1N5NzhpVFhvcDBrZG9LTAo5Z3REbDBpd2lYS1JCYURRZnhEd3VmZlZ1TzdSV1p4NDF6aVpsU1RBanhOa2Z1RGwwVFJpRzRlaytwcVJtWjNGCjFsT0Vja0NnckhpQ2NHRlpUQXNSdVlSeGRpbEtXSSthRU15L2lxUzFja0dPR0NuNUVMSmNpT21HV0hTQTF2eFgKZ1FMaVZ1Nkh1b0ZpcW1Jd3NWeHF5MFczN1ZDUEpVWFRXVmhCOGRXbjJscklyd3U0WWhhZzEwcXJ3cnJIQzFIWAo3a0hyU3JJRGdCSzBNdlQyTFh1TUU2eFcxT2k2N2k2RG05UHNrVXRIanVUNEpWTk94YUdRVUVBQm5YSkd5Y1MzCkRFVUFoR25qRmdpY29vM2JoQXZrdmh4WXNFdUdVRDZiVlZ1UTN0MytnbW0rWWVoSlUxdms4bldLYVdhOG14WEYKQWYrZmJIZ2c4TWxaVnJjOWpQQmZJZDQxdEZlUFpQbXpaaWFHRThnbHJ1d2xzQkhHa2F5WG4xYWtqM3JiV2NscgpLSWlRemJ4UjczTmZVTHNhZ2ZtL3lJWjBQeDRjWHgydG9zYUZiU3lkbGZLVDhaNGNEUUtDQVFFQTVyUVRwT3QzCjQ3VkZZUkw1ZTNaZTc3Rjc1MG9WN0pob3Q5YkZjenJFQTl6cVRFTC9Ua2pGbVE3RGR4aFgwTVo5YVZnMklMY3EKaThOdnpZWWhiMDlwd1ZsUWpzQmtocXA5WjlYWWU5bkNJRHBCMFhQWnRUM2pqbUtFcUYwZC8yT0FHeW16bWhVcwp6ajhnNDRsbFV5K1dvc1p2L2NicmF1RHZaUDBqbWd6cTRWSDlRK21VR2RKZlRDNXpBYldnZ3RodjhyWXd4YUVyCnhxOE9DNVp0V1BWK3BwN1dKV3ZSRWs4WWtTcFZVWjJVV2R0V2VaUk5hNWIxUHBUS3J4VGRxNkUvb3V4d1UwOTMKYWIwWDVlMGs3bnErVyttK1Y5Uks0NEUyL0x1c3JUWUlkRVRuWUpsRVo0aU9EUEJGNit5QlBtMVBKbGtSN3RjQwpNelhsUlNXRllUQjA0d0tDQVFFQTQ2TGNNUlZpdHd5QWkzamY1STd0amc4TmFpREw4dkJ1QzFzRXlMMndKaFNUClVXU0tiWmZIcEVpNlI1UVR2L01sd0RHNUZ4VThUWk1pSHl1RFFMR0pwYnQ3Vk5mZFVpWlozZkJiRkNKK3htaDMKOU1FRWszcWZJZlVHN0d3RmlkUEx4R3Q0OGMrREFNZW5nYnVhNlpMRTdvd21SZW8wK3k3cmRpQ3d3MEY3MWxUSApzbVd1aEhCa1hoTFlpU1lpZ1ZRTVJDR0U3aW5LdUdkQTdLd2dpTWQ4VVhrV3NibDJZRmFmd3JSRlVZVmJRUlJ2ClZTVnVMYVVoYTZCbzFkbGxLZ1lrVFIwOEZRNWhrVisvaGtZNlZoM1BCRzZEZXhJR1FiNlJrQjdDNWprSVhDdlMKUzFvOWF6aENreTFralo4YXpuMUg2Ky9xM25qckl5UXNtcWdoZUpBZFB3S0NBUUJBNG1Tai9aVzZkVUVPREVnZQpjU3hDUGFpYlpEckdVQmNqblVQckpKdjhlaVZyVFd5QWwvYjdGU3ZrVXZSZnczT0NMVTBMNW5nUTF1YWE1eDZBCkw5V09pNUFjbGYrdjRFTms4TC95RlV5RHc5Ni9DZFl4SXpiYzFOaDZnYlh1SGczcGxkRHRoUWNVK3F4RlVsOHQKQmpWWGtuZnM2QVZPQ2ZWS2NlZVJiQkNqVG12c3JjVDVmakZQTzhFY3VmaHExSFNuenBYby8ydFFkZXQ5VnRGcQpNNkZyTzBEL1JWT0gwcmNXSE5IaUltK1cxaGw4R0RtdUNNYncwdWd1VmJBQ2xWZFFleThjUHoxV2Y5ZzQwbm1RCm1QVHc1TXlqNXhFbzZ5Nkw1anlxZW9mbUszcm5zRE9NNnRzSXlJcmh6NktKN0RSV2xMWjJkZ0lvWlFBV2NuY1EKM3BBQkFvSUJBQmQ2Q1dtS2doYk0xRWtPRzFFd0tISFpQWkh2ZGZsRk1LUTlLOTRrS2hHVFY2b3lTMUNJTWMvUQpyRjJMZVFuMzRySFNydnNoZG9tdG5meEcrWTluZ0FHMnR6NkYwTTZUSS91T3VXWDNOTW56cGtONDBLY0JJMzVXCkRmTytKRWdWcnROQUhrWWFGN0d4NWFXc21vcHlWNXNlbXlma3dyZ1JHN21nSDNyVHV4amN2NGUza3VzWHlGSW4KY1d1Ym9qMWlWSzJHSTNhSW10NnZ6M05aUVRXNkZTazE2dEJEaDJEaUxqSGZjN0szcFRTdURkbGpOZHpCUmhRYQpoQlZpQ1Z2dkxEbER4Wm1LVlNld0QwbWkzb3RaSWF1Y1ZqVVFJOU1OKzJjNHRQTVhlTFJBMUx4dXZ4emF2WXIrClNIdU9xQzRabjV4R3J4dG9yeDk5c0pmMnRSVUJEL01DZ2dFQkFMRExRNzg0SDU2QjhlZU5zZE1wUk1mTitxZDQKclFlVTB2NUxUOElxdldTcmpLZTlHS3J4NTNSZ3RPNEtRTkdYc3FoMkROdktobFNDMys0amxCdms2UmxxUFdFYwpOSnRZSEw4UFBwby9hOEphM0F1RlpYL0NLNFFCeHR2SEdodjdPeG9JMG9zd0EzNG5NcVk2QXJaSktuSjEvcDljCmYxQk03TGRZQk1VNmVEeDBsSDVFM2xkM2lXVFN1ZUdWVk5PdzBpNmpoeDl3MUp0LzZwRis5NDJqdDFiRUoyN3YKYVdXT2REQ1g0SVIxMStiRlhhOEZJcEhCbStoTm1FdWRRc2hwN2pId2hCTjNiZnNSeHJXWGUyd1cvYkthdFBqWAo1N0p1bEFQVlN3L3h1TGJZZFZiVGlvdmRsMWxObXFJZEpqYVZma2ZZSzVJUVR1R0pxVHNzdVkvbWNITT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K")) }.freeze
20
+ end
21
+ memoize :keys
22
+
23
+ def algorithms
24
+ { "wisoky.co" => "HS256",
25
+ "powlowski.info" => "RS256",
26
+ "mcglynn.org" => "HS256",
27
+ "okon.info" => "RS256",
28
+ "ebert.biz" => "HS512",
29
+ "olsonjacobi.name" => "RS384",
30
+ "okunevabednar.io" => "RS512",
31
+ "gerhold.co" => "HS384",
32
+ "hoegerrenner.info" => "RS384",
33
+ "rice.com" => "HS512" }.freeze
34
+ end
35
+ memoize :algorithms
36
+
37
+ def public_keychain
38
+ keys.each_with_object({}) do |(id, key), memo|
39
+ # HMAC uses single secret for encoding & decoding.
40
+ memo[id] = algorithms.fetch(id).start_with?("HS") ? key.to_pem : key.public_key.to_pem
41
+ end.freeze
42
+ end
43
+ memoize :public_keychain
44
+
45
+ def private_keychain
46
+ keys.each_with_object({}) { |(id, key), memo| memo[id] = key.to_pem }.freeze
47
+ end
48
+ memoize :private_keychain
49
+ end
50
+
51
+ Test::Unit::TestCase.send :include, TestHelper
@@ -0,0 +1,32 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test-helper"
5
+
6
+ class JWSGeneratorTest < Test::Unit::TestCase
7
+ def test_trivial_generation
8
+ [
9
+ ["wisoky.co", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"wisoky.co"},"signature":"83XVcMqm7pMa0tvgCHWsjyPCOdGfWnc0-czu96n_Efw"})],
10
+ ["powlowski.info", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"powlowski.info"},"signature":"lXJ9N_c87BxxjhB162zTNuGhoBxjBdot6E8UEhDczmWhctQTZgzTTPrMa3X9fVLVkc57tucQh13eVU6p3gAppqy6Y2B5933BiCeQdHHao5sEOcXwbIvFMi-IOcloFXHhEw8IzIa6ZlugmWII_eHZGHF3czLqkww9pjUBPYI7Z5EinG4Co7rySIM8D2XzQZ7Q-c_05StpjYIeGszY8ihJkKm0aDLnVMIoQo_22vwl1rXHUd8XBUg020Oqwxk1iI49YkzxdDdOJO5M2RqYCHn5hi8QpVAU0zzag8gHjfB12A5c-rAVl3Pj_EBjNN3FEo9Xb1L860uAKHAO8XUjNFujGJdQ_ANUkT0CbGq4wB0JXY4ml8nN_ROOTjHpDalbHXojv80OW0GFSWRCKLNQ24OiFsesTBOHBnszYtHaTep37GdL4GZogUNyHzX7jggq904WTfwVkVUJtzkCUE-9D1jdwv8mpTRdYDO4sX2AlbhSOEW8AIjCmTr__ai4mAUK0JLJ3_dvFQHG7cXahPyh3MPsR4Rk1tl2VJ1o4Ont_SxfAM3l8ssgpaaFUSkYxhCIo7rT2VThSOI9FVbf7eakIZG0n3jv562jABh5nsmX9k9gBxkMMKLw4tw0URjtpERLj9x0K3EP-NpJT0-GD3nHU3lSxgZFSXbCNJa38Z1GeJBcPtE"})],
11
+ ["mcglynn.org", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"mcglynn.org"},"signature":"MAttuD_FCMFOTAcGlJJinPRoe3NHqWp5-ImFvVevv30"})],
12
+ ["ebert.biz", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"ebert.biz"},"signature":"g5sDKJSix8I8RbLd4l3exK6TH0TCJNbd9xV5MMt0xL16PGPX9pLC8ukvkjrncdGQEHTmEpbTp-AROigRdBS8yg"})],
13
+ ["olsonjacobi.name", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"olsonjacobi.name"},"signature":"P4uy4N7L_Fox0KuRQtV8TT3xeX8-EV6ZU8a-csDqC0IpCFBfgmCZr2yg3TdCogAS-R3ZKbfiooL8GyqkmSZt_A-HXd0O4NY4MokHmn2AQun7pWzytKu8zYa1spZVncKmvaGeS8NEpzc7nA7cYbpF7oYsMN0_oWwkUkJHAacDVIy7hbUHNYQbR0Tx9eJwLWrLEeU6Mk9fNKjT5MvpKzi5gHlUNtXEniEP3Y9hkU206_9w52yIKbiefZC5xB108JCrRM-yIePMRW3IUwAk8CP_bGEJQ4cuwl-6r1P_Wpdip7xrARFSLmn4FhdR-XKVA41bCBDt3bVuRFtMcUhuGOk44A"})],
14
+ ["okunevabednar.io", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJSUzUxMiJ9","header":{"kid":"okunevabednar.io"},"signature":"LesBgs8x3DNynPhoqSTiVoIRd9gl-Y8yntMvVe8-7Xe9KAlxExNCUaCJgsfIidCMD69O_D7wIsSlxIqFRj8K8bCpn7LGQm5pxOJlHy_UPvOVczuiTp50nynxcXimAfBoLHPA8d8EcVDo9CgjJszehOggIQJxMusiAcTCVgWMf__TziMa-IIB1MMMGsmnoZCmMdF_eQpthYIjOVIz6wXzNS7RhcYPD48lVO0Q56sGK1hS1ejM1l6qKeUQQp3PbN9G24OAvIlhVMlrOLDPCS3dwKQZjgtaNcNyNVeoNRe0MfyPcCJqD6OTyCiwlplqCr2uFjYztiEDH1uI7SP_ehTR-A"})],
15
+ ["gerhold.co", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJIUzM4NCJ9","header":{"kid":"gerhold.co"},"signature":"hYN5Iv4bWEVtEukppQDPc4cHWYN9gBzDsgyKgVqi3VheFFCfJG0Jp4Z5ugPuoBub"})],
16
+ ["hoegerrenner.info", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"hoegerrenner.info"},"signature":"LjvdPRqVo_RuhYUwVKOk_ZX0eqyYmDKxetzjepqm46oKyK30VUK6srLFzg9WrtQcT777vK7tLRcUSxgIsyNuDJCd7A9yuhkadGkK3NGyGa7lv2JYfIcqrUe6DKTIKvzjsm5u2-mDLdPgUHWt-T8f64ogAnAxdEVmj_zq_wKQwwminq81DSWGxE1hkIivBhtkmJSzjQW-1iA3Bg589mTJP-13L2cjUUMsjpwqj7Yh5fobEVFl7x1b9sodAKrbft0934uPF2QlZta3V8D5XiW2uF9kf-yROjhieF5aAe7ImaV4xtyS03vJaKxaSVy-66PKttqeyZolufqRtKp_DOV2sCi_sE-1SzqHR2dCp-tMAnRI_3QsOFGb6yFJfgjv6634K6DW3hZysz9TEJehKUCYi3MNGLc9LiSLUg9dW4tcb-D0Ds-EpA9QFwOdBxlQ6ane4uzxv4U6YX2Fo5X5PXxadw6tpxIYB_Gm7rPtf7opYJECJVRv1WA4ojIH24GTiQVW"})],
17
+ ["rice.com", { data: [{ x: 1 }, { y: 2 }, { z: 3 }] }, %({"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"rice.com"},"signature":"68LWv4eb_m57prEo4pqcFwAVjW9seU6nhIFFduxyPxG8hD2UFVYNl3Da_xMGji--yVPQCp05JOQriAsu3zw7pQ"})]
18
+ ].shuffle.each { |args| example(*args) }
19
+ end
20
+
21
+ private
22
+
23
+ def example(signer, payload, expected)
24
+ # Pass instance of OpenSSL::PKey::PKey.
25
+ returned = JWT::Multisig.generate_jws(payload, signer, keys.fetch(signer), algorithms.fetch(signer))
26
+ assert_equal expected, JSON.dump(returned)
27
+
28
+ # Pass key in PEM format.
29
+ returned = JWT::Multisig.generate_jws(payload, signer, keys.fetch(signer).to_pem, algorithms.fetch(signer))
30
+ assert_equal expected, JSON.dump(returned)
31
+ end
32
+ end
@@ -0,0 +1,86 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test-helper"
5
+
6
+ class JWSVerificatorTest < Test::Unit::TestCase
7
+ # rubocop:disable Style/NumericLiterals
8
+ def test_trivial_verification_of_signature
9
+ jws = %({"protected":"eyJhbGciOiJSUzUxMiJ9","header":{"kid":"okunevabednar.io"},"signature":"Lbu_mFwHTsR41og-_sbLW8HN7FXy6tLuC_4hbBWHrnj5HEh4f5RlhXvnyWdew7rXm8hflFj24ESEekFCXcydNUYAO4sr8blYFqoFJVVYoiQRTWM3zA2FzqutOufDDbqbujBpE0xTRT0UqU72kVqczRbFwIY0j-8Aby5B4w5JrUHo2AyWe10hezah886pzu6BO0pfShQZrXgRyFV4Sg63labEMwCL5nhi-bHjeH4ZrUR50NfEOqSOKglI4XniOkYXCIX7zDg4YZc6XEos3CJbh93-AJ_vMJKlJ-s-zVK5av5onI6YZMbKKlgsYL5CyxiJkJSVw4cly5eshixson1HVw"})
10
+ payload = {
11
+ data: { action: "detonate a bomb" },
12
+ exp: 4577496916,
13
+ jti: "683c7b99-1042-4e1a-81b7-3bc0284d8ec0",
14
+ iss: "government" }
15
+ example jws, payload, { verify_iss: true, iss: "government" }, payload.to_json
16
+ end
17
+ # rubocop:enable Style/NumericLiterals
18
+
19
+ def test_trivial_verification_of_issuer
20
+ jws = %({"protected":"eyJhbGciOiJIUzM4NCJ9","header":{"kid":"gerhold.co"},"signature":"JQq8ZrqO3DfOXbsdfhzF7qXwAdXunAdjUX_iJoIHOqFWvB7IfHLHYcIVIBUb-AH8"})
21
+ payload = { data: { x: 1 }, iss: "ryaneffertz" }
22
+ e = assert_raise { example jws, payload, { verify_iss: true, iss: "schumm" }, payload.to_json }
23
+ assert_kind_of JWT::InvalidIssuerError, e
24
+ assert_match(/\binvalid issuer\b/i, e.message)
25
+ end
26
+
27
+ def test_protected_data_is_required
28
+ jws = %({"header":{"kid":"ebert.biz"},"signature":"3nSc9aeRuDyrq_dYQRQX5tnM1wVw6reoUlmQ4JqWIV3LM7yeIDgcVLRYxyb7UUBM0gNqA4QJj3CpwS6vg-EHYQ"})
29
+ payload = { foo: "bar" }
30
+ e = assert_raise { example jws, payload, {}, payload.to_json }
31
+ assert_kind_of JWT::DecodeError, e
32
+ assert_match(/key not found: "protected"/i, e.message)
33
+ end
34
+
35
+ def test_signature_is_required
36
+ jws = %({"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"powlowski.info"}})
37
+ payload = {}
38
+ e = assert_raise { example jws, payload, {}, payload.to_json }
39
+ assert_kind_of JWT::DecodeError, e
40
+ assert_match(/key not found: "signature"/i, e.message)
41
+ end
42
+
43
+ def test_protected_data_is_base64_encoded
44
+ jws = %({"protected":"qwerty","header":{"kid":"rice.com"},"signature":"yVzIjLYCl5gaLHAhKYQmyEnvlYq8rhohYVcyqI-zvTJ0ccU4MojHw9_5GvAyeECF1_DXDvY7wbiyRu4nCN1rMw"})
45
+ payload = {}
46
+ e = assert_raise { example jws, payload, {}, payload.to_json }
47
+ assert_kind_of JWT::DecodeError, e
48
+ assert_match(/JSON::ParserError/i, e.message.encode("UTF-8", invalid: :replace, undef: :replace))
49
+ end
50
+
51
+ def test_header_is_required
52
+ jws = %({"protected":"eyJhbGciOiJSUzUxMiJ9","signature":"oRN-lE_OqSRtUeI1ZkyftpV2PmJPArrX68_3Zm6BHTxjKemyLHdR2D3z58Fm8a-9XnbRpqpawKDoHx3AB2EKZayw8WChKTZv0qZeUx0SH2oo27nCC9b--99D3_E7D4eqb6qlmML7gAlJyeFbl3QD8qEuMC-EyjSm-kyXmxZcNW5myHC4XZayE0GBfS1yzKYbpSI16PKZOUHoFHjMAHm79bFg37V6FB4qKszMyjss_pl6dK0VdGSiDpX-LPaTdh67joPQHIcmDprfMF0pn50RNvorS-5qa8Ev79mozcDLMUb4hrLXZ_x8AWen6XHbwo34nSrd_Fn7-GOaDtsGc0XdfQ"})
53
+ payload = {}
54
+ e = assert_raise { example jws, payload, {}, payload.to_json }
55
+ assert_kind_of JWT::DecodeError, e
56
+ assert_match(/key not found: "header"/i, e.message)
57
+ end
58
+
59
+ def test_algorithm_is_required
60
+ jws = %({"protected":"e30","header":{"kid":"wisoky.co"},"signature":"eygCpYrkji7pmmA5sRUFUnwsW-ciZFHSwGVmCSya8Kk"})
61
+ payload = {}
62
+ e = assert_raise { example jws, payload, {}, payload.to_json }
63
+ assert_kind_of JWT::DecodeError, e
64
+ assert_match(/key not found: "alg"/i, e.message)
65
+ end
66
+
67
+ def test_invalid_signature_is_handled_with_exception
68
+ jws = %({"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"wisoky.co"},"signature":"qwerty"})
69
+ payload = {}
70
+ e = assert_raise { example jws, payload, {}, payload.to_json }
71
+ assert_kind_of JWT::VerificationError, e
72
+ end
73
+
74
+ private
75
+
76
+ def example(jws, payload, options, expected)
77
+ encoded_payload = JWT::Base64.url_encode(JSON.dump(payload))
78
+ # Pass instance of OpenSSL::PKey::PKey.
79
+ returned = JWT::Multisig.verify_jws(JSON.parse(jws), encoded_payload, public_keychain, options)
80
+ assert_equal expected, JSON.dump(returned)
81
+
82
+ # Pass key in PEM format.
83
+ returned = JWT::Multisig.verify_jws(JSON.parse(jws), encoded_payload, public_keychain, options)
84
+ assert_equal expected, JSON.dump(returned)
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test-helper"
5
+
6
+ class JWTEditorTest < Test::Unit::TestCase
7
+ def test_add_jws
8
+ jwt = JSON.parse(%{{"payload":"eyJmb28iOjEsImJhciI6MiwiYmF6IjozfQ","signatures":[{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"powlowski.info"},"signature":"SHGndP6B-pk7qF6AQTpvlqPpFI8FzidpM-bzYlVp-6_5Ti8kabbordJtgMDt6d03WdcTVdYXd_FQTfBUyASNvTj3EPxpRgbosxEAyu2nb909WgxWM0xusMxDWlUmNx4Q1dbrlBUmfxVjCWbKcfmGqbIFN7SYUvbi-ScIpXWK3dtImbp3OKYNdpDY-MqSX2dN8_v73LJD66fYne1F5AOsYmzucnYmHggqWZymqVGwRUluG5VWdXFWSwavVBfZGQLE05l1WiwU5HoxgS8BiuPX8nohgHUbQym1kOQHgvXHvnhGTg-rKYjisdDEqv6Ol5soWBEPmYkKhepkp0SXCG5bLiZMIn-dhN1hPZmcn9Iwp3gUTQQx-PBB04LXJghpBAhsFG54cKm_kdiCo1vf9bMEhIl2cbaNrbITU0cZ27947gJCuguXcuw2Fts80TNgZLg5abmt5MXOErK7C85ABZ3WxFlcXaIIy-2msoFg7Q5YRUIUZcODSMcswnrgQy5bqq57vzA5Wx3b6nuYPo7dLPquIVnHDSDK5sNf0V1muLKqLWArPveMBx6GZxxH8j-EB7VnkoilrzMOay-s9z0uKFYYAPLfXjD2Bh-iS8-0mXmsQ8Kigf1fJIG4QFu-PLs4_7xA_mqo-GstshpzThXZpqfVwLMBCgNBhKysJrzbHF5f48g"},{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"hoegerrenner.info"},"signature":"LalThItuDiAsEfWSy1sbAXggtq4w0P9ptgeUmtj75jDgrTevLrCRWBux5sgcwQUKDB1Ap6yYXaPDHGgDm_20AhpWOBgKijp5mIsG542G7n_hVuu3siaX4yN4DoY4OOWmeGduiP1w_M_Da53xajBqBJcgj9Zs090xFnewUAsv11n8Yk2DKrP2nKfhyaF210-cCDcZCjiUNF2uwxaYjosG5ijFXEadxcqNfuxc2Qzk2Qt47dYhN1pL1--sHl0EyjLZIrC1zRJxN7vLv0BG4adoGq5fxVbKcqfV2v9DIloxjP4O7HcRVkPXdfv764ZhrfY8w3HWR85j8j5NGE6lRug-DtGy8R7Y0FhLadJMa9i4G0fRq11soVyNoIs3-zBgpp23m4_FWI5AirF00HODC1Jg2E1Nhjx5Mf9SB6RpVHLE0D7EgkAgr9KQqCrPJF-uP5U3ADLK7zu7bts0pBKZCA-dfnVKXKkEP7h0s3RXx4awTjeDfdIvJpS-Y2SGKHgGsvir"}]}})
9
+ new_signer = "ebert.biz"
10
+ 2.times do
11
+ new_jwt = JWT::Multisig.add_jws(jwt, new_signer, private_keychain.fetch(new_signer), algorithms.fetch(new_signer))
12
+ assert_equal %({"payload":"eyJmb28iOjEsImJhciI6MiwiYmF6IjozfQ","signatures":[{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"powlowski.info"},"signature":"SHGndP6B-pk7qF6AQTpvlqPpFI8FzidpM-bzYlVp-6_5Ti8kabbordJtgMDt6d03WdcTVdYXd_FQTfBUyASNvTj3EPxpRgbosxEAyu2nb909WgxWM0xusMxDWlUmNx4Q1dbrlBUmfxVjCWbKcfmGqbIFN7SYUvbi-ScIpXWK3dtImbp3OKYNdpDY-MqSX2dN8_v73LJD66fYne1F5AOsYmzucnYmHggqWZymqVGwRUluG5VWdXFWSwavVBfZGQLE05l1WiwU5HoxgS8BiuPX8nohgHUbQym1kOQHgvXHvnhGTg-rKYjisdDEqv6Ol5soWBEPmYkKhepkp0SXCG5bLiZMIn-dhN1hPZmcn9Iwp3gUTQQx-PBB04LXJghpBAhsFG54cKm_kdiCo1vf9bMEhIl2cbaNrbITU0cZ27947gJCuguXcuw2Fts80TNgZLg5abmt5MXOErK7C85ABZ3WxFlcXaIIy-2msoFg7Q5YRUIUZcODSMcswnrgQy5bqq57vzA5Wx3b6nuYPo7dLPquIVnHDSDK5sNf0V1muLKqLWArPveMBx6GZxxH8j-EB7VnkoilrzMOay-s9z0uKFYYAPLfXjD2Bh-iS8-0mXmsQ8Kigf1fJIG4QFu-PLs4_7xA_mqo-GstshpzThXZpqfVwLMBCgNBhKysJrzbHF5f48g"},{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"hoegerrenner.info"},"signature":"LalThItuDiAsEfWSy1sbAXggtq4w0P9ptgeUmtj75jDgrTevLrCRWBux5sgcwQUKDB1Ap6yYXaPDHGgDm_20AhpWOBgKijp5mIsG542G7n_hVuu3siaX4yN4DoY4OOWmeGduiP1w_M_Da53xajBqBJcgj9Zs090xFnewUAsv11n8Yk2DKrP2nKfhyaF210-cCDcZCjiUNF2uwxaYjosG5ijFXEadxcqNfuxc2Qzk2Qt47dYhN1pL1--sHl0EyjLZIrC1zRJxN7vLv0BG4adoGq5fxVbKcqfV2v9DIloxjP4O7HcRVkPXdfv764ZhrfY8w3HWR85j8j5NGE6lRug-DtGy8R7Y0FhLadJMa9i4G0fRq11soVyNoIs3-zBgpp23m4_FWI5AirF00HODC1Jg2E1Nhjx5Mf9SB6RpVHLE0D7EgkAgr9KQqCrPJF-uP5U3ADLK7zu7bts0pBKZCA-dfnVKXKkEP7h0s3RXx4awTjeDfdIvJpS-Y2SGKHgGsvir"},{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"ebert.biz"},"signature":"qD-u3ioPpLvrG-lMojA_ceLUUT0F3oYuK-Tuh7K5PWbSkxuCQqwiiK4Jqlur2QzNc6vkHWtwlZSH8wwhGVAQ3Q"}]}), new_jwt.to_json
13
+ end
14
+ end
15
+
16
+ def test_remove_jws
17
+ jwt = JSON.parse(%({"payload":"eyJpc3MiOiJteWNvbXBhbnkuZXhhbXBsZSIsImRhdGEiOlsxLDIsM119","signatures":[{"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"mcglynn.org"},"signature":"3hBmZPpW0IsfSIuJNb3H8-6cKJ2V5PiCmcaKLoIah0M"}]}))
18
+ 2.times do
19
+ new_jwt = JWT::Multisig.remove_jws(jwt, "mcglynn.org")
20
+ assert_equal %({"payload":"eyJpc3MiOiJteWNvbXBhbnkuZXhhbXBsZSIsImRhdGEiOlsxLDIsM119","signatures":[]}), new_jwt.to_json
21
+ end
22
+ end
23
+
24
+ def test_remove_jws_when_no_jws_exist
25
+ jwt = JSON.parse(%({"payload":"eyJxdXgiOiJxdXgifQ"}))
26
+ new_jwt = JWT::Multisig.remove_jws(jwt, "olsonjacobi.name")
27
+ assert_equal %({"payload":"eyJxdXgiOiJxdXgifQ","signatures":[]}), new_jwt.to_json
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test-helper"
5
+
6
+ class JWTGeneratorTest < Test::Unit::TestCase
7
+ # rubocop:disable Style/NumericLiterals
8
+ def test_encoding_with_two_signers
9
+ signers = %w[okon.info gerhold.co]
10
+ payload = {
11
+ user: { email: "orlo@reynoldsoconnell.co", role: "admin" },
12
+ iat: 1521823259,
13
+ exp: 4677496916,
14
+ jti: "22cd9c3a-55a7-4024-acb4-17a3ebeeeaac",
15
+ sub: "session",
16
+ iss: "raynor",
17
+ aud: ["hermistonherman"] }
18
+ expected = %({"payload":"eyJ1c2VyIjp7ImVtYWlsIjoib3Jsb0ByZXlub2xkc29jb25uZWxsLmNvIiwicm9sZSI6ImFkbWluIn0sImlhdCI6MTUyMTgyMzI1OSwiZXhwIjo0Njc3NDk2OTE2LCJqdGkiOiIyMmNkOWMzYS01NWE3LTQwMjQtYWNiNC0xN2EzZWJlZWVhYWMiLCJzdWIiOiJzZXNzaW9uIiwiaXNzIjoicmF5bm9yIiwiYXVkIjpbImhlcm1pc3Rvbmhlcm1hbiJdfQ","signatures":[{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"okon.info"},"signature":"nPo7Wn_jEkOVBXaqq90eS0MOD_lIJ_6_TD3zCuPnvp1sTTkN79tREI53-YpplWHXEfplJE59npuVqQN7R8k16u4EMAG9OFfU3TRbQ6dj9_syJ-ACiRYiA8J177RAu7BFK4Y2xZHpkdDhxvFmi8ewR98VHWX0XNrMJMdduWsxS1wmEJKGHGzOIynIbEVfrX0LcI-g35f8XbIpe3c5xaVfbtIuR1asSJJ_bFFYGk1STKIfBrfbLvQkTdWZAgZyT5P5WBemkzV56r_PokEZdi_eaQSJf8wt_G6GFZbmFPiaEwDxN5heiCvLjhwXMbTkl3tRdFOEsxAjy7Sg7lhdqBRE9p0GiuBZGgbCLwtGxYoeL6N2oL3-ZoHmC_BoQDhKv0eR65ItcLAKL3o0aviryA59VvQNVZtk3cbGO0IstQRAUbEtYomLoQO8FdYfhR6QpV1zKCb4z5k0MsqAhlNDCOLzfm_OT_JQj404e3pg72k10BlmcXRJR-koHWx9lm0B04hm"},{"protected":"eyJhbGciOiJIUzM4NCJ9","header":{"kid":"gerhold.co"},"signature":"y8r7BD6ivAZfs8WpQoFh6q15teeiXWsYDQd44I3tmrngZ7ZobOH0WvbEjAncgMcM"}]})
19
+ returned = JWT::Multisig.generate_jwt(payload, private_keychain.slice(*signers), algorithms.slice(*signers))
20
+ assert_equal expected, JSON.dump(returned)
21
+ end
22
+ # rubocop:enable Style/NumericLiterals
23
+
24
+ # rubocop:disable Style/NumericLiterals
25
+ def test_encoding_with_four_signers
26
+ signers = %w[okon.info ebert.biz olsonjacobi.name rice.com]
27
+ payload = {
28
+ data: { currency: "btc", amount: "1.75", destination: "13bwBSNY9Q2ZDMcdCRM5PdjXpJuLiyLLRj" },
29
+ iat: 1521824704,
30
+ exp: 4577496916,
31
+ jti: "3fb35606-d61a-42df-8c29-d041350d8c60",
32
+ sub: "withdraw",
33
+ iss: "oharaupton",
34
+ aud: %w[douglas crist] }
35
+ expected = %({"payload":"eyJkYXRhIjp7ImN1cnJlbmN5IjoiYnRjIiwiYW1vdW50IjoiMS43NSIsImRlc3RpbmF0aW9uIjoiMTNid0JTTlk5UTJaRE1jZENSTTVQZGpYcEp1TGl5TExSaiJ9LCJpYXQiOjE1MjE4MjQ3MDQsImV4cCI6NDU3NzQ5NjkxNiwianRpIjoiM2ZiMzU2MDYtZDYxYS00MmRmLThjMjktZDA0MTM1MGQ4YzYwIiwic3ViIjoid2l0aGRyYXciLCJpc3MiOiJvaGFyYXVwdG9uIiwiYXVkIjpbImRvdWdsYXMiLCJjcmlzdCJdfQ","signatures":[{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"okon.info"},"signature":"lGqBHSPEDRK_JYhwspujZYE-ri_wS56ukF-GT-GKugr0XMsisuYUDj6NLWMBZcHbvg_TQP2LS5C_X4EJlxrJ-mHStp8KvEQtuON-E06PxrOli2j1LgwUPlwrbV9ujfqdwwRblGnOX3mDtXn0XUeWOaIoMBQV4BvfvF-6EuGFTp9bPRNnxyw135GSKxlT6s2IwxUqcXzweK-pzh-OAi6Tny22SSjtP00DqajkhNoDZ66jQMiH8939E09mZhJwABrWqd-v9Saa31RQZp_TOaLuKcMcIVNVcsqFdJyS3J7nsKvclq102lmyD9dZVwteTNOtmpdytpSNoIXK0piBBK3OZ_uYQKkM7dlw-TzIqedTCkpXpxm_x5Q1-SQOt1LuEU4YXdcLFt-G9JrUag-olciMTylo2EISw0dVnRU9ZusX4VwZEU6Z5O0yNAOy1oJYLn72XQud1woR5BXKe9CUZb6maA7WcS5WOJpw2SmkHXVVoQBj1ZbWa6mHLk-lKO3skvk2"},{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"ebert.biz"},"signature":"sIQQyqmxM2D8U7O1g3WG2NfLo10HyqFg_fzXfhzuNATJOAxE4YR-Bz_f3srs-bEAOy_bNpfH-9FIDupYLVXpOw"},{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"olsonjacobi.name"},"signature":"ZjwWEWZYiNHGwrmbfR7KSdJI6JuqKJ5YcpsfOxs8RZ3XpG0d-7Uua_nzcnm7_DpbyXZfltmH7901gLy8XTFsnRmAeRdpgPDu7s_zTUAW-I-XIMGsGfz5oS_dzoZVjXzW82LxZAC4cZTAS-32AuNReef-SVYJVplJGsdpd633cyMm2QKxM3aQRiuQ7Ogq0tJROtHyuSF4qnmyW75KBOhAWYChc5WjNxLSpaG3WcDV_--NvyYM1INfTWeIYayTE9Y5AB611dRR9w-Cg2qh8JfhBFkOoOuZBfel5Kl94PNST1tp7oLImuuZlgpEEV0_rXd1BAbz7P-XpJEzMGcDuEFEiA"},{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"rice.com"},"signature":"rqE6POMDDY35AfoEqPI0rQhTOrGQKPDj4gT8aXC34n6Aw6tOvwx7ULaEPEfAq5T026F3nhvULBbyYP9X5okL8w"}]})
36
+ returned = JWT::Multisig.generate_jwt(payload, private_keychain.slice(*signers), algorithms.slice(*signers))
37
+ assert_equal expected, JSON.dump(returned)
38
+ end
39
+ # rubocop:enable Style/NumericLiterals
40
+
41
+ def test_encoding_with_one_signer
42
+ signers = %w[rice.com]
43
+ payload = { bar: "baz" }
44
+ expected = %({"payload":"eyJiYXIiOiJiYXoifQ","signatures":[{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"rice.com"},"signature":"CnjElUe4Ng1yKiLmG2d6lDWHw-HQDuH_haHM26izIcQDWKe6waF-4uTfPJrzvdh8Jw7A1MOnzUmKBKErivI9Mw"}]})
45
+ returned = JWT::Multisig.generate_jwt(payload, private_keychain.slice(*signers), algorithms.slice(*signers))
46
+ assert_equal expected, JSON.dump(returned)
47
+ end
48
+
49
+ def test_algorithm_is_required
50
+ signers = %w[olsonjacobi.name ebert.biz]
51
+ e = assert_raises JWT::EncodeError do
52
+ JWT::Multisig.generate_jwt({}, private_keychain.slice(*signers), algorithms.slice(signers.sample))
53
+ end
54
+ assert_match(/key not found/i, e.message)
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test-helper"
5
+
6
+ class JWTVerificatorTest < Test::Unit::TestCase
7
+ def test_trivial_verification
8
+ jwt = %({"payload":"eyJpc3MiOiJmb28iLCJiYXIiOnsiYmF6IjoicXV4In19","signatures":[{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"ebert.biz"},"signature":"1koPnSwejNF5aCRsqlySX9Td7_gc-dfUkko5G0Svccw-WkBYrwoJJwRJ2Op_-OxjoqSe3ViBGGCbgVUz0khuJQ"},{"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"wisoky.co"},"signature":"AqtFKTlaVDqg2dOfLBODMhcBlg1gm9ejn6hYQynTyto"},{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"hoegerrenner.info"},"signature":"LR9TpJTLwgducdCkN1KmfwXXxd3pp7Xe5fJXJZZM8FVrFrVOEAGQcPnMPIgfPA1UckIXnzih46j4qPOQdotVHEvYvUuvLLT8QQi8y6-vBMlsP-cQehKGpI1T4N5qPzvJqPmhVzZYedWzlvr-VV9wd0BYeBgr65m9BSpFjLFhWVH4NJZuHFPxeYuDEpYoM-lPHdTzdf1E8xd_xwbpz9WpNh0MQib387-wakGWz-UGt9BmJLU8KV01FTAoR0EO9rQfIm5HQ3wGQ7t8U4N4HsOmsXkWF_fRgxjhMHeChDES2awwB4G4KCNw-6ezSBCD7FZcxzbCL2657OEPHNuHA36M91j54jjm1tweYhYJxuUOk5c8j_wSxtieeaORCxOrPp3mshHS_FE0sI_TNNBsIDI_sQwiS08y3d6tv7H4a_MZj_Pe7JWJ3TXlcsaSHy3xuSLYxCZQeLBwJtyz2ERCZOA9ew0BY34tpRwDKxbgF51X7t7uilYxnBn2rBdQeWQKb9q2"}]})
9
+ example jwt, public_keychain, { iss: "foo" }, %({"payload":{"iss":"foo","bar":{"baz":"qux"}},"verified":["ebert.biz","wisoky.co","hoegerrenner.info"],"unverified":[]})
10
+ end
11
+
12
+ # rubocop:disable Style/NumericLiterals
13
+ def test_verification_of_two_from_three_signatures
14
+ signers = %w[ebert.biz okunevabednar.io powlowski.info mcglynn.org]
15
+ jwt = %({"payload":"eyJwYXNzd29yZCI6InNlY3JldCIsImlhdCI6MTUyMjA3NzIyNH0","signatures":[{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"ebert.biz"},"signature":"XrreeK0i4zcaEQ0ntKpZVDRzEZLjZUXnWijC73TPC0-1xLK67qSmNt7oxBhnLV8VZVrqvusI-GAE9cEyTPcO4Q"},{"protected":"eyJhbGciOiJSUzUxMiJ9","header":{"kid":"okunevabednar.io"},"signature":"rP4Yx3LcFA9UBHjZCoPQOqETWTblW6FjA4LYoeLe15GTlNRmQLdQYnpaRIpQQ8NP_8PAx5YzIkZNhLEGv2oly0I4FhNp2OBLKw_Mq-XKpwMDKB22gbvZVM1so0fqsh1Muo7V64vk8UkQTlC6Zz_tOlhuH36rMl1YPmypnC6yhO5ocOKU7S50Fzr-s4MmsH2oGaODqvk7U4pKKNjj7Ru8t-4kpmRmYeMTFuS4X6527EIA0Lvav4rsqO_KXFbw8Qokn8hp15OZMgbwYjX_PAbFzFKuR49eUhUyUYotDGoZgO_EhFvEiaF17PEaG9UTCvOXyeMYUbTfjGrXJmo8OgZlnQ"},{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"powlowski.info"},"signature":"KhBCjqlwo12p3D-tUgNiRtHS4h71VaKqw4r9974UPKXG3gFdVbZkS3dfPZSpcVOa8cas_Olpv2681BO142yI2vd-0Cm-5imeavb1HPZL6gfYZj4u4peTdA7MkBeBaIz3v6biLcUu0HQEYjg1kom7jlafT-NDx1_AWRd4onD8B9DaB-A0ZR3Hhx5VZrA8CdHz2BiBfNRKiOB4beIW5DN3RIGvxN7XVwnuWato06yytZuMWidVfAwDoO7Kyu3V3rOLDf-c92lxQyAw9VlIBMuerfdTD-H11sw-dqY6N-dyIfFhg7a97hFCB_as4TrY5Tdn1uHVokfkrgoz73eZDxPjDSVyIiZDzJZuh1PxparJgktfVl0531ihi5ehFTA2Vi26tz2qha1IhgzTzU_Mxoq15UcI8jcmFuJeP3lr8KJY-dP5oEMcSlTV4xsDgyyf5E8JBSgRSC4jy7dxmRc7n4MRYaY6yK1aWS4y1xwBNkFMk6L09QTUHX3r9XE9alo6rgi6bhi5yMSty8k7XEmUqIINWvm_JzGTzkuBpbFtLWzRKjhz_M79lOyn13si6iYXrjbjCs1_DFurtCu_r_k0ry_WsDGyEHazqgdCY6FM6cRQ00i0NtDS_V7s3IaOdLKHmg_f3C8wIOFJMz3qB1nUNPrn4u-UEHDxBrSzSGyT98AkNOs"},{"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"mcglynn.org"},"signature":"gTMqrByZC7cpLxHob1WYnUAsu3HYTgasHJvdQcVuBag"}]})
16
+ example jwt, public_keychain.slice(*signers.first(2)), { iat_leeway: 3153600000 }, %({"payload":{"password":"secret","iat":1522077224},"verified":["ebert.biz","okunevabednar.io"],"unverified":["powlowski.info","mcglynn.org"]})
17
+ end
18
+ # rubocop:enable Style/NumericLiterals
19
+
20
+ def test_verification_of_reserved_fields
21
+ jwt = %({"payload":"eyJ4IjoieSJ9","signatures":[{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"olsonjacobi.name"},"signature":"qSYw9q_auEHYUwMvGCPRKdBIkqTzTnqIbZ-v9cq4wCkFGz0kk0J0VC3aA6E9ghT49UY9lh6j0TbvaEjPSaP4EWWjawE5hk31_h2Db5-lmgARtxCuESWkWvwaroPidtsNST3yHRS6_YFZ-QBXEgkOnMRDDBnd5cXJeaAahIVXS1mUVtTGttWpg6s577Cnmw2zo7vTAbq9Yg7-Y1s2wRzCF8oablahDXjyrc5aRfzml33Qjvafo_o6BlUJ_D_rI5lmR0Y0E_i7H6wLXtT_jp7E0ORs3dp40SSzkNIcnbPpXx0Zp8y32Dw7_mxYrclKeaPEmQ_DpuhYMGrp9iNF15JjKA"}]})
22
+ e = assert_raise do
23
+ example jwt, public_keychain, { verify_jti: true }, %({"payload":{"x":"y"},"verified":["olsonjacobi.name"],"unverified":[]})
24
+ end
25
+ assert_kind_of JWT::DecodeError, e
26
+ assert_match(/missing jti/i, e.message)
27
+ end
28
+
29
+ def test_verification_with_empty_keychain
30
+ jwt = %({"payload":"eyJ4eHgiOiJ6enoifQ","signatures":[{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"hoegerrenner.info"},"signature":"TE-XldA2YE3sERvW-ue7GU2lY32ZlV1NxymkEdtoTTsqFgliU8ggqbevLD0USpC-xQgdGJjOE2x2qm0wE4jxGRlJo70eHCqVz8I4s5b-h5OwbG2chRm7kZ0xiYnlV-Q_99tiT101EXOTys_QSEG2TnNhHwGXPPpinzcc_0ND8ATt9Gu5zmOq4sQdYyLY9ELOW6o8nHumPw4DTv2VBN5TAHEGASfstjN2MgME4-f3NYy82iBB75gCkHq1DnLWWfLLBpHdJR9f0L9rgILw0l6QUjf5OHhp_LjoK_qH2IVnjBCGQBkH12TEINZO2ZJygnWrqIx3bAgwzjcqKm9rgVRNG7IQ4G2luPp_usT2X0qsa-kWQm2id3FavaaWe5wkeL154V0e0hE7CVXH33GQ7af7EaDw4Lxqs3C0_10xOVoOeOxjYB9upDfr5Pmilu3NRiWYErRAfBfZ624KDpjtwcwjK2QcUh1jUceGuItQiMveIRxCflifbHGk4-rx8AYup4nw"},{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"powlowski.info"},"signature":"AX4UR4Hul7TxsTusfW4afQbV_fz-gbcaYgmXMqA_pUOPWZZcFKaZhfHRuII66DYXR_zrkrjtILxNWf5AbFsJZeMY3pWFurV0eqz5HVLTSZVYFxpCCkwL8E_lSk9tEkXh-YVMgeLgrDky3CtONtdl4qHj0YPN9Q7teFspx-v_mWwoIxuCS9o87uTJzmYHjPN2tF26ngNsNTl_y18R_CkP5XN1E9rZPccbyEuecbKJBCMIKwGCyfwBFvYVxu2rizNA6FBdchtFfRKq_jfVUbWDQpFQgR9GqmelZk1lm63KfnOAHG-49XzIQbFA7BF4IxVqVlp9uZG-cJlrnlllvhjknAyCdKjI-XIVDyubWNrpZG8HpxLweydzb0Ba9G97cvMBGEadMhjCxu54-lOyHoDqFstqOPZL8MlczxWFtcz1tM2EwBv0HZ6Tq7lCKQ0a5BeAyNWrJnoHIAlMxhaYw_Hs-C9hLjj37t5Zv5YrIwBC9gWHvTfpr1ifTL3ETKl6e5LG2Oq0--TflZNAnIYdIRV1OlAly7qhyqupEcjkqoizWDr90OX4lFzQssWY1WLq1fWwI1o0acPSvnRObhUjpfja4ZE92S98kW5BpHIN6qmtCezfLGIWfkhqLqSMfIrrP784kRauKNLxE9l3I6SAmYADCEzVe7nHSLVdwSx4KWCvh5g"}]})
31
+ example jwt, {}, {}, %({"payload":{"xxx":"zzz"},"verified":[],"unverified":["hoegerrenner.info","powlowski.info"]})
32
+ end
33
+
34
+ def test_both_symbols_and_strings_are_supported
35
+ jwt = %({"payload":"eyJpc3MiOiJmb28iLCJiYXIiOnsiYmF6IjoicXV4In19","signatures":[{"protected":"eyJhbGciOiJIUzUxMiJ9","header":{"kid":"ebert.biz"},"signature":"1koPnSwejNF5aCRsqlySX9Td7_gc-dfUkko5G0Svccw-WkBYrwoJJwRJ2Op_-OxjoqSe3ViBGGCbgVUz0khuJQ"},{"protected":"eyJhbGciOiJIUzI1NiJ9","header":{"kid":"wisoky.co"},"signature":"AqtFKTlaVDqg2dOfLBODMhcBlg1gm9ejn6hYQynTyto"},{"protected":"eyJhbGciOiJSUzM4NCJ9","header":{"kid":"hoegerrenner.info"},"signature":"LR9TpJTLwgducdCkN1KmfwXXxd3pp7Xe5fJXJZZM8FVrFrVOEAGQcPnMPIgfPA1UckIXnzih46j4qPOQdotVHEvYvUuvLLT8QQi8y6-vBMlsP-cQehKGpI1T4N5qPzvJqPmhVzZYedWzlvr-VV9wd0BYeBgr65m9BSpFjLFhWVH4NJZuHFPxeYuDEpYoM-lPHdTzdf1E8xd_xwbpz9WpNh0MQib387-wakGWz-UGt9BmJLU8KV01FTAoR0EO9rQfIm5HQ3wGQ7t8U4N4HsOmsXkWF_fRgxjhMHeChDES2awwB4G4KCNw-6ezSBCD7FZcxzbCL2657OEPHNuHA36M91j54jjm1tweYhYJxuUOk5c8j_wSxtieeaORCxOrPp3mshHS_FE0sI_TNNBsIDI_sQwiS08y3d6tv7H4a_MZj_Pe7JWJ3TXlcsaSHy3xuSLYxCZQeLBwJtyz2ERCZOA9ew0BY34tpRwDKxbgF51X7t7uilYxnBn2rBdQeWQKb9q2"}]})
36
+ keychain = {
37
+ "hoegerrenner.info": public_keychain["hoegerrenner.info"],
38
+ "wisoky.co": public_keychain["wisoky.co"],
39
+ "ebert.biz" => public_keychain["ebert.biz"] }
40
+ example jwt, keychain, { iss: "foo" }, %({"payload":{"iss":"foo","bar":{"baz":"qux"}},"verified":["ebert.biz","wisoky.co","hoegerrenner.info"],"unverified":[]})
41
+ end
42
+
43
+ private
44
+
45
+ def example(jwt, keychain, options, expected)
46
+ returned = JWT::Multisig.verify_jwt(JSON.parse(jwt), keychain, options)
47
+ assert_equal expected, JSON.dump(returned)
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwt-multi-signatures
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.9
5
+ platform: ruby
6
+ authors:
7
+ - Vitalspec.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.4.7
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.4.7
55
+ description: The tool for working with JWT signed by multiple verificators as per
56
+ RFC 7515. Based on the RubyGem «jwt» under the hood.
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".drone.yml"
63
+ - ".gitignore"
64
+ - ".rubocop.yml"
65
+ - ".ruby-version"
66
+ - ".travis.yml"
67
+ - Gemfile
68
+ - Gemfile.lock
69
+ - LICENSE.md
70
+ - README.md
71
+ - Rakefile
72
+ - jwt-multi-signatures.gemspec
73
+ - lib/jwt-multi-signatures.rb
74
+ - lib/jwt-multi-signatures/version.rb
75
+ - metadata
76
+ - test/test-helper.rb
77
+ - test/test-jws-generator.rb
78
+ - test/test-jws-verificator.rb
79
+ - test/test-jwt-editor.rb
80
+ - test/test-jwt-generator.rb
81
+ - test/test-jwt-verificator.rb
82
+ homepage: https://github.com/vitalspec/jwt-multi-signatures
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.0.8
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: The tool for working with multi-signature JWT.
105
+ test_files: []