http_signature 1.3.1 → 1.5.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/.github/dependabot.yml +9 -0
- data/AGENTS.md +1 -1
- data/CLAUDE.md +1 -0
- data/Gemfile.lock +26 -7
- data/README.md +137 -45
- data/http_signature.gemspec +3 -0
- data/lib/http_signature/faraday.rb +38 -14
- data/lib/http_signature/rails.rb +4 -2
- data/lib/http_signature/version.rb +1 -1
- data/lib/http_signature.rb +35 -4
- metadata +45 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c57222b061b994515d691691c87192a612eec84694085b03db51e3ea1fa2c75
|
|
4
|
+
data.tar.gz: ecc70179bb4d76a955b4711b6494e4e0a862ccf811b47d7d4aa3d46253f63066
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0da5638c340b56fb03fda5aed0e827ef2881e1515da8217c87aa45312d5d81880be3e37179100c9ace8f6788d38b99c916ea6de5a60881d057125d61754d8745
|
|
7
|
+
data.tar.gz: 600dfb9d3d1a6b4e89c73b9717b5a023b2ccb72c6a38f63dec3ba503e85c417d1af2e7cf8a126192802b63edd547ff4a8a96888d3d933131cea7e28cadf3a7fe
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Please see the documentation for all configuration options:
|
|
2
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
3
|
+
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: "bundler"
|
|
7
|
+
directory: "/"
|
|
8
|
+
schedule:
|
|
9
|
+
interval: "weekly"
|
data/AGENTS.md
CHANGED
|
@@ -18,4 +18,4 @@ Always run linters before commit
|
|
|
18
18
|
- Update version number in lib/http_signature/version.rb
|
|
19
19
|
- Run `bundle install`
|
|
20
20
|
- Commit with message "Version x.x.x" and push
|
|
21
|
-
- Create release on github with correct
|
|
21
|
+
- Create release on github with title "Version x.x.x", correct tag as "vx.x.x", e.g., v1.0.0, and auto generated notes
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
http_signature (1.
|
|
4
|
+
http_signature (1.5.0)
|
|
5
5
|
base64
|
|
6
6
|
starry (~> 0.2)
|
|
7
7
|
|
|
@@ -44,15 +44,26 @@ GEM
|
|
|
44
44
|
concurrent-ruby (1.3.6)
|
|
45
45
|
connection_pool (3.0.2)
|
|
46
46
|
crass (1.0.6)
|
|
47
|
+
csv (3.3.5)
|
|
47
48
|
docile (1.4.1)
|
|
48
49
|
drb (2.2.3)
|
|
49
50
|
erubi (1.13.1)
|
|
51
|
+
ethon (0.15.0)
|
|
52
|
+
ffi (>= 1.15.0)
|
|
53
|
+
excon (1.4.0)
|
|
54
|
+
logger
|
|
50
55
|
faraday (2.14.1)
|
|
51
56
|
faraday-net_http (>= 2.0, < 3.5)
|
|
52
57
|
json
|
|
53
58
|
logger
|
|
54
59
|
faraday-net_http (3.4.2)
|
|
55
60
|
net-http (~> 0.5)
|
|
61
|
+
ffi (1.17.3-arm64-darwin)
|
|
62
|
+
ffi (1.17.3-x86_64-linux-gnu)
|
|
63
|
+
httparty (0.24.2)
|
|
64
|
+
csv
|
|
65
|
+
mini_mime (>= 1.0.0)
|
|
66
|
+
multi_xml (>= 0.5.2)
|
|
56
67
|
i18n (1.14.8)
|
|
57
68
|
concurrent-ruby (~> 1.0)
|
|
58
69
|
json (2.18.1)
|
|
@@ -62,8 +73,11 @@ GEM
|
|
|
62
73
|
loofah (2.25.0)
|
|
63
74
|
crass (~> 1.0.2)
|
|
64
75
|
nokogiri (>= 1.12.0)
|
|
76
|
+
mini_mime (1.1.5)
|
|
65
77
|
minitest (6.0.0)
|
|
66
78
|
prism (~> 1.5)
|
|
79
|
+
multi_xml (0.8.1)
|
|
80
|
+
bigdecimal (>= 3.1, < 5)
|
|
67
81
|
net-http (0.9.1)
|
|
68
82
|
uri (>= 0.11.1)
|
|
69
83
|
nokogiri (1.19.0-arm64-darwin)
|
|
@@ -71,10 +85,10 @@ GEM
|
|
|
71
85
|
nokogiri (1.19.0-x86_64-linux-gnu)
|
|
72
86
|
racc (~> 1.4)
|
|
73
87
|
parallel (1.27.0)
|
|
74
|
-
parser (3.3.10.
|
|
88
|
+
parser (3.3.10.2)
|
|
75
89
|
ast (~> 2.4.1)
|
|
76
90
|
racc
|
|
77
|
-
prism (1.
|
|
91
|
+
prism (1.9.0)
|
|
78
92
|
racc (1.8.1)
|
|
79
93
|
rack (3.2.5)
|
|
80
94
|
rack-session (2.1.1)
|
|
@@ -92,7 +106,7 @@ GEM
|
|
|
92
106
|
rainbow (3.1.1)
|
|
93
107
|
rake (13.3.1)
|
|
94
108
|
regexp_parser (2.11.3)
|
|
95
|
-
rubocop (1.
|
|
109
|
+
rubocop (1.84.2)
|
|
96
110
|
json (~> 2.3)
|
|
97
111
|
language_server-protocol (~> 3.17.0.2)
|
|
98
112
|
lint_roller (~> 1.1.0)
|
|
@@ -100,7 +114,7 @@ GEM
|
|
|
100
114
|
parser (>= 3.3.0.2)
|
|
101
115
|
rainbow (>= 2.2.2, < 4.0)
|
|
102
116
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
103
|
-
rubocop-ast (>= 1.
|
|
117
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
104
118
|
ruby-progressbar (~> 1.7)
|
|
105
119
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
106
120
|
rubocop-ast (1.49.0)
|
|
@@ -118,10 +132,10 @@ GEM
|
|
|
118
132
|
simplecov_json_formatter (~> 0.1)
|
|
119
133
|
simplecov-html (0.13.2)
|
|
120
134
|
simplecov_json_formatter (0.1.4)
|
|
121
|
-
standard (1.
|
|
135
|
+
standard (1.54.0)
|
|
122
136
|
language_server-protocol (~> 3.17.0.2)
|
|
123
137
|
lint_roller (~> 1.0)
|
|
124
|
-
rubocop (~> 1.
|
|
138
|
+
rubocop (~> 1.84.0)
|
|
125
139
|
standard-custom (~> 1.0.0)
|
|
126
140
|
standard-performance (~> 1.8)
|
|
127
141
|
standard-custom (1.0.2)
|
|
@@ -132,6 +146,8 @@ GEM
|
|
|
132
146
|
rubocop-performance (~> 1.26.0)
|
|
133
147
|
starry (0.2.0)
|
|
134
148
|
base64
|
|
149
|
+
typhoeus (1.5.0)
|
|
150
|
+
ethon (>= 0.9.0, < 0.16.0)
|
|
135
151
|
tzinfo (2.0.6)
|
|
136
152
|
concurrent-ruby (~> 1.0)
|
|
137
153
|
unicode-display_width (3.2.0)
|
|
@@ -147,13 +163,16 @@ PLATFORMS
|
|
|
147
163
|
DEPENDENCIES
|
|
148
164
|
actionpack (>= 6.1)
|
|
149
165
|
bundler
|
|
166
|
+
excon
|
|
150
167
|
faraday (>= 2.7)
|
|
151
168
|
http_signature!
|
|
169
|
+
httparty
|
|
152
170
|
minitest (>= 5.24)
|
|
153
171
|
rack
|
|
154
172
|
rake
|
|
155
173
|
simplecov
|
|
156
174
|
standard
|
|
175
|
+
typhoeus
|
|
157
176
|
|
|
158
177
|
BUNDLED WITH
|
|
159
178
|
4.0.2
|
data/README.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# HTTP Signature
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Ruby gem for signing and verifying HTTP requests. You pick which parts of the request to sign (method, URL, headers, body), and the receiver can verify nothing was tampered with — and that it came from someone who holds the key.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Built on [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) (HTTP Message Signatures). Works with HMAC, RSA, ECDSA, and Ed25519.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
When using a standard to sign your HTTP requests you don't need to write a custom implementation every time. There's [loads of libraries](https://httpsig.org/) across languages that's implementing it!
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Signature-Input: sig1=("@method" "@target-uri" "date");created=1767816111;keyid="Test";alg="hmac-sha256"
|
|
11
|
-
Signature: sig1=:7a1ajkE2rOu+gnW3WLZ4ZEcgCm3TfExmypM/giIgdM0=:
|
|
12
|
-
```
|
|
9
|
+
Use your favorite http clients to sign requests and your favorite frameworks to verify them, see [Outgoing request examples](#outgoing-request-examples) and [Incoming request examples](#incoming-request-examples)
|
|
13
10
|
|
|
14
11
|
## Installation
|
|
15
12
|
|
|
@@ -21,22 +18,29 @@ bundle add http_signature
|
|
|
21
18
|
|
|
22
19
|
### Create signature
|
|
23
20
|
|
|
24
|
-
`HTTPSignature.create` returns
|
|
21
|
+
`HTTPSignature.create` returns `Signature-Input`, `Signature` and `Content-Digest` headers that you can include in your request.
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
This example will sign:
|
|
24
|
+
- The whole `url` string: `https://example.com/foo?pet=dog`
|
|
25
|
+
- The HTTP method: `POST`
|
|
26
|
+
- The headers `Content-Type` and `Content-Digest`
|
|
27
|
+
- The body, which is in the `Content-Digest` header and is automatically generated when a body is provided
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
```ruby
|
|
30
|
+
HTTPSignature.create(
|
|
30
31
|
url: "https://example.com/foo?pet=dog",
|
|
31
|
-
method: :
|
|
32
|
-
key_id: "
|
|
32
|
+
method: :post,
|
|
33
|
+
key_id: "key-1",
|
|
33
34
|
key: "secret",
|
|
34
|
-
headers:
|
|
35
|
-
|
|
35
|
+
headers: {"Content-Type" => "application/json"},
|
|
36
|
+
body: {payload: {foo: "bar"}}.to_json,
|
|
37
|
+
components: %w[@method @target-uri content-type content-digest]
|
|
36
38
|
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
# =>
|
|
40
|
+
# {"Signature-Input" =>
|
|
41
|
+
# "sig1=(\"@method\" \"@target-uri\" \"content-type\" \"content-digest\");created=1772541832;keyid=\"key-1\";alg=\"hmac-sha256\"",
|
|
42
|
+
# "Signature" => "sig1=:5ij6rnnwS9oOtu78zU4yBFy9uL3ItXM7ug368cJZuTU=:",
|
|
43
|
+
# "Content-Digest" => "sha-256=:zWToMIpmVcAx10/ZGOrMzi7HQyUBat/TskigQnncEQ8=:"}
|
|
40
44
|
```
|
|
41
45
|
#### All options
|
|
42
46
|
|
|
@@ -53,6 +57,7 @@ HTTPSignature.create(
|
|
|
53
57
|
created: Time.now.to_i, # Default: Time.now.to_i
|
|
54
58
|
expires: Time.now.to_i + 600, # Default: nil
|
|
55
59
|
nonce: "1", # Default: nil
|
|
60
|
+
tag: "web-bot-auth", # Default: nil
|
|
56
61
|
label: "sig1", # Default: "sig1"
|
|
57
62
|
query_string_params: {pet2: "cat"}, # Default: {}, you can pass query string params both here and in the `url` param
|
|
58
63
|
algorithm: "hmac-sha512", # Default: "hmac-sha256"
|
|
@@ -171,7 +176,7 @@ HTTPSignature.valid?(
|
|
|
171
176
|
body: request_body, # Default: ""
|
|
172
177
|
key: "secret", # Default: nil, uses key_resolver or configured keys if nil
|
|
173
178
|
key_resolver: ->(key_id) { find_key(key_id) }, # Default: nil, called with the key_id from Signature-Input
|
|
174
|
-
label: "sig1", # Default:
|
|
179
|
+
label: "sig1", # Default: nil (uses first)
|
|
175
180
|
query_string_params: {}, # Default: {}
|
|
176
181
|
max_age: 300, # Default: nil, reject signatures older than N seconds
|
|
177
182
|
algorithm: "hmac-sha256", # Default: nil, uses alg from Signature-Input or hmac-sha256
|
|
@@ -254,37 +259,35 @@ end
|
|
|
254
259
|
|
|
255
260
|
## Outgoing request examples
|
|
256
261
|
|
|
257
|
-
###
|
|
262
|
+
### Net::HTTP
|
|
258
263
|
|
|
259
264
|
```ruby
|
|
260
265
|
require "net/http"
|
|
261
266
|
require "http_signature"
|
|
262
267
|
|
|
263
268
|
uri = URI("http://example.com/hello")
|
|
269
|
+
body = {name: "World"}.to_json
|
|
270
|
+
headers = {"Content-Type" => "application/json"}
|
|
264
271
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
key_id: "KEY_1",
|
|
274
|
-
algorithm: "hmac-sha256",
|
|
275
|
-
body: request.body || ""
|
|
276
|
-
)
|
|
272
|
+
sig_headers = HTTPSignature.create(
|
|
273
|
+
url: uri.to_s,
|
|
274
|
+
method: :post,
|
|
275
|
+
headers:,
|
|
276
|
+
key: "MYSECRETKEY",
|
|
277
|
+
key_id: "KEY_1",
|
|
278
|
+
body:
|
|
279
|
+
)
|
|
277
280
|
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
req = Net::HTTP::Post.new(uri)
|
|
282
|
+
headers.merge(sig_headers).each { |k, v| req[k] = v }
|
|
283
|
+
req.body = body
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
end
|
|
285
|
+
Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
|
|
283
286
|
```
|
|
284
287
|
|
|
285
288
|
### Faraday
|
|
286
289
|
|
|
287
|
-
As a
|
|
290
|
+
As a Faraday middleware
|
|
288
291
|
|
|
289
292
|
```ruby
|
|
290
293
|
require "http_signature/faraday"
|
|
@@ -292,17 +295,96 @@ require "http_signature/faraday"
|
|
|
292
295
|
HTTPSignature::Faraday.key = "secret"
|
|
293
296
|
HTTPSignature::Faraday.key_id = "key-1"
|
|
294
297
|
|
|
295
|
-
Faraday.new("http://example.com") do |
|
|
296
|
-
|
|
297
|
-
faraday.adapter(Faraday.default_adapter)
|
|
298
|
+
conn = Faraday.new("http://example.com") do |f|
|
|
299
|
+
f.use(HTTPSignature::Faraday)
|
|
298
300
|
end
|
|
299
301
|
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
+
# Requests will automatically include Signature-Input, Signature, and Content-Digest headers
|
|
303
|
+
conn.post("/hello") do |req|
|
|
304
|
+
req.headers["Content-Type"] = "application/json"
|
|
305
|
+
req.body = {name: "World"}.to_json
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Example with setting params per request:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
conn = Faraday.new("http://example.com") do |f|
|
|
313
|
+
f.use(
|
|
314
|
+
HTTPSignature::Faraday,
|
|
315
|
+
key: "secret",
|
|
316
|
+
key_id: "key-1",
|
|
317
|
+
components: ["@method", "@target-uri", "content-type", "content-digest"]
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The middleware also accepts the params: `created:`, `expires:`, `nonce:`, `tag:`, `label:`, `algorithm:`, and `include_alg:`.
|
|
323
|
+
|
|
324
|
+
### HTTParty
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
require "httparty"
|
|
328
|
+
require "http_signature"
|
|
329
|
+
|
|
330
|
+
url = "http://example.com/hello"
|
|
331
|
+
body = {name: "World"}.to_json
|
|
332
|
+
headers = {"Content-Type" => "application/json"}
|
|
333
|
+
|
|
334
|
+
sig_headers = HTTPSignature.create(
|
|
335
|
+
url:,
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers:,
|
|
338
|
+
key: "MYSECRETKEY",
|
|
339
|
+
key_id: "KEY_1",
|
|
340
|
+
body:
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
HTTParty.post(url, body:, headers: headers.merge(sig_headers))
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Excon
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
require "excon"
|
|
350
|
+
require "http_signature"
|
|
351
|
+
|
|
352
|
+
url = "http://example.com/hello"
|
|
353
|
+
body = {name: "World"}.to_json
|
|
354
|
+
headers = {"Content-Type" => "application/json"}
|
|
355
|
+
|
|
356
|
+
sig_headers = HTTPSignature.create(
|
|
357
|
+
url:,
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers:,
|
|
360
|
+
key: "MYSECRETKEY",
|
|
361
|
+
key_id: "KEY_1",
|
|
362
|
+
body:
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
Excon.post(url, headers: headers.merge(sig_headers), body:)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Typhoeus
|
|
302
369
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
370
|
+
```ruby
|
|
371
|
+
require "typhoeus"
|
|
372
|
+
require "http_signature"
|
|
373
|
+
|
|
374
|
+
url = "http://example.com/hello"
|
|
375
|
+
body = {name: "World"}.to_json
|
|
376
|
+
headers = {"Content-Type" => "application/json"}
|
|
377
|
+
|
|
378
|
+
sig_headers = HTTPSignature.create(
|
|
379
|
+
url:,
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers:,
|
|
382
|
+
key: "MYSECRETKEY",
|
|
383
|
+
key_id: "KEY_1",
|
|
384
|
+
body:
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
Typhoeus.post(url, body:, headers: headers.merge(sig_headers))
|
|
306
388
|
```
|
|
307
389
|
|
|
308
390
|
## Incoming request examples
|
|
@@ -341,6 +423,16 @@ class Api::BaseController < ApplicationController
|
|
|
341
423
|
end
|
|
342
424
|
```
|
|
343
425
|
|
|
426
|
+
To enforce a specific signature label or age limit, use a block:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
class Api::BaseController < ApplicationController
|
|
430
|
+
include HTTPSignature::Rails::Controller
|
|
431
|
+
|
|
432
|
+
before_action -> { verify_http_signature!(label: "sig2", max_age: 300) }
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
344
436
|
Set the keys in an initializer
|
|
345
437
|
```ruby
|
|
346
438
|
# config/initializers/http_signature.rb
|
data/http_signature.gemspec
CHANGED
|
@@ -28,6 +28,9 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.add_development_dependency "standard"
|
|
29
29
|
spec.add_development_dependency "simplecov"
|
|
30
30
|
spec.add_development_dependency "actionpack", ">= 6.1"
|
|
31
|
+
spec.add_development_dependency "httparty"
|
|
32
|
+
spec.add_development_dependency "excon"
|
|
33
|
+
spec.add_development_dependency "typhoeus"
|
|
31
34
|
|
|
32
35
|
spec.add_dependency "base64"
|
|
33
36
|
spec.add_dependency "starry", "~> 0.2"
|
|
@@ -8,8 +8,16 @@ class HTTPSignature::Faraday < Faraday::Middleware
|
|
|
8
8
|
attr_accessor :key, :key_id
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def initialize(app, options = nil)
|
|
12
|
+
super(app)
|
|
13
|
+
@options = options || {}
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
def call(env)
|
|
12
|
-
|
|
17
|
+
options = merged_options(env)
|
|
18
|
+
key = options.fetch(:key, self.class.key)
|
|
19
|
+
key_id = options.fetch(:key_id, self.class.key_id)
|
|
20
|
+
raise "key and key_id needs to be set" if key.nil? || key_id.nil?
|
|
13
21
|
|
|
14
22
|
body =
|
|
15
23
|
if env[:body]&.respond_to?(:read)
|
|
@@ -20,19 +28,7 @@ class HTTPSignature::Faraday < Faraday::Middleware
|
|
|
20
28
|
env[:body].to_s
|
|
21
29
|
end
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
filtered_headers = %w[Host Date Content-Digest]
|
|
25
|
-
headers_to_sign = env[:request_headers].select { |k, _v| filtered_headers.include?(k.to_s) }
|
|
26
|
-
|
|
27
|
-
signature_headers = HTTPSignature.create(
|
|
28
|
-
url: env[:url],
|
|
29
|
-
method: env[:method],
|
|
30
|
-
headers: headers_to_sign,
|
|
31
|
-
key: self.class.key,
|
|
32
|
-
key_id: self.class.key_id,
|
|
33
|
-
algorithm: "hmac-sha256",
|
|
34
|
-
body: body
|
|
35
|
-
)
|
|
31
|
+
signature_headers = HTTPSignature.create(**signature_options(env:, key:, key_id:, body:, options:))
|
|
36
32
|
|
|
37
33
|
signature_headers.each do |header, value|
|
|
38
34
|
env[:request_headers][header] = value
|
|
@@ -40,4 +36,32 @@ class HTTPSignature::Faraday < Faraday::Middleware
|
|
|
40
36
|
|
|
41
37
|
@app.call(env)
|
|
42
38
|
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def merged_options(env)
|
|
43
|
+
request_options = env.request.context&.dig(:http_signature) || {}
|
|
44
|
+
@options.merge(request_options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def signature_options(env:, key:, key_id:, body:, options:)
|
|
48
|
+
{
|
|
49
|
+
url: env[:url],
|
|
50
|
+
method: env[:method],
|
|
51
|
+
headers: env[:request_headers],
|
|
52
|
+
key:,
|
|
53
|
+
key_id:,
|
|
54
|
+
body:,
|
|
55
|
+
**create_options(options)
|
|
56
|
+
}.tap do |signature_options|
|
|
57
|
+
if options.key?(:components) && !options[:components].nil?
|
|
58
|
+
signature_options[:components] = options[:components]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_options(options)
|
|
64
|
+
options.slice(:created, :expires, :nonce, :tag, :label, :include_alg, :algorithm)
|
|
65
|
+
.reject { |_key, value| value.nil? }
|
|
66
|
+
end
|
|
43
67
|
end
|
data/lib/http_signature/rails.rb
CHANGED
|
@@ -11,7 +11,7 @@ module HTTPSignature
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
# Use as a Rails before_action to enforce HTTP Message Signatures on an action.
|
|
14
|
-
def verify_http_signature!
|
|
14
|
+
def verify_http_signature!(label: nil, max_age: nil)
|
|
15
15
|
request_headers = normalized_request_headers
|
|
16
16
|
signature_input_header = request_headers["signature-input"]
|
|
17
17
|
signature_header = request_headers["signature"]
|
|
@@ -25,7 +25,9 @@ module HTTPSignature
|
|
|
25
25
|
method: request.request_method,
|
|
26
26
|
headers: request_headers,
|
|
27
27
|
body: request_body || "",
|
|
28
|
-
key_resolver: ->(key_id) { HTTPSignature.key(key_id) }
|
|
28
|
+
key_resolver: ->(key_id) { HTTPSignature.key(key_id) },
|
|
29
|
+
label:,
|
|
30
|
+
max_age:
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
nil
|
data/lib/http_signature.rb
CHANGED
|
@@ -63,6 +63,7 @@ module HTTPSignature
|
|
|
63
63
|
# @param created [Integer] Unix timestamp for signature creation (default: Time.now.to_i)
|
|
64
64
|
# @param expires [Integer, nil] Unix timestamp when signature expires (default: nil)
|
|
65
65
|
# @param nonce [String, nil] Random value for signature uniqueness (default: nil)
|
|
66
|
+
# @param tag [String, nil] Application-specific tag parameter included in signature metadata (default: nil)
|
|
66
67
|
# @param label [String] Signature label in headers (default: "sig1")
|
|
67
68
|
# @param query_string_params [Hash] Additional query params to merge into URL (default: {})
|
|
68
69
|
# @param include_alg [Boolean] Whether to include alg in signature metadata (default: true)
|
|
@@ -84,6 +85,7 @@ module HTTPSignature
|
|
|
84
85
|
created: Time.now.to_i,
|
|
85
86
|
expires: nil,
|
|
86
87
|
nonce: nil,
|
|
88
|
+
tag: nil,
|
|
87
89
|
label: DEFAULT_LABEL,
|
|
88
90
|
query_string_params: {},
|
|
89
91
|
include_alg: true,
|
|
@@ -108,6 +110,8 @@ module HTTPSignature
|
|
|
108
110
|
|
|
109
111
|
validate_components!(components)
|
|
110
112
|
|
|
113
|
+
had_content_digest = normalized_headers.key?("content-digest")
|
|
114
|
+
|
|
111
115
|
normalized_headers =
|
|
112
116
|
if components.include?("content-digest")
|
|
113
117
|
ensure_content_digest(normalized_headers, body)
|
|
@@ -134,16 +138,23 @@ module HTTPSignature
|
|
|
134
138
|
key_id:,
|
|
135
139
|
alg: include_alg ? algorithm : nil,
|
|
136
140
|
nonce:,
|
|
141
|
+
tag:,
|
|
137
142
|
canonical_components:
|
|
138
143
|
)
|
|
139
144
|
|
|
140
145
|
signature_bytes = sign(base_string, key: key, algorithm: algorithm_entry)
|
|
141
146
|
signature_header = build_signature_header(label, signature_bytes)
|
|
142
147
|
|
|
143
|
-
{
|
|
148
|
+
result = {
|
|
144
149
|
"Signature-Input" => signature_input_header,
|
|
145
150
|
"Signature" => signature_header
|
|
146
151
|
}
|
|
152
|
+
|
|
153
|
+
if !had_content_digest && normalized_headers["content-digest"]
|
|
154
|
+
result["Content-Digest"] = normalized_headers["content-digest"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
result
|
|
147
158
|
end
|
|
148
159
|
|
|
149
160
|
# Verify RFC 9421 Signature headers
|
|
@@ -154,7 +165,8 @@ module HTTPSignature
|
|
|
154
165
|
# @param body [String] Request body (default: "")
|
|
155
166
|
# @param key [String, OpenSSL::PKey::PKey, nil] Verification key. If nil, uses key_resolver or configured keys
|
|
156
167
|
# @param key_resolver [Proc, nil] Callable that receives key_id and returns the key (default: nil)
|
|
157
|
-
# @param label [String] Signature label to verify
|
|
168
|
+
# @param label [String, nil] Signature label to verify. If nil, auto-detects
|
|
169
|
+
# the first label from the Signature-Input header (default: nil)
|
|
158
170
|
# @param query_string_params [Hash] Additional query params to merge into URL (default: {})
|
|
159
171
|
# @param max_age [Integer, nil] Maximum signature age in seconds. Takes precedence over
|
|
160
172
|
# the expires timestamp in the signature (default: nil)
|
|
@@ -174,7 +186,7 @@ module HTTPSignature
|
|
|
174
186
|
body: "",
|
|
175
187
|
key: nil,
|
|
176
188
|
key_resolver: nil,
|
|
177
|
-
label:
|
|
189
|
+
label: nil,
|
|
178
190
|
query_string_params: {},
|
|
179
191
|
max_age: nil,
|
|
180
192
|
algorithm: nil,
|
|
@@ -191,6 +203,8 @@ module HTTPSignature
|
|
|
191
203
|
signature_header = normalized_headers["signature"]
|
|
192
204
|
raise SignatureError, "Signature headers are required for verification" unless signature_input_header && signature_header
|
|
193
205
|
|
|
206
|
+
label ||= detect_label(signature_input_header)
|
|
207
|
+
|
|
194
208
|
parsed_input = parse_signature_input(signature_input_header, label)
|
|
195
209
|
validate_components!(parsed_input[:components])
|
|
196
210
|
parsed_signature = parse_signature(signature_header, label)
|
|
@@ -238,6 +252,7 @@ module HTTPSignature
|
|
|
238
252
|
key_id:,
|
|
239
253
|
alg: parsed_input[:params][:alg],
|
|
240
254
|
nonce: parsed_input[:params][:nonce],
|
|
255
|
+
tag: parsed_input[:params][:tag],
|
|
241
256
|
canonical_components:
|
|
242
257
|
)
|
|
243
258
|
|
|
@@ -338,7 +353,11 @@ module HTTPSignature
|
|
|
338
353
|
def self.verify_content_digest!(header_value, body)
|
|
339
354
|
verified = false
|
|
340
355
|
|
|
341
|
-
header_value
|
|
356
|
+
split_header(header_value).each do |entry|
|
|
357
|
+
alg, digest_value = entry.split("=", 2)
|
|
358
|
+
next unless alg && digest_value&.start_with?(":") && digest_value.end_with?(":")
|
|
359
|
+
|
|
360
|
+
encoded_digest = digest_value[1...-1]
|
|
342
361
|
digest = case alg
|
|
343
362
|
when "sha-256" then Digest::SHA256.digest(body)
|
|
344
363
|
when "sha-512" then Digest::SHA512.digest(body)
|
|
@@ -490,6 +509,7 @@ module HTTPSignature
|
|
|
490
509
|
key_id:,
|
|
491
510
|
alg:,
|
|
492
511
|
nonce:,
|
|
512
|
+
tag:,
|
|
493
513
|
canonical_components:
|
|
494
514
|
)
|
|
495
515
|
component_tokens = components.map { |c| serialize_component_id(c) }.join(" ")
|
|
@@ -498,6 +518,7 @@ module HTTPSignature
|
|
|
498
518
|
params << %(keyid="#{escape_structured_string(key_id)}")
|
|
499
519
|
params << %(alg="#{escape_structured_string(alg)}") if alg
|
|
500
520
|
params << %(nonce="#{escape_structured_string(nonce)}") if nonce
|
|
521
|
+
params << %(tag="#{escape_structured_string(tag)}") if tag
|
|
501
522
|
|
|
502
523
|
signature_params = "(#{component_tokens});#{params.join(";")}"
|
|
503
524
|
signature_input_header = "#{label}=#{signature_params}"
|
|
@@ -624,6 +645,16 @@ module HTTPSignature
|
|
|
624
645
|
header.to_s.split(/,(?=[^,]+=)/).map(&:strip)
|
|
625
646
|
end
|
|
626
647
|
|
|
648
|
+
def self.detect_label(signature_input_header)
|
|
649
|
+
entry = split_header(signature_input_header).first
|
|
650
|
+
raise SignatureError, "Signature-Input missing" unless entry
|
|
651
|
+
|
|
652
|
+
label = entry.split("=", 2).first
|
|
653
|
+
raise SignatureError, "Signature-Input missing" if label.nil? || label.empty?
|
|
654
|
+
|
|
655
|
+
label
|
|
656
|
+
end
|
|
657
|
+
|
|
627
658
|
def self.key_from_store(key_id)
|
|
628
659
|
return unless keys && key_id
|
|
629
660
|
|
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.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joel Larsson
|
|
@@ -121,6 +121,48 @@ dependencies:
|
|
|
121
121
|
- - ">="
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '6.1'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: httparty
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: excon
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: typhoeus
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '0'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0'
|
|
124
166
|
- !ruby/object:Gem::Dependency
|
|
125
167
|
name: base64
|
|
126
168
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -156,12 +198,14 @@ executables: []
|
|
|
156
198
|
extensions: []
|
|
157
199
|
extra_rdoc_files: []
|
|
158
200
|
files:
|
|
201
|
+
- ".github/dependabot.yml"
|
|
159
202
|
- ".github/workflows/ci.yml"
|
|
160
203
|
- ".github/workflows/push_gem.yml"
|
|
161
204
|
- ".github/workflows/standardrb.yml"
|
|
162
205
|
- ".gitignore"
|
|
163
206
|
- ".ruby-version"
|
|
164
207
|
- AGENTS.md
|
|
208
|
+
- CLAUDE.md
|
|
165
209
|
- Gemfile
|
|
166
210
|
- Gemfile.lock
|
|
167
211
|
- README.md
|