jwt 2.5.0 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3098671a837e7b291103cde1921277c61ecaa0f0797b955e6adc65328498f0d
4
- data.tar.gz: 3253833ac6d7743e40a5d5157b161cd0daecc9b77f61dfa7687d6b3da1be56ca
3
+ metadata.gz: 11007e8ec36d148026cd5b6761681b0a71437b7b461efba4ae492622fc5ff27b
4
+ data.tar.gz: 8090bbba3dce57e42cc203ef168d3d00c624e79e076de0e949b4390b531b4d55
5
5
  SHA512:
6
- metadata.gz: 306c946b1199301a3f1000c8ffba4a77d07fd05dd83f769da86fd29f254827b5af8488a4b6a54b11f1f7f3a028cb88caafb7ed67528e7004c0337f6506e595ea
7
- data.tar.gz: 57d1eba7a06bc9d9f9fcb76b42aa3808415af5020c53969b4cada890b1646e7d348a96ce18010ab0a978e42825febbeb7b3f205b72e8ce60ef90132cf5887599
6
+ metadata.gz: a035f44be760ad325105329cbaec12e90515afdade9649e798ee5c7cefced3271d4f3084afeef693c03c961541d9e5e45b2876734d1947dccdd24cc194acbca2
7
+ data.tar.gz: 61e3d071ce44809767f3501f12b452cae574f9ea0de668ca937731838d58e2a9fa85831829ce564f6a015177a86b2a05b1b6ddf60ba34ac515ea12c69ac618fe
data/CHANGELOG.md CHANGED
@@ -1,22 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.7.1](https://github.com/jwt/ruby-jwt/tree/v2.8.0) (2023-06-09)
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.7.0...v2.8.0)
5
6
 
6
- [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...master)
7
+ **Fixes and enhancements:**
8
+
9
+ - Handle invalid algorithm when decoding JWT [#559](https://github.com/jwt/ruby-jwt/pull/559) - [@nataliastanko](https://github.com/nataliastanko)
10
+ - Do not raise error when verifying bad HMAC signature [#563](https://github.com/jwt/ruby-jwt/pull/563) - [@hieuk09](https://github.com/hieuk09)
11
+
12
+ ## [v2.7.0](https://github.com/jwt/ruby-jwt/tree/v2.7.0) (2023-02-01)
13
+
14
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.6.0...v2.7.0)
15
+
16
+ **Features:**
17
+
18
+ - Support OKP (Ed25519) keys for JWKs [#540](https://github.com/jwt/ruby-jwt/pull/540) ([@anakinj](https://github.com/anakinj))
19
+ - JWK Sets can now be used for tokens with nil kid [#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum))
20
+
21
+ **Fixes and enhancements:**
22
+
23
+ - Fix issue with multiple keys returned by keyfinder and multiple allowed algorithms [#545](https://github.com/jwt/ruby-jwt/pull/545) ([@mpospelov](https://github.com/mpospelov))
24
+ - Non-string `kid` header values are now rejected [#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum))
25
+
26
+ ## [v2.6.0](https://github.com/jwt/ruby-jwt/tree/v2.6.0) (2022-12-22)
27
+
28
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.5.0...v2.6.0)
29
+
30
+ **Features:**
31
+
32
+ - Support custom algorithms by passing algorithm objects[#512](https://github.com/jwt/ruby-jwt/pull/512) ([@anakinj](https://github.com/anakinj)).
33
+ - Support descriptive (not key related) JWK parameters[#520](https://github.com/jwt/ruby-jwt/pull/520) ([@bellebaum](https://github.com/bellebaum)).
34
+ - Support for JSON Web Key Sets[#525](https://github.com/jwt/ruby-jwt/pull/525) ([@bellebaum](https://github.com/bellebaum)).
35
+ - Support HMAC keys over 32 chars when using RbNaCl[#521](https://github.com/jwt/ruby-jwt/pull/521) ([@anakinj](https://github.com/anakinj)).
36
+
37
+ **Fixes and enhancements:**
38
+
39
+ - 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)).
40
+
41
+ ## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (2022-08-25)
42
+
43
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...v2.5.0)
7
44
 
8
45
  **Features:**
9
46
 
10
47
  - Support JWK thumbprints as key ids [#481](https://github.com/jwt/ruby-jwt/pull/481) ([@anakinj](https://github.com/anakinj)).
11
- - Your contribution here
48
+ - Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)).
12
49
 
13
50
  **Fixes and enhancements:**
14
51
  - Bring back the old Base64 (RFC2045) deocode mechanisms [#488](https://github.com/jwt/ruby-jwt/pull/488) ([@anakinj](https://github.com/anakinj)).
15
52
  - Rescue RbNaCl exception for EdDSA wrong key [#491](https://github.com/jwt/ruby-jwt/pull/491) ([@n-studio](https://github.com/n-studio)).
16
53
  - 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
54
  - 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
55
 
21
56
  ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07)
22
57
 
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,115 @@ 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, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve.
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.signing_key, 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
+ Tokens without a specified `kid` are rejected by default.
605
+ This behaviour may be overwritten by setting the `allow_nil_kid` option for `decode` to `true`.
606
+
607
+ ```ruby
608
+ jwks_loader = ->(options) do
609
+ # The jwk loader would fetch the set of JWKs from a trusted source.
610
+ # To avoid malicious requests triggering cache invalidations there needs to be
611
+ # some kind of grace time or other logic for determining the validity of the invalidation.
612
+ # This example only allows cache invalidations every 5 minutes.
613
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
614
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
615
+ @cached_keys = nil
616
+ end
617
+ @cached_keys ||= begin
618
+ @cache_last_update = Time.now.to_i
619
+ # Replace with your own JWKS fetching routine
620
+ jwks = JWT::JWK::Set.new(jwks_hash)
621
+ jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
622
+ jwks
623
+ end
624
+ end
625
+
626
+ begin
627
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
628
+ rescue JWT::JWKError
629
+ # Handle problems with the provided JWKs
630
+ rescue JWT::DecodeError
631
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
632
+ end
585
633
  ```
586
634
 
587
635
  ### Importing and exporting JSON Web Keys
588
636
 
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.
637
+ The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys
638
+ and export to either format with and without the private key included.
639
+
640
+ To include the private key in the export pass the `include_private` parameter to the export method.
590
641
 
591
642
  ```ruby
592
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
643
+ # Import a JWK Hash (showing an HMAC example)
644
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
593
645
 
646
+ # Import an OpenSSL key
647
+ # You can optionally add descriptive parameters to the JWK
648
+ desc_params = { kid: 'my-kid', use: 'sig' }
649
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params)
650
+
651
+ # Export as JWK Hash (public key only by default)
594
652
  jwk_hash = jwk.export
595
653
  jwk_hash_with_private_key = jwk.export(include_private: true)
654
+
655
+ # Export as OpenSSL key
656
+ public_key = jwk.verify_key
657
+ private_key = jwk.signing_key if jwk.private?
658
+
659
+ # You can also import and export entire JSON Web Key Sets
660
+ jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
661
+ jwks = JWT::JWK::Set.new(jwks_hash)
662
+ jwks_hash = jwks.export
596
663
  ```
597
664
 
598
665
  ### Key ID (kid) and JWKs
599
666
 
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.
667
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
668
+ To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
669
+ or can be given to the JWK instance on initialization.
601
670
 
602
671
  ```ruby
603
672
  JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
604
673
  # OR
605
674
  JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
606
675
  # OR
607
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint)
676
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
608
677
 
609
678
  jwk_hash = jwk.export
610
679
 
611
680
  thumbprint_as_the_kid = jwk_hash[:kid]
612
-
613
681
  ```
614
682
 
615
683
  # Development and Tests
@@ -0,0 +1,26 @@
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
+ end
24
+ end
25
+ end
26
+ 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
@@ -39,11 +38,10 @@ module JWT
39
38
  end
40
39
 
41
40
  digest = OpenSSL::Digest.new(curve_definition[:digest])
42
- SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
41
+ 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
@@ -51,7 +49,9 @@ module JWT
51
49
  end
52
50
 
53
51
  digest = OpenSSL::Digest.new(curve_definition[:digest])
54
- public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
52
+ public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
53
+ rescue OpenSSL::PKey::PKeyError
54
+ raise JWT::VerificationError, 'Signature verification raised'
55
55
  end
56
56
 
57
57
  def curve_by_name(name)
@@ -59,6 +59,18 @@ module JWT
59
59
  raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
60
60
  end
61
61
  end
62
+
63
+ def raw_to_asn1(signature, private_key)
64
+ byte_size = (private_key.group.degree + 7) / 8
65
+ sig_bytes = signature[0..(byte_size - 1)]
66
+ sig_char = signature[byte_size..-1] || ''
67
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
68
+ end
69
+
70
+ def asn1_to_raw(signature, public_key)
71
+ byte_size = (public_key.group.degree + 7) / 8
72
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
73
+ end
62
74
  end
63
75
  end
64
76
  end
@@ -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, ::RbNaCl::LengthError
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, ::RbNaCl::LengthError
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