jose 0.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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