jwt 2.5.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3098671a837e7b291103cde1921277c61ecaa0f0797b955e6adc65328498f0d
4
- data.tar.gz: 3253833ac6d7743e40a5d5157b161cd0daecc9b77f61dfa7687d6b3da1be56ca
3
+ metadata.gz: 6c18ec5fbed5aff7aa65bfe9e7893583c677d0d18269c1ebd9cff7761916e298
4
+ data.tar.gz: e1e270fff52673d769982888b97c741a4b5c38b34214ee9d547857c0244ff0db
5
5
  SHA512:
6
- metadata.gz: 306c946b1199301a3f1000c8ffba4a77d07fd05dd83f769da86fd29f254827b5af8488a4b6a54b11f1f7f3a028cb88caafb7ed67528e7004c0337f6506e595ea
7
- data.tar.gz: 57d1eba7a06bc9d9f9fcb76b42aa3808415af5020c53969b4cada890b1646e7d348a96ce18010ab0a978e42825febbeb7b3f205b72e8ce60ef90132cf5887599
6
+ metadata.gz: 452b6056da93ed535d8e93fc17d3ec69105a623b217f38d816ab1dc298dbcb93b1e45143732c3d13b752f421495e3b56f8cf51b5a8a802570bc5832072f28a26
7
+ data.tar.gz: 0a5616fd089942547a6222eb6a7fd22f7825e0b7f219336a6e5904f58ede7bf58e454390c2ebcb2cc026951549d739661c515db1ec3cda9d2ede7009ea668bc8
data/CHANGELOG.md CHANGED
@@ -1,22 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.6.0](https://github.com/jwt/ruby-jwt/tree/v2.6.0) (2022-12-22)
3
4
 
4
- ## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (NEXT)
5
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.5.0...v2.6.0)
5
6
 
6
- [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...master)
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)).
13
+
14
+ **Fixes and enhancements:**
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)
7
21
 
8
22
  **Features:**
9
23
 
10
24
  - Support JWK thumbprints as key ids [#481](https://github.com/jwt/ruby-jwt/pull/481) ([@anakinj](https://github.com/anakinj)).
11
- - Your contribution here
25
+ - Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)).
12
26
 
13
27
  **Fixes and enhancements:**
14
28
  - Bring back the old Base64 (RFC2045) deocode mechanisms [#488](https://github.com/jwt/ruby-jwt/pull/488) ([@anakinj](https://github.com/anakinj)).
15
29
  - Rescue RbNaCl exception for EdDSA wrong key [#491](https://github.com/jwt/ruby-jwt/pull/491) ([@n-studio](https://github.com/n-studio)).
16
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)).
17
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)).
18
- - Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)).
19
- - Your contribution here
20
32
 
21
33
  ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07)
22
34
 
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,11 +1,10 @@
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
 
@@ -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
 
@@ -160,7 +159,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
160
159
  gem 'rbnacl'
161
160
  ```
162
161
 
163
- 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.
164
163
 
165
164
  * ED25519
166
165
 
@@ -212,6 +211,33 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
212
211
  puts decoded_token
213
212
  ```
214
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
+
215
241
  ## Support for reserved claim names
216
242
  JSON Web Token defines some reserved claim names and defines how they should be
217
243
  used. JWT supports these reserved claim names:
@@ -543,73 +569,112 @@ end
543
569
 
544
570
  ### JSON Web Key (JWK)
545
571
 
546
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. The `jwks` option can be given as a lambda that evaluates every time a kid is resolved.
572
+ JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC and HMAC keys.
547
573
 
548
- If the 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`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
574
+ To encode a JWT using your JWK:
549
575
 
550
576
  ```ruby
551
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid')
552
- payload = { data: 'data' }
553
- headers = { 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])
556
583
 
557
- # The jwk loader would fetch the set of JWKs from a trusted source,
558
- # to avoid malicious requests triggering cache invalidations there needs to be some kind of grace time or other logic for determining the validity of the invalidation.
559
- # This example only allows cache invalidations every 5 minutes.
560
- jwk_loader = ->(options) do
561
- if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
562
- logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
563
- @cached_keys = nil
564
- end
565
- @cached_keys ||= begin
566
- @cache_last_update = Time.now.to_i
567
- { keys: [jwk.export] }
568
- end
569
- end
570
-
571
- begin
572
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
573
- rescue JWT::JWKError
574
- # Handle problems with the provided JWKs
575
- rescue JWT::DecodeError
576
- # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
577
- end
584
+ # JSON Web Key Set for advertising your signing keys
585
+ jwks_hash = JWT::JWK::Set.new(jwk).export
578
586
  ```
579
587
 
580
- or by passing the JWKs as a simple Hash
588
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
581
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)
582
595
  ```
583
- jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
584
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
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
621
+ end
622
+
623
+ begin
624
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
625
+ rescue JWT::JWKError
626
+ # Handle problems with the provided JWKs
627
+ rescue JWT::DecodeError
628
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
629
+ end
585
630
  ```
586
631
 
587
632
  ### Importing and exporting JSON Web Keys
588
633
 
589
- 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.
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.
636
+
637
+ To include the private key in the export pass the `include_private` parameter to the export method.
590
638
 
591
639
  ```ruby
592
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
640
+ # Import a JWK Hash (showing an HMAC example)
641
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
593
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)
594
649
  jwk_hash = jwk.export
595
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
596
660
  ```
597
661
 
598
662
  ### Key ID (kid) and JWKs
599
663
 
600
- The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization.
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.
601
667
 
602
668
  ```ruby
603
669
  JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
604
670
  # OR
605
671
  JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
606
672
  # OR
607
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint)
673
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
608
674
 
609
675
  jwk_hash = jwk.export
610
676
 
611
677
  thumbprint_as_the_kid = jwk_hash[:kid]
612
-
613
678
  ```
614
679
 
615
680
  # Development and Tests
@@ -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,8 +18,7 @@ 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
@@ -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
data/lib/jwt/algos/ps.rb CHANGED
@@ -9,11 +9,9 @@ module JWT
9
9
 
10
10
  SUPPORTED = %w[PS256 PS384 PS512].freeze
11
11
 
12
- def sign(to_sign)
12
+ def sign(algorithm, msg, key)
13
13
  require_openssl!
14
14
 
15
- algorithm, msg, key = to_sign.values
16
-
17
15
  key_class = key.class
18
16
 
19
17
  raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String
@@ -23,10 +21,10 @@ module JWT
23
21
  key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
24
22
  end
25
23
 
26
- def verify(to_verify)
24
+ def verify(algorithm, public_key, signing_input, signature)
27
25
  require_openssl!
28
26
 
29
- SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
27
+ SecurityUtils.verify_ps(algorithm, public_key, signing_input, signature)
30
28
  end
31
29
 
32
30
  def require_openssl!
data/lib/jwt/algos/rsa.rb CHANGED
@@ -7,15 +7,14 @@ module JWT
7
7
 
8
8
  SUPPORTED = %w[RS256 RS384 RS512].freeze
9
9
 
10
- def sign(to_sign)
11
- algorithm, msg, key = to_sign.values
10
+ def sign(algorithm, msg, key)
12
11
  raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.instance_of?(String)
13
12
 
14
13
  key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
15
14
  end
16
15
 
17
- def verify(to_verify)
18
- SecurityUtils.verify_rsa(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
16
+ def verify(algorithm, public_key, signing_input, signature)
17
+ SecurityUtils.verify_rsa(algorithm, public_key, signing_input, signature)
19
18
  end
20
19
  end
21
20
  end