simple_oauth 0.5.0 → 0.5.2

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: 7eca3b8d3940706967367e25b8f42261d48b325c8f2cd1c1cc01ad58a800f284
4
- data.tar.gz: 63cec8ec4204dd64c40fdf61ecf1782df0364687794896199ded8a6dcb890ca3
3
+ metadata.gz: 88c89c6dff93b31b6279da0d85c0b719cc94bac966356ae1783702ed3cbfe8e5
4
+ data.tar.gz: 1febe046539f65e64a5b3d6b787282e5ce91178bf1617f97ce6a08a5a159318e
5
5
  SHA512:
6
- metadata.gz: 967d4c8e1f174da75c8818fa0b94432ca2878d44474e5433930aa5bca728d4636b1ae4594e930be98fc68d6d9d57a40622dd5075ff507242100f424b95213e75
7
- data.tar.gz: 7b24159117153d934834c2767692be078a78c58ec6fc34775d91bd90794767bbf86cbde9fb6306bcf5640b2a7b212f8bd0866e980ed3a871a3e0212120a7494b
6
+ metadata.gz: 1f2b608cbf969b4983f29c746a5e892b0d5b1d321a6e044b8c01f645be8733aade948f5410a0654a6ece51778050cdb5a846e7ca02d70c1bd53cdf4d943aab8d
7
+ data.tar.gz: 5c76e78b8b537367e00833e063f9a23bacd5e8ea00563e5e41536e0ee223f4c257032001411059380a6f3b39ab883e5253304a12e59eba2da4d88d6ebe130af7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## [0.5.2] - 2026-11-14
2
+
3
+ ### Security
4
+
5
+ * `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))
6
+ * Given a `signature_method`, the header must claim that method
7
+ * Without one, the header must claim an RSA method for an RSA key and an HMAC method for any other secret
8
+ * PLAINTEXT signatures verify only when the caller names `PLAINTEXT`
9
+
10
+ ### Deprecated
11
+
12
+ * Calling `Header#valid?` without a `signature_method`, which a later major version will require
13
+
14
+ ## [0.5.1] - 2026-09-12
15
+
16
+ ### Fixed
17
+
18
+ * Build the signature base string with a String conversion that every supported Ruby offers; `Header#url` called `URI::Generic#to_str`, which arrived in `uri` 0.13, so it raised `NoMethodError` on a stock Ruby 3.2, the oldest version the gem claims to support
19
+ * Sign a parameter that carries no value, such as a bare `?flag` in the query string or the `c2` of the RFC 5849 Section 3.4.1.3.1 example, as `name=` rather than dropping it from the signature base string; a request carrying one signed differently than the server computes, so it was rejected
20
+
1
21
  ## [0.5.0] - 2026-09-12
2
22
 
3
23
  ### Added
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:
@@ -71,7 +71,7 @@ module SimpleOAuth
71
71
  def from_request(request, oauth = {})
72
72
  uri = request.uri || raise(ArgumentError, "The request has no URI")
73
73
  body = request.body
74
- return new(request.method, uri, CGI.parse(body.to_s), oauth) if form_encoded?(request)
74
+ return new(request.method, uri, form_params(body), oauth) if form_encoded?(request)
75
75
 
76
76
  no_params = {} #: Header::request_params
77
77
  new(request.method, uri, no_params, oauth, body)
@@ -117,6 +117,21 @@ module SimpleOAuth
117
117
 
118
118
  private
119
119
 
120
+ # Parses a form-encoded body into the parameter pairs to sign
121
+ #
122
+ # A parameter with no value, such as the "c2" of the RFC 5849 Section 3.4.1.3.1
123
+ # example, is signed with an empty value rather than dropped.
124
+ #
125
+ # @api private
126
+ # @param body [String, nil] the form-encoded body
127
+ # @return [Array<Array(String, String)>] the parameter pairs
128
+ def form_params(body)
129
+ CGI.parse(body.to_s).flat_map do |key, values|
130
+ # A parameter with no value still makes one pair, carrying an empty value
131
+ (values.empty? ? [""] : values).map { |value| [key, value] }
132
+ end
133
+ end
134
+
120
135
  # Checks whether a request carries a form-encoded body
121
136
  #
122
137
  # @api private
@@ -33,11 +33,15 @@ module SimpleOAuth
33
33
 
34
34
  # Extracts query parameters from the request URL
35
35
  #
36
+ # A parameter with no value, such as the "c2" of the RFC 5849 Section 3.4.1.3.1
37
+ # example, is signed with an empty value rather than dropped.
38
+ #
36
39
  # @api private
37
40
  # @return [Array<Array>] URL query parameters as key-value pairs
38
41
  def url_params
39
42
  CGI.parse(@uri.query || "").flat_map do |key, values|
40
- values.sort.map { |value| [key, value] }
43
+ # A parameter with no value still makes one pair, carrying an empty value
44
+ (values.empty? ? [""] : values.sort).map { |value| [key, value] }
41
45
  end
42
46
  end
43
47
 
@@ -23,6 +23,9 @@ module SimpleOAuth
23
23
  # Default signature method per RFC 5849
24
24
  DEFAULT_SIGNATURE_METHOD = "HMAC-SHA1".freeze
25
25
 
26
+ # The signature method that sends the secrets themselves, which verifying accepts only when named
27
+ PLAINTEXT_SIGNATURE_METHOD = "PLAINTEXT".freeze
28
+
26
29
  # OAuth version
27
30
  OAUTH_VERSION = "1.0".freeze
28
31
 
@@ -102,7 +105,9 @@ module SimpleOAuth
102
105
  # header.url
103
106
  # # => "https://api.example.com/path"
104
107
  def url
105
- @uri.dup.tap { |uri| uri.query = nil }.to_str
108
+ # String() takes whichever conversion the installed uri defines: it gained to_str in
109
+ # 0.13, and the uri that ships with Ruby 3.2 offers only to_s
110
+ String(@uri.dup.tap { |uri| uri.query = nil })
106
111
  end
107
112
 
108
113
  # Returns the OAuth Authorization header string
@@ -120,18 +125,30 @@ module SimpleOAuth
120
125
 
121
126
  # Validates the signature in the header against the provided secrets
122
127
  #
128
+ # The method a received header claims is chosen by whoever sent it, so it is checked before it
129
+ # is used. Given a signature_method, the header must claim that method. Without one, the header
130
+ # must claim a method that fits the key: an RSA method for an RSA key, and an HMAC method for any
131
+ # other secret. PLAINTEXT is accepted only when named, as it sends the secrets themselves.
132
+ # Without that check, a sender holding only an RSA public key could sign with it as an HMAC or
133
+ # PLAINTEXT secret.
134
+ #
123
135
  # @api public
124
- # @param secrets [Hash] the consumer_secret and token_secret for validation
136
+ # @param secrets [Hash] the consumer_secret and token_secret for validation, and the
137
+ # signature_method the header must use, which a later major version will require
125
138
  # @return [Boolean] true if the signature is valid, false otherwise
139
+ # @raise [ArgumentError] if the expected signature method is not registered
126
140
  # @note When the header was built with a body, the signed oauth_body_hash must match that body,
127
141
  # so a tampered body fails verification even though its signature covers the claimed hash
128
142
  # @example
129
143
  # parsed_header = SimpleOAuth::Header.new(:get, url, {}, authorization_header)
130
- # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret")
144
+ # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret",
145
+ # signature_method: "HMAC-SHA1")
131
146
  # # => true
132
147
  def valid?(secrets = {})
133
- body_hash_valid? && Signature.verify(options.fetch(:signature_method), signing_key(options.merge(secrets)),
134
- signature_base, options.fetch(:signature))
148
+ 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))
135
152
  end
136
153
 
137
154
  # Returns the OAuth attributes including the signature
@@ -213,6 +230,50 @@ module SimpleOAuth
213
230
  Signature.rsa?(options.fetch(:signature_method)) ? options[:consumer_secret] : secret(options)
214
231
  end
215
232
 
233
+ # Checks that the header claims a signature method the verifier accepts
234
+ #
235
+ # @api private
236
+ # @param verifying [Hash] the header's options merged with the verifier's secrets
237
+ # @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
273
+ rescue OpenSSL::PKey::PKeyError
274
+ false
275
+ end
276
+
216
277
  # Checks the body against the oauth_body_hash the header carries
217
278
  #
218
279
  # 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.0".freeze
4
+ VERSION = "0.5.2".freeze
5
5
  end
@@ -28,6 +28,9 @@ module SimpleOAuth
28
28
 
29
29
  private
30
30
 
31
+ # Parses a form-encoded body into the parameter pairs to sign
32
+ def form_params: (String? body) -> Array[[String, String]]
33
+
31
34
  # Checks whether a request carries a form-encoded body
32
35
  def form_encoded?: (Header::_Request request) -> bool
33
36
 
data/sig/simple_oauth.rbs CHANGED
@@ -58,6 +58,9 @@ 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
+
61
64
  # What from_request needs from a request object, such as a Net::HTTPRequest
62
65
  interface _Request
63
66
  def method: () -> String
@@ -144,6 +147,15 @@ module SimpleOAuth
144
147
  # The key for signing and verifying
145
148
  def signing_key: (oauth_options options) -> String?
146
149
 
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
158
+
147
159
  # Checks the body against the oauth_body_hash the header carries
148
160
  def body_hash_valid?: () -> bool
149
161
 
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.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Richert
@@ -72,7 +72,6 @@ files:
72
72
  - sig/simple_oauth/parser.rbs
73
73
  - sig/simple_oauth/signature.rbs
74
74
  - sig/strscan.rbs
75
- - sig/uri_ext.rbs
76
75
  homepage: https://github.com/laserlemon/simple_oauth
77
76
  licenses:
78
77
  - MIT
@@ -96,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
95
  - !ruby/object:Gem::Version
97
96
  version: '0'
98
97
  requirements: []
99
- rubygems_version: 4.0.20
98
+ rubygems_version: 4.0.24
100
99
  specification_version: 4
101
100
  summary: Simply builds and verifies OAuth headers
102
101
  test_files: []
data/sig/uri_ext.rbs DELETED
@@ -1,7 +0,0 @@
1
- # Extensions to the URI module
2
- module URI
3
- class Generic
4
- # Returns the URI as a String (implicit conversion)
5
- def to_str: () -> String
6
- end
7
- end