simple_oauth 1.0.1 → 1.0.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: 48873a9ac8c602b3dc3d305445c3e44524eee89e876e53222928fdf17902b22b
4
- data.tar.gz: 166bcb795c1b953adf9c9dfaea2d6c69d8f00ba1bbc2953706ea9e7cca3fa87a
3
+ metadata.gz: ed41a0a0f5917e46e47d2fc785730f9c194a0541af8199b46a4928405000f110
4
+ data.tar.gz: 34d5043273278eb6571534259ee8628268c851d468462e6a25806402ce2afd83
5
5
  SHA512:
6
- metadata.gz: 377164f7799c7e6f330b2ac80fcf144380c270e6a65c40b6fb272e1fca6fd9a52515161f7237a96a3fefd9d6efd03000fd675141fc52f6330fe4daeabd6ee854
7
- data.tar.gz: 6585353e86aa82b24cb3d08dc41e5ec12162b5866b2e35b25832a1603114cb1472f6323ab503be5ff47d69eedddff63ff136e1726e7e0672b677aaa681a22a52
6
+ metadata.gz: eca0708e81fc8a823e7c5c3e87b48003bce4f6da9ac16d9c8fc0f6a5a45003261ecfaae9bf787e54f5f46b89acdaaf2c1719ad9ba9ac52c4523aaf201cd1aaac
7
+ data.tar.gz: ba7096154201bf4a19cfa3c34e5cf92a5c566e46165be3d1bf9632bbeb28bcd78b891b07a6743d631140a18a1d3bd7170176d65585af3da0f455a01a06236479
data/.rubocop.yml CHANGED
@@ -7,8 +7,14 @@ plugins:
7
7
  - rubocop-performance
8
8
  - rubocop-rake
9
9
 
10
+ inherit_mode:
11
+ merge:
12
+ - Include
13
+
10
14
  AllCops:
11
15
  NewCops: enable
16
+ Include:
17
+ - .duel.rb
12
18
  TargetRubyVersion: 3.2
13
19
 
14
20
  Layout/ArgumentAlignment:
data/CHANGELOG.md CHANGED
@@ -1,103 +1,156 @@
1
- ## [1.0.1] - 2026-09-30
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.3] - 2026-11-14
2
9
 
3
10
  ### Fixed
4
11
 
5
- * Ship RBS signatures that leave core and the standard library alone: the gem no longer redeclares `MatchData#[]`, `OpenSSL.secure_compare`, `OpenSSL::PKey::PKey#sign` and `#verify`, or `StringScanner`, so code that loads its signatures through `rbs collection` no longer fails `rbs validate` with a duplicated `MatchData#[]` definition
6
- * Add `sig/manifest.yaml`, naming the standard libraries the signatures refer to (`json`, `openssl`, `securerandom`, `strscan`, and `uri`), so `rbs collection` loads them for code that depends on the gem
12
+ - 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
7
13
 
8
- ## [1.0.0] - 2026-09-12
14
+ ## [0.5.3] - 2026-11-14
9
15
 
10
- ### Changed
16
+ ### Fixed
17
+
18
+ - 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
19
+
20
+ ## [1.0.2] - 2026-11-14
21
+
22
+ ### Security
23
+
24
+ - `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))
25
+ - Given a `signature_method`, the header must claim that method
26
+ - Without one, the header must claim an RSA method for an RSA key and an HMAC method for any other secret
27
+ - PLAINTEXT signatures verify only when the caller names `PLAINTEXT`
28
+
29
+ ### Deprecated
30
+
31
+ - Calling `Header#valid?` without a `signature_method`, which a later major version will require
32
+
33
+ ## [0.5.2] - 2026-11-14
34
+
35
+ ### Security
36
+
37
+ - `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))
38
+ - Given a `signature_method`, the header must claim that method
39
+ - Without one, the header must claim an RSA method for an RSA key and an HMAC method for any other secret
40
+ - PLAINTEXT signatures verify only when the caller names `PLAINTEXT`
11
41
 
12
- * Every file declares `frozen_string_literal: true`, and the explicit `freeze` calls on string literals it makes redundant are gone
13
- * Drop the `base64` runtime dependency: `pack("m0")` and `unpack1("m")` encode and decode Base64 in core Ruby, so the gem now has no runtime dependencies at all
14
- * Drop the `cgi` runtime dependency: query strings and form bodies are read with `URI.decode_www_form`, which Ruby ships in every supported version
15
- * **Breaking**: raise `ParseError` when an Authorization header, form body, or query string repeats an OAuth protocol parameter, which RFC 5849 Section 3.2 does not allow; `Header.parse` previously kept the last value and `Header.parse_form_body` the first, so the two disagreed about which one a request carried
16
- * **Breaking**: rename `Signature.methods` to `Signature.registered_methods`, so that `Signature.methods` is the module's own method list again
17
- * **Breaking**: define `VERSION` in `SimpleOAuth`, the module the rest of the library uses, rather than in a second `SimpleOauth` module; `SimpleOauth::VERSION` is gone and `SimpleOAuth::VERSION` now resolves
18
- * **Breaking**: `Signature.rsa?` raises `ArgumentError` for a signature method that is not registered, as `Signature.digest`, `sign`, and `verify` already do, rather than answering false
19
- * `SimpleOAuth::Error` is the base of every error the library raises, so `rescue SimpleOAuth::Error` catches `ParseError`, `InvalidOptionsError`, and `OAuth2::Error` alike
42
+ ### Deprecated
43
+
44
+ - Calling `Header#valid?` without a `signature_method`, which a later major version will require
45
+
46
+ ## [1.0.1] - 2026-09-30
20
47
 
21
48
  ### Fixed
22
49
 
23
- * Treat only `&` as a parameter separator when reading a query string or form body; a `;` is part of the value, as every current server reads it, so `?a=1;b=2` no longer signs two parameters where the server sees one
50
+ - Ship RBS signatures that leave core and the standard library alone: the gem no longer redeclares `MatchData#[]`, `OpenSSL.secure_compare`, `OpenSSL::PKey::PKey#sign` and `#verify`, or `StringScanner`, so code that loads its signatures through `rbs collection` no longer fails `rbs validate` with a duplicated `MatchData#[]` definition
51
+ - Add `sig/manifest.yaml`, naming the standard libraries the signatures refer to (`json`, `openssl`, `securerandom`, `strscan`, and `uri`), so `rbs collection` loads them for code that depends on the gem
52
+
53
+ ## [1.0.0] - 2026-09-12
24
54
 
25
55
  ### Added
26
56
 
27
- * OAuth 2.0 request builders and response parsers in `SimpleOAuth::OAuth2`, which make no HTTP requests:
28
- * `Client#authorization_url` for the authorization code flow, which names `pkce` so that a client either sends a challenge or says `pkce: nil`, as OAuth 2.1 asks every client for one; a `state` is optional alongside a challenge, and required without one
29
- * `Client#authorization_code_request`, `#refresh_token_request`, and `#client_credentials_request` for the token endpoint
30
- * `Client#revocation_request` for the revocation endpoint (RFC 7009)
31
- * `client_secret_basic` and `client_secret_post` authentication for confidential clients, and public clients without a secret
32
- * `PKCE` verifiers with `S256` and `plain` challenges (RFC 7636)
33
- * `AuthorizationResponse.parse`, which reads the response an authorization server returns to the redirect URI: it raises the error the server reported, a `state` that is not the one the request sent, an `iss` that is not the expected issuer (RFC 9207), a repeated parameter, or a response with no code
34
- * A `params` option on every request builder, for what an extension adds to a request, such as the resource indicator of RFC 8707
35
- * `Token.from_response` and `Error.from_response` for token and error responses, rejecting a response whose access token is missing, null, or empty, or whose lifetime is not a number of seconds
57
+ - OAuth 2.0 request builders and response parsers in `SimpleOAuth::OAuth2`, which make no HTTP requests:
58
+ - `Client#authorization_url` for the authorization code flow, which names `pkce` so that a client either sends a challenge or says `pkce: nil`, as OAuth 2.1 asks every client for one; a `state` is optional alongside a challenge, and required without one
59
+ - `Client#authorization_code_request`, `#refresh_token_request`, and `#client_credentials_request` for the token endpoint
60
+ - `Client#revocation_request` for the revocation endpoint (RFC 7009)
61
+ - `client_secret_basic` and `client_secret_post` authentication for confidential clients, and public clients without a secret
62
+ - `PKCE` verifiers with `S256` and `plain` challenges (RFC 7636)
63
+ - `AuthorizationResponse.parse`, which reads the response an authorization server returns to the redirect URI: it raises the error the server reported, a `state` that is not the one the request sent, an `iss` that is not the expected issuer (RFC 9207), a repeated parameter, or a response with no code
64
+ - A `params` option on every request builder, for what an extension adds to a request, such as the resource indicator of RFC 8707
65
+ - `Token.from_response` and `Error.from_response` for token and error responses, rejecting a response whose access token is missing, null, or empty, or whose lifetime is not a number of seconds
66
+
67
+ ### Changed
68
+
69
+ - Every file declares `frozen_string_literal: true`, and the explicit `freeze` calls on string literals it makes redundant are gone
70
+ - Drop the `base64` runtime dependency: `pack("m0")` and `unpack1("m")` encode and decode Base64 in core Ruby, so the gem now has no runtime dependencies at all
71
+ - Drop the `cgi` runtime dependency: query strings and form bodies are read with `URI.decode_www_form`, which Ruby ships in every supported version
72
+ - **Breaking:** Raise `ParseError` when an Authorization header, form body, or query string repeats an OAuth protocol parameter, which RFC 5849 Section 3.2 does not allow; `Header.parse` previously kept the last value and `Header.parse_form_body` the first, so the two disagreed about which one a request carried
73
+ - **Breaking:** Rename `Signature.methods` to `Signature.registered_methods`, so that `Signature.methods` is the module's own method list again
74
+ - **Breaking:** Define `VERSION` in `SimpleOAuth`, the module the rest of the library uses, rather than in a second `SimpleOauth` module; `SimpleOauth::VERSION` is gone and `SimpleOAuth::VERSION` now resolves
75
+ - **Breaking:** `Signature.rsa?` raises `ArgumentError` for a signature method that is not registered, as `Signature.digest`, `sign`, and `verify` already do, rather than answering false
76
+ - `SimpleOAuth::Error` is the base of every error the library raises, so `rescue SimpleOAuth::Error` catches `ParseError`, `InvalidOptionsError`, and `OAuth2::Error` alike
77
+
78
+ ### Fixed
79
+
80
+ - Treat only `&` as a parameter separator when reading a query string or form body; a `;` is part of the value, as every current server reads it, so `?a=1;b=2` no longer signs two parameters where the server sees one
36
81
 
37
82
  ## [0.5.1] - 2026-09-12
38
83
 
39
84
  ### Fixed
40
85
 
41
- * 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
42
- * 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
86
+ - 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
87
+ - 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
43
88
 
44
89
  ## [0.5.0] - 2026-09-12
45
90
 
46
91
  ### Added
47
92
 
48
- * `Header.from_request`, which builds a header for a request object such as a `Net::HTTPRequest`, signing its query parameters, its form-encoded body, or hashing any other body
49
- * `Header.parse_query`, for OAuth credentials sent in a query string
50
- * `Signature.digest`, and a `digest:` option on `Signature.register`, which gives the hash algorithm a signature method signs with
51
- * `Signature.verify` and a `verify:` option on `Signature.register`, for signature methods that cannot be verified by recomputing the signature
52
- * `Signature.decode_base64`
93
+ - `Header.from_request`, which builds a header for a request object such as a `Net::HTTPRequest`, signing its query parameters, its form-encoded body, or hashing any other body
94
+ - `Header.parse_query`, for OAuth credentials sent in a query string
95
+ - `Signature.digest`, and a `digest:` option on `Signature.register`, which gives the hash algorithm a signature method signs with
96
+ - `Signature.verify` and a `verify:` option on `Signature.register`, for signature methods that cannot be verified by recomputing the signature
97
+ - `Signature.decode_base64`
53
98
 
54
99
  ### Fixed
55
100
 
56
- * Check `oauth_body_hash` against the body a header was built with when verifying, so a body changed after signing no longer verifies against the hash its signature covers
57
- * Verify signatures without merging the given secrets into the header's own options, where anything else reading the header could see them
58
- * Compare signatures in constant time when verifying
59
- * Compute `oauth_body_hash` with the hash algorithm of the signature method, such as SHA-256 for HMAC-SHA256; it was always SHA-1
60
- * Sign a parameter whose value is an Array as one parameter per value, as a repeated parameter; the Array was previously signed as its Ruby representation
61
- * Verify RSA signatures with the signer's public key, which is all a verifier has; `Header#valid?` previously recomputed the signature and so needed the private key
62
- * Match the form-encoded media type exactly when signing a body, rather than by prefix, so a media type such as `application/x-www-form-urlencoded-json` is hashed instead of signed as parameters, and `Application/X-WWW-Form-Urlencoded` is recognized
101
+ - Check `oauth_body_hash` against the body a header was built with when verifying, so a body changed after signing no longer verifies against the hash its signature covers
102
+ - Verify signatures without merging the given secrets into the header's own options, where anything else reading the header could see them
103
+ - Compare signatures in constant time when verifying
104
+ - Compute `oauth_body_hash` with the hash algorithm of the signature method, such as SHA-256 for HMAC-SHA256; it was always SHA-1
105
+ - Sign a parameter whose value is an Array as one parameter per value, as a repeated parameter; the Array was previously signed as its Ruby representation
106
+ - Verify RSA signatures with the signer's public key, which is all a verifier has; `Header#valid?` previously recomputed the signature and so needed the private key
107
+ - Match the form-encoded media type exactly when signing a body, rather than by prefix, so a media type such as `application/x-www-form-urlencoded-json` is hashed instead of signed as parameters, and `Application/X-WWW-Form-Urlencoded` is recognized
63
108
 
64
109
  ## [0.4.2] - 2026-09-12
65
110
 
66
111
  ### Added
67
112
 
68
- * Document passing `Header` parameters as an Array of key-value pairs when a key repeats
113
+ - Document passing `Header` parameters as an Array of key-value pairs when a key repeats
69
114
 
70
115
  ### Fixed
71
116
 
72
- * Accept an Array of key-value pairs as `Header` parameters in the RBS signatures, which already worked at runtime
117
+ - Accept an Array of key-value pairs as `Header` parameters in the RBS signatures, which already worked at runtime
73
118
 
74
119
  ## [0.4.1] - 2026-04-20
75
120
 
76
121
  ### Fixed
77
122
 
78
- * Remove `URI::RFC2396_PARSER` stub from RBS signatures to avoid duplicate declaration error with RBS 4.0.2, which now ships the constant in its stdlib `uri` signatures
123
+ - Remove `URI::RFC2396_PARSER` stub from RBS signatures to avoid duplicate declaration error with RBS 4.0.2, which now ships the constant in its stdlib `uri` signatures
79
124
 
80
125
  ## [0.4.0] - 2026-02-01
81
126
 
82
127
  ### Added
83
128
 
84
- * Extensible signature method registry allowing custom signature methods to be registered at runtime
85
- * Support for RSA-SHA256 and HMAC-SHA256 signature methods
86
- * OAuth Request Body Hash support (`oauth_body_hash` parameter) for signing requests with non-form-encoded bodies
87
- * Support for parsing OAuth credentials from POST body via `Header.parse_form_body`
88
- * Support for `realm` parameter in OAuth Authorization header
89
-
90
- ### Fixed
91
-
92
- * Avoid symbolizing untrusted input in parse methods for security
93
- * Refactored `Header.parse` for improved robustness using StringScanner
129
+ - Extensible signature method registry allowing custom signature methods to be registered at runtime
130
+ - Support for RSA-SHA256 and HMAC-SHA256 signature methods
131
+ - OAuth Request Body Hash support (`oauth_body_hash` parameter) for signing requests with non-form-encoded bodies
132
+ - Support for parsing OAuth credentials from POST body via `Header.parse_form_body`
133
+ - Support for `realm` parameter in OAuth Authorization header
94
134
 
95
135
  ### Changed
96
136
 
97
- * **Breaking**: `Signature.rsa?` raises `ArgumentError` for a signature method that is not registered, as `Signature.digest`, `sign`, and `verify` already do, rather than answering false
98
- * `SimpleOAuth::Error` is the base of every error the library raises, so `rescue SimpleOAuth::Error` catches `ParseError`, `InvalidOptionsError`, and `OAuth2::Error` alike
137
+ - **Breaking:** Require Ruby 3.2 or later; supports Ruby 3.2, 3.3, 3.4, and 4.0
138
+ - Added `base64` and `cgi` as explicit runtime dependencies
139
+ - Migrated test suite from RSpec to Minitest
99
140
 
100
- * Supports Ruby 3.2, 3.3, 3.4, and 4.0
101
- * Added `base64` and `cgi` as explicit runtime dependencies
102
- * Migrated test suite from RSpec to Minitest
141
+ ### Fixed
103
142
 
143
+ - Avoid symbolizing untrusted input in parse methods for security
144
+ - Refactored `Header.parse` for improved robustness using StringScanner
145
+
146
+ [1.0.3]: https://github.com/laserlemon/simple_oauth/compare/v1.0.2...v1.0.3
147
+ [0.5.3]: https://github.com/laserlemon/simple_oauth/compare/v0.5.2...v0.5.3
148
+ [1.0.2]: https://github.com/laserlemon/simple_oauth/compare/v1.0.1...v1.0.2
149
+ [0.5.2]: https://github.com/laserlemon/simple_oauth/compare/v0.5.1...v0.5.2
150
+ [1.0.1]: https://github.com/laserlemon/simple_oauth/compare/v1.0.0...v1.0.1
151
+ [1.0.0]: https://github.com/laserlemon/simple_oauth/compare/v0.5.1...v1.0.0
152
+ [0.5.1]: https://github.com/laserlemon/simple_oauth/compare/v0.5.0...v0.5.1
153
+ [0.5.0]: https://github.com/laserlemon/simple_oauth/compare/v0.4.2...v0.5.0
154
+ [0.4.2]: https://github.com/laserlemon/simple_oauth/compare/v0.4.1...v0.4.2
155
+ [0.4.1]: https://github.com/laserlemon/simple_oauth/compare/v0.4.0...v0.4.1
156
+ [0.4.0]: https://github.com/laserlemon/simple_oauth/compare/v0.3.1...v0.4.0
data/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![Lint](https://github.com/laserlemon/simple_oauth/actions/workflows/lint.yml/badge.svg)](https://github.com/laserlemon/simple_oauth/actions/workflows/lint.yml)
7
7
  [![Typecheck](https://github.com/laserlemon/simple_oauth/actions/workflows/typecheck.yml/badge.svg)](https://github.com/laserlemon/simple_oauth/actions/workflows/typecheck.yml)
8
8
  [![Yardstick](https://github.com/laserlemon/simple_oauth/actions/workflows/yardstick.yml/badge.svg)](https://github.com/laserlemon/simple_oauth/actions/workflows/yardstick.yml)
9
+ [![Duel](https://raw.githubusercontent.com/laserlemon/simple_oauth/duel/badge/badge.svg)](https://github.com/sferik/duel-ruby)
9
10
 
10
11
  Simply builds and verifies OAuth 1.0 headers per [RFC 5849](https://tools.ietf.org/html/rfc5849), and builds OAuth 2.0 requests per [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749), [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636), and [RFC 7009](https://www.rfc-editor.org/rfc/rfc7009).
11
12
 
@@ -158,16 +159,18 @@ parsed = SimpleOAuth::Header.parse_query("oauth_consumer_key=key&status=hello")
158
159
  header = SimpleOAuth::Header.new(:get, request_url, params, authorization_header)
159
160
 
160
161
  # Verify the signature
161
- header.valid?(consumer_secret: "secret", token_secret: "token_secret")
162
+ header.valid?(consumer_secret: "secret", token_secret: "token_secret", signature_method: "HMAC-SHA1")
162
163
  # => true
163
164
  ```
164
165
 
166
+ 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.
167
+
165
168
  Verifying compares signatures in constant time and leaves the header's own options untouched, so the secrets stay with the caller.
166
169
 
167
170
  RSA signatures verify with the client's public key, which is all a server has:
168
171
 
169
172
  ```ruby
170
- header.valid?(consumer_secret: File.read("client_public_key.pem"))
173
+ header.valid?(consumer_secret: File.read("client_public_key.pem"), signature_method: "RSA-SHA1")
171
174
  ```
172
175
 
173
176
  Custom signature methods that cannot be verified by recomputing the signature register a `verify` block:
data/Rakefile CHANGED
@@ -41,3 +41,10 @@ task :steep do
41
41
  end
42
42
 
43
43
  task default: %i[test rubocop standard mutant yardstick steep]
44
+
45
+ begin
46
+ require "duel/rake_task"
47
+ Duel::RakeTask.new
48
+ rescue LoadError
49
+ # Duel requires Ruby 3.4 or later
50
+ end
@@ -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
@@ -24,6 +25,9 @@ module SimpleOAuth
24
25
  # Default signature method per RFC 5849
25
26
  DEFAULT_SIGNATURE_METHOD = "HMAC-SHA1"
26
27
 
28
+ # The signature method that sends the secrets themselves, which verifying accepts only when named
29
+ PLAINTEXT_SIGNATURE_METHOD = "PLAINTEXT"
30
+
27
31
  # OAuth version
28
32
  OAUTH_VERSION = "1.0"
29
33
 
@@ -65,6 +69,7 @@ module SimpleOAuth
65
69
  attr_reader :options
66
70
 
67
71
  include Params
72
+ include Verification
68
73
 
69
74
  extend ClassMethods
70
75
  extend Encoding
@@ -123,18 +128,29 @@ module SimpleOAuth
123
128
 
124
129
  # Validates the signature in the header against the provided secrets
125
130
  #
131
+ # The method a received header claims is chosen by whoever sent it, so it is checked before it
132
+ # is used. Given a signature_method, the header must claim that method. Without one, the header
133
+ # must claim a method that fits the key: an RSA method for an RSA key, and an HMAC method for any
134
+ # other secret. PLAINTEXT is accepted only when named, as it sends the secrets themselves.
135
+ # Without that check, a sender holding only an RSA public key could sign with it as an HMAC or
136
+ # PLAINTEXT secret.
137
+ #
126
138
  # @api public
127
- # @param secrets [Hash] the consumer_secret and token_secret for validation
139
+ # @param secrets [Hash] the consumer_secret and token_secret for validation, and the
140
+ # signature_method the header must use, which a later major version will require
128
141
  # @return [Boolean] true if the signature is valid, false otherwise
142
+ # @raise [ArgumentError] if the expected signature method is not registered
129
143
  # @note When the header was built with a body, the signed oauth_body_hash must match that body,
130
144
  # so a tampered body fails verification even though its signature covers the claimed hash
131
145
  # @example
132
146
  # parsed_header = SimpleOAuth::Header.new(:get, url, {}, authorization_header)
133
- # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret")
147
+ # parsed_header.valid?(consumer_secret: "secret", token_secret: "token_secret",
148
+ # signature_method: "HMAC-SHA1")
134
149
  # # => true
135
150
  def valid?(secrets = {})
136
- body_hash_valid? && Signature.verify(options.fetch(:signature_method), signing_key(options.merge(secrets)),
137
- signature_base, options.fetch(:signature))
151
+ verifying = options.merge(secrets)
152
+ expected = secrets[:signature_method]
153
+ signature_method_acceptable?(verifying, expected) && body_hash_valid? && signature_valid?(verifying, expected)
138
154
  end
139
155
 
140
156
  # Returns the OAuth attributes including the signature
@@ -216,6 +232,28 @@ module SimpleOAuth
216
232
  Signature.rsa?(options.fetch(:signature_method)) ? options[:consumer_secret] : secret(options)
217
233
  end
218
234
 
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.
242
+ #
243
+ # @api private
244
+ # @param verifying [Hash] the header's options merged with the verifier's secrets
245
+ # @param expected [String, nil] the signature method the verifier expects, if it names one
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))
251
+ rescue OpenSSL::PKey::PKeyError
252
+ raise if expected
253
+
254
+ false
255
+ end
256
+
219
257
  # Checks the body against the oauth_body_hash the header carries
220
258
  #
221
259
  # A header parsed from a request claims a body hash that its signature covers, so the claim must be
@@ -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.1"
6
+ VERSION = "1.0.3"
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
data/sig/simple_oauth.rbs CHANGED
@@ -62,6 +62,11 @@ module SimpleOAuth
62
62
  # Valid keys when parsing OAuth parameters (ATTRIBUTE_KEYS + signature)
63
63
  PARSE_KEYS: Array[Symbol]
64
64
 
65
+ # The signature method that sends the secrets themselves, which verifying accepts only when named
66
+ PLAINTEXT_SIGNATURE_METHOD: String
67
+
68
+
69
+
65
70
  # What from_request needs from a request object, such as a Net::HTTPRequest
66
71
  interface _Request
67
72
  def method: () -> String
@@ -95,6 +100,9 @@ module SimpleOAuth
95
100
  # Parameter and attribute normalization from the Params module
96
101
  include Params
97
102
 
103
+ # Checks of the signature method a received header claims
104
+ include Verification
105
+
98
106
  # Class methods from ClassMethods module
99
107
  extend ClassMethods
100
108
 
@@ -148,6 +156,9 @@ module SimpleOAuth
148
156
  # The key for signing and verifying
149
157
  def signing_key: (oauth_options options) -> String?
150
158
 
159
+ # Verifies the signature with the method the header claims
160
+ def signature_valid?: (oauth_options verifying, String? expected) -> bool
161
+
151
162
  # Checks the body against the oauth_body_hash the header carries
152
163
  def body_hash_valid?: () -> bool
153
164
 
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.1
4
+ version: 1.0.3
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
@@ -43,11 +44,11 @@ files:
43
44
  - lib/simple_oauth/parser.rb
44
45
  - lib/simple_oauth/signature.rb
45
46
  - lib/simple_oauth/version.rb
46
- - mutant.yml
47
47
  - sig/manifest.yaml
48
48
  - sig/simple_oauth.rbs
49
49
  - sig/simple_oauth/header/class_methods.rbs
50
50
  - sig/simple_oauth/header/params.rbs
51
+ - sig/simple_oauth/header/verification.rbs
51
52
  - sig/simple_oauth/oauth2.rbs
52
53
  - sig/simple_oauth/parser.rbs
53
54
  - sig/simple_oauth/signature.rbs
@@ -74,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
75
  - !ruby/object:Gem::Version
75
76
  version: '0'
76
77
  requirements: []
77
- rubygems_version: 4.0.22
78
+ rubygems_version: 4.0.24
78
79
  specification_version: 4
79
80
  summary: Simply builds and verifies OAuth 1.0 headers and builds OAuth 2.0 requests
80
81
  test_files: []
data/mutant.yml DELETED
@@ -1,17 +0,0 @@
1
- usage: opensource
2
- integration:
3
- name: minitest
4
- includes:
5
- - lib
6
- - test
7
- requires:
8
- - simple_oauth
9
- mutation:
10
- operators: full
11
- timeout: 5.0
12
- matcher:
13
- subjects:
14
- - SimpleOAuth*
15
- coverage_criteria:
16
- process_abort: true
17
- test_result: true