jwt 2.3.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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