simple_oauth 0.5.2 → 0.5.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: 88c89c6dff93b31b6279da0d85c0b719cc94bac966356ae1783702ed3cbfe8e5
4
- data.tar.gz: 1febe046539f65e64a5b3d6b787282e5ce91178bf1617f97ce6a08a5a159318e
3
+ metadata.gz: 8245692f23124570c69c39922493673741d0b63805aa68bad6bc36a10dfc873d
4
+ data.tar.gz: 371f536fbc9ce63a659b359887800b44aca7736f91720a3a17d1688c4afd9132
5
5
  SHA512:
6
- metadata.gz: 1f2b608cbf969b4983f29c746a5e892b0d5b1d321a6e044b8c01f645be8733aade948f5410a0654a6ece51778050cdb5a846e7ca02d70c1bd53cdf4d943aab8d
7
- data.tar.gz: 5c76e78b8b537367e00833e063f9a23bacd5e8ea00563e5e41536e0ee223f4c257032001411059380a6f3b39ab883e5253304a12e59eba2da4d88d6ebe130af7
6
+ metadata.gz: 73639dcce60dff2674874af945f5c8f710475b6a18c5842fb14dd4ac0eb180659e885a8e4e2f3c7593b4de0d6aa7c46ce951f22f2af8ca23358e4580f2150383
7
+ data.tar.gz: 4dacc0d32fa47802f531c1f2b25ab6ec4d669ce1d6e3f31973194f05e8f1ea45982a380b52ae78e0d8cd6c523df94ad855aef781d08529728a332ae6c4876cdd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.5.4] - 2026-11-14
2
+
3
+ ### Changed
4
+
5
+ * 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
6
+
7
+ ## [0.5.3] - 2026-11-14
8
+
9
+ ### Fixed
10
+
11
+ * 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
12
+
1
13
  ## [0.5.2] - 2026-11-14
2
14
 
3
15
  ### Security
@@ -0,0 +1,82 @@
1
+ require "openssl"
2
+
3
+ module SimpleOAuth
4
+ class Header
5
+ # Checks of the signature method a received header claims, before it is verified with it
6
+ #
7
+ # @api private
8
+ module Verification
9
+ # The first byte of a key in DER, which encodes every key as an ASN.1 SEQUENCE
10
+ ASN1_SEQUENCE = 0x30
11
+
12
+ # The start of the line that opens every key in PEM
13
+ PEM_BOUNDARY = "-----BEGIN".freeze
14
+
15
+ private
16
+
17
+ # Checks that the header claims a signature method the verifier accepts
18
+ #
19
+ # @api private
20
+ # @param verifying [Hash] the header's options merged with the verifier's secrets
21
+ # @param expected [String, nil] the signature method the verifier expects, if it names one
22
+ # @return [Boolean] true if the claimed method is the expected one, or fits the key
23
+ # @raise [ArgumentError] if the expected signature method is not registered
24
+ def signature_method_acceptable?(verifying, expected)
25
+ claimed = options[:signature_method].to_s
26
+ return expected_signature_method?(claimed, expected) if expected
27
+
28
+ warn("SimpleOAuth::Header#valid? without a signature_method is deprecated. " \
29
+ "Name the method to verify with, such as signature_method: \"HMAC-SHA1\"", category: :deprecated)
30
+ key = verifying[:consumer_secret].to_s
31
+ Signature.registered?(claimed) && !PLAINTEXT_SIGNATURE_METHOD.casecmp(claimed).eql?(0) &&
32
+ (Signature.rsa?(claimed) ? key_encoded?(key) : !rsa_key?(key))
33
+ end
34
+
35
+ # Checks that the header claims the signature method the verifier names
36
+ #
37
+ # Signature method names are compared without regard to ASCII case, as the registry looks
38
+ # them up, and without allocating. A header that claims the expected method needs no other
39
+ # check here, since Signature.verify raises for a method that is not registered.
40
+ #
41
+ # @api private
42
+ # @param claimed [String] the signature method the header claims
43
+ # @param expected [String] the signature method the verifier expects
44
+ # @return [Boolean] true if the header claims the expected method
45
+ # @raise [ArgumentError] if the expected signature method is not registered
46
+ def expected_signature_method?(claimed, expected)
47
+ return true if claimed.casecmp(expected).eql?(0)
48
+ raise ArgumentError, "Unknown signature method: #{expected}" unless Signature.registered?(expected)
49
+
50
+ false
51
+ end
52
+
53
+ # Checks whether a verifying key is encoded as a key could be
54
+ #
55
+ # Only a key in PEM, which always holds a "-----BEGIN" line, or in DER, which always starts with
56
+ # the byte of an ASN.1 SEQUENCE, can be an RSA key, as those are the encodings OpenSSL decodes.
57
+ # Any other secret need not be parsed, which matters since a parse that fails is far slower
58
+ # than verifying a signature.
59
+ #
60
+ # @api private
61
+ # @param key [String] the consumer_secret the verifier gave
62
+ # @return [Boolean] true if the key is in PEM or could be in DER
63
+ def key_encoded?(key)
64
+ key.getbyte(0).eql?(ASN1_SEQUENCE) || key.include?(PEM_BOUNDARY)
65
+ end
66
+
67
+ # Checks whether a verifying key is an RSA key
68
+ #
69
+ # @api private
70
+ # @param key [String] the consumer_secret the verifier gave
71
+ # @return [Boolean] true if the key parses as an RSA key
72
+ def rsa_key?(key)
73
+ return false unless key_encoded?(key)
74
+
75
+ OpenSSL::PKey::RSA.new(key)
76
+ true
77
+ rescue OpenSSL::PKey::PKeyError
78
+ false
79
+ end
80
+ end
81
+ end
82
+ end
@@ -6,6 +6,7 @@ require_relative "parser"
6
6
  require_relative "signature"
7
7
  require_relative "header/class_methods"
8
8
  require_relative "header/params"
9
+ require_relative "header/verification"
9
10
 
10
11
  module SimpleOAuth
11
12
  # Generates OAuth 1.0 Authorization headers for HTTP requests
@@ -67,6 +68,7 @@ module SimpleOAuth
67
68
  attr_reader :options
68
69
 
69
70
  include Params
71
+ include Verification
70
72
 
71
73
  extend ClassMethods
72
74
  extend Encoding
@@ -146,9 +148,8 @@ module SimpleOAuth
146
148
  # # => true
147
149
  def valid?(secrets = {})
148
150
  verifying = options.merge(secrets)
149
- signature_method_acceptable?(verifying, secrets[:signature_method]) && body_hash_valid? &&
150
- Signature.verify(options.fetch(:signature_method), signing_key(verifying), signature_base,
151
- options.fetch(:signature))
151
+ expected = secrets[:signature_method]
152
+ signature_method_acceptable?(verifying, expected) && body_hash_valid? && signature_valid?(verifying, expected)
152
153
  end
153
154
 
154
155
  # Returns the OAuth attributes including the signature
@@ -230,47 +231,25 @@ module SimpleOAuth
230
231
  Signature.rsa?(options.fetch(:signature_method)) ? options[:consumer_secret] : secret(options)
231
232
  end
232
233
 
233
- # Checks that the header claims a signature method the verifier accepts
234
+ # Verifies the signature with the method the header claims
235
+ #
236
+ # A header that claims an RSA method, with no signature_method named, is verified without first
237
+ # parsing the key to check that it is an RSA key, so the key is parsed once rather than twice. The verifier
238
+ # raises PKeyError only for a key that is not an RSA key, which makes the header not valid, as
239
+ # the key does not fit the method. A verifier that names a method keeps the error, since naming
240
+ # an RSA method with another key is a mistake to report.
234
241
  #
235
242
  # @api private
236
243
  # @param verifying [Hash] the header's options merged with the verifier's secrets
237
244
  # @param expected [String, nil] the signature method the verifier expects, if it names one
238
- # @return [Boolean] true if the claimed method is the expected one, or fits the key
239
- # @raise [ArgumentError] if the expected signature method is not registered
240
- def signature_method_acceptable?(verifying, expected)
241
- claimed = options[:signature_method].to_s
242
- return expected_signature_method?(claimed, expected) if expected
243
-
244
- warn("SimpleOAuth::Header#valid? without a signature_method is deprecated. " \
245
- "Name the method to verify with, such as signature_method: \"HMAC-SHA1\"", category: :deprecated)
246
- Signature.registered?(claimed) && !PLAINTEXT_SIGNATURE_METHOD.casecmp?(claimed) &&
247
- Signature.rsa?(claimed).eql?(rsa_key?(verifying[:consumer_secret]))
248
- end
249
-
250
- # Checks that the header claims the signature method the verifier names
251
- #
252
- # Signature method names are compared without regard to case, as the registry looks them up.
253
- #
254
- # @api private
255
- # @param claimed [String] the signature method the header claims
256
- # @param expected [String] the signature method the verifier expects
257
- # @return [Boolean] true if the header claims the expected method
258
- # @raise [ArgumentError] if the expected signature method is not registered
259
- def expected_signature_method?(claimed, expected)
260
- raise ArgumentError, "Unknown signature method: #{expected}" unless Signature.registered?(expected)
261
-
262
- claimed.casecmp?(expected)
263
- end
264
-
265
- # Checks whether a verifying key is an RSA key
266
- #
267
- # @api private
268
- # @param key [String, nil] the consumer_secret the verifier gave
269
- # @return [Boolean] true if the key parses as an RSA key
270
- def rsa_key?(key)
271
- OpenSSL::PKey::RSA.new(key.to_s)
272
- true
245
+ # @return [Boolean] true if the signature is valid
246
+ # @raise [OpenSSL::PKey::PKeyError] if the verifier names an RSA method with a key that is not an RSA key
247
+ def signature_valid?(verifying, expected)
248
+ Signature.verify(options.fetch(:signature_method), signing_key(verifying), signature_base,
249
+ options.fetch(:signature))
273
250
  rescue OpenSSL::PKey::PKeyError
251
+ raise if expected
252
+
274
253
  false
275
254
  end
276
255
 
@@ -26,6 +26,10 @@ module SimpleOAuth
26
26
  # Registry of signature method implementations
27
27
  @registry = {}
28
28
 
29
+ # The registry key of each name a signature method was registered under, so a lookup by that
30
+ # name, as nearly every lookup is, needs no normalizing and allocates nothing
31
+ @names = {}
32
+
29
33
  class << self
30
34
  # Registers a custom signature method
31
35
  #
@@ -47,7 +51,9 @@ module SimpleOAuth
47
51
  # )
48
52
  # end
49
53
  def register(name, rsa: false, verify: nil, digest: DEFAULT_DIGEST, &block)
50
- @registry[normalize_name(name)] = {implementation: block, rsa: rsa, verifier: verify, digest: digest}
54
+ key = normalize_name(name)
55
+ @names[name] = key
56
+ @registry[key] = {implementation: block, rsa: rsa, verifier: verify, digest: digest}
51
57
  end
52
58
 
53
59
  # Checks if a signature method is registered
@@ -58,7 +64,7 @@ module SimpleOAuth
58
64
  # @example
59
65
  # SimpleOAuth::Signature.registered?("HMAC-SHA1") # => true
60
66
  def registered?(name)
61
- @registry.key?(normalize_name(name))
67
+ @registry.key?(registry_key(name))
62
68
  end
63
69
 
64
70
  # Returns list of registered signature method names
@@ -80,7 +86,7 @@ module SimpleOAuth
80
86
  # SimpleOAuth::Signature.rsa?("RSA-SHA1") # => true
81
87
  # SimpleOAuth::Signature.rsa?("HMAC-SHA1") # => false
82
88
  def rsa?(name)
83
- @registry.dig(normalize_name(name), :rsa) || false
89
+ @registry.dig(registry_key(name), :rsa) || false
84
90
  end
85
91
 
86
92
  # Returns the hash algorithm a signature method signs with
@@ -136,7 +142,9 @@ module SimpleOAuth
136
142
  # @example
137
143
  # SimpleOAuth::Signature.unregister("HMAC-SHA512")
138
144
  def unregister(name)
139
- @registry.delete(normalize_name(name))
145
+ key = registry_key(name)
146
+ @names.delete_if { |_, registered| registered.eql?(key) }
147
+ @registry.delete(key)
140
148
  end
141
149
 
142
150
  # Resets the registry to only built-in methods (useful for testing)
@@ -147,6 +155,7 @@ module SimpleOAuth
147
155
  # SimpleOAuth::Signature.reset!
148
156
  def reset!
149
157
  @registry.clear
158
+ @names.clear
150
159
  register_builtin_methods
151
160
  end
152
161
 
@@ -183,12 +192,25 @@ module SimpleOAuth
183
192
  # @return [Hash] the registry entry
184
193
  # @raise [ArgumentError] if the signature method is not registered
185
194
  def fetch(name)
186
- @registry.fetch(normalize_name(name)) do
195
+ @registry.fetch(registry_key(name)) do
187
196
  raise ArgumentError, "Unknown signature method: #{name}. " \
188
197
  "Registered methods: #{@registry.keys.join(", ")}"
189
198
  end
190
199
  end
191
200
 
201
+ # Finds the registry key for a signature method name
202
+ #
203
+ # A name a method was registered under is found in the index of those names. Any other
204
+ # spelling is normalized, without being added to the index, so names from received headers
205
+ # cannot grow it.
206
+ #
207
+ # @api private
208
+ # @param name [String] the signature method name
209
+ # @return [String] the registry key (lowercase, dashes to underscores)
210
+ def registry_key(name)
211
+ @names[name] || normalize_name(name)
212
+ end
213
+
192
214
  # Normalizes signature method name for registry lookup
193
215
  #
194
216
  # @api private
@@ -1,5 +1,5 @@
1
1
  # OAuth 1.0 header generation library
2
2
  module SimpleOauth
3
3
  # The current version of the SimpleOAuth gem
4
- VERSION = "0.5.2".freeze
4
+ VERSION = "0.5.4".freeze
5
5
  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
@@ -61,6 +61,8 @@ module SimpleOAuth
61
61
  # The signature method that sends the secrets themselves, which verifying accepts only when named
62
62
  PLAINTEXT_SIGNATURE_METHOD: String
63
63
 
64
+
65
+
64
66
  # What from_request needs from a request object, such as a Net::HTTPRequest
65
67
  interface _Request
66
68
  def method: () -> String
@@ -94,6 +96,9 @@ module SimpleOAuth
94
96
  # Parameter and attribute normalization from the Params module
95
97
  include Params
96
98
 
99
+ # Checks of the signature method a received header claims
100
+ include Verification
101
+
97
102
  # Class methods from ClassMethods module
98
103
  extend ClassMethods
99
104
 
@@ -147,14 +152,8 @@ module SimpleOAuth
147
152
  # The key for signing and verifying
148
153
  def signing_key: (oauth_options options) -> String?
149
154
 
150
- # Checks that the header claims a signature method the verifier accepts
151
- def signature_method_acceptable?: (oauth_options verifying, String? expected) -> bool
152
-
153
- # Checks that the header claims the signature method the verifier names
154
- def expected_signature_method?: (String claimed, String expected) -> bool
155
-
156
- # Checks whether a verifying key is an RSA key
157
- def rsa_key?: (String? key) -> bool
155
+ # Verifies the signature with the method the header claims
156
+ def signature_valid?: (oauth_options verifying, String? expected) -> bool
158
157
 
159
158
  # Checks the body against the oauth_body_hash the header carries
160
159
  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: 0.5.2
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Richert
@@ -60,6 +60,7 @@ files:
60
60
  - lib/simple_oauth/header.rb
61
61
  - lib/simple_oauth/header/class_methods.rb
62
62
  - lib/simple_oauth/header/params.rb
63
+ - lib/simple_oauth/header/verification.rb
63
64
  - lib/simple_oauth/parser.rb
64
65
  - lib/simple_oauth/signature.rb
65
66
  - lib/simple_oauth/version.rb
@@ -69,6 +70,7 @@ files:
69
70
  - sig/simple_oauth.rbs
70
71
  - sig/simple_oauth/header/class_methods.rbs
71
72
  - sig/simple_oauth/header/params.rbs
73
+ - sig/simple_oauth/header/verification.rbs
72
74
  - sig/simple_oauth/parser.rbs
73
75
  - sig/simple_oauth/signature.rbs
74
76
  - sig/strscan.rbs