jose 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # @title Signature Algorithms
2
+
3
+ # Signature Algorithms
4
+
5
+ The basic parameters for a {JOSE::JWS JOSE::JWS} header are:
6
+
7
+ - `"alg"` **(required)** - Cryptographic Algorithm used to secure the JWS.
8
+
9
+ See [RFC 7515](https://tools.ietf.org/html/rfc7515#section-4.1) for more information about other header parameters.
10
+
11
+ ### `alg` Header Parameter
12
+
13
+ Here are the supported options for the `alg` parameter, grouped by similar funcionality:
14
+
15
+ - Elliptic Curve Digital Signature Algorithm (ECDSA)
16
+ - [`ES256`](http://www.rubydoc.info/gems/jose/JOSE/JWS#ECDSA-group)
17
+ - [`ES384`](http://www.rubydoc.info/gems/jose/JOSE/JWS#ECDSA-group)
18
+ - [`ES512`](http://www.rubydoc.info/gems/jose/JOSE/JWS#ECDSA-group)
19
+ - Edwards-curve Digital Signature Algorithm (EdDSA)
20
+ - [`Ed25519`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-25519-group)
21
+ - [`Ed25519ph`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-25519-group)
22
+ - [`Ed448`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-448-group)
23
+ - [`Ed448ph`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-448-group)
24
+ - HMAC using SHA-2
25
+ - [`HS256`](http://www.rubydoc.info/gems/jose/JOSE/JWS#HMACSHA2-group)
26
+ - [`HS384`](http://www.rubydoc.info/gems/jose/JOSE/JWS#HMACSHA2-group)
27
+ - [`HS512`](http://www.rubydoc.info/gems/jose/JOSE/JWS#HMACSHA2-group)
28
+ - RSASSA PSS using SHA-2 and MGF1 with SHA-2
29
+ - [`PS256`](http://www.rubydoc.info/gems/jose/JOSE/JWS#RSASSAPSS-group)
30
+ - [`PS384`](http://www.rubydoc.info/gems/jose/JOSE/JWS#RSASSAPSS-group)
31
+ - [`PS512`](http://www.rubydoc.info/gems/jose/JOSE/JWS#RSASSAPSS-group)
32
+ - RSASSA PKCS#1.5 using SHA-2
33
+ - [`RS256`](http://www.rubydoc.info/gems/jose/JOSE/JWS#RSASSAPKCS1_5-group)
34
+ - [`RS384`](http://www.rubydoc.info/gems/jose/JOSE/JWS#RSASSAPKCS1_5-group)
35
+ - [`RS512`](http://www.rubydoc.info/gems/jose/JOSE/JWS#RSASSAPKCS1_5-group)
@@ -6,68 +6,129 @@ require 'json'
6
6
  require 'openssl'
7
7
  require 'thread'
8
8
 
9
- module JOSE
10
- class Map < Hamster::Hash; end
11
- end
12
-
9
+ # JOSE stands for JSON Object Signing and Encryption which is a is a set of
10
+ # standards established by the [JOSE Working Group](https://datatracker.ietf.org/wg/jose).
11
+ #
12
+ # JOSE is split into 5 main components:
13
+ #
14
+ # * {JOSE::JWA JOSE::JWA} - JSON Web Algorithms (JWA) {https://tools.ietf.org/html/rfc7518 RFC 7518}
15
+ # * {JOSE::JWE JOSE::JWE} - JSON Web Encryption (JWE) {https://tools.ietf.org/html/rfc7516 RFC 7516}
16
+ # * {JOSE::JWK JOSE::JWK} - JSON Web Key (JWK) {https://tools.ietf.org/html/rfc7517 RFC 7517}
17
+ # * {JOSE::JWS JOSE::JWS} - JSON Web Signature (JWS) {https://tools.ietf.org/html/rfc7515 RFC 7515}
18
+ # * {JOSE::JWT JOSE::JWT} - JSON Web Token (JWT) {https://tools.ietf.org/html/rfc7519 RFC 7519}
19
+ #
20
+ # Additional specifications and drafts implemented:
21
+ #
22
+ # * JSON Web Key (JWK) Thumbprint [RFC 7638](https://tools.ietf.org/html/rfc7638)
23
+ # * JWS Unencoded Payload Option [draft-ietf-jose-jws-signing-input-options-04](https://tools.ietf.org/html/draft-ietf-jose-jws-signing-input-options-04)
13
24
  module JOSE
14
25
 
15
- extend self
16
-
26
+ # @!visibility private
17
27
  MUTEX = Mutex.new
18
28
 
29
+ # Immutable Map structure based on `Hamster::Hash`.
30
+ class Map < Hamster::Hash; end
31
+
19
32
  @__crypto_fallback__ = ENV['JOSE_CRYPTO_FALLBACK'] ? true : false
20
33
  @__unsecured_signing__ = ENV['JOSE_UNSECURED_SIGNING'] ? true : false
21
34
 
22
- def __crypto_fallback__
35
+ # Gets the current Cryptographic Algorithm Fallback state, defaults to `false`.
36
+ # @return [Boolean]
37
+ def self.crypto_fallback
23
38
  return @__crypto_fallback__
24
39
  end
25
40
 
26
- def __crypto_fallback__=(boolean)
41
+ # Sets the current Cryptographic Algorithm Fallback state.
42
+ # @param [Boolean] boolean
43
+ # @return [Boolean]
44
+ def self.crypto_fallback=(boolean)
27
45
  boolean = !!boolean
28
46
  MUTEX.synchronize {
29
47
  @__crypto_fallback__ = boolean
30
48
  __config_change__
31
49
  }
50
+ return boolean
32
51
  end
33
52
 
34
- def __curve25519_module__
53
+ # Gets the current Curve25519 module used by {JOSE::JWA::Curve25519 JOSE::JWA::Curve25519}, see {.curve25519_module=} for default.
54
+ # @return [Module]
55
+ def self.curve25519_module
35
56
  return JOSE::JWA::Curve25519.__implementation__
36
57
  end
37
58
 
38
- def __curve25519_module__=(m)
39
- JOSE::JWA::Curve25519.__implementation__ = m
59
+ # Sets the current Curve25519 module used by {JOSE::JWA::Curve25519 JOSE::JWA::Curve25519}.
60
+ #
61
+ # Currently supported Curve25519 modules (first found is used as default):
62
+ #
63
+ # * {https://github.com/cryptosphere/rbnacl `RbNaCl`}
64
+ # * {JOSE::JWA::Curve25519_Ruby JOSE::JWA::Curve25519_Ruby} - only supported when {.crypto_fallback} is `true`
65
+ #
66
+ # Additional modules that implement the functions specified in {JOSE::JWA::Curve25519 JOSE::JWA::Curve25519} may also be used.
67
+ # @param [Module] mod
68
+ # @return [Module]
69
+ def self.curve25519_module=(mod)
70
+ JOSE::JWA::Curve25519.__implementation__ = mod
40
71
  end
41
72
 
42
- def __curve448_module__
73
+ # Gets the current Curve448 module used by {JOSE::JWA::Curve448 JOSE::JWA::Curve448}, see {.curve25519_module=} for default.
74
+ # @return [Module]
75
+ def self.curve448_module
43
76
  return JOSE::JWA::Curve448.__implementation__
44
77
  end
45
78
 
46
- def __curve448_module__=(m)
47
- JOSE::JWA::Curve448.__implementation__ = m
79
+ # Sets the current Curve448 module used by {JOSE::JWA::Curve448 JOSE::JWA::Curve448}.
80
+ #
81
+ # Currently supported Curve448 modules (first found is used as default):
82
+ #
83
+ # * {JOSE::JWA::Curve448_Ruby JOSE::JWA::Curve448_Ruby} - only supported when {.crypto_fallback} is `true`
84
+ #
85
+ # Additional modules that implement the functions specified in {JOSE::JWA::Curve448 JOSE::JWA::Curve448} may also be used.
86
+ # @param [Module] mod
87
+ # @return [Module]
88
+ def self.curve448_module=(mod)
89
+ JOSE::JWA::Curve448.__implementation__ = mod
48
90
  end
49
91
 
50
- def decode(binary)
92
+ # Decode JSON binary to a term.
93
+ # @param [String] binary
94
+ # @return [Object]
95
+ def self.decode(binary)
51
96
  return JSON.load(binary)
52
97
  end
53
98
 
54
- def encode(term)
99
+ # Encode a term to JSON binary and sorts `Hash` and {JOSE::Map JOSE::Map} keys.
100
+ # @param [Object] term
101
+ # @return [Object]
102
+ def self.encode(term)
55
103
  return JSON.dump(sort_maps(term))
56
104
  end
57
105
 
58
- def __unsecured_signing__
106
+ # Gets the current Unsecured Signing state, defaults to `false`.
107
+ # @return [Boolean]
108
+ def self.unsecured_signing
59
109
  return @__unsecured_signing__
60
110
  end
61
111
 
62
- def __unsecured_signing__=(boolean)
112
+ # Sets the current Unsecured Signing state.
113
+ #
114
+ # Enables/disables the `"none"` algorithm used for signing and verifying.
115
+ #
116
+ # See {https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ Critical vulnerabilities in JSON Web Token libraries} for more information.
117
+ # @param [Boolean] boolean
118
+ # @return [Boolean]
119
+ def self.unsecured_signing=(boolean)
63
120
  boolean = !!boolean
64
121
  MUTEX.synchronize {
65
122
  @__unsecured_signing__ = boolean
66
123
  __config_change__
67
124
  }
125
+ return boolean
68
126
  end
69
127
 
70
- def urlsafe_decode64(binary)
128
+ # Returns the Base64Url decoded version of `binary` without padding.
129
+ # @param [String] binary
130
+ # @return [String]
131
+ def self.urlsafe_decode64(binary)
71
132
  case binary.bytesize % 4
72
133
  when 2
73
134
  binary += '=='
@@ -77,18 +138,21 @@ module JOSE
77
138
  return Base64.urlsafe_decode64(binary)
78
139
  end
79
140
 
80
- def urlsafe_encode64(binary)
141
+ # Returns the Base64Url encoded version of `binary` without padding.
142
+ # @param [String] binary
143
+ # @return [String]
144
+ def self.urlsafe_encode64(binary)
81
145
  return Base64.urlsafe_encode64(binary).tr('=', '')
82
146
  end
83
147
 
84
148
  private
85
149
 
86
- def __config_change__
150
+ def self.__config_change__
87
151
  JOSE::JWA::Curve25519.__config_change__
88
152
  JOSE::JWA::Curve448.__config_change__
89
153
  end
90
154
 
91
- def sort_maps(term)
155
+ def self.sort_maps(term)
92
156
  case term
93
157
  when Hash, JOSE::Map
94
158
  return term.keys.sort.each_with_object(Hash.new) do |key, hash|
@@ -1,4 +1,19 @@
1
1
  module JOSE
2
+ # JWA stands for JSON Web Algorithms which is defined in [RFC 7518](https://tools.ietf.org/html/rfc7518).
3
+ #
4
+ # ## Cryptographic Algorithm Fallback
5
+ #
6
+ # Native implementations of all cryptographic and public key algorithms
7
+ # required by the JWA specifications are not present in current versions
8
+ # of Ruby.
9
+ #
10
+ # JOSE will detect whether a specific algorithm is natively supported or not
11
+ # and, by default, it will mark the algorithm as unsupported if a native
12
+ # implementation is not found.
13
+ #
14
+ # However, JOSE also has pure Ruby versions of many of the missing algorithms
15
+ # which can be used as a fallback by calling {JOSE.crypto_fallback= JOSE.crypto_fallback=} and
16
+ # passing `true`.
2
17
  module JWA
3
18
 
4
19
  extend self
@@ -6,6 +21,10 @@ module JOSE
6
21
  UCHAR_PACK = 'C*'.freeze
7
22
  ZERO_PAD = [0].pack('C').force_encoding('BINARY').freeze
8
23
 
24
+ # Performs a constant time comparison between two binaries to help avoid [timing attacks](https://en.wikipedia.org/wiki/Timing_attack).
25
+ # @param [String] a
26
+ # @param [String] b
27
+ # @return [Boolean]
9
28
  def constant_time_compare(a, b)
10
29
  return false if a.empty? || b.empty? || a.bytesize != b.bytesize
11
30
  l = a.unpack "C#{a.bytesize}"
@@ -15,6 +34,7 @@ module JOSE
15
34
  return res == 0
16
35
  end
17
36
 
37
+ # @!visibility private
18
38
  def exor(a, b)
19
39
  a = a.to_bn if a.respond_to?(:to_bn)
20
40
  b = b.to_bn if b.respond_to?(:to_bn)
@@ -29,6 +49,279 @@ module JOSE
29
49
  end.reverse.pack(UCHAR_PACK), 2)
30
50
  end
31
51
 
52
+ # Returns the current listing of supported JOSE algorithms.
53
+ #
54
+ # !!!ruby
55
+ # JOSE::JWA.supports
56
+ # # => {:jwe=>
57
+ # # {:alg=>
58
+ # # ["A128GCMKW",
59
+ # # "A192GCMKW",
60
+ # # "A256GCMKW",
61
+ # # "A128KW",
62
+ # # "A192KW",
63
+ # # "A256KW",
64
+ # # "ECDH-ES",
65
+ # # "ECDH-ES+A128KW",
66
+ # # "ECDH-ES+A192KW",
67
+ # # "ECDH-ES+A256KW",
68
+ # # "PBES2-HS256+A128KW",
69
+ # # "PBES2-HS384+A192KW",
70
+ # # "PBES2-HS512+A256KW",
71
+ # # "RSA1_5",
72
+ # # "RSA-OAEP",
73
+ # # "RSA-OAEP-256",
74
+ # # "dir"],
75
+ # # :enc=>
76
+ # # ["A128GCM",
77
+ # # "A192GCM",
78
+ # # "A256GCM",
79
+ # # "A128CBC-HS256",
80
+ # # "A192CBC-HS384",
81
+ # # "A256CBC-HS512"],
82
+ # # :zip=>
83
+ # # ["DEF"]},
84
+ # # :jwk=>
85
+ # # {:kty=>
86
+ # # ["EC",
87
+ # # "OKP",
88
+ # # "RSA",
89
+ # # "oct"],
90
+ # # :kty_EC_crv=>
91
+ # # ["P-256",
92
+ # # "P-384",
93
+ # # "P-521"],
94
+ # # :kty_OKP_crv=>
95
+ # # ["Ed25519",
96
+ # # "Ed25519ph",
97
+ # # "Ed448",
98
+ # # "Ed448ph",
99
+ # # "X25519",
100
+ # # "X448"]},
101
+ # # :jws=>
102
+ # # {:alg=>
103
+ # # ["Ed25519",
104
+ # # "Ed25519ph",
105
+ # # "Ed448",
106
+ # # "Ed448ph",
107
+ # # "ES256",
108
+ # # "ES384",
109
+ # # "ES512",
110
+ # # "HS256",
111
+ # # "HS384",
112
+ # # "HS512",
113
+ # # "PS256",
114
+ # # "PS384",
115
+ # # "PS512",
116
+ # # "RS256",
117
+ # # "RS384",
118
+ # # "RS512",
119
+ # # "none"]}}
120
+ #
121
+ # @return [Hash]
122
+ def supports
123
+ jwe_enc = __jwe_enc_support_check__([
124
+ 'A128GCM',
125
+ 'A192GCM',
126
+ 'A256GCM',
127
+ 'A128CBC-HS256',
128
+ 'A192CBC-HS384',
129
+ 'A256CBC-HS512'
130
+ ])
131
+ jwe_alg = __jwe_alg_support_check__([
132
+ ['A128GCMKW', :block],
133
+ ['A192GCMKW', :block],
134
+ ['A256GCMKW', :block],
135
+ ['A128KW', :block],
136
+ ['A192KW', :block],
137
+ ['A256KW', :block],
138
+ ['ECDH-ES', :box],
139
+ ['ECDH-ES+A128KW', :box],
140
+ ['ECDH-ES+A192KW', :box],
141
+ ['ECDH-ES+A256KW', :box],
142
+ ['PBES2-HS256+A128KW', :block],
143
+ ['PBES2-HS384+A192KW', :block],
144
+ ['PBES2-HS512+A256KW', :block],
145
+ ['RSA1_5', :rsa],
146
+ ['RSA-OAEP', :rsa],
147
+ ['RSA-OAEP-256', :rsa],
148
+ ['dir', :direct]
149
+ ], jwe_enc)
150
+ jwe_zip = __jwe_zip_support_check__([
151
+ 'DEF'
152
+ ], jwe_enc)
153
+ jwk_kty, jwk_kty_EC_crv, jwk_kty_OKP_crv = __jwk_kty_support_check__([
154
+ ['EC', ['P-256', 'P-384', 'P-521']],
155
+ ['OKP', ['Ed25519', 'Ed25519ph', 'Ed448', 'Ed448ph', 'X25519', 'X448']],
156
+ 'RSA',
157
+ 'oct'
158
+ ])
159
+ jws_alg = __jws_alg_support_check__([
160
+ 'Ed25519',
161
+ 'Ed25519ph',
162
+ 'Ed448',
163
+ 'Ed448ph',
164
+ 'ES256',
165
+ 'ES384',
166
+ 'ES512',
167
+ 'HS256',
168
+ 'HS384',
169
+ 'HS512',
170
+ 'PS256',
171
+ 'PS384',
172
+ 'PS512',
173
+ 'RS256',
174
+ 'RS384',
175
+ 'RS512',
176
+ 'X25519',
177
+ 'X448',
178
+ 'none'
179
+ ])
180
+ return {
181
+ jwe: {
182
+ alg: jwe_alg,
183
+ enc: jwe_enc,
184
+ zip: jwe_zip
185
+ },
186
+ jwk: {
187
+ kty: jwk_kty,
188
+ kty_EC_crv: jwk_kty_EC_crv,
189
+ kty_OKP_crv: jwk_kty_OKP_crv
190
+ },
191
+ jws: {
192
+ alg: jws_alg
193
+ }
194
+ }
195
+ end
196
+
197
+ private
198
+
199
+ def __jwe_enc_support_check__(encryption_algorithms)
200
+ plain_text = SecureRandom.random_bytes(16)
201
+ return encryption_algorithms.select do |enc|
202
+ begin
203
+ jwe = JOSE::JWE.from({ 'alg' => 'dir', 'enc' => enc })
204
+ jwk = jwe.generate_key
205
+ cipher_text = jwk.block_encrypt(plain_text).compact
206
+ next jwk.block_decrypt(cipher_text).first == plain_text
207
+ rescue StandardError, NotImplementedError
208
+ next false
209
+ end
210
+ end
211
+ end
212
+
213
+ def __jwe_alg_support_check__(key_algorithms, encryption_algorithms)
214
+ return [] if encryption_algorithms.empty?
215
+ plain_text = SecureRandom.random_bytes(16)
216
+ enc = encryption_algorithms[0]
217
+ rsa = nil
218
+ return key_algorithms.select do |(alg, strategy)|
219
+ begin
220
+ if strategy == :block
221
+ jwe = JOSE::JWE.from({ 'alg' => alg, 'enc' => enc })
222
+ jwk = jwe.generate_key
223
+ cipher_text = jwk.block_encrypt(plain_text).compact
224
+ next jwk.block_decrypt(cipher_text).first == plain_text
225
+ elsif strategy == :box
226
+ jwe = JOSE::JWE.from({ 'alg' => alg, 'enc' => enc })
227
+ send_jwk = jwe.generate_key
228
+ recv_jwk = jwe.generate_key
229
+ cipher_text = recv_jwk.box_encrypt(plain_text, send_jwk).compact
230
+ next recv_jwk.box_decrypt(cipher_text).first == plain_text
231
+ elsif strategy == :rsa
232
+ rsa ||= JOSE::JWK.generate_key([:rsa, 1024])
233
+ cipher_text = rsa.block_encrypt(plain_text, { 'alg' => alg, 'enc' => enc }).compact
234
+ next rsa.block_decrypt(cipher_text).first == plain_text
235
+ elsif strategy == :direct
236
+ next true
237
+ else
238
+ next false
239
+ end
240
+ rescue StandardError, NotImplementedError
241
+ next false
242
+ end
243
+ end.transpose.first
244
+ end
245
+
246
+ def __jwe_zip_support_check__(zip_algorithms, encryption_algorithms)
247
+ return [] if encryption_algorithms.empty?
248
+ plain_text = SecureRandom.random_bytes(16)
249
+ alg = 'dir'
250
+ enc = encryption_algorithms[0]
251
+ return zip_algorithms.select do |zip|
252
+ begin
253
+ jwe = JOSE::JWE.from({ 'alg' => alg, 'enc' => enc, 'zip' => zip })
254
+ jwk = jwe.generate_key
255
+ cipher_text = jwk.block_encrypt(plain_text, jwe).compact
256
+ next jwk.block_decrypt(cipher_text).first == plain_text
257
+ rescue StandardError, NotImplementedError
258
+ next false
259
+ end
260
+ end
261
+ end
262
+
263
+ def __jwk_kty_support_check__(key_types)
264
+ kty = []
265
+ kty_EC_crv = []
266
+ kty_OKP_crv = []
267
+ key_types.each do |(key_type, curves)|
268
+ case key_type
269
+ when 'EC'
270
+ curves.each do |curve|
271
+ begin
272
+ JOSE::JWK.generate_key([:ec, curve])
273
+ kty_EC_crv.push(curve)
274
+ rescue StandardError, NotImplementedError
275
+ next
276
+ end
277
+ end
278
+ kty.push(key_type) if not kty_EC_crv.empty?
279
+ when 'OKP'
280
+ curves.each do |curve|
281
+ begin
282
+ JOSE::JWK.generate_key([:okp, curve.to_sym])
283
+ kty_OKP_crv.push(curve)
284
+ rescue StandardError, NotImplementedError
285
+ next
286
+ end
287
+ end
288
+ kty.push(key_type) if not kty_OKP_crv.empty?
289
+ when 'RSA'
290
+ begin
291
+ JOSE::JWK.generate_key([:rsa, 256])
292
+ kty.push(key_type)
293
+ rescue StandardError, NotImplementedError
294
+ # do nothing
295
+ end
296
+ when 'oct'
297
+ begin
298
+ JOSE::JWK.generate_key([:oct, 8])
299
+ kty.push(key_type)
300
+ rescue StandardError, NotImplementedError
301
+ # do nothing
302
+ end
303
+ end
304
+ end
305
+ return kty, kty_EC_crv, kty_OKP_crv
306
+ end
307
+
308
+ def __jws_alg_support_check__(signature_algorithms)
309
+ plain_text = SecureRandom.random_bytes(16)
310
+ rsa = nil
311
+ return signature_algorithms.select do |alg|
312
+ begin
313
+ jwk = nil
314
+ jwk ||= JOSE::JWK.generate_key([:oct, 0]).merge({ 'alg' => alg, 'use' => 'sig' }) if alg == 'none'
315
+ jwk ||= (rsa ||= JOSE::JWK.generate_key([:rsa, 1024])).merge({ 'alg' => alg, 'use' => 'sig' }) if alg.start_with?('RS') or alg.start_with?('PS')
316
+ jwk ||= JOSE::JWS.generate_key({ 'alg' => alg })
317
+ signed_text = jwk.sign(plain_text).compact
318
+ next jwk.verify_strict(signed_text, [alg]).first
319
+ rescue StandardError, NotImplementedError
320
+ next false
321
+ end
322
+ end
323
+ end
324
+
32
325
  end
33
326
  end
34
327