jwt-multisignature 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +107 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +41 -0
- data/LICENSE +201 -0
- data/README.md +17 -0
- data/Rakefile +8 -0
- data/jwt-multisignature.gemspec +23 -0
- data/lib/jwt-multisignature.rb +252 -0
- data/lib/jwt-multisignature/version.rb +8 -0
- data/test/test-helper.rb +51 -0
- data/test/test-jws-generator.rb +32 -0
- data/test/test-jws-verificator.rb +85 -0
- data/test/test-jwt-editor.rb +29 -0
- data/test/test-jwt-generator.rb +56 -0
- data/test/test-jwt-verificator.rb +49 -0
- metadata +115 -0
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
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
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,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.
|
data/test/test-helper.rb
ADDED
@@ -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
|