linzer 0.7.9.beta2 → 0.7.9

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.
@@ -39,6 +39,14 @@ module Linzer
39
39
 
40
40
  private
41
41
 
42
+ # Parses an unserialized component identifier with parameters.
43
+ #
44
+ # Splits on +;+ to separate the field name from parameters,
45
+ # then serializes the field name and collects parameters.
46
+ #
47
+ # @param field_name [String] e.g. +"content-type;bs"+ or
48
+ # +"example-dict;key=\"a\""+
49
+ # @return [Starry::Item] the parsed item with parameters
42
50
  def parse_unserialized_input(field_name)
43
51
  field, *raw_params = field_name.split(";")
44
52
  item = Starry.parse_item(Starry.serialize(field))
@@ -46,6 +54,13 @@ module Linzer
46
54
  item
47
55
  end
48
56
 
57
+ # Parses raw parameter strings into a merged Hash.
58
+ #
59
+ # Handles both boolean parameters (+";bs"+ → +{"bs" => true}+)
60
+ # and key-value parameters (+";key=\"a\""+ → +{"key" => "a"}+).
61
+ #
62
+ # @param str [Array<String>] raw parameter strings
63
+ # @return [Hash] merged parameter hash
49
64
  def collect_parameters(str)
50
65
  params = str.map do |param|
51
66
  if (tokens = param.split("=")) == [param] # e.g.: ";bs"
@@ -48,8 +48,14 @@ module Linzer
48
48
 
49
49
  attr_reader :adapters
50
50
 
51
- # Finds an adapter by checking if operation inherits from a known class.
52
- # This allows subclasses of Net::HTTPRequest etc. to work automatically.
51
+ # Finds an adapter by checking the operation's ancestry.
52
+ #
53
+ # This allows subclasses of registered classes (e.g.
54
+ # +Net::HTTP::Get < Net::HTTPRequest+) to use the parent's adapter
55
+ # without explicit registration.
56
+ #
57
+ # @param operation [Object] the HTTP message object
58
+ # @return [Class, nil] the adapter class, or +nil+ if no ancestor matches
53
59
  def find_ancestor(operation)
54
60
  adapters
55
61
  .select { |klz, adpt| operation.is_a? klz }
@@ -57,6 +63,10 @@ module Linzer
57
63
  .first
58
64
  end
59
65
 
66
+ # Raises an error for unsupported HTTP message types.
67
+ #
68
+ # @param operation [Object] the unsupported HTTP message
69
+ # @raise [Linzer::Error] with a message suggesting +register_adapter+
60
70
  def fail_with_unsupported(operation)
61
71
  err_msg = <<~EOM
62
72
  Unknown/unsupported HTTP message class: '#{operation.class}'!
@@ -102,6 +102,26 @@ module Linzer
102
102
  (Time.now.to_i - created) > seconds
103
103
  end
104
104
 
105
+ # Checks if the signature has expired based on the `expires` parameter.
106
+ #
107
+ # If the `expires` parameter is not present, the signature is considered
108
+ # not expired (returns false). If the parameter is present but not a valid
109
+ # integer, an error is raised.
110
+ #
111
+ # @return [Boolean] true if the signature has expired
112
+ # @raise [Error] If the `expires` parameter is not a valid integer
113
+ #
114
+ # @example Check if a signature has expired
115
+ # signature.expired? # => true or false
116
+ #
117
+ # @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.3 RFC 9421 Section 2.3
118
+ def expired?
119
+ return false if !parameters.key?("expires")
120
+ Time.now.to_i >= Integer(parameters["expires"])
121
+ rescue ArgumentError, TypeError
122
+ raise Error.new "Signature has a non-integer `expires` parameter"
123
+ end
124
+
105
125
  # Converts the signature to HTTP header format.
106
126
  #
107
127
  # Returns a hash suitable for setting as HTTP headers on a request or
@@ -23,6 +23,7 @@ module Linzer
23
23
  # - All covered components exist in the message
24
24
  # - The signature base matches what was signed
25
25
  # - The cryptographic signature is valid for the public key
26
+ # - The signature has not expired (if `expires` parameter is present)
26
27
  # - The signature is not older than `no_older_than` (if specified)
27
28
  #
28
29
  # @param key [Linzer::Key] The public key to verify with. Must respond to
@@ -85,6 +86,13 @@ module Linzer
85
86
  raise VerifyError, ex.message, cause: ex
86
87
  end
87
88
 
89
+ begin
90
+ exp_sig_msg = "Signature has expired or is invalid"
91
+ raise VerifyError, exp_sig_msg if signature.expired?
92
+ rescue Error => ex
93
+ raise VerifyError, ex.message, cause: ex
94
+ end
95
+
88
96
  return unless no_older_than
89
97
  old_sig_msg = "Signature created more than #{no_older_than} seconds ago"
90
98
  begin
@@ -3,5 +3,5 @@
3
3
  module Linzer
4
4
  # Current version of the Linzer gem.
5
5
  # @return [String]
6
- VERSION = "0.7.9.beta2"
6
+ VERSION = "0.7.9"
7
7
  end
data/lib/linzer.rb CHANGED
@@ -4,7 +4,6 @@ require "starry"
4
4
  require "openssl"
5
5
  require "rack"
6
6
  require "uri"
7
- require "stringio"
8
7
  require "net/http"
9
8
 
10
9
  require_relative "linzer/version"
@@ -5,42 +5,82 @@ require "yaml"
5
5
  module Rack
6
6
  module Auth
7
7
  class Signature
8
+ # Shared helpers for the Rack signature verification middleware.
9
+ #
10
+ # Organizes functionality into three sub-modules:
11
+ # - {Parameters} — validates required signature parameters
12
+ # - {Configuration} — loads and merges middleware options
13
+ # - {Key} — resolves verification keys by keyid
14
+ #
15
+ # @api private
8
16
  module Helpers
17
+ # Validates the presence of required signature parameters.
18
+ #
19
+ # Each method checks whether a specific parameter is required
20
+ # (per configuration) and, if so, whether it is present and valid
21
+ # in the current signature.
22
+ #
23
+ # @api private
9
24
  module Parameters
10
25
  private
11
26
 
27
+ # Checks if the +created+ parameter requirement is satisfied.
28
+ # @return [Boolean] +true+ if not required or present and valid
12
29
  def created?
13
30
  !options[:signatures][:created_required] || !!Integer(params.fetch("created"))
14
31
  end
15
32
 
33
+ # Checks if the +expires+ parameter requirement is satisfied.
34
+ # @return [Boolean] +true+ if not required or present and not yet expired
16
35
  def expires?
17
36
  return true if !options[:signatures][:expires_required]
18
37
  Integer(params.fetch("expires")) > Time.now.to_i
19
38
  end
20
39
 
40
+ # Checks if the +keyid+ parameter requirement is satisfied.
41
+ # @return [Boolean] +true+ if not required or present
21
42
  def keyid?
22
43
  !options[:signatures][:keyid_required] || String(params.fetch("keyid"))
23
44
  end
24
45
 
46
+ # Checks if the +nonce+ parameter requirement is satisfied.
47
+ # @return [Boolean] +true+ if not required or present
25
48
  def nonce?
26
49
  !options[:signatures][:nonce_required] || String(params.fetch("nonce"))
27
50
  end
28
51
 
52
+ # Checks if the +alg+ parameter requirement is satisfied.
53
+ # @return [Boolean] +true+ if not required or present
29
54
  def alg?
30
55
  !options[:signatures][:alg_required] || String(params.fetch("alg"))
31
56
  end
32
57
 
58
+ # Checks if the +tag+ parameter requirement is satisfied.
59
+ # @return [Boolean] +true+ if not required or present
33
60
  def tag?
34
61
  !options[:signatures][:tag_required] || String(params.fetch("tag"))
35
62
  end
36
63
  end
37
64
 
65
+ # Handles loading and merging of middleware configuration.
66
+ #
67
+ # Configuration can come from three sources (in order of precedence):
68
+ # 1. Options passed directly to the middleware constructor
69
+ # 2. A YAML configuration file (via +:config_path+)
70
+ # 3. {DEFAULT_OPTIONS}
71
+ #
72
+ # @api private
38
73
  module Configuration
74
+ # Returns the default covered components for signature verification.
75
+ # @return [Array<String>] the default components from {Linzer::Options::DEFAULT}
39
76
  def default_covered_components
40
77
  Linzer::Options::DEFAULT[:covered_components]
41
78
  end
42
79
  module_function :default_covered_components
43
80
 
81
+ # Default middleware configuration.
82
+ #
83
+ # @api private
44
84
  DEFAULT_OPTIONS = {
45
85
  signatures: {
46
86
  reject_older_than: 900,
@@ -62,6 +102,10 @@ module Rack
62
102
 
63
103
  private
64
104
 
105
+ # Loads and merges options from all sources.
106
+ #
107
+ # @param options [Hash] options passed to the middleware constructor
108
+ # @return [Hash] the merged configuration
65
109
  def load_options(options)
66
110
  options_from_file = load_options_from_config_file(options)
67
111
  {
@@ -78,6 +122,10 @@ module Rack
78
122
  }
79
123
  end
80
124
 
125
+ # Loads configuration from a YAML file.
126
+ #
127
+ # @param options [Hash] options containing +:config_path+
128
+ # @return [Hash] parsed configuration, or empty hash if unavailable
81
129
  def load_options_from_config_file(options)
82
130
  config_path = options[:config_path]
83
131
  YAML.safe_load_file(config_path, symbolize_names: true)
@@ -86,13 +134,29 @@ module Rack
86
134
  end
87
135
  end
88
136
 
137
+ # Resolves verification keys from the middleware configuration.
138
+ #
139
+ # Keys can be configured inline (with +:material+) or via file path
140
+ # (with +:path+). When a +keyid+ is present in the signature, the
141
+ # corresponding key is looked up in the +:keys+ hash. If not found,
142
+ # the +:default_key+ is used as fallback.
143
+ #
144
+ # @api private
89
145
  module Key
90
146
  private
91
147
 
148
+ # Returns the verification key for the current signature.
149
+ # @return [Linzer::Key] the resolved key
150
+ # @raise [Linzer::Error] if no key can be found
92
151
  def key
93
152
  build_key(params["keyid"])
94
153
  end
95
154
 
155
+ # Builds a key instance from configuration.
156
+ #
157
+ # @param keyid [String, nil] the key identifier from the signature
158
+ # @return [Linzer::Key] the resolved key
159
+ # @raise [Linzer::Error] if no matching key configuration is found
96
160
  def build_key(keyid)
97
161
  key_data = if keyid.nil? ||
98
162
  (!options[:keys].key?(keyid.to_sym) && options[:default_key])
@@ -114,6 +178,14 @@ module Rack
114
178
  instantiate_key(keyid || :default, alg, key_data)
115
179
  end
116
180
 
181
+ # Instantiates the appropriate key class for the given algorithm.
182
+ #
183
+ # @param keyid [String, Symbol] the key identifier
184
+ # @param alg [String, Symbol] the algorithm identifier
185
+ # (e.g. +"ed25519"+, +"rsa-pss-sha512"+)
186
+ # @param key_data [Hash] key configuration with +:material+
187
+ # @return [Linzer::Key] the instantiated key
188
+ # @raise [Linzer::Error] if the algorithm is unsupported
117
189
  def instantiate_key(keyid, alg, key_data)
118
190
  key_methods = {
119
191
  "rsa-pss-sha512" => :new_rsa_pss_sha512_key,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linzer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.9.beta2
4
+ version: 0.7.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miguel Landaeta
@@ -63,26 +63,6 @@ dependencies:
63
63
  - - ">="
64
64
  - !ruby/object:Gem::Version
65
65
  version: 1.0.2
66
- - !ruby/object:Gem::Dependency
67
- name: stringio
68
- requirement: !ruby/object:Gem::Requirement
69
- requirements:
70
- - - "~>"
71
- - !ruby/object:Gem::Version
72
- version: '3.1'
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: 3.1.2
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '3.1'
83
- - - ">="
84
- - !ruby/object:Gem::Version
85
- version: 3.1.2
86
66
  - !ruby/object:Gem::Dependency
87
67
  name: logger
88
68
  requirement: !ruby/object:Gem::Requirement
@@ -182,10 +162,14 @@ files:
182
162
  - examples/sinatra/myapp.rb
183
163
  - flake.lock
184
164
  - flake.nix
165
+ - lib/faraday/http_signature.rb
166
+ - lib/faraday/http_signature/middleware.rb
185
167
  - lib/linzer.rb
186
168
  - lib/linzer/common.rb
187
169
  - lib/linzer/ecdsa.rb
188
170
  - lib/linzer/ed25519.rb
171
+ - lib/linzer/faraday.rb
172
+ - lib/linzer/faraday/utils.rb
189
173
  - lib/linzer/helper.rb
190
174
  - lib/linzer/hmac.rb
191
175
  - lib/linzer/http.rb
@@ -197,6 +181,8 @@ files:
197
181
  - lib/linzer/message.rb
198
182
  - lib/linzer/message/adapter.rb
199
183
  - lib/linzer/message/adapter/abstract.rb
184
+ - lib/linzer/message/adapter/faraday/request.rb
185
+ - lib/linzer/message/adapter/faraday/response.rb
200
186
  - lib/linzer/message/adapter/generic/request.rb
201
187
  - lib/linzer/message/adapter/generic/response.rb
202
188
  - lib/linzer/message/adapter/http_gem/common.rb