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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +96 -12
- data/lib/faraday/http_signature/middleware.rb +296 -0
- data/lib/faraday/http_signature.rb +36 -0
- data/lib/linzer/faraday/utils.rb +29 -0
- data/lib/linzer/faraday.rb +29 -0
- data/lib/linzer/http.rb +43 -1
- data/lib/linzer/message/adapter/abstract.rb +65 -9
- data/lib/linzer/message/adapter/faraday/request.rb +63 -0
- data/lib/linzer/message/adapter/faraday/response.rb +46 -0
- data/lib/linzer/message/adapter/generic/request.rb +27 -20
- data/lib/linzer/message/adapter/generic/response.rb +11 -8
- data/lib/linzer/message/adapter/http_gem/common.rb +16 -8
- data/lib/linzer/message/adapter/http_gem/request.rb +8 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +7 -0
- data/lib/linzer/message/adapter/net_http/request.rb +8 -0
- data/lib/linzer/message/adapter/net_http/response.rb +7 -0
- data/lib/linzer/message/adapter/rack/common.rb +37 -0
- data/lib/linzer/message/adapter/rack/request.rb +11 -8
- data/lib/linzer/message/adapter/rack/response.rb +11 -8
- data/lib/linzer/message/field/parser.rb +15 -0
- data/lib/linzer/message/wrapper.rb +12 -2
- data/lib/linzer/signature.rb +20 -0
- data/lib/linzer/verifier.rb +8 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +0 -1
- data/lib/rack/auth/signature/helpers.rb +72 -0
- metadata +7 -21
|
@@ -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
|
|
52
|
-
#
|
|
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}'!
|
data/lib/linzer/signature.rb
CHANGED
|
@@ -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
|
data/lib/linzer/verifier.rb
CHANGED
|
@@ -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
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
|
@@ -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
|
|
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
|