jwt 2.4.1 → 2.6.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -2
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +135 -31
  5. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  6. data/lib/jwt/algos/ecdsa.rb +2 -4
  7. data/lib/jwt/algos/eddsa.rb +4 -4
  8. data/lib/jwt/algos/hmac.rb +54 -17
  9. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  10. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  11. data/lib/jwt/algos/none.rb +3 -1
  12. data/lib/jwt/algos/ps.rb +3 -5
  13. data/lib/jwt/algos/rsa.rb +3 -4
  14. data/lib/jwt/algos.rb +38 -15
  15. data/lib/jwt/base64.rb +19 -0
  16. data/lib/jwt/configuration/container.rb +21 -0
  17. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  18. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  19. data/lib/jwt/configuration.rb +15 -0
  20. data/lib/jwt/decode.rb +48 -27
  21. data/lib/jwt/encode.rb +30 -20
  22. data/lib/jwt/jwk/ec.rb +131 -62
  23. data/lib/jwt/jwk/hmac.rb +59 -24
  24. data/lib/jwt/jwk/key_base.rb +43 -7
  25. data/lib/jwt/jwk/key_finder.rb +14 -34
  26. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  27. data/lib/jwt/jwk/rsa.rb +128 -53
  28. data/lib/jwt/jwk/set.rb +80 -0
  29. data/lib/jwt/jwk/thumbprint.rb +26 -0
  30. data/lib/jwt/jwk.rb +13 -11
  31. data/lib/jwt/security_utils.rb +0 -27
  32. data/lib/jwt/version.rb +23 -2
  33. data/lib/jwt/x5c_key_finder.rb +1 -1
  34. data/lib/jwt.rb +5 -4
  35. data/ruby-jwt.gemspec +8 -4
  36. metadata +15 -30
  37. data/.codeclimate.yml +0 -8
  38. data/.github/workflows/coverage.yml +0 -27
  39. data/.github/workflows/test.yml +0 -66
  40. data/.gitignore +0 -13
  41. data/.reek.yml +0 -22
  42. data/.rspec +0 -2
  43. data/.rubocop.yml +0 -67
  44. data/.sourcelevel.yml +0 -17
  45. data/Appraisals +0 -13
  46. data/Gemfile +0 -7
  47. data/Rakefile +0 -16
  48. data/lib/jwt/default_options.rb +0 -18
  49. data/lib/jwt/signature.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e7f3474ee58d51ca5646f48ca28bf669b40a4b7676cbe7211597ca6ae69f672
4
- data.tar.gz: 570e6930c9094afea40ea8e8a6a7c9b3293890b121893f5148914b0a8e7d11f8
3
+ metadata.gz: 6c18ec5fbed5aff7aa65bfe9e7893583c677d0d18269c1ebd9cff7761916e298
4
+ data.tar.gz: e1e270fff52673d769982888b97c741a4b5c38b34214ee9d547857c0244ff0db
5
5
  SHA512:
6
- metadata.gz: 3249529ec6bacc8e655e2830949af61c10e235a569f9dc67d3880335d5939b8afc56c180145d3e02dd09744288d50c31547338e105cf55ae4e0fbe237eb2a0e8
7
- data.tar.gz: dd415314a7bd048d8b2b5b630d5b7011128932bf207dc785ac6154748aff68836a1c39e766dc176e225c643fc406fe9fdc5c510b36dc939e36722e327d8fe92f
6
+ metadata.gz: 452b6056da93ed535d8e93fc17d3ec69105a623b217f38d816ab1dc298dbcb93b1e45143732c3d13b752f421495e3b56f8cf51b5a8a802570bc5832072f28a26
7
+ data.tar.gz: 0a5616fd089942547a6222eb6a7fd22f7825e0b7f219336a6e5904f58ede7bf58e454390c2ebcb2cc026951549d739661c515db1ec3cda9d2ede7009ea668bc8
data/CHANGELOG.md CHANGED
@@ -1,11 +1,42 @@
1
1
  # Changelog
2
- ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07)
2
+
3
+ ## [v2.6.0](https://github.com/jwt/ruby-jwt/tree/v2.6.0) (2022-12-22)
4
+
5
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.5.0...v2.6.0)
6
+
7
+ **Features:**
8
+
9
+ - Support custom algorithms by passing algorithm objects[#512](https://github.com/jwt/ruby-jwt/pull/512) ([@anakinj](https://github.com/anakinj)).
10
+ - Support descriptive (not key related) JWK parameters[#520](https://github.com/jwt/ruby-jwt/pull/520) ([@bellebaum](https://github.com/bellebaum)).
11
+ - Support for JSON Web Key Sets[#525](https://github.com/jwt/ruby-jwt/pull/525) ([@bellebaum](https://github.com/bellebaum)).
12
+ - Support HMAC keys over 32 chars when using RbNaCl[#521](https://github.com/jwt/ruby-jwt/pull/521) ([@anakinj](https://github.com/anakinj)).
3
13
 
4
14
  **Fixes and enhancements:**
5
- - Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)).
15
+
16
+ - Raise descriptive error on empty hmac_secret and OpenSSL 3.0/openssl gem <3.0.1[#530](https://github.com/jwt/ruby-jwt/pull/530) ([@jonmchan](https://github.com/jonmchan)).
17
+
18
+ ## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (2022-08-25)
19
+
20
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...v2.5.0)
21
+
22
+ **Features:**
23
+
24
+ - Support JWK thumbprints as key ids [#481](https://github.com/jwt/ruby-jwt/pull/481) ([@anakinj](https://github.com/anakinj)).
25
+ - Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)).
26
+
27
+ **Fixes and enhancements:**
28
+ - Bring back the old Base64 (RFC2045) deocode mechanisms [#488](https://github.com/jwt/ruby-jwt/pull/488) ([@anakinj](https://github.com/anakinj)).
29
+ - Rescue RbNaCl exception for EdDSA wrong key [#491](https://github.com/jwt/ruby-jwt/pull/491) ([@n-studio](https://github.com/n-studio)).
30
+ - New parameter name for cases when kid is not found using JWK key loader proc [#501](https://github.com/jwt/ruby-jwt/pull/501) ([@anakinj](https://github.com/anakinj)).
31
+ - Fix NoMethodError when a 2 segment token is missing 'alg' header [#502](https://github.com/jwt/ruby-jwt/pull/502) ([@cmrd-senya](https://github.com/cmrd-senya)).
32
+
33
+ ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07)
6
34
 
7
35
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.0...v2.4.1)
8
36
 
37
+ **Fixes and enhancements:**
38
+ - Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)).
39
+
9
40
  ## [v2.4.0](https://github.com/jwt/ruby-jwt/tree/v2.4.0) (2022-06-06)
10
41
 
11
42
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.3.0...v2.4.0)
data/CONTRIBUTING.md CHANGED
@@ -12,19 +12,19 @@ git remote add upstream https://github.com/jwt/ruby-jwt
12
12
 
13
13
  ## Create a branch for your implementation
14
14
 
15
- Make sure you have the latest upstream master branch of the project.
15
+ Make sure you have the latest upstream main branch of the project.
16
16
 
17
17
  ```
18
18
  git fetch --all
19
- git checkout master
20
- git rebase upstream/master
21
- git push origin master
19
+ git checkout main
20
+ git rebase upstream/main
21
+ git push origin main
22
22
  git checkout -b fix-a-little-problem
23
23
  ```
24
24
 
25
25
  ## Running the tests and linter
26
26
 
27
- Before you start with your implementation make sure you are able to get a succesful test run with the current revision.
27
+ Before you start with your implementation make sure you are able to get a successful test run with the current revision.
28
28
 
29
29
  The tests are written with rspec and [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
30
30
 
@@ -78,12 +78,12 @@ A maintainer will review and probably merge you changes when time allows, be pat
78
78
 
79
79
  ## Keeping your branch up-to-date
80
80
 
81
- It's recommended that you keep your branch up-to-date by rebasing to the upstream master.
81
+ It's recommended that you keep your branch up-to-date by rebasing to the upstream main.
82
82
 
83
83
  ```
84
84
  git fetch upstream
85
85
  git checkout fix-a-little-problem
86
- git rebase upstream/master
86
+ git rebase upstream/main
87
87
  git push origin fix-a-little-problem -f
88
88
  ```
89
89
 
data/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # JWT
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/jwt.svg)](https://badge.fury.io/rb/jwt)
4
- [![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=master)](https://github.com/jwt/ruby-jwt/actions)
4
+ [![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=main)](https://github.com/jwt/ruby-jwt/actions)
5
5
  [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
6
6
  [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
7
7
  [![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
8
- [![SourceLevel](https://app.sourcelevel.io/github/jwt/-/ruby-jwt.svg)](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
9
8
 
10
9
  A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
11
10
 
12
11
  If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
13
12
 
14
13
  ## Announcements
15
- * Ruby 2.4 support is going to be dropped in version 2.4.0
14
+ * Ruby 2.4 support was dropped in version 2.4.0
16
15
  * Ruby 1.9.3 support was dropped at December 31st, 2016.
17
16
  * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133)
18
17
 
@@ -78,7 +77,7 @@ puts decoded_token
78
77
  * HS512 - HMAC using SHA-512 hash algorithm
79
78
 
80
79
  ```ruby
81
- # The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
80
+ # The secret must be a string. With OpenSSL 3.0/openssl gem `<3.0.1`, JWT::DecodeError will be raised if it isn't provided.
82
81
  hmac_secret = 'my$ecretK3y'
83
82
 
84
83
  token = JWT.encode payload, hmac_secret, 'HS256'
@@ -96,9 +95,9 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
96
95
  puts decoded_token
97
96
  ```
98
97
 
99
- Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512-256, and HMAC-SHA512. RbNaCl enforces a maximum key size of 32 bytes for these algorithms.
98
+ Note: If [RbNaCl](https://github.com/RubyCrypto/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512-256, and HMAC-SHA512. RbNaCl prior to 6.0.0 only support a maximum key size of 32 bytes for these algorithms.
100
99
 
101
- [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
100
+ [RbNaCl](https://github.com/RubyCrypto/rbnacl) requires
102
101
  [libsodium](https://github.com/jedisct1/libsodium), it can be installed
103
102
  on MacOS with `brew install libsodium`.
104
103
 
@@ -135,17 +134,14 @@ puts decoded_token
135
134
  * ES256K - ECDSA using P-256K and SHA-256
136
135
 
137
136
  ```ruby
138
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
139
- ecdsa_key.generate_key
140
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
141
- ecdsa_public.private_key = nil
137
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
142
138
 
143
139
  token = JWT.encode payload, ecdsa_key, 'ES256'
144
140
 
145
141
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
146
142
  puts token
147
143
 
148
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
144
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
149
145
 
150
146
  # Array
151
147
  # [
@@ -163,7 +159,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
163
159
  gem 'rbnacl'
164
160
  ```
165
161
 
166
- For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
162
+ For more detailed installation instruction check the official [repository](https://github.com/RubyCrypto/rbnacl) on GitHub.
167
163
 
168
164
  * ED25519
169
165
 
@@ -186,7 +182,7 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
186
182
 
187
183
  ### **RSASSA-PSS**
188
184
 
189
- In order to use this algorithm you need to add the `openssl` gem to you `Gemfile` with a version greater or equal to `2.1`.
185
+ In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`.
190
186
 
191
187
  ```ruby
192
188
  gem 'openssl', '~> 2.1'
@@ -215,6 +211,33 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
215
211
  puts decoded_token
216
212
  ```
217
213
 
214
+ ### **Custom algorithms**
215
+
216
+ An object implementing custom signing or verification behaviour can be passed in the `algorithm` option when encoding and decoding. The given object needs to implement the method `valid_alg?` and `verify` and/or `alg` and `sign`, depending if object is used for encoding or decoding.
217
+
218
+ ```ruby
219
+ module CustomHS512Algorithm
220
+ def self.alg
221
+ 'HS512'
222
+ end
223
+
224
+ def self.valid_alg?(alg_to_validate)
225
+ alg_to_validate == alg
226
+ end
227
+
228
+ def self.sign(data:, signing_key:)
229
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), data, signing_key)
230
+ end
231
+
232
+ def self.verify(data:, signature:, verification_key:)
233
+ ::OpenSSL.secure_compare(sign(data: data, signing_key: verification_key), signature)
234
+ end
235
+ end
236
+
237
+ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
238
+ payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
239
+ ```
240
+
218
241
  ## Support for reserved claim names
219
242
  JSON Web Token defines some reserved claim names and defines how they should be
220
243
  used. JWT supports these reserved claim names:
@@ -546,22 +569,59 @@ end
546
569
 
547
570
  ### JSON Web Key (JWK)
548
571
 
549
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys.
572
+ JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC and HMAC keys.
573
+
574
+ To encode a JWT using your JWK:
550
575
 
551
576
  ```ruby
552
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
553
- payload, headers = { data: 'data' }, { kid: jwk.kid }
577
+ optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
578
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
554
579
 
555
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
580
+ # Encoding
581
+ payload = { data: 'data' }
582
+ token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid])
583
+
584
+ # JSON Web Key Set for advertising your signing keys
585
+ jwks_hash = JWT::JWK::Set.new(jwk).export
586
+ ```
556
587
 
557
- # The jwk loader would fetch the set of JWKs from a trusted source
558
- jwk_loader = ->(options) do
559
- @cached_keys = nil if options[:invalidate] # need to reload the keys
560
- @cached_keys ||= { keys: [jwk.export] }
588
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
589
+
590
+ ```ruby
591
+ jwks = JWT::JWK::Set.new(jwks_hash)
592
+ jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
593
+ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
594
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
595
+ ```
596
+
597
+
598
+ The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
599
+ This can be used to implement caching of remotely fetched JWK Sets.
600
+
601
+ If the requested `kid` is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`.
602
+ The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
603
+
604
+ ```ruby
605
+ jwks_loader = ->(options) do
606
+ # The jwk loader would fetch the set of JWKs from a trusted source.
607
+ # To avoid malicious requests triggering cache invalidations there needs to be
608
+ # some kind of grace time or other logic for determining the validity of the invalidation.
609
+ # This example only allows cache invalidations every 5 minutes.
610
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
611
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
612
+ @cached_keys = nil
613
+ end
614
+ @cached_keys ||= begin
615
+ @cache_last_update = Time.now.to_i
616
+ # Replace with your own JWKS fetching routine
617
+ jwks = JWT::JWK::Set.new(jwks_hash)
618
+ jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
619
+ jwks
620
+ end
561
621
  end
562
622
 
563
623
  begin
564
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
624
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
565
625
  rescue JWT::JWKError
566
626
  # Handle problems with the provided JWKs
567
627
  rescue JWT::DecodeError
@@ -569,26 +629,70 @@ rescue JWT::DecodeError
569
629
  end
570
630
  ```
571
631
 
572
- or by passing JWK as a simple Hash
632
+ ### Importing and exporting JSON Web Keys
633
+
634
+ The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys
635
+ and export to either format with and without the private key included.
573
636
 
574
- ```
575
- jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
576
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
637
+ To include the private key in the export pass the `include_private` parameter to the export method.
638
+
639
+ ```ruby
640
+ # Import a JWK Hash (showing an HMAC example)
641
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
642
+
643
+ # Import an OpenSSL key
644
+ # You can optionally add descriptive parameters to the JWK
645
+ desc_params = { kid: 'my-kid', use: 'sig' }
646
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params)
647
+
648
+ # Export as JWK Hash (public key only by default)
649
+ jwk_hash = jwk.export
650
+ jwk_hash_with_private_key = jwk.export(include_private: true)
651
+
652
+ # Export as OpenSSL key
653
+ public_key = jwk.public_key
654
+ private_key = jwk.keypair if jwk.private?
655
+
656
+ # You can also import and export entire JSON Web Key Sets
657
+ jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
658
+ jwks = JWT::JWK::Set.new(jwks_hash)
659
+ jwks_hash = jwks.export
577
660
  ```
578
661
 
579
- ### Importing and exporting JSON Web Keys
662
+ ### Key ID (kid) and JWKs
580
663
 
581
- The ::JWT::JWK class can be used to import and export both the public key (default behaviour) and the private key. To include the private key in the export pass the `include_private` parameter to the export method.
664
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
665
+ To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
666
+ or can be given to the JWK instance on initialization.
582
667
 
583
668
  ```ruby
584
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
669
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
670
+ # OR
671
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
672
+ # OR
673
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
585
674
 
586
675
  jwk_hash = jwk.export
587
- jwk_hash_with_private_key = jwk.export(include_private: true)
676
+
677
+ thumbprint_as_the_kid = jwk_hash[:kid]
588
678
  ```
589
679
 
590
- ## How to contribute
680
+ # Development and Tests
681
+
682
+ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
683
+
684
+ ```bash
685
+ rake release
686
+ ```
687
+
688
+ The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
689
+
690
+ ```bash
691
+ bundle install
692
+ bundle exec appraisal rake test
693
+ ```
591
694
 
695
+ ## How to contribute
592
696
  See [CONTRIBUTING](CONTRIBUTING.md).
593
697
 
594
698
  ## Contributors
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ class AlgoWrapper
6
+ attr_reader :alg, :cls
7
+
8
+ def initialize(alg, cls)
9
+ @alg = alg
10
+ @cls = cls
11
+ end
12
+
13
+ def valid_alg?(alg_to_check)
14
+ alg.casecmp(alg_to_check)&.zero? == true
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ cls.sign(alg, data, signing_key)
19
+ end
20
+
21
+ def verify(data:, signature:, verification_key:)
22
+ cls.verify(alg, verification_key, data, signature)
23
+ rescue OpenSSL::PKey::PKeyError # These should be moved to the algorithms that actually need this, but left here to ensure nothing will break.
24
+ raise JWT::VerificationError, 'Signature verification raised'
25
+ ensure
26
+ OpenSSL.errors.clear
27
+ end
28
+ end
29
+ end
30
+ end
@@ -30,8 +30,7 @@ module JWT
30
30
 
31
31
  SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
32
32
 
33
- def sign(to_sign)
34
- algorithm, msg, key = to_sign.values
33
+ def sign(algorithm, msg, key)
35
34
  curve_definition = curve_by_name(key.group.curve_name)
36
35
  key_algorithm = curve_definition[:algorithm]
37
36
  if algorithm != key_algorithm
@@ -42,8 +41,7 @@ module JWT
42
41
  SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
43
42
  end
44
43
 
45
- def verify(to_verify)
46
- algorithm, public_key, signing_input, signature = to_verify.values
44
+ def verify(algorithm, public_key, signing_input, signature)
47
45
  curve_definition = curve_by_name(public_key.group.curve_name)
48
46
  key_algorithm = curve_definition[:algorithm]
49
47
  if algorithm != key_algorithm
@@ -7,8 +7,7 @@ module JWT
7
7
 
8
8
  SUPPORTED = %w[ED25519 EdDSA].freeze
9
9
 
10
- def sign(to_sign)
11
- algorithm, msg, key = to_sign.values
10
+ def sign(algorithm, msg, key)
12
11
  if key.class != RbNaCl::Signatures::Ed25519::SigningKey
13
12
  raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
14
13
  end
@@ -19,14 +18,15 @@ module JWT
19
18
  key.sign(msg)
20
19
  end
21
20
 
22
- def verify(to_verify)
23
- algorithm, public_key, signing_input, signature = to_verify.values
21
+ def verify(algorithm, public_key, signing_input, signature)
24
22
  unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
25
23
  raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
26
24
  end
27
25
  raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
28
26
 
29
27
  public_key.verify(signature, signing_input)
28
+ rescue RbNaCl::CryptoError
29
+ false
30
30
  end
31
31
  end
32
32
  end
@@ -5,32 +5,69 @@ module JWT
5
5
  module Hmac
6
6
  module_function
7
7
 
8
- SUPPORTED = %w[HS256 HS512256 HS384 HS512].freeze
8
+ MAPPING = {
9
+ 'HS256' => OpenSSL::Digest::SHA256,
10
+ 'HS384' => OpenSSL::Digest::SHA384,
11
+ 'HS512' => OpenSSL::Digest::SHA512
12
+ }.freeze
9
13
 
10
- def sign(to_sign)
11
- algorithm, msg, key = to_sign.values
14
+ SUPPORTED = MAPPING.keys
15
+
16
+ def sign(algorithm, msg, key)
12
17
  key ||= ''
13
- authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
14
- if authenticator && padded_key
15
- authenticator.auth(padded_key, msg.encode('binary'))
16
- else
17
- OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
18
+
19
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
20
+
21
+ OpenSSL::HMAC.digest(MAPPING[algorithm].new, key, msg)
22
+ rescue OpenSSL::HMACError => e
23
+ if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
24
+ raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret'
18
25
  end
26
+
27
+ raise e
28
+ end
29
+
30
+ def verify(algorithm, key, signing_input, signature)
31
+ SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key))
19
32
  end
20
33
 
21
- def verify(to_verify)
22
- algorithm, public_key, signing_input, signature = to_verify.values
23
- authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
24
- if authenticator && padded_key
25
- begin
26
- authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
27
- rescue RbNaCl::BadAuthenticatorError
28
- false
34
+ # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
35
+ # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
36
+ module SecurityUtils
37
+ # Constant time string comparison, for fixed length strings.
38
+ #
39
+ # The values compared should be of fixed length, such as strings
40
+ # that have already been processed by HMAC. Raises in case of length mismatch.
41
+
42
+ if defined?(OpenSSL.fixed_length_secure_compare)
43
+ def fixed_length_secure_compare(a, b)
44
+ OpenSSL.fixed_length_secure_compare(a, b)
29
45
  end
30
46
  else
31
- SecurityUtils.secure_compare(signature, sign(JWT::Signature::ToSign.new(algorithm, signing_input, public_key)))
47
+ def fixed_length_secure_compare(a, b)
48
+ raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
49
+
50
+ l = a.unpack "C#{a.bytesize}"
51
+
52
+ res = 0
53
+ b.each_byte { |byte| res |= byte ^ l.shift }
54
+ res == 0
55
+ end
56
+ end
57
+ module_function :fixed_length_secure_compare
58
+
59
+ # Secure string comparison for strings of variable length.
60
+ #
61
+ # While a timing attack would not be able to discern the content of
62
+ # a secret compared via secure_compare, it is possible to determine
63
+ # the secret length. This should be considered when using secure_compare
64
+ # to compare weak, short secrets to user input.
65
+ def secure_compare(a, b)
66
+ a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
32
67
  end
68
+ module_function :secure_compare
33
69
  end
70
+ # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
34
71
  end
35
72
  end
36
73
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ module HmacRbNaCl
6
+ module_function
7
+
8
+ MAPPING = {
9
+ 'HS256' => ::RbNaCl::HMAC::SHA256,
10
+ 'HS512256' => ::RbNaCl::HMAC::SHA512256,
11
+ 'HS384' => nil,
12
+ 'HS512' => ::RbNaCl::HMAC::SHA512
13
+ }.freeze
14
+
15
+ SUPPORTED = MAPPING.keys
16
+
17
+ def sign(algorithm, msg, key)
18
+ if (hmac = resolve_algorithm(algorithm))
19
+ hmac.auth(key_for_rbnacl(hmac, key).encode('binary'), msg.encode('binary'))
20
+ else
21
+ Hmac.sign(algorithm, msg, key)
22
+ end
23
+ end
24
+
25
+ def verify(algorithm, key, signing_input, signature)
26
+ if (hmac = resolve_algorithm(algorithm))
27
+ hmac.verify(key_for_rbnacl(hmac, key).encode('binary'), signature.encode('binary'), signing_input.encode('binary'))
28
+ else
29
+ Hmac.verify(algorithm, key, signing_input, signature)
30
+ end
31
+ rescue ::RbNaCl::BadAuthenticatorError
32
+ false
33
+ end
34
+
35
+ def key_for_rbnacl(hmac, key)
36
+ key ||= ''
37
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
38
+
39
+ return padded_empty_key(hmac.key_bytes) if key == ''
40
+
41
+ key
42
+ end
43
+
44
+ def resolve_algorithm(algorithm)
45
+ MAPPING.fetch(algorithm)
46
+ end
47
+
48
+ def padded_empty_key(length)
49
+ Array.new(length, 0x0).pack('C*').encode('binary')
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ module HmacRbNaClFixed
6
+ module_function
7
+
8
+ MAPPING = {
9
+ 'HS256' => ::RbNaCl::HMAC::SHA256,
10
+ 'HS512256' => ::RbNaCl::HMAC::SHA512256,
11
+ 'HS384' => nil,
12
+ 'HS512' => ::RbNaCl::HMAC::SHA512
13
+ }.freeze
14
+
15
+ SUPPORTED = MAPPING.keys
16
+
17
+ def sign(algorithm, msg, key)
18
+ key ||= ''
19
+
20
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
21
+
22
+ if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes
23
+ hmac.auth(padded_key_bytes(key, hmac.key_bytes), msg.encode('binary'))
24
+ else
25
+ Hmac.sign(algorithm, msg, key)
26
+ end
27
+ end
28
+
29
+ def verify(algorithm, key, signing_input, signature)
30
+ key ||= ''
31
+
32
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
33
+
34
+ if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes
35
+ hmac.verify(padded_key_bytes(key, hmac.key_bytes), signature.encode('binary'), signing_input.encode('binary'))
36
+ else
37
+ Hmac.verify(algorithm, key, signing_input, signature)
38
+ end
39
+ rescue ::RbNaCl::BadAuthenticatorError
40
+ false
41
+ end
42
+
43
+ def resolve_algorithm(algorithm)
44
+ MAPPING.fetch(algorithm)
45
+ end
46
+
47
+ def padded_key_bytes(key, bytesize)
48
+ key.bytes.fill(0, key.bytesize...bytesize).pack('C*')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -7,7 +7,9 @@ module JWT
7
7
 
8
8
  SUPPORTED = %w[none].freeze
9
9
 
10
- def sign(*); end
10
+ def sign(*)
11
+ ''
12
+ end
11
13
 
12
14
  def verify(*)
13
15
  true