linzer 0.7.9.beta2 → 0.7.9.beta3

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.
@@ -96,7 +96,10 @@ module Linzer
96
96
  private
97
97
 
98
98
  # Parses a field name string into a FieldId.
99
- # @return [FieldId, nil] The parsed identifier, or nil if invalid
99
+ #
100
+ # @param field_name [String] the component identifier string
101
+ # @return [FieldId, nil] the parsed identifier, or +nil+ if invalid
102
+ # @raise [Error] if +@status+ is used in a request message
100
103
  def parse_field_name(field_name)
101
104
  field_id = FieldId.new(field_name: field_name)
102
105
  component = field_id.item
@@ -117,8 +120,12 @@ module Linzer
117
120
  raise Linzer::Error, msg unless message.request?
118
121
  end
119
122
 
120
- # Validates component identifier parameters.
121
- # @return [Object, nil] The validated name, or nil if invalid
123
+ # Validates component identifier parameters against RFC 9421 rules.
124
+ #
125
+ # @param name [Starry::Item] the parsed component identifier
126
+ # @param method [Symbol] +:derived+ or +:field+
127
+ # @return [Starry::Item, nil] the validated name, or +nil+ if
128
+ # the parameter combination is invalid
122
129
  def validate_parameters(name, method)
123
130
  has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
124
131
  return nil if has_unknown
@@ -149,6 +156,13 @@ module Linzer
149
156
  private_constant :KNOWN_PARAMETERS
150
157
 
151
158
  # Retrieves a component value with parameter processing.
159
+ #
160
+ # Handles +;req+, +;sf+, +;key+, and +;bs+ parameters by
161
+ # delegating to the corresponding helper methods.
162
+ #
163
+ # @param name [Starry::Item] the parsed component identifier
164
+ # @param method [Symbol] +:derived+ or +:field+
165
+ # @return [String, Integer, nil] the component value
152
166
  def retrieve(name, method)
153
167
  if !name.parameters.empty?
154
168
  valid_params = validate_parameters(name, method)
@@ -176,6 +190,10 @@ module Linzer
176
190
  end
177
191
 
178
192
  # Processes a structured field value with optional key extraction.
193
+ #
194
+ # @param value [String] the raw header value to parse as a dictionary
195
+ # @param key [String, nil] if present, extracts a single dictionary member
196
+ # @return [String] the serialized structured field value
179
197
  # @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
180
198
  def sf(value, key = nil)
181
199
  dict = Starry.parse_dictionary(value)
@@ -188,19 +206,31 @@ module Linzer
188
206
  end
189
207
  end
190
208
 
191
- # Binary-wraps a field value.
209
+ # Binary-wraps a field value as a byte sequence.
210
+ #
211
+ # @param value [String] the header value to wrap
212
+ # @return [String] the serialized byte sequence
192
213
  # @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
193
214
  def bs(value)
194
215
  Starry.serialize(value.encode(Encoding::ASCII_8BIT))
195
216
  end
196
217
 
197
218
  # Retrieves a trailer field value.
219
+ #
198
220
  # @abstract Subclasses should implement if trailer support is needed.
221
+ # @param trailer [Object] the trailer field identifier
222
+ # @return [String, nil] the trailer value
223
+ # @raise [Error] always, since no built-in adapters support trailers
199
224
  def tr(trailer)
200
225
  raise Error, "Sub-classes are required to implement this method!"
201
226
  end
202
227
 
203
- # Retrieves a field from the attached request.
228
+ # Retrieves a field from the attached request (for +;req+ parameter).
229
+ #
230
+ # @param field [Starry::Item] the component identifier
231
+ # @param method [Symbol] +:derived+ or +:field+
232
+ # @return [String, nil] the value from the attached request, or
233
+ # +nil+ if no request is attached
204
234
  def req(field, method)
205
235
  attached_request? ? @attached_request[String(field)] : nil
206
236
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module Faraday
7
+ # Adapter for {::Faraday::Request} objects from the faraday gem.
8
+ #
9
+ # Extends the generic request adapter with faraday-specific
10
+ # derived component retrieval, field lookup, and URI handling.
11
+ #
12
+ # @note Not loaded automatically to avoid making faraday a hard
13
+ # dependency. Require +"linzer/faraday"+ to register this adapter.
14
+ #
15
+ # @see Generic::Request
16
+ # @see https://github.com/lostisland/faraday faraday gem
17
+ class Request < Generic::Request
18
+ private
19
+
20
+ # Resolves a derived component value from the request.
21
+ #
22
+ # @param name [Starry::Item] the parsed component identifier
23
+ # @return [String, nil] the derived value, or +nil+ if unknown
24
+ def derived(name)
25
+ url = @operation.path
26
+ case name.value
27
+ when "@method" then @operation.http_method.to_s.upcase
28
+ when "@target-uri" then uri.to_s
29
+ when "@authority" then url.authority.downcase
30
+ when "@scheme" then url.scheme.downcase
31
+ when "@request-target" then uri.request_uri
32
+ when "@path" then url.path
33
+ when "@query" then "?%s" % String(uri_query)
34
+ when "@query-param" then query_param(uri_query, name)
35
+ end
36
+ end
37
+
38
+ # Builds the full URI including query parameters.
39
+ #
40
+ # @return [URI] the complete request URI with encoded query string
41
+ def uri
42
+ uri = @operation.path.dup
43
+ uri.query = URI.encode_www_form(@operation.params)
44
+ uri
45
+ end
46
+
47
+ # Returns the raw query string from the request URI.
48
+ #
49
+ # Prefers the raw query string from the URI when available,
50
+ # as Faraday normalises percent-encoding when parsing params
51
+ # (e.g. +%2D+ becomes +-+), which would break signature
52
+ # verification.
53
+ #
54
+ # @return [String, nil] the raw query string
55
+ def uri_query
56
+ url = @operation.path
57
+ url.query || uri.query
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module Faraday
7
+ # Adapter for {::Faraday::Response} objects from the faraday gem.
8
+ #
9
+ # Extends the generic response adapter with faraday-specific
10
+ # derived component retrieval (e.g. +@status+) and header
11
+ # attachment.
12
+ #
13
+ # @note Not loaded automatically to avoid making faraday a hard
14
+ # dependency. Require +"linzer/faraday"+ to register this adapter.
15
+ #
16
+ # @see Generic::Response
17
+ # @see https://github.com/lostisland/faraday faraday gem
18
+ class Response < Generic::Response
19
+ # Attaches a signature to the underlying response headers.
20
+ #
21
+ # @param signature [Linzer::Signature] the signature to attach
22
+ # @return [::Faraday::Response] the underlying response object
23
+ def attach!(signature)
24
+ signature.to_h.each { |h, v| @operation.headers[h] = v }
25
+ @operation
26
+ end
27
+
28
+ private
29
+
30
+ # Resolves a derived component value from the response.
31
+ #
32
+ # @param name [Starry::Item] the parsed component identifier
33
+ # @return [Integer, nil] the HTTP status code for +@status+,
34
+ # or +nil+ if the component is unknown
35
+ def derived(name)
36
+ case name.value
37
+ when "@status" then @operation.status.to_i
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -51,31 +51,35 @@ module Linzer
51
51
  private
52
52
 
53
53
  def derived(name)
54
- unimplemented_method = 'Derived field "@method" lookup is not implemented!'
54
+ unimplemented_method = 'Derived field "%s" lookup is not implemented!'
55
+
56
+ uri = @operation.uri rescue nil
57
+ raise Error, unimplemented_method % name.value if uri.nil?
58
+
55
59
  case name.value
56
- when "@method" then raise Error, unimplemented_method
57
- when "@target-uri" then @operation.uri.to_s
58
- when "@authority" then @operation.uri.authority.downcase
59
- when "@scheme" then @operation.uri.scheme.downcase
60
- when "@request-target" then @operation.uri.request_uri
61
- when "@path" then @operation.uri.path
62
- when "@query" then "?%s" % String(@operation.uri.query)
63
- when "@query-param" then query_param(name)
60
+ when "@method" then raise Error, unimplemented_method % name.value
61
+ when "@target-uri" then uri.to_s
62
+ when "@authority" then uri.authority.downcase
63
+ when "@scheme" then uri.scheme.downcase
64
+ when "@request-target" then uri.request_uri
65
+ when "@path" then uri.path
66
+ when "@query" then "?%s" % String(uri.query)
67
+ when "@query-param" then query_param(uri.query, name)
64
68
  end
65
69
  end
66
70
 
67
- def query_param(name)
71
+ def query_param(uri_query, name)
68
72
  param_name = name.parameters["name"]
69
73
  return nil if !param_name
70
74
  decoded_param_name = URI.decode_uri_component(param_name)
71
- params = CGI.parse(@operation.uri.query)
75
+ params = CGI.parse(uri_query)
72
76
  URI.encode_uri_component(params[decoded_param_name]&.first)
73
77
  end
74
78
 
75
79
  def field(name)
76
80
  has_tr = name.parameters["tr"]
77
81
  return nil if has_tr # HTTP requests don't have trailer fields
78
- value = @operation[name.value.to_s]
82
+ value = header(name.value.to_s)
79
83
  value.dup&.strip
80
84
  end
81
85
  end
@@ -30,6 +30,11 @@ module Linzer
30
30
 
31
31
  private
32
32
 
33
+ # Retrieves an HTTP field value from the request or response headers.
34
+ #
35
+ # @param name [Starry::Item] the parsed component identifier
36
+ # @return [String, nil] the stripped header value, or +nil+ if the
37
+ # field has a +tr+ (trailer) parameter or is not present
33
38
  def field(name)
34
39
  has_tr = name.parameters["tr"]
35
40
  return nil if has_tr # XXX: is there a library actually supporting trailers?
@@ -15,6 +15,14 @@ module Linzer
15
15
 
16
16
  private
17
17
 
18
+ # Resolves a derived component value from the request.
19
+ #
20
+ # Overrides the generic implementation for http.rb-specific
21
+ # accessor methods: +uri.host+ for +@authority+ and
22
+ # +verb+ for +@method+.
23
+ #
24
+ # @param name [Starry::Item] the parsed component identifier
25
+ # @return [String, nil] the derived value
18
26
  def derived(name)
19
27
  return @operation.uri.host if name.value == "@authority"
20
28
  return @operation.verb.to_s.upcase if name.value == "@method"
@@ -19,6 +19,13 @@ module Linzer
19
19
 
20
20
  private
21
21
 
22
+ # Resolves a derived component value from the response.
23
+ #
24
+ # Uses +HTTP::Response#status+ converted to Integer for
25
+ # the +@status+ component.
26
+ #
27
+ # @param name [Starry::Item] the parsed component identifier
28
+ # @return [Integer, nil] the HTTP status code, or +nil+ if unknown
22
29
  def derived(name)
23
30
  case name.value
24
31
  when "@status" then @operation.status.to_i
@@ -14,6 +14,14 @@ module Linzer
14
14
  class Request < Generic::Request
15
15
  private
16
16
 
17
+ # Resolves a derived component value from the request.
18
+ #
19
+ # Overrides the generic implementation to use
20
+ # +Net::HTTPRequest#method+ for the +@method+ component,
21
+ # which returns the HTTP verb as an uppercase string.
22
+ #
23
+ # @param name [Starry::Item] the parsed component identifier
24
+ # @return [String, nil] the derived value
17
25
  def derived(name)
18
26
  return @operation.method if name.value == "@method"
19
27
  super
@@ -11,6 +11,13 @@ module Linzer
11
11
  class Response < Generic::Response
12
12
  private
13
13
 
14
+ # Resolves a derived component value from the response.
15
+ #
16
+ # Uses +Net::HTTPResponse#code+ (a String) converted to Integer
17
+ # for the +@status+ component.
18
+ #
19
+ # @param name [Starry::Item] the parsed component identifier
20
+ # @return [Integer, nil] the HTTP status code, or +nil+ if unknown
14
21
  def derived(name)
15
22
  case name.value
16
23
  when "@status" then @operation.code.to_i
@@ -24,11 +24,19 @@ module Linzer
24
24
 
25
25
  private
26
26
 
27
+ # Validates that the operation is exclusively a request or response.
28
+ # @raise [Error] if the operation is both or neither
27
29
  def validate
28
30
  msg = "Message instance must be an HTTP request or response"
29
31
  raise Error.new msg if response? == request?
30
32
  end
31
33
 
34
+ # Validates that a header name is non-empty.
35
+ #
36
+ # @param name [String] the header name
37
+ # @return [String] the validated header name
38
+ # @raise [ArgumentError] if the name is blank
39
+ # @raise [Linzer::Error] if the name is otherwise invalid
32
40
  def validate_header_name(name)
33
41
  raise ArgumentError.new, "Blank header name." if name.empty?
34
42
  name.to_str
@@ -40,6 +48,13 @@ module Linzer
40
48
  # :nocov:
41
49
  end
42
50
 
51
+ # Converts an HTTP header name to Rack's environment key format.
52
+ #
53
+ # Rack stores headers as uppercase with underscores and an +HTTP_+
54
+ # prefix, except for +Content-Type+ and +Content-Length+.
55
+ #
56
+ # @param field_name [String] the HTTP header name (e.g. +"content-type"+)
57
+ # @return [String] the Rack env key (e.g. +"CONTENT_TYPE"+ or +"HTTP_ACCEPT"+)
43
58
  def rack_header_name(field_name)
44
59
  validate_header_name field_name
45
60
 
@@ -52,6 +67,10 @@ module Linzer
52
67
  end
53
68
  end
54
69
 
70
+ # Resolves a derived component value from the Rack request/response.
71
+ #
72
+ # @param name [Starry::Item] the parsed component identifier
73
+ # @return [String, nil] the derived value, or +nil+ if unknown
55
74
  def derived(name)
56
75
  method = DERIVED_COMPONENT[name.value]
57
76
 
@@ -64,6 +83,11 @@ module Linzer
64
83
  value || derive(@operation, method)
65
84
  end
66
85
 
86
+ # Retrieves an HTTP field value from the Rack request or response.
87
+ #
88
+ # @param name [Starry::Item] the parsed component identifier
89
+ # @return [String, nil] the stripped header value, or +nil+ if the
90
+ # field has a +tr+ (trailer) parameter or is not present
67
91
  def field(name)
68
92
  has_tr = name.parameters["tr"]
69
93
  return nil if has_tr
@@ -79,6 +103,14 @@ module Linzer
79
103
  field_value.dup&.strip
80
104
  end
81
105
 
106
+ # Invokes a method on the Rack operation to extract a derived value.
107
+ #
108
+ # Applies post-processing for +@query+ (prepends +?+) and
109
+ # +@authority+/+@scheme+ (downcases).
110
+ #
111
+ # @param operation [Rack::Request, Rack::Response] the Rack object
112
+ # @param method [Symbol] the method to call
113
+ # @return [String, nil] the derived value
82
114
  def derive(operation, method)
83
115
  return nil unless operation.respond_to?(method)
84
116
  value = operation.public_send(method)
@@ -87,6 +119,11 @@ module Linzer
87
119
  value
88
120
  end
89
121
 
122
+ # Extracts a single query parameter value by name.
123
+ #
124
+ # @param name [Starry::Item] the component with a +name+ parameter
125
+ # @return [String, nil] the percent-encoded parameter value, or
126
+ # +nil+ if the parameter is missing or not found
90
127
  def query_param(name)
91
128
  param_name = name.parameters["name"]
92
129
  return nil if !param_name
@@ -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.beta3"
7
7
  end
@@ -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,