jwt-multisignature 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7255527131c473ac91de7bd0acf3cbcd2cc04feaa9676372b68f2450732d207d
4
+ data.tar.gz: db3a0beb3825b06cf7679e947328ef93efab64b9da34a7ba4ba89e01a3018c4a
5
+ SHA512:
6
+ metadata.gz: 8d726976082a6a8b12125f2fd1902d750c4a9168d61d86ebfa65a10d58481a4abc825395fc9bdadb04826d9d57266c0aa8023823b0cc550654136538811c9e4d
7
+ data.tar.gz: bdb1a6561c14f7ea0caaf197948878c7ac8f0b2514c33d0dee4d7034405387ca8f947f8f8de4e2c7d73291d750d08f9ed26f88ef9e1193e69029c4df0ee051d6
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.5.0
data/.travis.yml ADDED
@@ -0,0 +1,21 @@
1
+ language: ruby
2
+
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - 2.2
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
10
+
11
+ env:
12
+ - RAKE_ENV=test BUNDLE_PATH=vendor/bundle
13
+
14
+ before_install:
15
+ - gem install bundler
16
+
17
+ install:
18
+ - bundle install
19
+
20
+ script:
21
+ - 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,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jwt-multisignature (1.0.1)
5
+ activesupport (>= 4.0, < 6.0)
6
+ jwt (~> 2.1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (5.2.3)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 0.7, < 2)
14
+ minitest (~> 5.1)
15
+ tzinfo (~> 1.1)
16
+ concurrent-ruby (1.1.5)
17
+ i18n (1.6.0)
18
+ concurrent-ruby (~> 1.0)
19
+ jwt (2.2.1)
20
+ memoist (0.16.0)
21
+ minitest (5.11.3)
22
+ power_assert (1.1.4)
23
+ rake (12.3.2)
24
+ test-unit (3.3.3)
25
+ power_assert
26
+ thread_safe (0.3.6)
27
+ tzinfo (1.2.5)
28
+ thread_safe (~> 0.1)
29
+
30
+ PLATFORMS
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ bundler (~> 1.16)
35
+ jwt-multisignature!
36
+ memoist (~> 0.16)
37
+ rake (~> 12.3)
38
+ test-unit (~> 3.1)
39
+
40
+ BUNDLED WITH
41
+ 1.17.2
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2018 Yaroslav Konoplov <eahome00@gmail.com>
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # JWT::Multisignature
2
+
3
+ ## Usage
4
+
5
+ `JWT::Multisignature.generate_jwt(payload, private_keychain, algorithms)`
6
+
7
+ `JWT::Multisignature.generate_jws(payload, key_id, key_value, algorithm)`
8
+
9
+ `JWT::Multisignature.verify_jwt(jwt, public_keychain, options)`
10
+
11
+ `JWT::Multisignature.verify_jws(jws, payload, public_keychain, options)`
12
+
13
+ `JWT::Multisignature.add_jws(jwt, key_id, key_value, algorithm)`
14
+
15
+ `JWT::Multisignature.remove_jws(jwt, key_id)`
16
+
17
+ The full documentation is available at [rubydoc.info](http://www.rubydoc.info/gems/jwt-multisignature).
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,23 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "lib/jwt-multisignature/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "jwt-multisignature"
8
+ s.version = JWT::Multisignature::VERSION
9
+ s.author = "Yaroslav Konoplov"
10
+ s.email = "eahome00@gmail.com"
11
+ s.summary = "Implements JWT with multiple signatures (RFC 7515)."
12
+ s.description = "The gem implements support of RFC 7515 providing easy way to create JWT and add/remove/verify signatures."
13
+ s.homepage = "https://github.com/yivo/jwt-multisignature"
14
+ s.license = "Apache-2.0"
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
+ s.required_ruby_version = "~> 2.5"
19
+
20
+ s.add_dependency "jwt", "~> 2.1"
21
+ s.add_dependency "activesupport", ">= 4.0", "< 6.0"
22
+ s.add_development_dependency "bundler", "~> 1.16"
23
+ end
@@ -0,0 +1,252 @@
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 Multisignature
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
+ algorithms_mapping = algorithms.with_indifferent_access
43
+ { payload: base64_encode(JSON.generate(payload)),
44
+ signatures: private_keychain.map do |id, value|
45
+ generate_jws(payload, id, value, algorithms_mapping.fetch(id))
46
+ end }
47
+ end
48
+
49
+ #
50
+ # Generates and adds new JWS to existing JWT.
51
+ #
52
+ # @param jwt [Hash]
53
+ # The existing JWT.
54
+ # @param key_id [String]
55
+ # The JWS key ID.
56
+ # @param key_value [String, OpenSSL::PKey::PKey]
57
+ # The private key in PEM format or as instance of {OpenSSL::PKey::PKey}.
58
+ # @param algorithm [String]
59
+ # The signature algorithm.
60
+ # @return [Hash]
61
+ # The JWT with added JWS.
62
+ # @raise [JWT::EncodeError]
63
+ def add_jws(jwt, key_id, key_value, algorithm)
64
+ remove_jws(jwt, key_id).tap do |new_jwt|
65
+ payload = JSON.parse(base64_decode(new_jwt.fetch(:payload)))
66
+ new_jwt.fetch(:signatures) << generate_jws(payload, key_id, key_value, algorithm)
67
+ end
68
+ end
69
+
70
+ #
71
+ # Removes all JWS associated with given key ID.
72
+ #
73
+ # @param jwt [Hash]
74
+ # The existing JWT.
75
+ # @param key_id [String]
76
+ # The key ID to match JWS by.
77
+ # @return [Hash]
78
+ # The JWT with all matched JWS removed.
79
+ def remove_jws(jwt, key_id)
80
+ jwt.deep_symbolize_keys.tap do |new_jwt|
81
+ new_jwt[:signatures] = new_jwt.fetch(:signatures, []).reject do |jws|
82
+ jws.fetch(:header).fetch(:kid) == key_id
83
+ end
84
+ end
85
+ end
86
+
87
+ #
88
+ # Verifies JWT.
89
+ #
90
+ # @param jwt [Hash]
91
+ # The JWT in the format as defined in RFC 7515.
92
+ # Example:
93
+ # { "payload" => "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
94
+ # "signatures" => [
95
+ # { "protected" => "eyJhbGciOiJSUzI1NiJ9",
96
+ # "header" => { "kid" => "2010-12-29" },
97
+ # "signature" => "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"
98
+ # },
99
+ # { "protected" => "eyJhbGciOiJFUzI1NiJ9",
100
+ # "header" => { "kid" => "e9bc097a-ce51-4036-9562-d2ade882db0d" },
101
+ # "signature" => "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
102
+ # }
103
+ # ]
104
+ # }
105
+ # @param public_keychain [Hash]
106
+ # The hash which consists of pairs: key ID => public key.
107
+ # The key may be presented as string in PEM format or as instance of {OpenSSL::PKey::PKey}.
108
+ # The implementation only verifies signatures for which public key exists in keychain.
109
+ # @param options [Hash]
110
+ # The rules for verifying JWT. The variable «algorithms» is always overwritten by the value from JWS header.
111
+ # @return [Hash]
112
+ # The returning value contains payload, list of verified, and unverified signatures (key ID).
113
+ # Example:
114
+ # { payload: { sub: "session", profile: { email: "username@mailbox.example" },
115
+ # verified: [:"backend-1.mycompany.example", :"backend-3.mycompany.example"],
116
+ # unverified: [:"backend-2.mycompany.example"] }
117
+ # }
118
+ # @raise [JWT::DecodeError]
119
+ def verify_jwt(jwt, public_keychain, options = {})
120
+ keychain = public_keychain.with_indifferent_access
121
+ serialized_payload = base64_decode(jwt.fetch("payload"))
122
+ payload = JSON.parse(serialized_payload)
123
+ verified = []
124
+ unverified = []
125
+
126
+ jwt.fetch("signatures").each do |jws|
127
+ key_id = jws.fetch("header").fetch("kid")
128
+ if keychain.key?(key_id)
129
+ verify_jws(jws, payload, public_keychain, options)
130
+ verified << key_id
131
+ else
132
+ unverified << key_id
133
+ end
134
+ end
135
+ { payload: payload.deep_symbolize_keys,
136
+ verified: verified.uniq.map(&:to_sym),
137
+ unverified: unverified.uniq.map(&:to_sym) }
138
+ end
139
+
140
+ #
141
+ # Generates new JWS based on payload, key, and algorithm.
142
+ #
143
+ # @param payload [Hash]
144
+ # @param key_id [String]
145
+ # The value which is used as «kid» in JWS header.
146
+ # @param key_value [String, OpenSSL::PKey::PKey]
147
+ # The private key.
148
+ # @param algorithm [String]
149
+ # The signature algorithm.
150
+ # @return [Hash]
151
+ # The JWS in the format as defined in RFC 7515.
152
+ # Example:
153
+ # { protected: "eyJhbGciOiJFUzI1NiJ9",
154
+ # header: {
155
+ # kid: "e9bc097a-ce51-4036-9562-d2ade882db0d"
156
+ # },
157
+ # signature: "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
158
+ # }
159
+ # @raise [JWT::EncodeError]
160
+ def generate_jws(payload, key_id, key_value, algorithm)
161
+ protected, _, signature = JWT.encode(payload, to_pem_or_key(key_value, algorithm), algorithm).split(".")
162
+ { protected: protected,
163
+ header: { kid: key_id },
164
+ signature: signature }
165
+ end
166
+
167
+ #
168
+ # Verifies JWS.
169
+ #
170
+ # @param jws [Hash]
171
+ # The JWS in the format as defined in RFC 7515.
172
+ # Example:
173
+ # { "protected" => "eyJhbGciOiJFUzI1NiJ9",
174
+ # "header" => {
175
+ # "kid" => "e9bc097a-ce51-4036-9562-d2ade882db0d"
176
+ # },
177
+ # "signature" => "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
178
+ # }
179
+ # @param payload [Hash]
180
+ # @param public_keychain [Hash]
181
+ # The hash which consists of pairs: key ID => public key.
182
+ # The key may be presented as string in PEM format or as instance of {OpenSSL::PKey::PKey}.
183
+ # @param options [Hash]
184
+ # The rules for verifying JWT. The variable «algorithms» is always overwritten by the value from JWS header.
185
+ # @return [Hash]
186
+ # Returns payload if signature is valid.
187
+ # @raise [JWT::DecodeError]
188
+ def verify_jws(jws, payload, public_keychain, options = {})
189
+ encoded_header = jws.fetch("protected")
190
+ serialized_header = base64_decode(encoded_header)
191
+ serialized_payload = JSON.generate(payload)
192
+ encoded_payload = base64_encode(serialized_payload)
193
+ signature = jws.fetch("signature")
194
+ public_key = public_keychain.with_indifferent_access.fetch(jws.fetch("header").fetch("kid"))
195
+ jwt = [encoded_header, encoded_payload, signature].join(".")
196
+ algorithm = JSON.parse(serialized_header).fetch("alg")
197
+ JWT.decode(jwt, to_pem_or_key(public_key, algorithm), true, options.merge(algorithms: [algorithm])).first
198
+ end
199
+
200
+ private
201
+
202
+ #
203
+ # Transforms key into string (PEM format) or returns as {OpenSSL::PKey::PKey} depending on given algorithm.
204
+ # This operation is needed to satisfy {JWT#encode} and {JWT#decode} APIs.
205
+ #
206
+ # @param key [String, OpenSSL::PKey::PKey]
207
+ # @param algorithm [String]
208
+ # @return [String, OpenSSL::PKey::PKey]
209
+ # Returns PEM for HMAC algorithms, {OpenSSL::PKey::PKey} in other cases.
210
+ def to_pem_or_key(key, algorithm)
211
+ if algorithm.start_with?("HS")
212
+ OpenSSL::PKey::PKey === key ? key.to_pem : key
213
+ else
214
+ OpenSSL::PKey::PKey === key ? key : OpenSSL::PKey.read(key)
215
+ end
216
+ end
217
+
218
+ #
219
+ # Encodes string in Base64 format (URL-safe).
220
+ #
221
+ # @param string [String]
222
+ # @return [String]
223
+ if JWT::Encode.respond_to?(:base64url_encode)
224
+ def base64_encode(string)
225
+ JWT::Encode.base64url_encode(string)
226
+ end
227
+ else
228
+ def base64_encode(string)
229
+ JWT::Base64.url_encode(string)
230
+ end
231
+ end
232
+
233
+
234
+ #
235
+ # Decodes string from Base64 format (URL-safe).
236
+ #
237
+ # @param string [String]
238
+ # @return [String]
239
+ if JWT::Decode.respond_to?(:base64url_decode)
240
+ def base64_decode(string)
241
+ JWT::Decode.base64url_decode(string)
242
+ end
243
+ else
244
+ def base64_decode(string)
245
+ JWT::Base64.url_decode(string)
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ JWT::Multisig = JWT::Multisignature # Compatibility.
@@ -0,0 +1,8 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module JWT
5
+ module Multisignature
6
+ VERSION = "1.0.1"
7
+ end
8
+ end
@@ -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::Multisignature.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::Multisignature.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,85 @@
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
+ # Pass instance of OpenSSL::PKey::PKey.
78
+ returned = JWT::Multisignature.verify_jws(JSON.parse(jws), payload, public_keychain, options)
79
+ assert_equal expected, JSON.dump(returned)
80
+
81
+ # Pass key in PEM format.
82
+ returned = JWT::Multisignature.verify_jws(JSON.parse(jws), payload, public_keychain, options)
83
+ assert_equal expected, JSON.dump(returned)
84
+ end
85
+ 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::Multisignature.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::Multisignature.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::Multisignature.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::Multisignature.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::Multisignature.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::Multisignature.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::Multisignature.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::Multisignature.verify_jwt(JSON.parse(jwt), keychain, options)
47
+ assert_equal expected, JSON.dump(returned)
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwt-multisignature
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yaroslav Konoplov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-31 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.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
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
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '6.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '4.0'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.16'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.16'
61
+ description: The gem implements support of RFC 7515 providing easy way to create JWT
62
+ and add/remove/verify signatures.
63
+ email: eahome00@gmail.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ".gitignore"
69
+ - ".rubocop.yml"
70
+ - ".ruby-version"
71
+ - ".travis.yml"
72
+ - Gemfile
73
+ - Gemfile.lock
74
+ - LICENSE
75
+ - README.md
76
+ - Rakefile
77
+ - jwt-multisignature.gemspec
78
+ - lib/jwt-multisignature.rb
79
+ - lib/jwt-multisignature/version.rb
80
+ - test/test-helper.rb
81
+ - test/test-jws-generator.rb
82
+ - test/test-jws-verificator.rb
83
+ - test/test-jwt-editor.rb
84
+ - test/test-jwt-generator.rb
85
+ - test/test-jwt-verificator.rb
86
+ homepage: https://github.com/yivo/jwt-multisignature
87
+ licenses:
88
+ - Apache-2.0
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '2.5'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.0.1
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Implements JWT with multiple signatures (RFC 7515).
109
+ test_files:
110
+ - test/test-helper.rb
111
+ - test/test-jws-generator.rb
112
+ - test/test-jws-verificator.rb
113
+ - test/test-jwt-editor.rb
114
+ - test/test-jwt-generator.rb
115
+ - test/test-jwt-verificator.rb