http_signature 1.0.1 → 1.1.0
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 +4 -4
- data/AGENTS.md +4 -0
- data/Gemfile.lock +1 -1
- data/README.md +49 -27
- data/http_signature.gemspec +1 -1
- data/lib/http_signature/rack.rb +1 -3
- data/lib/http_signature/rails.rb +3 -5
- data/lib/http_signature/version.rb +1 -1
- data/lib/http_signature.rb +51 -24
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d7340510585b31400802574581fc40aa006889f7f43e67e6ac4d5c926ddc3ce
|
|
4
|
+
data.tar.gz: 3364453874b93eb6b37a2ef13208dbb2792b4b50975c872459fea1a1d70a68d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba102a57a504be38d46ff962442d273dcee1866e5e49127927f638774dc30a66b92c063edb62670ba09b42b14bb2257772992183b606ef2abc7c1d1340677717
|
|
7
|
+
data.tar.gz: b2ee1d08d85958e663760b999c45a8865e2db4f43920fbf9a77bae78479a5192e939d0cfdab26a6d47dd936c1bb17311b809ac6b8278f92d1c8ced43de957d9d
|
data/AGENTS.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# HTTP signature gem
|
|
2
|
+
|
|
3
|
+
This is a Ruby gem implementing the [HTTP Message Signatures RFC 9421 standard](https://www.rfc-editor.org/rfc/rfc9421.txt). Always adhere to the standard
|
|
4
|
+
|
|
1
5
|
## Tests
|
|
2
6
|
- Run all tests: `bundle exec rake test`
|
|
3
7
|
- Run single test file: `bundle exec rake test TEST=test/http_signature_test.rb`
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -23,21 +23,40 @@ bundle add http_signature
|
|
|
23
23
|
|
|
24
24
|
`HTTPSignature.create` returns both `Signature-Input` and `Signature` headers that you can include in your request.
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
```ruby
|
|
28
|
-
headers = {
|
|
27
|
+
headers = { "date" => "Tue, 20 Apr 2021 02:07:55 GMT" }
|
|
29
28
|
|
|
30
29
|
sig_headers = HTTPSignature.create(
|
|
31
|
-
url:
|
|
30
|
+
url: "https://example.com/foo?pet=dog",
|
|
32
31
|
method: :get,
|
|
32
|
+
key_id: "Test",
|
|
33
|
+
key: "secret",
|
|
33
34
|
headers: headers,
|
|
34
|
-
|
|
35
|
-
key: 'secret',
|
|
36
|
-
covered_components: %w[@method @target-uri date],
|
|
35
|
+
components: %w[@method @target-uri date]
|
|
37
36
|
)
|
|
38
37
|
|
|
39
|
-
request[
|
|
40
|
-
request[
|
|
38
|
+
request["Signature-Input"] = sig_headers["Signature-Input"]
|
|
39
|
+
request["Signature"] = sig_headers["Signature"]
|
|
40
|
+
```
|
|
41
|
+
#### All options
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
HTTPSignature.create(
|
|
45
|
+
url: "https://example.com/foo?pet=dog",
|
|
46
|
+
method: :get,
|
|
47
|
+
key_id: "Test",
|
|
48
|
+
key: "secret",
|
|
49
|
+
# Optional arguments
|
|
50
|
+
headers: headers, # Default: {}
|
|
51
|
+
body: "Hello world", # Default: ""
|
|
52
|
+
components: %w[@method @target-uri date], # Default: %w[@method @target-uri content-digest content-type]
|
|
53
|
+
created: Time.now.to_i, # Default: Time.now.to_i
|
|
54
|
+
expires: Time.now.to_i + 600, # Default: nil
|
|
55
|
+
nonce: "1", # Default: nil
|
|
56
|
+
label: "sig1", # Default: "sig1",
|
|
57
|
+
query_string_params: {pet2: "cat"} # Default: {}, you can pass query string params both here and in the `url` param
|
|
58
|
+
algorithm: "hmac-sha512" # Default: "hmac-sha256"
|
|
59
|
+
)
|
|
41
60
|
```
|
|
42
61
|
|
|
43
62
|
|
|
@@ -52,6 +71,9 @@ HTTPSignature.valid?(
|
|
|
52
71
|
headers: headers,
|
|
53
72
|
key: "secret"
|
|
54
73
|
)
|
|
74
|
+
|
|
75
|
+
# Returns true when all is good.
|
|
76
|
+
# Raises `SignatureError` for invalid signatures
|
|
55
77
|
```
|
|
56
78
|
|
|
57
79
|
## Outgoing request examples
|
|
@@ -59,10 +81,10 @@ HTTPSignature.valid?(
|
|
|
59
81
|
### NET::HTTP
|
|
60
82
|
|
|
61
83
|
```ruby
|
|
62
|
-
require
|
|
63
|
-
require
|
|
84
|
+
require "net/http"
|
|
85
|
+
require "http_signature"
|
|
64
86
|
|
|
65
|
-
uri = URI(
|
|
87
|
+
uri = URI("http://example.com/hello")
|
|
66
88
|
|
|
67
89
|
Net::HTTP.start(uri.host, uri.port) do |http|
|
|
68
90
|
request = Net::HTTP::Get.new(uri)
|
|
@@ -71,14 +93,14 @@ Net::HTTP.start(uri.host, uri.port) do |http|
|
|
|
71
93
|
url: request.uri,
|
|
72
94
|
method: request.method,
|
|
73
95
|
headers: request.each_header.map { |k, v| [k, v] }.to_h,
|
|
74
|
-
key:
|
|
75
|
-
key_id:
|
|
76
|
-
algorithm:
|
|
77
|
-
body: request.body
|
|
96
|
+
key: "MYSECRETKEY",
|
|
97
|
+
key_id: "KEY_1",
|
|
98
|
+
algorithm: "hmac-sha256",
|
|
99
|
+
body: request.body || ""
|
|
78
100
|
)
|
|
79
101
|
|
|
80
|
-
request[
|
|
81
|
-
request[
|
|
102
|
+
request["Signature-Input"] = sig_headers["Signature-Input"]
|
|
103
|
+
request["Signature"] = sig_headers["Signature"]
|
|
82
104
|
|
|
83
105
|
response = http.request(request) # Net::HTTPResponse
|
|
84
106
|
end
|
|
@@ -89,18 +111,18 @@ end
|
|
|
89
111
|
As a faraday middleware
|
|
90
112
|
|
|
91
113
|
```ruby
|
|
92
|
-
require
|
|
114
|
+
require "http_signature/faraday"
|
|
93
115
|
|
|
94
|
-
HTTPSignature::Faraday.key =
|
|
95
|
-
HTTPSignature::Faraday.key_id =
|
|
116
|
+
HTTPSignature::Faraday.key = "secret"
|
|
117
|
+
HTTPSignature::Faraday.key_id = "key-1"
|
|
96
118
|
|
|
97
|
-
Faraday.new(
|
|
119
|
+
Faraday.new("http://example.com") do |faraday|
|
|
98
120
|
faraday.use(HTTPSignature::Faraday)
|
|
99
121
|
faraday.adapter(Faraday.default_adapter)
|
|
100
122
|
end
|
|
101
123
|
|
|
102
124
|
# Now this request will contain the `Signature-Input` and `Signature` headers
|
|
103
|
-
response = conn.get(
|
|
125
|
+
response = conn.get("/")
|
|
104
126
|
|
|
105
127
|
# Request looking like:
|
|
106
128
|
# Signature-Input: sig1=("@method" "@authority" "@target-uri" "date");created=...
|
|
@@ -115,14 +137,14 @@ Rack middlewares sits in between your app and the HTTP request and validate the
|
|
|
115
137
|
Here is how it could be used with sinatra:
|
|
116
138
|
|
|
117
139
|
```ruby
|
|
118
|
-
require
|
|
140
|
+
require "http_signature/rack"
|
|
119
141
|
|
|
120
142
|
HTTPSignature.configure do |config|
|
|
121
143
|
config.keys = [
|
|
122
|
-
{id:
|
|
144
|
+
{id: "key-1", value: "MySecureKey"}
|
|
123
145
|
]
|
|
124
146
|
end
|
|
125
|
-
HTTPSignature::Rack.exclude_paths = [
|
|
147
|
+
HTTPSignature::Rack.exclude_paths = ["/", "/hello/*"]
|
|
126
148
|
|
|
127
149
|
use HTTPSignature::Rack
|
|
128
150
|
run MyApp
|
|
@@ -134,7 +156,7 @@ Opt-in per controller/action using a before_action. It responds with `401 Unauth
|
|
|
134
156
|
```ruby
|
|
135
157
|
# app/controllers/api/base_controller.rb
|
|
136
158
|
|
|
137
|
-
require
|
|
159
|
+
require "http_signature/rails"
|
|
138
160
|
|
|
139
161
|
class Api::BaseController < ApplicationController
|
|
140
162
|
include HTTPSignature::Rails::Controller
|
|
@@ -149,7 +171,7 @@ Set the keys in an initializer
|
|
|
149
171
|
|
|
150
172
|
HTTPSignature.configure do |config|
|
|
151
173
|
config.keys = [
|
|
152
|
-
{id:
|
|
174
|
+
{id: "key-1", value: "MySecureKey"}
|
|
153
175
|
]
|
|
154
176
|
end
|
|
155
177
|
```
|
data/http_signature.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
|
-
spec.required_ruby_version = ">= 3.
|
|
21
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
22
22
|
|
|
23
23
|
spec.add_development_dependency "bundler"
|
|
24
24
|
spec.add_development_dependency "rake"
|
data/lib/http_signature/rack.rb
CHANGED
|
@@ -33,7 +33,7 @@ class HTTPSignature::Rack
|
|
|
33
33
|
else
|
|
34
34
|
""
|
|
35
35
|
end
|
|
36
|
-
|
|
36
|
+
HTTPSignature.valid?(
|
|
37
37
|
url: request.url,
|
|
38
38
|
method: request.request_method,
|
|
39
39
|
headers: request_headers,
|
|
@@ -44,8 +44,6 @@ class HTTPSignature::Rack
|
|
|
44
44
|
return [401, {}, ["Invalid signature"]]
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
return [401, {}, ["Invalid signature"]] unless valid_signature
|
|
48
|
-
|
|
49
47
|
@app.call(env)
|
|
50
48
|
end
|
|
51
49
|
|
data/lib/http_signature/rails.rb
CHANGED
|
@@ -20,7 +20,7 @@ module HTTPSignature
|
|
|
20
20
|
|
|
21
21
|
request_body = read_request_body
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
HTTPSignature.valid?(
|
|
24
24
|
url: request.url,
|
|
25
25
|
method: request.request_method,
|
|
26
26
|
headers: request_headers,
|
|
@@ -28,10 +28,8 @@ module HTTPSignature
|
|
|
28
28
|
key_resolver: ->(key_id) { HTTPSignature.key(key_id) }
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
render status: :unauthorized, plain: "Invalid signature"
|
|
34
|
-
rescue HTTPSignature::SignatureError, ArgumentError
|
|
31
|
+
nil
|
|
32
|
+
rescue HTTPSignature::SignatureError
|
|
35
33
|
render status: :unauthorized, plain: "Invalid signature"
|
|
36
34
|
end
|
|
37
35
|
|
data/lib/http_signature.rb
CHANGED
|
@@ -17,6 +17,7 @@ module HTTPSignature
|
|
|
17
17
|
class SignatureError < StandardError; end
|
|
18
18
|
class MissingComponent < SignatureError; end
|
|
19
19
|
class UnsupportedAlgorithm < SignatureError; end
|
|
20
|
+
class ExpiredError < SignatureError; end
|
|
20
21
|
|
|
21
22
|
Algorithm = Struct.new(:type, :digest_name, :curve)
|
|
22
23
|
ALGORITHMS = {
|
|
@@ -56,18 +57,28 @@ module HTTPSignature
|
|
|
56
57
|
headers: {},
|
|
57
58
|
body: "",
|
|
58
59
|
algorithm: DEFAULT_ALGORITHM,
|
|
59
|
-
|
|
60
|
+
components: nil,
|
|
60
61
|
created: Time.now.to_i,
|
|
62
|
+
expires: nil,
|
|
61
63
|
nonce: nil,
|
|
62
64
|
label: DEFAULT_LABEL,
|
|
63
65
|
query_string_params: {}
|
|
64
66
|
)
|
|
67
|
+
unless created.is_a?(Integer)
|
|
68
|
+
raise ArgumentError, "created must be a Unix timestamp integer"
|
|
69
|
+
end
|
|
70
|
+
if expires && !expires.is_a?(Integer)
|
|
71
|
+
raise ArgumentError, "expires must be a Unix timestamp integer"
|
|
72
|
+
end
|
|
73
|
+
if expires && created > expires
|
|
74
|
+
raise ArgumentError, "expires (#{expires}) must be greater than created (#{created})"
|
|
75
|
+
end
|
|
65
76
|
algorithm_entry = algorithm_entry_for(algorithm)
|
|
66
77
|
normalized_headers = normalize_headers(headers)
|
|
67
78
|
uri = apply_query_params(URI(url), query_string_params)
|
|
68
79
|
|
|
69
|
-
components
|
|
70
|
-
|
|
80
|
+
components ||=
|
|
81
|
+
default_components(normalized_headers, body:)
|
|
71
82
|
|
|
72
83
|
normalized_headers =
|
|
73
84
|
if components.include?("content-digest")
|
|
@@ -77,20 +88,21 @@ module HTTPSignature
|
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
canonical_components = build_components(
|
|
80
|
-
uri
|
|
81
|
-
method
|
|
91
|
+
uri:,
|
|
92
|
+
method:,
|
|
82
93
|
headers: normalized_headers,
|
|
83
|
-
|
|
94
|
+
components:
|
|
84
95
|
)
|
|
85
96
|
|
|
86
97
|
signature_input_header, base_string = build_signature_input(
|
|
87
|
-
label
|
|
88
|
-
components
|
|
89
|
-
created
|
|
90
|
-
|
|
98
|
+
label:,
|
|
99
|
+
components:,
|
|
100
|
+
created:,
|
|
101
|
+
expires:,
|
|
102
|
+
key_id:,
|
|
91
103
|
alg: algorithm,
|
|
92
|
-
nonce
|
|
93
|
-
canonical_components:
|
|
104
|
+
nonce:,
|
|
105
|
+
canonical_components:
|
|
94
106
|
)
|
|
95
107
|
|
|
96
108
|
signature_bytes = sign(base_string, key: key, algorithm: algorithm_entry)
|
|
@@ -104,7 +116,7 @@ module HTTPSignature
|
|
|
104
116
|
|
|
105
117
|
# Verify RFC 9421 Signature headers
|
|
106
118
|
#
|
|
107
|
-
# @return [Boolean]
|
|
119
|
+
# @return [Boolean] true when signature verification succeeds
|
|
108
120
|
def self.valid?(
|
|
109
121
|
url:,
|
|
110
122
|
method:,
|
|
@@ -126,6 +138,12 @@ module HTTPSignature
|
|
|
126
138
|
|
|
127
139
|
algorithm_entry = algorithm_entry_for(parsed_input[:params][:alg] || DEFAULT_ALGORITHM)
|
|
128
140
|
key_id = parsed_input[:params][:keyid]
|
|
141
|
+
created = parsed_input[:params][:created].to_i
|
|
142
|
+
expires = parsed_input[:params][:expires]&.to_i
|
|
143
|
+
now = Time.now.to_i
|
|
144
|
+
if expires && (created > expires || now > expires)
|
|
145
|
+
raise ExpiredError, "Signature expired at #{expires}"
|
|
146
|
+
end
|
|
129
147
|
resolved_key = key || key_resolver&.call(key_id) || key_from_store(key_id)
|
|
130
148
|
raise SignatureError, "Key is required for verification" unless resolved_key
|
|
131
149
|
|
|
@@ -135,23 +153,27 @@ module HTTPSignature
|
|
|
135
153
|
end
|
|
136
154
|
|
|
137
155
|
canonical_components = build_components(
|
|
138
|
-
uri
|
|
139
|
-
method
|
|
156
|
+
uri:,
|
|
157
|
+
method:,
|
|
140
158
|
headers: normalized_headers,
|
|
141
|
-
|
|
159
|
+
components: parsed_input[:components]
|
|
142
160
|
)
|
|
143
161
|
|
|
144
162
|
_, base_string = build_signature_input(
|
|
145
|
-
label
|
|
163
|
+
label:,
|
|
146
164
|
components: parsed_input[:components],
|
|
147
|
-
created
|
|
148
|
-
|
|
165
|
+
created:,
|
|
166
|
+
expires:,
|
|
167
|
+
key_id:,
|
|
149
168
|
alg: parsed_input[:params][:alg],
|
|
150
169
|
nonce: parsed_input[:params][:nonce],
|
|
151
|
-
canonical_components:
|
|
170
|
+
canonical_components:
|
|
152
171
|
)
|
|
153
172
|
|
|
154
|
-
verify_signature(base_string, parsed_signature, resolved_key, algorithm_entry)
|
|
173
|
+
verified = verify_signature(base_string, parsed_signature, resolved_key, algorithm_entry)
|
|
174
|
+
raise SignatureError, "Invalid signature" unless verified
|
|
175
|
+
|
|
176
|
+
true
|
|
155
177
|
end
|
|
156
178
|
|
|
157
179
|
# -- Private-ish helpers --
|
|
@@ -194,8 +216,8 @@ module HTTPSignature
|
|
|
194
216
|
headers.merge("content-digest" => "sha-256=:#{Base64.strict_encode64(digest)}:")
|
|
195
217
|
end
|
|
196
218
|
|
|
197
|
-
def self.build_components(uri:, method:, headers:,
|
|
198
|
-
|
|
219
|
+
def self.build_components(uri:, method:, headers:, components:)
|
|
220
|
+
components.map do |component|
|
|
199
221
|
if component.start_with?("@")
|
|
200
222
|
[component, derived_component(component, uri, method)]
|
|
201
223
|
else
|
|
@@ -236,13 +258,16 @@ module HTTPSignature
|
|
|
236
258
|
label:,
|
|
237
259
|
components:,
|
|
238
260
|
created:,
|
|
261
|
+
expires:,
|
|
239
262
|
key_id:,
|
|
240
263
|
alg:,
|
|
241
264
|
nonce:,
|
|
242
265
|
canonical_components:
|
|
243
266
|
)
|
|
244
267
|
component_tokens = components.map { |c| %("#{escape_structured_string(c)}") }.join(" ")
|
|
245
|
-
params = ["created=#{created}"
|
|
268
|
+
params = ["created=#{created}"]
|
|
269
|
+
params << "expires=#{expires}" unless expires.nil?
|
|
270
|
+
params << %(keyid="#{escape_structured_string(key_id)}")
|
|
246
271
|
params << %(alg="#{escape_structured_string(alg)}") if alg
|
|
247
272
|
params << %(nonce="#{escape_structured_string(nonce)}") if nonce
|
|
248
273
|
|
|
@@ -349,6 +374,8 @@ module HTTPSignature
|
|
|
349
374
|
|
|
350
375
|
encoded = match[1]
|
|
351
376
|
Base64.strict_decode64(encoded)
|
|
377
|
+
rescue ArgumentError
|
|
378
|
+
raise SignatureError, "Invalid signature format"
|
|
352
379
|
end
|
|
353
380
|
|
|
354
381
|
def self.split_header(header)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: http_signature
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joel Larsson
|
|
@@ -170,7 +170,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
170
170
|
requirements:
|
|
171
171
|
- - ">="
|
|
172
172
|
- !ruby/object:Gem::Version
|
|
173
|
-
version: 3.
|
|
173
|
+
version: 3.1.0
|
|
174
174
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
175
|
requirements:
|
|
176
176
|
- - ">="
|