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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcc1de3b03efae4926bf2dcbd2b8b6ee628691b6aa1044fbefe59f76dadb3264
4
- data.tar.gz: f86411fa7e90f74067e74e38dbdbd75ab377a47a9a9cbef3b87289298bff8baf
3
+ metadata.gz: 5c57222b061b994515d691691c87192a612eec84694085b03db51e3ea1fa2c75
4
+ data.tar.gz: ecc70179bb4d76a955b4711b6494e4e0a862ccf811b47d7d4aa3d46253f63066
5
5
  SHA512:
6
- metadata.gz: 69ce187ea87533cde9ad1978217f9852eeb12159a78dd37369ccdff76f92cfd2815f99953d623456f98fde87f2013749c7ddbe38ab35e40a5b74a9cf53abd320
7
- data.tar.gz: 6f67d20c2ee63cfc2a5f97014d738ec0dcd39533bf7f84d405823dfdf3464edc42ec986c66fc6de92d0f293171882690d55b3fa19bb799c139e641ea13918ac2
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 version tag
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.3.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.0)
88
+ parser (3.3.10.2)
75
89
  ast (~> 2.4.1)
76
90
  racc
77
- prism (1.7.0)
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.81.7)
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.47.1, < 2.0)
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.52.0)
135
+ standard (1.54.0)
122
136
  language_server-protocol (~> 3.17.0.2)
123
137
  lint_roller (~> 1.0)
124
- rubocop (~> 1.81.7)
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
- Create and validate HTTP Message Signatures per [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) using the `Signature-Input` and `Signature` headers.
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
- TL;DR: You specify what should be signed in `Signature-Input` with [components](https://www.rfc-editor.org/rfc/rfc9421#name-derived-components) and lowercase headers. And then the signature is in the `Signature` header
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
- Example:
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 both `Signature-Input` and `Signature` headers that you can include in your request.
21
+ `HTTPSignature.create` returns `Signature-Input`, `Signature` and `Content-Digest` headers that you can include in your request.
25
22
 
26
- ```ruby
27
- headers = { "date" => "Tue, 20 Apr 2021 02:07:55 GMT" }
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
- sig_headers = HTTPSignature.create(
29
+ ```ruby
30
+ HTTPSignature.create(
30
31
  url: "https://example.com/foo?pet=dog",
31
- method: :get,
32
- key_id: "Test",
32
+ method: :post,
33
+ key_id: "key-1",
33
34
  key: "secret",
34
- headers: headers,
35
- components: %w[@method @target-uri date]
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
- request["Signature-Input"] = sig_headers["Signature-Input"]
39
- request["Signature"] = sig_headers["Signature"]
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: "sig1"
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
- ### NET::HTTP
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
- Net::HTTP.start(uri.host, uri.port) do |http|
266
- request = Net::HTTP::Get.new(uri)
267
-
268
- sig_headers = HTTPSignature.create(
269
- url: request.uri,
270
- method: request.method,
271
- headers: request.each_header.map { |k, v| [k, v] }.to_h,
272
- key: "MYSECRETKEY",
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
- request["Signature-Input"] = sig_headers["Signature-Input"]
279
- request["Signature"] = sig_headers["Signature"]
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
- response = http.request(request) # Net::HTTPResponse
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 faraday middleware
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 |faraday|
296
- faraday.use(HTTPSignature::Faraday)
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
- # Now this request will contain the `Signature-Input` and `Signature` headers
301
- response = conn.get("/")
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
- # Request looking like:
304
- # Signature-Input: sig1=("@method" "@authority" "@target-uri" "date");created=...
305
- # Signature: sig1=:BASE64_SIGNATURE:
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
@@ -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
- raise "key and key_id needs to be set" if self.class.key.nil? || self.class.key_id.nil?
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
- # Choose which headers to sign
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPSignature
4
- VERSION = "1.3.1"
4
+ VERSION = "1.5.0"
5
5
  end
@@ -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 (default: "sig1")
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: DEFAULT_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.scan(/([a-z0-9-]+)=:([A-Za-z0-9+\/=]+):/).each do |alg, encoded_digest|
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.3.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