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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a052e0252607d934ef7221c6aa3ea32fb21644af4f1af77985677db0cb2e2bb3
4
- data.tar.gz: 843effaf6aac8b647b29d2d19f5414513be7efb76a0a1eb837b9cc2872fe3727
3
+ metadata.gz: 4d7340510585b31400802574581fc40aa006889f7f43e67e6ac4d5c926ddc3ce
4
+ data.tar.gz: 3364453874b93eb6b37a2ef13208dbb2792b4b50975c872459fea1a1d70a68d7
5
5
  SHA512:
6
- metadata.gz: 5aedcedf0056a4f98414a599a51ec89484500c3447f124c4f070b11d7567ec2c8b5f682aa17e8f1e6c1dcf09f0c1e796530ada79bca633e228e3cfe98154f575
7
- data.tar.gz: 61e44809bd68c3d072b94faee4f1ba7915aa49e2acfde2131b55c2442982a14cd199e288eff6cf34533440bebfb3b788997a395f618830da4a4c7d96407e7e01
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- http_signature (1.0.1)
4
+ http_signature (1.1.0)
5
5
  base64
6
6
 
7
7
  GEM
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 = { 'date' => 'Tue, 20 Apr 2021 02:07:55 GMT' }
27
+ headers = { "date" => "Tue, 20 Apr 2021 02:07:55 GMT" }
29
28
 
30
29
  sig_headers = HTTPSignature.create(
31
- url: 'https://example.com/foo?pet=dog',
30
+ url: "https://example.com/foo?pet=dog",
32
31
  method: :get,
32
+ key_id: "Test",
33
+ key: "secret",
33
34
  headers: headers,
34
- key_id: 'Test',
35
- key: 'secret',
36
- covered_components: %w[@method @target-uri date],
35
+ components: %w[@method @target-uri date]
37
36
  )
38
37
 
39
- request['Signature-Input'] = sig_headers['Signature-Input']
40
- request['Signature'] = sig_headers['Signature']
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 'net/http'
63
- require 'http_signature'
84
+ require "net/http"
85
+ require "http_signature"
64
86
 
65
- uri = URI('http://example.com/hello')
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: 'MYSECRETKEY',
75
- key_id: 'KEY_1',
76
- algorithm: 'hmac-sha256',
77
- body: request.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['Signature-Input'] = sig_headers['Signature-Input']
81
- request['Signature'] = sig_headers['Signature']
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 'http_signature/faraday'
114
+ require "http_signature/faraday"
93
115
 
94
- HTTPSignature::Faraday.key = 'secret'
95
- HTTPSignature::Faraday.key_id = 'key-1'
116
+ HTTPSignature::Faraday.key = "secret"
117
+ HTTPSignature::Faraday.key_id = "key-1"
96
118
 
97
- Faraday.new('http://example.com') do |faraday|
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 'http_signature/rack'
140
+ require "http_signature/rack"
119
141
 
120
142
  HTTPSignature.configure do |config|
121
143
  config.keys = [
122
- {id: 'key-1', value: 'MySecureKey'}
144
+ {id: "key-1", value: "MySecureKey"}
123
145
  ]
124
146
  end
125
- HTTPSignature::Rack.exclude_paths = ['/', '/hello/*']
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 'http_signature/rails'
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: 'key-1', value: 'MySecureKey'}
174
+ {id: "key-1", value: "MySecureKey"}
153
175
  ]
154
176
  end
155
177
  ```
@@ -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.0.0"
21
+ spec.required_ruby_version = ">= 3.1.0"
22
22
 
23
23
  spec.add_development_dependency "bundler"
24
24
  spec.add_development_dependency "rake"
@@ -33,7 +33,7 @@ class HTTPSignature::Rack
33
33
  else
34
34
  ""
35
35
  end
36
- valid_signature = HTTPSignature.valid?(
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
 
@@ -20,7 +20,7 @@ module HTTPSignature
20
20
 
21
21
  request_body = read_request_body
22
22
 
23
- valid_signature = HTTPSignature.valid?(
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
- return if valid_signature
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPSignature
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -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
- covered_components: nil,
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
- covered_components || default_components(normalized_headers, body:)
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: uri,
81
- method: method,
91
+ uri:,
92
+ method:,
82
93
  headers: normalized_headers,
83
- covered_components: components
94
+ components:
84
95
  )
85
96
 
86
97
  signature_input_header, base_string = build_signature_input(
87
- label: label,
88
- components: components,
89
- created: created,
90
- key_id: key_id,
98
+ label:,
99
+ components:,
100
+ created:,
101
+ expires:,
102
+ key_id:,
91
103
  alg: algorithm,
92
- nonce: nonce,
93
- canonical_components: 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: uri,
139
- method: method,
156
+ uri:,
157
+ method:,
140
158
  headers: normalized_headers,
141
- covered_components: parsed_input[:components]
159
+ components: parsed_input[:components]
142
160
  )
143
161
 
144
162
  _, base_string = build_signature_input(
145
- label: label,
163
+ label:,
146
164
  components: parsed_input[:components],
147
- created: parsed_input[:params][:created].to_i,
148
- key_id: key_id,
165
+ created:,
166
+ expires:,
167
+ key_id:,
149
168
  alg: parsed_input[:params][:alg],
150
169
  nonce: parsed_input[:params][:nonce],
151
- canonical_components: 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:, covered_components:)
198
- covered_components.map do |component|
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}", %(keyid="#{escape_structured_string(key_id)}")]
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.1
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.0.0
173
+ version: 3.1.0
174
174
  required_rubygems_version: !ruby/object:Gem::Requirement
175
175
  requirements:
176
176
  - - ">="