simple_oauth 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c80dd2b6480173dc8278a8b927f6b25929bddb430a72e152abbd29c8cae4c4da
4
- data.tar.gz: ac11a3c3e75fb0d1eb7964d25e5f53b002b334e27273e9e6428b5a6b9226481f
3
+ metadata.gz: '085f7b377ecba7e5a389e1fa122341ca4c5b517b65cf77ac41d4e69c6f3ac2d0'
4
+ data.tar.gz: d38ee1bd5f40c23c29182df41d687f6eee8bb04004b9aee9152ed04b61e532bd
5
5
  SHA512:
6
- metadata.gz: 7749f45d33c7f55a4186b9a744ea4119fe9d0ce1211ee26c89789887cad66f0a478dfddf4146e585044078adbaae82f548620794b71961a9da832cd8888ee5d3
7
- data.tar.gz: bc81ed8ad865e68843a764e75fc04b06afa4cd441fb4b24a22406c8512ec844dfd01a975dde11ea88bf96f1d4679a96b23fbe63463db5735803193781d72c39c
6
+ metadata.gz: 111934cffa979ed69516a53f3dbe875e8f4ebc3dfb60890dd202019df92f267ae46cbcf8dd313c3241889548e05e026583e72ee455283635f0e06822a639d063
7
+ data.tar.gz: 267bbab778351538d6173534b4315ca9a953911cbc74c610f3c1395f9cbea6fc854ae9b274cfcaeb4ba62a58158d43232ad67f75fc745aeecc6246b79b4ab98d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.4] - 2026-11-14
9
+
10
+ ### Changed
11
+
12
+ - Look up a signature method by the name it was registered under without allocating, so signing a request allocates at least 4 fewer objects and verifying one at least 6 fewer. Verifying without a `signature_method` now allocates fewer objects than in 1.0.1
13
+
14
+ ## [0.5.4] - 2026-11-14
15
+
16
+ ### Changed
17
+
18
+ - Look up a signature method by the name it was registered under without allocating, so signing a request allocates at least 4 fewer objects and verifying one at least 6 fewer. Verifying without a `signature_method` now allocates fewer objects than in 0.5.1
19
+
20
+ ## [1.0.3] - 2026-11-14
21
+
22
+ ### Fixed
23
+
24
+ - Verify as fast as 1.0.1 when `Header#valid?` is not given a `signature_method`. 1.0.2 parsed the `consumer_secret` as an RSA key on every verification, which takes far longer to fail for a shared secret than verifying does, and parsed an RSA key twice. That made such a verification about 60 to 70 percent slower. Only a secret in PEM, or one that could be in DER, is now parsed, and an RSA key only once
25
+
26
+ ## [0.5.3] - 2026-11-14
27
+
28
+ ### Fixed
29
+
30
+ - Verify as fast as 0.5.1 when `Header#valid?` is not given a `signature_method`. 0.5.2 parsed the `consumer_secret` as an RSA key on every verification, which takes far longer to fail for a shared secret than verifying does, and parsed an RSA key twice. That made such a verification about 60 to 70 percent slower. Only a secret in PEM, or one that could be in DER, is now parsed, and an RSA key only once
31
+
8
32
  ## [1.0.2] - 2026-11-14
9
33
 
10
34
  ### Security
@@ -18,6 +42,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
42
 
19
43
  - Calling `Header#valid?` without a `signature_method`, which a later major version will require
20
44
 
45
+ ## [0.5.2] - 2026-11-14
46
+
47
+ ### Security
48
+
49
+ - `Header#valid?` no longer verifies with whatever signature method the received header claims. A server verifying RSA signatures with a client's public key, as the README showed, accepted forged requests signed with that public key as an HMAC-SHA1 or PLAINTEXT secret. Reported by Rathnadeepan (HotWater, [@HotWater-015](https://github.com/HotWater-015))
50
+ - Given a `signature_method`, the header must claim that method
51
+ - Without one, the header must claim an RSA method for an RSA key and an HMAC method for any other secret
52
+ - PLAINTEXT signatures verify only when the caller names `PLAINTEXT`
53
+
54
+ ### Deprecated
55
+
56
+ - Calling `Header#valid?` without a `signature_method`, which a later major version will require
57
+
21
58
  ## [1.0.1] - 2026-09-30
22
59
 
23
60
  ### Fixed
@@ -118,7 +155,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
118
155
  - Avoid symbolizing untrusted input in parse methods for security
119
156
  - Refactored `Header.parse` for improved robustness using StringScanner
120
157
 
158
+ [1.0.4]: https://github.com/laserlemon/simple_oauth/compare/v1.0.3...v1.0.4
159
+ [0.5.4]: https://github.com/laserlemon/simple_oauth/compare/v0.5.3...v0.5.4
160
+ [1.0.3]: https://github.com/laserlemon/simple_oauth/compare/v1.0.2...v1.0.3
161
+ [0.5.3]: https://github.com/laserlemon/simple_oauth/compare/v0.5.2...v0.5.3
121
162
  [1.0.2]: https://github.com/laserlemon/simple_oauth/compare/v1.0.1...v1.0.2
163
+ [0.5.2]: https://github.com/laserlemon/simple_oauth/compare/v0.5.1...v0.5.2
122
164
  [1.0.1]: https://github.com/laserlemon/simple_oauth/compare/v1.0.0...v1.0.1
123
165
  [1.0.0]: https://github.com/laserlemon/simple_oauth/compare/v0.5.1...v1.0.0
124
166
  [0.5.1]: https://github.com/laserlemon/simple_oauth/compare/v0.5.0...v0.5.1
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module SimpleOAuth
6
+ class Header
7
+ # Checks of the signature method a received header claims, before it is verified with it
8
+ #
9
+ # @api private
10
+ module Verification
11
+ # The first byte of a key in DER, which encodes every key as an ASN.1 SEQUENCE
12
+ ASN1_SEQUENCE = 0x30
13
+
14
+ # The start of the line that opens every key in PEM
15
+ PEM_BOUNDARY = "-----BEGIN"
16
+
17
+ private
18
+
19
+ # Checks that the header claims a signature method the verifier accepts
20
+ #
21
+ # @api private
22
+ # @param verifying [Hash] the header's options merged with the verifier's secrets
23
+ # @param expected [String, nil] the signature method the verifier expects, if it names one
24
+ # @return [Boolean] true if the claimed method is the expected one, or fits the key
25
+ # @raise [ArgumentError] if the expected signature method is not registered
26
+ def signature_method_acceptable?(verifying, expected)
27
+ claimed = options[:signature_method].to_s
28
+ return expected_signature_method?(claimed, expected) if expected
29
+
30
+ warn("SimpleOAuth::Header#valid? without a signature_method is deprecated. " \
31
+ "Name the method to verify with, such as signature_method: \"HMAC-SHA1\"", category: :deprecated)
32
+ key = verifying[:consumer_secret].to_s
33
+ Signature.registered?(claimed) && !PLAINTEXT_SIGNATURE_METHOD.casecmp(claimed).eql?(0) &&
34
+ (Signature.rsa?(claimed) ? key_encoded?(key) : !rsa_key?(key))
35
+ end
36
+
37
+ # Checks that the header claims the signature method the verifier names
38
+ #
39
+ # Signature method names are compared without regard to ASCII case, as the registry looks
40
+ # them up, and without allocating. A header that claims the expected method needs no other
41
+ # check here, since Signature.verify raises for a method that is not registered.
42
+ #
43
+ # @api private
44
+ # @param claimed [String] the signature method the header claims
45
+ # @param expected [String] the signature method the verifier expects
46
+ # @return [Boolean] true if the header claims the expected method
47
+ # @raise [ArgumentError] if the expected signature method is not registered
48
+ def expected_signature_method?(claimed, expected)
49
+ return true if claimed.casecmp(expected).eql?(0)
50
+ raise ArgumentError, "Unknown signature method: #{expected}" unless Signature.registered?(expected)
51
+
52
+ false
53
+ end
54
+
55
+ # Checks whether a verifying key is encoded as a key could be
56
+ #
57
+ # Only a key in PEM, which always holds a "-----BEGIN" line, or in DER, which always starts with
58
+ # the byte of an ASN.1 SEQUENCE, can be an RSA key, as those are the encodings OpenSSL decodes.
59
+ # Any other secret need not be parsed, which matters since a parse that fails is far slower
60
+ # than verifying a signature.
61
+ #
62
+ # @api private
63
+ # @param key [String] the consumer_secret the verifier gave
64
+ # @return [Boolean] true if the key is in PEM or could be in DER
65
+ def key_encoded?(key)
66
+ key.getbyte(0).eql?(ASN1_SEQUENCE) || key.include?(PEM_BOUNDARY)
67
+ end
68
+
69
+ # Checks whether a verifying key is an RSA key
70
+ #
71
+ # @api private
72
+ # @param key [String] the consumer_secret the verifier gave
73
+ # @return [Boolean] true if the key parses as an RSA key
74
+ def rsa_key?(key)
75
+ return false unless key_encoded?(key)
76
+
77
+ OpenSSL::PKey::RSA.new(key)
78
+ true
79
+ rescue OpenSSL::PKey::PKeyError
80
+ false
81
+ end
82
+ end
83
+ end
84
+ end
@@ -7,6 +7,7 @@ require_relative "parser"
7
7
  require_relative "signature"
8
8
  require_relative "header/class_methods"
9
9
  require_relative "header/params"
10
+ require_relative "header/verification"
10
11
 
11
12
  module SimpleOAuth
12
13
  # Generates OAuth 1.0 Authorization headers for HTTP requests
@@ -68,6 +69,7 @@ module SimpleOAuth
68
69
  attr_reader :options
69
70
 
70
71
  include Params
72
+ include Verification
71
73
 
72
74
  extend ClassMethods
73
75
  extend Encoding
@@ -147,9 +149,8 @@ module SimpleOAuth
147
149
  # # => true
148
150
  def valid?(secrets = {})
149
151
  verifying = options.merge(secrets)
150
- signature_method_acceptable?(verifying, secrets[:signature_method]) && body_hash_valid? &&
151
- Signature.verify(options.fetch(:signature_method), signing_key(verifying), signature_base,
152
- options.fetch(:signature))
152
+ expected = secrets[:signature_method]
153
+ signature_method_acceptable?(verifying, expected) && body_hash_valid? && signature_valid?(verifying, expected)
153
154
  end
154
155
 
155
156
  # Returns the OAuth attributes including the signature
@@ -231,47 +232,25 @@ module SimpleOAuth
231
232
  Signature.rsa?(options.fetch(:signature_method)) ? options[:consumer_secret] : secret(options)
232
233
  end
233
234
 
234
- # Checks that the header claims a signature method the verifier accepts
235
+ # Verifies the signature with the method the header claims
236
+ #
237
+ # A header that claims an RSA method, with no signature_method named, is verified without first
238
+ # parsing the key to check that it is an RSA key, so the key is parsed once rather than twice. The verifier
239
+ # raises PKeyError only for a key that is not an RSA key, which makes the header not valid, as
240
+ # the key does not fit the method. A verifier that names a method keeps the error, since naming
241
+ # an RSA method with another key is a mistake to report.
235
242
  #
236
243
  # @api private
237
244
  # @param verifying [Hash] the header's options merged with the verifier's secrets
238
245
  # @param expected [String, nil] the signature method the verifier expects, if it names one
239
- # @return [Boolean] true if the claimed method is the expected one, or fits the key
240
- # @raise [ArgumentError] if the expected signature method is not registered
241
- def signature_method_acceptable?(verifying, expected)
242
- claimed = options[:signature_method].to_s
243
- return expected_signature_method?(claimed, expected) if expected
244
-
245
- warn("SimpleOAuth::Header#valid? without a signature_method is deprecated. " \
246
- "Name the method to verify with, such as signature_method: \"HMAC-SHA1\"", category: :deprecated)
247
- Signature.registered?(claimed) && !PLAINTEXT_SIGNATURE_METHOD.casecmp?(claimed) &&
248
- Signature.rsa?(claimed).eql?(rsa_key?(verifying[:consumer_secret]))
249
- end
250
-
251
- # Checks that the header claims the signature method the verifier names
252
- #
253
- # Signature method names are compared without regard to case, as the registry looks them up.
254
- #
255
- # @api private
256
- # @param claimed [String] the signature method the header claims
257
- # @param expected [String] the signature method the verifier expects
258
- # @return [Boolean] true if the header claims the expected method
259
- # @raise [ArgumentError] if the expected signature method is not registered
260
- def expected_signature_method?(claimed, expected)
261
- raise ArgumentError, "Unknown signature method: #{expected}" unless Signature.registered?(expected)
262
-
263
- claimed.casecmp?(expected)
264
- end
265
-
266
- # Checks whether a verifying key is an RSA key
267
- #
268
- # @api private
269
- # @param key [String, nil] the consumer_secret the verifier gave
270
- # @return [Boolean] true if the key parses as an RSA key
271
- def rsa_key?(key)
272
- OpenSSL::PKey::RSA.new(key.to_s)
273
- true
246
+ # @return [Boolean] true if the signature is valid
247
+ # @raise [OpenSSL::PKey::PKeyError] if the verifier names an RSA method with a key that is not an RSA key
248
+ def signature_valid?(verifying, expected)
249
+ Signature.verify(options.fetch(:signature_method), signing_key(verifying), signature_base,
250
+ options.fetch(:signature))
274
251
  rescue OpenSSL::PKey::PKeyError
252
+ raise if expected
253
+
275
254
  false
276
255
  end
277
256
 
@@ -27,6 +27,10 @@ module SimpleOAuth
27
27
  # Registry of signature method implementations
28
28
  @registry = {}
29
29
 
30
+ # The registry key of each name a signature method was registered under, so a lookup by that
31
+ # name, as nearly every lookup is, needs no normalizing and allocates nothing
32
+ @names = {}
33
+
30
34
  class << self
31
35
  # Registers a custom signature method
32
36
  #
@@ -48,7 +52,9 @@ module SimpleOAuth
48
52
  # )
49
53
  # end
50
54
  def register(name, rsa: false, verify: nil, digest: DEFAULT_DIGEST, &block)
51
- @registry[normalize_name(name)] = {implementation: block, rsa: rsa, verifier: verify, digest: digest}
55
+ key = normalize_name(name)
56
+ @names[name] = key
57
+ @registry[key] = {implementation: block, rsa: rsa, verifier: verify, digest: digest}
52
58
  end
53
59
 
54
60
  # Checks if a signature method is registered
@@ -59,7 +65,7 @@ module SimpleOAuth
59
65
  # @example
60
66
  # SimpleOAuth::Signature.registered?("HMAC-SHA1") # => true
61
67
  def registered?(name)
62
- @registry.key?(normalize_name(name))
68
+ @registry.key?(registry_key(name))
63
69
  end
64
70
 
65
71
  # Returns list of registered signature method names
@@ -139,7 +145,9 @@ module SimpleOAuth
139
145
  # @example
140
146
  # SimpleOAuth::Signature.unregister("HMAC-SHA512")
141
147
  def unregister(name)
142
- @registry.delete(normalize_name(name))
148
+ key = registry_key(name)
149
+ @names.delete_if { |_, registered| registered.eql?(key) }
150
+ @registry.delete(key)
143
151
  end
144
152
 
145
153
  # Resets the registry to only built-in methods (useful for testing)
@@ -150,6 +158,7 @@ module SimpleOAuth
150
158
  # SimpleOAuth::Signature.reset!
151
159
  def reset!
152
160
  @registry.clear
161
+ @names.clear
153
162
  register_builtin_methods
154
163
  end
155
164
 
@@ -188,12 +197,25 @@ module SimpleOAuth
188
197
  # @return [Hash] the registry entry
189
198
  # @raise [ArgumentError] if the signature method is not registered
190
199
  def fetch(name)
191
- @registry.fetch(normalize_name(name)) do
200
+ @registry.fetch(registry_key(name)) do
192
201
  raise ArgumentError, "Unknown signature method: #{name}. " \
193
202
  "Registered methods: #{@registry.keys.join(", ")}"
194
203
  end
195
204
  end
196
205
 
206
+ # Finds the registry key for a signature method name
207
+ #
208
+ # A name a method was registered under is found in the index of those names. Any other
209
+ # spelling is normalized, without being added to the index, so names from received headers
210
+ # cannot grow it.
211
+ #
212
+ # @api private
213
+ # @param name [String] the signature method name
214
+ # @return [String] the registry key (lowercase, dashes to underscores)
215
+ def registry_key(name)
216
+ @names[name] || normalize_name(name)
217
+ end
218
+
197
219
  # Normalizes signature method name for registry lookup
198
220
  #
199
221
  # @api private
@@ -3,5 +3,5 @@
3
3
  # OAuth 1.0 header generation library
4
4
  module SimpleOAuth
5
5
  # The current version of the SimpleOAuth gem
6
- VERSION = "1.0.2"
6
+ VERSION = "1.0.4"
7
7
  end
@@ -0,0 +1,28 @@
1
+ module SimpleOAuth
2
+ class Header
3
+ # What the Verification module needs from the header that includes it
4
+ interface _Verifiable
5
+ def options: () -> Header::oauth_options
6
+ end
7
+
8
+ # Checks of the signature method a received header claims, before it is verified with it
9
+ module Verification : _Verifiable
10
+ # The first byte of a key in DER, which encodes every key as an ASN.1 SEQUENCE
11
+ ASN1_SEQUENCE: Integer
12
+
13
+ # The start of the line that opens every key in PEM
14
+ PEM_BOUNDARY: String
15
+
16
+ private
17
+
18
+ # Checks that the header claims a signature method the verifier accepts
19
+ def signature_method_acceptable?: (Header::oauth_options verifying, String? expected) -> bool
20
+ # Checks that the header claims the signature method the verifier names
21
+ def expected_signature_method?: (String claimed, String expected) -> bool
22
+ # Checks whether a verifying key is encoded as a key could be
23
+ def key_encoded?: (String key) -> bool
24
+ # Checks whether a verifying key is an RSA key
25
+ def rsa_key?: (String key) -> bool
26
+ end
27
+ end
28
+ end
@@ -19,6 +19,9 @@ module SimpleOAuth
19
19
  # Registry of signature method implementations (class-level instance variable)
20
20
  self.@registry: Hash[String, registry_entry]
21
21
 
22
+ # The registry key of each name a signature method was registered under
23
+ self.@names: Hash[String | Symbol, String]
24
+
22
25
  # Registers a custom signature method
23
26
  def self.register: (String | Symbol name, ?rsa: bool, ?verify: verifier_block?, ?digest: String) { (untyped, String) -> String } -> void
24
27
 
@@ -57,6 +60,9 @@ module SimpleOAuth
57
60
  # Looks up a registered signature method
58
61
  def self.fetch: (String | Symbol name) -> registry_entry
59
62
 
63
+ # Finds the registry key for a signature method name
64
+ def self.registry_key: (String | Symbol name) -> String
65
+
60
66
  # Normalizes signature method name for registry lookup
61
67
  def self.normalize_name: (String | Symbol name) -> String
62
68
 
data/sig/simple_oauth.rbs CHANGED
@@ -65,6 +65,8 @@ module SimpleOAuth
65
65
  # The signature method that sends the secrets themselves, which verifying accepts only when named
66
66
  PLAINTEXT_SIGNATURE_METHOD: String
67
67
 
68
+
69
+
68
70
  # What from_request needs from a request object, such as a Net::HTTPRequest
69
71
  interface _Request
70
72
  def method: () -> String
@@ -98,6 +100,9 @@ module SimpleOAuth
98
100
  # Parameter and attribute normalization from the Params module
99
101
  include Params
100
102
 
103
+ # Checks of the signature method a received header claims
104
+ include Verification
105
+
101
106
  # Class methods from ClassMethods module
102
107
  extend ClassMethods
103
108
 
@@ -151,14 +156,8 @@ module SimpleOAuth
151
156
  # The key for signing and verifying
152
157
  def signing_key: (oauth_options options) -> String?
153
158
 
154
- # Checks that the header claims a signature method the verifier accepts
155
- def signature_method_acceptable?: (oauth_options verifying, String? expected) -> bool
156
-
157
- # Checks that the header claims the signature method the verifier names
158
- def expected_signature_method?: (String claimed, String expected) -> bool
159
-
160
- # Checks whether a verifying key is an RSA key
161
- def rsa_key?: (String? key) -> bool
159
+ # Verifies the signature with the method the header claims
160
+ def signature_valid?: (oauth_options verifying, String? expected) -> bool
162
161
 
163
162
  # Checks the body against the oauth_body_hash the header carries
164
163
  def body_hash_valid?: () -> bool
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Richert
@@ -32,6 +32,7 @@ files:
32
32
  - lib/simple_oauth/header.rb
33
33
  - lib/simple_oauth/header/class_methods.rb
34
34
  - lib/simple_oauth/header/params.rb
35
+ - lib/simple_oauth/header/verification.rb
35
36
  - lib/simple_oauth/oauth2.rb
36
37
  - lib/simple_oauth/oauth2/authorization_response.rb
37
38
  - lib/simple_oauth/oauth2/client.rb
@@ -47,6 +48,7 @@ files:
47
48
  - sig/simple_oauth.rbs
48
49
  - sig/simple_oauth/header/class_methods.rbs
49
50
  - sig/simple_oauth/header/params.rbs
51
+ - sig/simple_oauth/header/verification.rbs
50
52
  - sig/simple_oauth/oauth2.rbs
51
53
  - sig/simple_oauth/parser.rbs
52
54
  - sig/simple_oauth/signature.rbs