jwt 2.5.0 → 2.7.1
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 +4 -4
- data/CHANGELOG.md +40 -5
- data/CONTRIBUTING.md +7 -7
- data/README.md +109 -41
- data/lib/jwt/algos/algo_wrapper.rb +26 -0
- data/lib/jwt/algos/ecdsa.rb +18 -6
- data/lib/jwt/algos/eddsa.rb +2 -4
- data/lib/jwt/algos/hmac.rb +54 -17
- data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
- data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
- data/lib/jwt/algos/none.rb +3 -1
- data/lib/jwt/algos/ps.rb +7 -9
- data/lib/jwt/algos/rsa.rb +5 -4
- data/lib/jwt/algos.rb +37 -15
- data/lib/jwt/decode.rb +46 -23
- data/lib/jwt/encode.rb +29 -19
- data/lib/jwt/jwk/ec.rb +153 -116
- data/lib/jwt/jwk/hmac.rb +64 -28
- data/lib/jwt/jwk/key_base.rb +31 -11
- data/lib/jwt/jwk/key_finder.rb +19 -35
- data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
- data/lib/jwt/jwk/rsa.rb +142 -77
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk.rb +14 -11
- data/lib/jwt/version.rb +18 -2
- data/ruby-jwt.gemspec +8 -4
- metadata +10 -31
- data/.codeclimate.yml +0 -8
- data/.github/workflows/coverage.yml +0 -27
- data/.github/workflows/test.yml +0 -67
- data/.gitignore +0 -13
- data/.reek.yml +0 -22
- data/.rspec +0 -2
- data/.rubocop.yml +0 -67
- data/.sourcelevel.yml +0 -17
- data/Appraisals +0 -13
- data/Gemfile +0 -7
- data/Rakefile +0 -16
- data/lib/jwt/security_utils.rb +0 -59
- data/lib/jwt/signature.rb +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11007e8ec36d148026cd5b6761681b0a71437b7b461efba4ae492622fc5ff27b
|
4
|
+
data.tar.gz: 8090bbba3dce57e42cc203ef168d3d00c624e79e076de0e949b4390b531b4d55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
5
|
+
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.7.0...v2.8.0)
|
5
6
|
|
6
|
-
|
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
|
-
-
|
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
|
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
|
20
|
-
git rebase upstream/
|
21
|
-
git push origin
|
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
|
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
|
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/
|
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
|
[](https://badge.fury.io/rb/jwt)
|
4
|
-
[](https://github.com/jwt/ruby-jwt/actions)
|
5
5
|
[](https://codeclimate.com/github/jwt/ruby-jwt)
|
6
6
|
[](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
|
7
7
|
[](https://codeclimate.com/github/jwt/ruby-jwt)
|
8
|
-
[](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.
|
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/
|
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/
|
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/
|
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.
|
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
|
-
|
574
|
+
To encode a JWT using your JWK:
|
549
575
|
|
550
576
|
```ruby
|
551
|
-
|
552
|
-
|
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
|
-
|
580
|
+
# Encoding
|
581
|
+
payload = { data: 'data' }
|
582
|
+
token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
|
556
583
|
|
557
|
-
|
558
|
-
|
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
|
-
|
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
|
-
|
584
|
-
|
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
|
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
|
-
|
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.
|
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
|
data/lib/jwt/algos/ecdsa.rb
CHANGED
@@ -30,8 +30,7 @@ module JWT
|
|
30
30
|
|
31
31
|
SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
|
32
32
|
|
33
|
-
def 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
|
-
|
41
|
+
asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
|
43
42
|
end
|
44
43
|
|
45
|
-
def 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),
|
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
|
data/lib/jwt/algos/eddsa.rb
CHANGED
@@ -7,8 +7,7 @@ module JWT
|
|
7
7
|
|
8
8
|
SUPPORTED = %w[ED25519 EdDSA].freeze
|
9
9
|
|
10
|
-
def 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(
|
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
|
data/lib/jwt/algos/hmac.rb
CHANGED
@@ -5,32 +5,69 @@ module JWT
|
|
5
5
|
module Hmac
|
6
6
|
module_function
|
7
7
|
|
8
|
-
|
8
|
+
MAPPING = {
|
9
|
+
'HS256' => OpenSSL::Digest::SHA256,
|
10
|
+
'HS384' => OpenSSL::Digest::SHA384,
|
11
|
+
'HS512' => OpenSSL::Digest::SHA512
|
12
|
+
}.freeze
|
9
13
|
|
10
|
-
|
11
|
-
|
14
|
+
SUPPORTED = MAPPING.keys
|
15
|
+
|
16
|
+
def sign(algorithm, msg, key)
|
12
17
|
key ||= ''
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|