linzer 0.7.0.beta1 → 0.7.0.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.
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module Rack
7
+ module Common
8
+ DERIVED_COMPONENT = {
9
+ method: :request_method,
10
+ authority: :authority,
11
+ path: :path_info,
12
+ status: :status,
13
+ "target-uri": :url,
14
+ scheme: :scheme,
15
+ "request-target": :fullpath,
16
+ query: :query_string
17
+ }.freeze
18
+ private_constant :DERIVED_COMPONENT
19
+
20
+ private
21
+
22
+ def validate
23
+ msg = "Message instance must be an HTTP request or response"
24
+ raise Error.new msg if response? == request?
25
+ end
26
+
27
+ def validate_header_name(name)
28
+ raise ArgumentError.new, "Blank header name." if name.empty?
29
+ name.to_str
30
+ rescue => ex
31
+ err_msg = "Invalid header name: '#{name}'"
32
+ raise Linzer::Error.new, err_msg, cause: ex
33
+ end
34
+
35
+ def rack_header_name(field_name)
36
+ validate_header_name field_name
37
+
38
+ rack_name = field_name.upcase.tr("-", "_")
39
+ case field_name.downcase
40
+ when "content-type", "content-length"
41
+ rack_name
42
+ else
43
+ "HTTP_#{rack_name}"
44
+ end
45
+ end
46
+
47
+ def rack_request_headers(rack_request)
48
+ rack_request
49
+ .each_header
50
+ .to_h
51
+ .select do |k, _|
52
+ k.start_with?("HTTP_") || %w[CONTENT_TYPE CONTENT_LENGTH].include?(k)
53
+ end
54
+ .transform_keys { |k| k.downcase.tr("_", "-") }
55
+ .transform_keys do |k|
56
+ %w[content-type content-length].include?(k) ? k : k.gsub(/^http-/, "")
57
+ end
58
+ end
59
+
60
+ def derived(name)
61
+ method = DERIVED_COMPONENT[name.value]
62
+
63
+ value = case name.value
64
+ when :query then derive(@operation, method)
65
+ when :"query-param" then query_param(name)
66
+ end
67
+
68
+ return nil if !method && !value
69
+ value || derive(@operation, method)
70
+ end
71
+
72
+ def field(name)
73
+ has_tr = name.parameters["tr"]
74
+ if has_tr
75
+ value = tr(name)
76
+ else
77
+ if request?
78
+ rack_header_name = rack_header_name(name.value.to_s)
79
+ value = @operation.env[rack_header_name]
80
+ end
81
+ value = @operation.headers[name.value.to_s] if response?
82
+ end
83
+ value.dup&.strip
84
+ end
85
+
86
+ def derive(operation, method)
87
+ return nil unless operation.respond_to?(method)
88
+ value = operation.public_send(method)
89
+ return "?" + value if method == :query_string
90
+ return value.downcase if %i[authority scheme].include?(method)
91
+ value
92
+ end
93
+
94
+ def query_param(name)
95
+ param_name = name.parameters["name"]
96
+ return nil if !param_name
97
+ decoded_param_name = URI.decode_uri_component(param_name)
98
+ URI.encode_uri_component(@operation.params.fetch(decoded_param_name))
99
+ rescue => _
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module Rack
7
+ class Request < Abstract
8
+ include Common
9
+
10
+ def initialize(operation, **options)
11
+ @operation = operation
12
+ validate
13
+ freeze
14
+ end
15
+
16
+ def headers
17
+ rack_request_headers(@operation)
18
+ end
19
+
20
+ def attach!(signature)
21
+ signature.to_h.each do |h, v|
22
+ @operation.set_header(rack_header_name(h), v)
23
+ end
24
+ @operation
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module Rack
7
+ class Response < Abstract
8
+ include Common
9
+
10
+ def initialize(operation, **options)
11
+ @operation = operation
12
+ validate
13
+ attached_request = options[:attached_request]
14
+ @attached_request = attached_request ? Message.new(attached_request) : nil
15
+ validate_attached_request @attached_request if @attached_request
16
+ freeze
17
+ end
18
+
19
+ def headers
20
+ @operation.headers
21
+ end
22
+
23
+ def attach!(signature)
24
+ signature.to_h.each { |h, v| @operation[h] = v }
25
+ @operation
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "adapter/abstract"
4
+ require_relative "adapter/rack/common"
5
+ require_relative "adapter/rack/request"
6
+ require_relative "adapter/rack/response"
7
+ require_relative "adapter/net_http/request"
8
+ require_relative "adapter/net_http/response"
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Wrapper
6
+ @adapters = {
7
+ Rack::Request => Linzer::Message::Adapter::Rack::Request,
8
+ Rack::Response => Linzer::Message::Adapter::Rack::Response,
9
+ Net::HTTPRequest => Linzer::Message::Adapter::NetHTTP::Request,
10
+ Net::HTTPResponse => Linzer::Message::Adapter::NetHTTP::Response
11
+ }
12
+
13
+ class << self
14
+ def wrap(operation, **options)
15
+ adapter_class = adapters[operation.class]
16
+
17
+ if !adapter_class
18
+ ancestor = find_ancestor(operation)
19
+ fail_with_unsupported(operation) unless ancestor
20
+ end
21
+
22
+ (adapter_class || ancestor).new(operation, **options)
23
+ end
24
+
25
+ def register_adapter(operation_class, adapter_class)
26
+ adapters[operation_class] = adapter_class
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :adapters
32
+
33
+ def find_ancestor(operation)
34
+ adapters
35
+ .select { |klz, adpt| operation.is_a? klz }
36
+ .values
37
+ .first
38
+ end
39
+
40
+ def fail_with_unsupported(operation)
41
+ err_msg = <<~EOM
42
+ Unknown/unsupported HTTP message class: '#{operation.class}'!
43
+
44
+ Linzer supports custom HTTP messages implementation by register them first
45
+ with `Linzer::Message.register_adapter` method.
46
+ EOM
47
+ raise Linzer::Error, err_msg
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,207 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Linzer
4
6
  class Message
7
+ extend Forwardable
8
+
5
9
  def initialize(operation, attached_request: nil)
6
- @operation = operation
7
- validate
8
- @attached_request = attached_request ? Message.new(attached_request) : nil
10
+ @adapter = Wrapper.wrap(operation, attached_request: attached_request)
9
11
  freeze
10
12
  end
11
13
 
12
- def request?
13
- @operation.is_a?(Rack::Request) || @operation.respond_to?(:request_method)
14
- end
15
-
16
- def response?
17
- @operation.is_a?(Rack::Response) || @operation.respond_to?(:status)
18
- end
19
-
20
- def attached_request?
21
- !!@attached_request
22
- end
23
-
24
- def headers
25
- return @operation.headers if response? || @operation.respond_to?(:headers)
14
+ # common predicates
15
+ def_delegators :@adapter, :request?, :response?, :attached_request?
26
16
 
27
- Request.headers(@operation)
28
- end
29
-
30
- def field?(f)
31
- !!self[f]
32
- end
33
-
34
- DERIVED_COMPONENT = {
35
- method: :request_method,
36
- authority: :authority,
37
- path: :path_info,
38
- status: :status,
39
- "target-uri": :url,
40
- scheme: :scheme,
41
- "request-target": :fullpath,
42
- query: :query_string
43
- }.freeze
17
+ # fields look up
18
+ def_delegators :@adapter, :headers, :field?, :[]
44
19
 
45
- def [](field_name)
46
- name = parse_field_name(field_name)
47
- return nil if name.nil?
48
-
49
- if field_name.start_with?("@")
50
- retrieve(name, :derived)
51
- else
52
- retrieve(name, :field)
53
- end
54
- end
20
+ # to attach a signature to the underlying HTTP message
21
+ def_delegators :@adapter, :attach!
55
22
 
56
23
  class << self
57
- def parse_structured_dictionary(str, field_name = nil)
58
- Starry.parse_dictionary(str)
59
- rescue Starry::ParseError => _
60
- raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
61
- end
62
- end
63
-
64
- private
65
-
66
- def validate
67
- msg = "Message instance must be an HTTP request or response"
68
- raise Error.new msg if response? == request?
69
- end
70
-
71
- def parse_field_name(field_name)
72
- if field_name&.start_with?("@")
73
- Starry.parse_item(field_name[1..])
74
- else
75
- Starry.parse_item(field_name)
76
- end
77
- rescue => _
78
- nil
79
- end
80
-
81
- def validate_parameters(name, method)
82
- has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
83
- return nil if has_unknown
84
-
85
- has_name = name.parameters["name"]
86
- has_req = name.parameters["req"]
87
- has_sf = name.parameters["sf"] || name.parameters.key?("key")
88
- has_bs = name.parameters["bs"]
89
- value = name.value
90
-
91
- # Section 2.2.8 of RFC 9421
92
- return nil if has_name && value != :"query-param"
93
-
94
- # No derived values come from trailers section
95
- return nil if method == :derived && name.parameters["tr"]
96
-
97
- # From: 2.1. HTTP Fields:
98
- # The bs parameter, which requires the raw bytes of the field values
99
- # from the message, is not compatible with the use of the sf or key
100
- # parameters, which require the parsed data structures of the field
101
- # values after combination
102
- return nil if has_sf && has_bs
103
-
104
- # req param only makes sense on responses with an associated request
105
- return nil if has_req && (!response? || !attached_request?)
106
-
107
- name
108
- end
109
-
110
- KNOWN_PARAMETERS = %w[sf key bs req tr name]
111
- private_constant :KNOWN_PARAMETERS
112
-
113
- def retrieve(name, method)
114
- if !name.parameters.empty?
115
- valid_params = validate_parameters(name, method)
116
- return nil if !valid_params
117
- end
118
-
119
- has_req = name.parameters["req"]
120
- has_sf = name.parameters["sf"] || name.parameters.key?("key")
121
- has_bs = name.parameters["bs"]
122
-
123
- if has_req
124
- name.parameters.delete("req")
125
- return req(name, method)
126
- end
127
-
128
- value = send(method, name)
129
-
130
- case
131
- when has_sf
132
- key = name.parameters["key"]
133
- sf(value, key)
134
- when has_bs then bs(value)
135
- else value
136
- end
137
- end
138
-
139
- def derived(name)
140
- method = DERIVED_COMPONENT[name.value]
141
-
142
- value = case name.value
143
- when :query then derive(@operation, method)
144
- when :"query-param" then query_param(name)
145
- end
146
-
147
- return nil if !method && !value
148
- value || derive(@operation, method)
149
- end
150
-
151
- def field(name)
152
- has_tr = name.parameters["tr"]
153
- if has_tr
154
- value = tr(name)
155
- else
156
- if request?
157
- rack_header_name = Request.rack_header_name(name.value.to_s)
158
- value = @operation.env[rack_header_name]
159
- end
160
- value = @operation.headers[name.value.to_s] if response?
161
- end
162
- value.dup&.strip
163
- end
164
-
165
- def derive(operation, method)
166
- return nil unless operation.respond_to?(method)
167
- value = operation.public_send(method)
168
- return "?" + value if method == :query_string
169
- return value.downcase if %i[authority scheme].include?(method)
170
- value
171
- end
172
-
173
- def query_param(name)
174
- param_name = name.parameters["name"]
175
- return nil if !param_name
176
- decoded_param_name = URI.decode_uri_component(param_name)
177
- URI.encode_uri_component(@operation.params.fetch(decoded_param_name))
178
- rescue => _
179
- nil
180
- end
181
-
182
- def sf(value, key = nil)
183
- dict = Starry.parse_dictionary(value)
184
-
185
- if key
186
- obj = dict[key]
187
- Starry.serialize(obj.is_a?(Starry::InnerList) ? [obj] : obj)
188
- else
189
- Starry.serialize(dict)
190
- end
191
- end
192
-
193
- def bs(value)
194
- Starry.serialize(value.encode(Encoding::ASCII_8BIT))
195
- end
196
-
197
- def tr(trailer)
198
- @operation.body.trailers[trailer.value.to_s]
199
- end
200
-
201
- def req(field, method)
202
- case method
203
- when :derived then @attached_request["@#{field}"]
204
- when :field then @attached_request[field.to_s]
24
+ def register_adapter(operation_class, adapter_class)
25
+ Wrapper.register_adapter(operation_class, adapter_class)
205
26
  end
206
27
  end
207
28
  end
@@ -86,11 +86,17 @@ module Linzer
86
86
  raise Error.new "Unexpected value for covered components."
87
87
  end
88
88
 
89
+ def parse_structured_dictionary(str, field_name = nil)
90
+ Starry.parse_dictionary(str)
91
+ rescue Starry::ParseError => _
92
+ raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
93
+ end
94
+
89
95
  def parse_structured_field(hsh, field_name)
90
96
  # Serialized Structured Field values for HTTP are ASCII strings.
91
97
  # See: RFC 8941 (https://datatracker.ietf.org/doc/html/rfc8941)
92
98
  value = hsh[field_name].encode(Encoding::US_ASCII)
93
- Message.parse_structured_dictionary(value, field_name)
99
+ parse_structured_dictionary(value, field_name)
94
100
  end
95
101
  end
96
102
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.7.0.beta1"
4
+ VERSION = "0.7.0.beta3"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -5,12 +5,13 @@ require "openssl"
5
5
  require "rack"
6
6
  require "uri"
7
7
  require "stringio"
8
+ require "net/http"
8
9
 
9
10
  require_relative "linzer/version"
10
11
  require_relative "linzer/common"
11
- require_relative "linzer/request"
12
- require_relative "linzer/response"
13
12
  require_relative "linzer/message"
13
+ require_relative "linzer/message/adapter"
14
+ require_relative "linzer/message/wrapper"
14
15
  require_relative "linzer/signature"
15
16
  require_relative "linzer/key"
16
17
  require_relative "linzer/rsa"
@@ -28,11 +29,6 @@ module Linzer
28
29
 
29
30
  class << self
30
31
  include Key::Helper
31
- include Response
32
-
33
- def new_request(verb, uri = "/", params = {}, headers = {})
34
- Linzer::Request.build(verb, uri, params, headers)
35
- end
36
32
 
37
33
  def verify(pubkey, message, signature, no_older_than: nil)
38
34
  Linzer::Verifier.verify(pubkey, message, signature, no_older_than: no_older_than)
@@ -41,5 +37,27 @@ module Linzer
41
37
  def sign(key, message, components, options = {})
42
38
  Linzer::Signer.sign(key, message, components, options)
43
39
  end
40
+
41
+ def sign!(request_or_response, **args)
42
+ message = Message.new(request_or_response)
43
+ options = {}
44
+
45
+ label = args[:label]
46
+ options[:label] = label if label
47
+ options.merge!(args.fetch(:params, {}))
48
+
49
+ key = args.fetch(:key)
50
+ signature = Linzer::Signer.sign(key, message, args.fetch(:components), options)
51
+ message.attach!(signature)
52
+ end
53
+
54
+ def verify!(request_or_response, key: nil, no_older_than: 900)
55
+ message = Message.new(request_or_response)
56
+ signature = Signature.build(message.headers.slice("signature", "signature-input"))
57
+ keyid = signature.parameters["keyid"]
58
+ raise Linzer::Error, "key not found" if !key && !keyid
59
+ verify_key = block_given? ? (yield keyid) : key
60
+ Linzer.verify(verify_key, message, signature, no_older_than: no_older_than)
61
+ end
44
62
  end
45
63
  end
@@ -0,0 +1,132 @@
1
+ require "yaml"
2
+
3
+ module Rack
4
+ module Auth
5
+ class Signature
6
+ module Helpers
7
+ module Parameters
8
+ private
9
+
10
+ def created?
11
+ !options[:signatures][:created_required] || !!Integer(params.fetch("created"))
12
+ end
13
+
14
+ def expires?
15
+ return true if !options[:signatures][:expires_required]
16
+ Integer(params.fetch("expires")) > Time.now.to_i
17
+ end
18
+
19
+ def keyid?
20
+ !options[:signatures][:keyid_required] || String(params.fetch("keyid"))
21
+ end
22
+
23
+ def nonce?
24
+ !options[:signatures][:nonce_required] || String(params.fetch("nonce"))
25
+ end
26
+
27
+ def alg?
28
+ !options[:signatures][:alg_required] || String(params.fetch("alg"))
29
+ end
30
+
31
+ def tag?
32
+ !options[:signatures][:tag_required] || String(params.fetch("tag"))
33
+ end
34
+ end
35
+
36
+ module Configuration
37
+ DEFAULT_OPTIONS = {
38
+ signatures: {
39
+ reject_older_than: 900,
40
+ created_required: true,
41
+ nonce_required: false,
42
+ alg_required: false,
43
+ tag_required: false,
44
+ expires_required: false,
45
+ keyid_required: false,
46
+ covered_components: %w[@method @request-target @authority date],
47
+ error_response: {body: [], status: 401, headers: {}}
48
+ },
49
+ keys: {}
50
+ }
51
+
52
+ private_constant :DEFAULT_OPTIONS
53
+
54
+ private
55
+
56
+ def load_options(options)
57
+ options_from_file = load_options_from_config_file(options)
58
+ {
59
+ except: options[:except] || options_from_file[:except],
60
+ default_key: options[:default_key] || options_from_file[:default_key],
61
+ signatures:
62
+ DEFAULT_OPTIONS[:signatures]
63
+ .merge(options_from_file[:signatures].to_h)
64
+ .merge(options[:signatures].to_h),
65
+ keys:
66
+ DEFAULT_OPTIONS[:keys]
67
+ .merge(options_from_file[:keys].to_h)
68
+ .merge(options[:keys].to_h)
69
+ }
70
+ end
71
+
72
+ def load_options_from_config_file(options)
73
+ config_path = options[:config_path]
74
+ YAML.safe_load_file(config_path, symbolize_names: true)
75
+ rescue
76
+ {}
77
+ end
78
+ end
79
+
80
+ module Key
81
+ private
82
+
83
+ def key
84
+ build_key(params["keyid"])
85
+ end
86
+
87
+ def build_key(keyid)
88
+ key_data = if keyid.nil? ||
89
+ (!options[:keys].key?(keyid.to_sym) && options[:default_key])
90
+ options[:default_key].to_h
91
+ else
92
+ options[:keys][keyid.to_sym] || {}
93
+ end
94
+
95
+ if key_data.key?(:path) && !key_data.key?(:material)
96
+ key_data[:material] = IO.read(key_data[:path]) rescue nil
97
+ end
98
+
99
+ if !key_data || key_data.empty? || !key_data[:material]
100
+ key_not_found = "Key not found. Signature cannot be verified."
101
+ raise Linzer::Error.new key_not_found
102
+ end
103
+
104
+ alg = @signature.parameters["alg"] || key_data[:alg] || :unknown
105
+ instantiate_key(keyid || :default, alg, key_data)
106
+ end
107
+
108
+ def instantiate_key(keyid, alg, key_data)
109
+ key_methods = {
110
+ "rsa-pss-sha512" => :new_rsa_pss_sha512_key,
111
+ "rsa-v1_5-sha256" => :new_rsa_v1_5_sha256_key,
112
+ "hmac-sha256" => :new_hmac_sha256_key,
113
+ "ecdsa-p256-sha256" => :new_ecdsa_p256_sha256_key,
114
+ "ecdsa-p384-sha384" => :new_ecdsa_p384_sha384_key,
115
+ "ed25519" => :new_ed25519_public_key
116
+ }
117
+ method = key_methods[alg]
118
+
119
+ alg_error = "Unsupported or unknown signature algorithm"
120
+ raise Linzer::Error.new alg_error unless method
121
+
122
+ Linzer.public_send(method, key_data[:material], keyid.to_s)
123
+ end
124
+ end
125
+
126
+ include Parameters
127
+ include Configuration
128
+ include Key
129
+ end
130
+ end
131
+ end
132
+ end