simple_oauth 0.5.1 → 0.5.3

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: 680161f3f3fc8cf54f78c5111c1f0c4bcd6380b0d29022fb9ac5fd407805d8b7
4
- data.tar.gz: 6951095c0b1b04b7755e8857a977946825053a141e3fd4c1c3b7e10cade061fe
3
+ metadata.gz: bd2a5227ccf41a180c48803278ab10caa9348f38e6a4009d9e3faad9eac6218e
4
+ data.tar.gz: 6e30b74dfb49f761430105c0fff030e40aaefea1682cf6101d8b168430e41445
5
5
  SHA512:
6
- metadata.gz: f852606e3aa48e70a5856f0cfccbfafe4c9235a6a701faca692446186b5bf8f30781b316577b7ac15a164b07293f9b550ad5cdfe0db58df6e57be396de380cf6
7
- data.tar.gz: d938005d0ad30817b294e0e8143b272be0dafb794dfa4a57ee012fc8f162c7d895d85a5a2ae0a57f4bc3b9e50c372fde93ac22b31bc01579ead3deec968f2d2a
6
+ metadata.gz: a6880bc574c06b6b4b20975ae4ee3ee29db0dd65b2ccec9bda5c59bc966c8ceebe355c2413e71aafc3ce141c5aaebd7f78b8262b701833134f16175886de0fd5
7
+ data.tar.gz: 6c2699ea118ed0d57c1987a691b0ddc7fb8d2264bee1527f14b9c621725adb8aaf5359971cbee43de3fe105bf7a8dac11439e1b8c592fa57e471412e1f43df48
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [0.5.3] - 2026-11-14
2
+
3
+ ### Fixed
4
+
5
+ * 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
6
+
7
+ ## [0.5.2] - 2026-11-14
8
+
9
+ ### Security
10
+
11
+ * `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))
12
+ * Given a `signature_method`, the header must claim that method
13
+ * Without one, the header must claim an RSA method for an RSA key and an HMAC method for any other secret
14
+ * PLAINTEXT signatures verify only when the caller names `PLAINTEXT`
15
+
16
+ ### Deprecated
17
+
18
+ * Calling `Header#valid?` without a `signature_method`, which a later major version will require
19
+
1
20
  ## [0.5.1] - 2026-09-12
2
21
 
3
22
  ### Fixed
data/README.md CHANGED
@@ -155,16 +155,18 @@ parsed = SimpleOAuth::Header.parse_query("oauth_consumer_key=key&status=hello")
155
155
  header = SimpleOAuth::Header.new(:get, request_url, params, authorization_header)
156
156
 
157
157
  # Verify the signature
158
- header.valid?(consumer_secret: "secret", token_secret: "token_secret")
158
+ header.valid?(consumer_secret: "secret", token_secret: "token_secret", signature_method: "HMAC-SHA1")
159
159
  # => true
160
160
  ```
161
161
 
162
+ Name the signature method you expect, and a header claiming any other method is not valid. The method a request claims is chosen by whoever sent it, so verifying with it unchecked would let anyone holding an RSA public key pass it off as an HMAC or PLAINTEXT secret. Leaving `signature_method` out is deprecated: the header must then claim an RSA method for an RSA key and an HMAC method for any other secret. PLAINTEXT signatures are accepted only when you name `PLAINTEXT`, and they send the secrets themselves, so accept them only over TLS.
163
+
162
164
  Verifying compares signatures in constant time and leaves the header's own options untouched, so the secrets stay with the caller.
163
165
 
164
166
  RSA signatures verify with the client's public key, which is all a server has:
165
167
 
166
168
  ```ruby
167
- header.valid?(consumer_secret: File.read("client_public_key.pem"))
169
+ header.valid?(consumer_secret: File.read("client_public_key.pem"), signature_method: "RSA-SHA1")
168
170
  ```
169
171
 
170
172
  Custom signature methods that cannot be verified by recomputing the signature register a `verify` block:
@@ -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
@@ -23,6 +24,9 @@ module SimpleOAuth
23
24
  # Default signature method per RFC 5849
24
25
  DEFAULT_SIGNATURE_METHOD = "HMAC-SHA1".freeze
25
26
 
27
+ # The signature method that sends the secrets themselves, which verifying accepts only when named
28
+ PLAINTEXT_SIGNATURE_METHOD = "PLAINTEXT".freeze
29
+
26
30
  # OAuth version
27
31
  OAUTH_VERSION = "1.0".freeze
28
32
 
@@ -64,6 +68,7 @@ module SimpleOAuth
64
68
  attr_reader :options
65
69
 
66
70
  include Params
71
+ include Verification
67
72
 
68
73
  extend ClassMethods
69
74
  extend Encoding
@@ -122,18 +127,29 @@ module SimpleOAuth
122
127
 
123
128
  # Validates the signature in the header against the provided secrets
124
129
  #
130
+ # The method a received header claims is chosen by whoever sent it, so it is checked before it
131
+ # is used. Given a signature_method, the header must claim that method. Without one, the header
132
+ # must claim a method that fits the key: an RSA method for an RSA key, and an HMAC method for any
133
+ # other secret. PLAINTEXT is accepted only when named, as it sends the secrets themselves.
134
+ # Without that check, a sender holding only an RSA public key could sign with it as an HMAC or
135
+ # PLAINTEXT secret.
136
+ #
125
137
  # @api public
126
- # @param secrets [Hash] the consumer_secret and token_secret for validation
138
+ # @param secrets [Hash] the consumer_secret and token_secret for validation, and the
139
+ # signature_method the header must use, which a later major version will require
127
140
  # @return [Boolean] true if the signature is valid, false otherwise
141
+ # @raise [ArgumentError] if the expected signature method is not registered
128
142
  # @note When the header was built with a body, the signed oauth_body_hash must match that body,
129
143
  # so a tampered body fails verification even though its signature covers the claimed hash
130
144
  # @example
131
145
  # parsed_header = SimpleOAuth::Header.new(:get, url, {}, authorization_header)
132
- # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret")
146
+ # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret",
147
+ # signature_method: "HMAC-SHA1")
133
148
  # # => true
134
149
  def valid?(secrets = {})
135
- body_hash_valid? && Signature.verify(options.fetch(:signature_method), signing_key(options.merge(secrets)),
136
- signature_base, options.fetch(:signature))
150
+ verifying = options.merge(secrets)
151
+ expected = secrets[:signature_method]
152
+ signature_method_acceptable?(verifying, expected) && body_hash_valid? && signature_valid?(verifying, expected)
137
153
  end
138
154
 
139
155
  # Returns the OAuth attributes including the signature
@@ -215,6 +231,28 @@ module SimpleOAuth
215
231
  Signature.rsa?(options.fetch(:signature_method)) ? options[:consumer_secret] : secret(options)
216
232
  end
217
233
 
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.
241
+ #
242
+ # @api private
243
+ # @param verifying [Hash] the header's options merged with the verifier's secrets
244
+ # @param expected [String, nil] the signature method the verifier expects, if it names one
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))
250
+ rescue OpenSSL::PKey::PKeyError
251
+ raise if expected
252
+
253
+ false
254
+ end
255
+
218
256
  # Checks the body against the oauth_body_hash the header carries
219
257
  #
220
258
  # A header parsed from a request claims a body hash that its signature covers, so the claim must be
@@ -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.1".freeze
4
+ VERSION = "0.5.3".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
data/sig/simple_oauth.rbs CHANGED
@@ -58,6 +58,11 @@ module SimpleOAuth
58
58
  # Valid keys when parsing OAuth parameters (ATTRIBUTE_KEYS + signature)
59
59
  PARSE_KEYS: Array[Symbol]
60
60
 
61
+ # The signature method that sends the secrets themselves, which verifying accepts only when named
62
+ PLAINTEXT_SIGNATURE_METHOD: String
63
+
64
+
65
+
61
66
  # What from_request needs from a request object, such as a Net::HTTPRequest
62
67
  interface _Request
63
68
  def method: () -> String
@@ -91,6 +96,9 @@ module SimpleOAuth
91
96
  # Parameter and attribute normalization from the Params module
92
97
  include Params
93
98
 
99
+ # Checks of the signature method a received header claims
100
+ include Verification
101
+
94
102
  # Class methods from ClassMethods module
95
103
  extend ClassMethods
96
104
 
@@ -144,6 +152,9 @@ module SimpleOAuth
144
152
  # The key for signing and verifying
145
153
  def signing_key: (oauth_options options) -> String?
146
154
 
155
+ # Verifies the signature with the method the header claims
156
+ def signature_valid?: (oauth_options verifying, String? expected) -> bool
157
+
147
158
  # Checks the body against the oauth_body_hash the header carries
148
159
  def body_hash_valid?: () -> bool
149
160
 
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.1
4
+ version: 0.5.3
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
@@ -95,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
97
  - !ruby/object:Gem::Version
96
98
  version: '0'
97
99
  requirements: []
98
- rubygems_version: 4.0.20
100
+ rubygems_version: 4.0.24
99
101
  specification_version: 4
100
102
  summary: Simply builds and verifies OAuth headers
101
103
  test_files: []