jwt 2.3.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +60 -53
  3. data/CHANGELOG.md +73 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +188 -40
  7. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  8. data/lib/jwt/algos/ecdsa.rb +39 -12
  9. data/lib/jwt/algos/eddsa.rb +7 -4
  10. data/lib/jwt/algos/hmac.rb +56 -17
  11. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  12. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  13. data/lib/jwt/algos/none.rb +5 -1
  14. data/lib/jwt/algos/ps.rb +6 -8
  15. data/lib/jwt/algos/rsa.rb +7 -5
  16. data/lib/jwt/algos/unsupported.rb +2 -0
  17. data/lib/jwt/algos.rb +38 -15
  18. data/lib/jwt/claims_validator.rb +3 -1
  19. data/lib/jwt/configuration/container.rb +21 -0
  20. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  21. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  22. data/lib/jwt/configuration.rb +15 -0
  23. data/lib/jwt/decode.rb +83 -26
  24. data/lib/jwt/encode.rb +30 -20
  25. data/lib/jwt/error.rb +1 -0
  26. data/lib/jwt/jwk/ec.rb +147 -61
  27. data/lib/jwt/jwk/hmac.rb +69 -24
  28. data/lib/jwt/jwk/key_base.rb +43 -6
  29. data/lib/jwt/jwk/key_finder.rb +19 -35
  30. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  31. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  32. data/lib/jwt/jwk/rsa.rb +142 -54
  33. data/lib/jwt/jwk/set.rb +80 -0
  34. data/lib/jwt/jwk/thumbprint.rb +26 -0
  35. data/lib/jwt/jwk.rb +15 -11
  36. data/lib/jwt/security_utils.rb +2 -27
  37. data/lib/jwt/verify.rb +10 -2
  38. data/lib/jwt/version.rb +22 -2
  39. data/lib/jwt/x5c_key_finder.rb +55 -0
  40. data/lib/jwt.rb +5 -4
  41. data/ruby-jwt.gemspec +12 -5
  42. metadata +20 -16
  43. data/.github/workflows/test.yml +0 -74
  44. data/.gitignore +0 -11
  45. data/.rspec +0 -2
  46. data/.rubocop.yml +0 -97
  47. data/.rubocop_todo.yml +0 -185
  48. data/.sourcelevel.yml +0 -18
  49. data/Appraisals +0 -10
  50. data/Gemfile +0 -5
  51. data/Rakefile +0 -14
  52. data/lib/jwt/default_options.rb +0 -16
  53. data/lib/jwt/signature.rb +0 -39
data/README.md CHANGED
@@ -1,21 +1,22 @@
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
-
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
 
18
+ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
19
+
19
20
  ## Sponsors
20
21
 
21
22
  |Logo|Message|
@@ -42,7 +43,7 @@ The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cr
42
43
 
43
44
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
44
45
 
45
- **NONE**
46
+ ### **NONE**
46
47
 
47
48
  * none - unsigned token
48
49
 
@@ -68,7 +69,7 @@ decoded_token = JWT.decode token, nil, false
68
69
  puts decoded_token
69
70
  ```
70
71
 
71
- **HMAC**
72
+ ### **HMAC**
72
73
 
73
74
  * HS256 - HMAC using SHA-256 hash algorithm
74
75
  * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
@@ -76,7 +77,7 @@ puts decoded_token
76
77
  * HS512 - HMAC using SHA-512 hash algorithm
77
78
 
78
79
  ```ruby
79
- # 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.
80
81
  hmac_secret = 'my$ecretK3y'
81
82
 
82
83
  token = JWT.encode payload, hmac_secret, 'HS256'
@@ -94,13 +95,13 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
94
95
  puts decoded_token
95
96
  ```
96
97
 
97
- 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.
98
99
 
99
- [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
100
+ [RbNaCl](https://github.com/RubyCrypto/rbnacl) requires
100
101
  [libsodium](https://github.com/jedisct1/libsodium), it can be installed
101
102
  on MacOS with `brew install libsodium`.
102
103
 
103
- **RSA**
104
+ ### **RSA**
104
105
 
105
106
  * RS256 - RSA using SHA-256 hash algorithm
106
107
  * RS384 - RSA using SHA-384 hash algorithm
@@ -125,24 +126,22 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
125
126
  puts decoded_token
126
127
  ```
127
128
 
128
- **ECDSA**
129
+ ### **ECDSA**
129
130
 
130
131
  * ES256 - ECDSA using P-256 and SHA-256
131
132
  * ES384 - ECDSA using P-384 and SHA-384
132
133
  * ES512 - ECDSA using P-521 and SHA-512
134
+ * ES256K - ECDSA using P-256K and SHA-256
133
135
 
134
136
  ```ruby
135
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
136
- ecdsa_key.generate_key
137
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
138
- ecdsa_public.private_key = nil
137
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
139
138
 
140
139
  token = JWT.encode payload, ecdsa_key, 'ES256'
141
140
 
142
141
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
143
142
  puts token
144
143
 
145
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
144
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
146
145
 
147
146
  # Array
148
147
  # [
@@ -152,7 +151,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
152
151
  puts decoded_token
153
152
  ```
154
153
 
155
- **EDDSA**
154
+ ### **EDDSA**
156
155
 
157
156
  In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
158
157
 
@@ -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
 
@@ -181,9 +180,9 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
181
180
 
182
181
  ```
183
182
 
184
- **RSASSA-PSS**
183
+ ### **RSASSA-PSS**
185
184
 
186
- 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`.
187
186
 
188
187
  ```ruby
189
188
  gem 'openssl', '~> 2.1'
@@ -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:
@@ -370,6 +396,36 @@ rescue JWT::InvalidIssuerError
370
396
  end
371
397
  ```
372
398
 
399
+ You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
400
+ On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
401
+ to convert them to proc (using `to_proc`)
402
+
403
+ ```ruby
404
+ JWT.decode token, hmac_secret, true,
405
+ iss: %r'https://my.awesome.website/',
406
+ verify_iss: true,
407
+ algorithm: 'HS256'
408
+ ```
409
+
410
+ ```ruby
411
+ JWT.decode token, hmac_secret, true,
412
+ iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
413
+ verify_iss: true,
414
+ algorithm: 'HS256'
415
+ ```
416
+
417
+ ```ruby
418
+ JWT.decode token, hmac_secret, true,
419
+ iss: method(:valid_issuer?),
420
+ verify_iss: true,
421
+ algorithm: 'HS256'
422
+
423
+ # somewhere in the same class:
424
+ def valid_issuer?(issuer)
425
+ # custom validation
426
+ end
427
+ ```
428
+
373
429
  ### Audience Claim
374
430
 
375
431
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -486,28 +542,89 @@ end
486
542
 
487
543
  You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
488
544
  ```ruby
489
- # Will raise a JWT::ExpiredSignature error if the 'exp' claim is absent
545
+ # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
490
546
  JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
491
547
  ```
492
548
 
549
+ ### X.509 certificates in x5c header
550
+
551
+ A JWT signature can be verified using certificate(s) given in the `x5c` header. Before doing that, the trustworthiness of these certificate(s) must be established. This is done in accordance with RFC 5280 which (among other things) verifies the certificate(s) are issued by a trusted root certificate, the timestamps are valid, and none of the certificate(s) are revoked (i.e. being present in the root certificate's Certificate Revocation List).
552
+
553
+ ```ruby
554
+ root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
555
+ crl_uris = root_certificates.map(&:crl_uris)
556
+ crls = crl_uris.map do |uri|
557
+ # look up cached CRL by `uri` and return it if found, otherwise continue
558
+ crl = Net::HTTP.get(uri)
559
+ crl = OpenSSL::X509::CRL.new(crl)
560
+ # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
561
+ end
562
+
563
+ begin
564
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
565
+ rescue JWT::DecodeError
566
+ # Handle error, e.g. x5c header certificate revoked or expired
567
+ end
568
+ ```
569
+
493
570
  ### JSON Web Key (JWK)
494
571
 
495
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys.
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.
573
+
574
+ To encode a JWT using your JWK:
575
+
576
+ ```ruby
577
+ optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
578
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
579
+
580
+ # Encoding
581
+ payload = { data: 'data' }
582
+ token = JWT.encode(payload, jwk.signing_key, 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
+ ```
587
+
588
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
496
589
 
497
590
  ```ruby
498
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
499
- payload, headers = { data: 'data' }, { kid: jwk.kid }
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
+ ```
500
596
 
501
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
502
597
 
503
- # The jwk loader would fetch the set of JWKs from a trusted source
504
- jwk_loader = ->(options) do
505
- @cached_keys = nil if options[:invalidate] # need to reload the keys
506
- @cached_keys ||= { keys: [jwk.export] }
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_jwks` 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
507
624
  end
508
625
 
509
626
  begin
510
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
627
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
511
628
  rescue JWT::JWKError
512
629
  # Handle problems with the provided JWKs
513
630
  rescue JWT::DecodeError
@@ -515,22 +632,52 @@ rescue JWT::DecodeError
515
632
  end
516
633
  ```
517
634
 
518
- or by passing JWK as a simple Hash
635
+ ### Importing and exporting JSON Web Keys
519
636
 
520
- ```
521
- jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
522
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
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.
641
+
642
+ ```ruby
643
+ # Import a JWK Hash (showing an HMAC example)
644
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
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)
652
+ jwk_hash = jwk.export
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
523
663
  ```
524
664
 
525
- ### Importing and exporting JSON Web Keys
665
+ ### Key ID (kid) and JWKs
526
666
 
527
- 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.
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.
528
670
 
529
671
  ```ruby
530
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
672
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
673
+ # OR
674
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
675
+ # OR
676
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
531
677
 
532
678
  jwk_hash = jwk.export
533
- jwk_hash_with_private_key = jwk.export(include_private: true)
679
+
680
+ thumbprint_as_the_kid = jwk_hash[:kid]
534
681
  ```
535
682
 
536
683
  # Development and Tests
@@ -548,12 +695,13 @@ bundle install
548
695
  bundle exec appraisal rake test
549
696
  ```
550
697
 
551
- **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
698
+ ## How to contribute
699
+ See [CONTRIBUTING](CONTRIBUTING.md).
552
700
 
553
701
  ## Contributors
554
702
 
555
- See `AUTHORS` file.
703
+ See [AUTHORS](AUTHORS).
556
704
 
557
705
  ## License
558
706
 
559
- See `LICENSE` file.
707
+ See [LICENSE](LICENSE).
@@ -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
@@ -1,35 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  module Algos
3
5
  module Ecdsa
4
6
  module_function
5
7
 
6
- SUPPORTED = %w[ES256 ES384 ES512].freeze
7
8
  NAMED_CURVES = {
8
- 'prime256v1' => 'ES256',
9
- 'secp384r1' => 'ES384',
10
- 'secp521r1' => 'ES512'
9
+ 'prime256v1' => {
10
+ algorithm: 'ES256',
11
+ digest: 'sha256'
12
+ },
13
+ 'secp256r1' => { # alias for prime256v1
14
+ algorithm: 'ES256',
15
+ digest: 'sha256'
16
+ },
17
+ 'secp384r1' => {
18
+ algorithm: 'ES384',
19
+ digest: 'sha384'
20
+ },
21
+ 'secp521r1' => {
22
+ algorithm: 'ES512',
23
+ digest: 'sha512'
24
+ },
25
+ 'secp256k1' => {
26
+ algorithm: 'ES256K',
27
+ digest: 'sha256'
28
+ }
11
29
  }.freeze
12
30
 
13
- def sign(to_sign)
14
- algorithm, msg, key = to_sign.values
15
- key_algorithm = NAMED_CURVES[key.group.curve_name]
31
+ SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
32
+
33
+ def sign(algorithm, msg, key)
34
+ curve_definition = curve_by_name(key.group.curve_name)
35
+ key_algorithm = curve_definition[:algorithm]
16
36
  if algorithm != key_algorithm
17
37
  raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
18
38
  end
19
39
 
20
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
40
+ digest = OpenSSL::Digest.new(curve_definition[:digest])
21
41
  SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
22
42
  end
23
43
 
24
- def verify(to_verify)
25
- algorithm, public_key, signing_input, signature = to_verify.values
26
- key_algorithm = NAMED_CURVES[public_key.group.curve_name]
44
+ def verify(algorithm, public_key, signing_input, signature)
45
+ curve_definition = curve_by_name(public_key.group.curve_name)
46
+ key_algorithm = curve_definition[:algorithm]
27
47
  if algorithm != key_algorithm
28
48
  raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
29
49
  end
30
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
50
+
51
+ digest = OpenSSL::Digest.new(curve_definition[:digest])
31
52
  public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
32
53
  end
54
+
55
+ def curve_by_name(name)
56
+ NAMED_CURVES.fetch(name) do
57
+ raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
58
+ end
59
+ end
33
60
  end
34
61
  end
35
62
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  module Algos
3
5
  module Eddsa
@@ -5,8 +7,7 @@ module JWT
5
7
 
6
8
  SUPPORTED = %w[ED25519 EdDSA].freeze
7
9
 
8
- def sign(to_sign)
9
- algorithm, msg, key = to_sign.values
10
+ def sign(algorithm, msg, key)
10
11
  if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
12
  raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
12
13
  end
@@ -17,13 +18,15 @@ module JWT
17
18
  key.sign(msg)
18
19
  end
19
20
 
20
- def verify(to_verify)
21
- algorithm, public_key, signing_input, signature = to_verify.values
21
+ def verify(algorithm, public_key, signing_input, signature)
22
22
  unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
23
23
  raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
24
24
  end
25
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
26
+
26
27
  public_key.verify(signature, signing_input)
28
+ rescue RbNaCl::CryptoError
29
+ false
27
30
  end
28
31
  end
29
32
  end
@@ -1,34 +1,73 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  module Algos
3
5
  module Hmac
4
6
  module_function
5
7
 
6
- 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
13
+
14
+ SUPPORTED = MAPPING.keys
7
15
 
8
- def sign(to_sign)
9
- algorithm, msg, key = to_sign.values
16
+ def sign(algorithm, msg, key)
10
17
  key ||= ''
11
- authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
12
- if authenticator && padded_key
13
- authenticator.auth(padded_key, msg.encode('binary'))
14
- else
15
- 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'
16
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))
17
32
  end
18
33
 
19
- def verify(to_verify)
20
- algorithm, public_key, signing_input, signature = to_verify.values
21
- authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
22
- if authenticator && padded_key
23
- begin
24
- authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
25
- rescue RbNaCl::BadAuthenticatorError
26
- 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)
27
45
  end
28
46
  else
29
- 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)
30
67
  end
68
+ module_function :secure_compare
31
69
  end
70
+ # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
32
71
  end
33
72
  end
34
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