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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +153 -39
- data/examples/sinatra/Gemfile +1 -1
- data/examples/sinatra/http-signatures.yml +25 -18
- data/examples/sinatra/myapp.rb +11 -0
- data/lib/linzer/hmac.rb +13 -0
- data/lib/linzer/message/adapter/abstract.rb +152 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +35 -0
- data/lib/linzer/message/adapter/net_http/request.rb +57 -0
- data/lib/linzer/message/adapter/net_http/response.rb +34 -0
- data/lib/linzer/message/adapter/rack/common.rb +106 -0
- data/lib/linzer/message/adapter/rack/request.rb +30 -0
- data/lib/linzer/message/adapter/rack/response.rb +31 -0
- data/lib/linzer/message/adapter.rb +8 -0
- data/lib/linzer/message/wrapper.rb +52 -0
- data/lib/linzer/message.rb +13 -192
- data/lib/linzer/signature.rb +7 -1
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +25 -7
- data/lib/rack/auth/signature/helpers.rb +132 -0
- data/lib/rack/auth/signature.rb +25 -116
- metadata +47 -5
- data/lib/linzer/request.rb +0 -95
- data/lib/linzer/response.rb +0 -9
@@ -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
|
data/lib/linzer/message.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
13
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
46
|
-
|
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
|
58
|
-
|
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
|
data/lib/linzer/signature.rb
CHANGED
@@ -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
|
-
|
99
|
+
parse_structured_dictionary(value, field_name)
|
94
100
|
end
|
95
101
|
end
|
96
102
|
end
|
data/lib/linzer/version.rb
CHANGED
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
|