linzer 0.7.9 → 0.8.0.beta1
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 +8 -0
- data/Rakefile +6 -0
- data/benchmarks/profile_sign_ed25519.rb +102 -0
- data/benchmarks/profile_verify_ed25519.rb +107 -0
- data/benchmarks/sign.rb +55 -0
- data/benchmarks/verify.rb +68 -0
- data/lib/linzer/common.rb +52 -22
- data/lib/linzer/ed25519.rb +4 -2
- data/lib/linzer/hmac.rb +14 -12
- data/lib/linzer/http/structured_field.rb +48 -0
- data/lib/linzer/jws.rb +4 -4
- data/lib/linzer/key.rb +20 -2
- data/lib/linzer/message/adapter/abstract.rb +6 -5
- data/lib/linzer/message/adapter.rb +0 -3
- data/lib/linzer/message/field.rb +53 -1
- data/lib/linzer/message/wrapper.rb +0 -2
- data/lib/linzer/rack.rb +24 -0
- data/lib/linzer/rsa_pss.rb +4 -4
- data/lib/linzer/signature.rb +143 -18
- data/lib/linzer/signer.rb +36 -15
- data/lib/linzer/verifier.rb +9 -3
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +1 -3
- metadata +7 -41
data/lib/linzer/key.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Linzer
|
|
|
38
38
|
@material = material
|
|
39
39
|
@params = Hash(params).clone.freeze
|
|
40
40
|
validate
|
|
41
|
+
@is_private = compute_private?
|
|
42
|
+
@is_public = compute_public?
|
|
41
43
|
freeze
|
|
42
44
|
end
|
|
43
45
|
|
|
@@ -84,14 +86,14 @@ module Linzer
|
|
|
84
86
|
#
|
|
85
87
|
# @return [Boolean] true if the key contains public key material
|
|
86
88
|
def public?
|
|
87
|
-
|
|
89
|
+
@is_public
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
# Checks if this key can be used for signing.
|
|
91
93
|
#
|
|
92
94
|
# @return [Boolean] true if the key contains private key material
|
|
93
95
|
def private?
|
|
94
|
-
|
|
96
|
+
@is_private
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
private
|
|
@@ -102,6 +104,22 @@ module Linzer
|
|
|
102
104
|
!material.nil? or raise Error.new "Invalid key. No key material provided."
|
|
103
105
|
end
|
|
104
106
|
|
|
107
|
+
# Computes whether the key contains private key material.
|
|
108
|
+
# Override in subclasses where the OpenSSL key object does not
|
|
109
|
+
# respond to +private?+ (e.g. Ed25519, RSA-PSS).
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def compute_private?
|
|
112
|
+
material.respond_to?(:private?) ? material.private? : false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Computes whether the key contains public key material.
|
|
116
|
+
# Override in subclasses where the OpenSSL key object does not
|
|
117
|
+
# respond to +public?+ (e.g. Ed25519, RSA-PSS).
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def compute_public?
|
|
120
|
+
material.respond_to?(:public?) ? material.public? : false
|
|
121
|
+
end
|
|
122
|
+
|
|
105
123
|
# Validates that a digest algorithm is configured.
|
|
106
124
|
# @raise [Error] If no digest algorithm is set
|
|
107
125
|
def validate_digest
|
|
@@ -70,7 +70,7 @@ module Linzer
|
|
|
70
70
|
# @example With structured field parameter
|
|
71
71
|
# adapter['"example-dict";key="a"'] # => "1"
|
|
72
72
|
def [](field)
|
|
73
|
-
field_id = field.is_a?(FieldId) ? field : parse_field_name(field)
|
|
73
|
+
field_id = (field.is_a?(FieldId) || field.is_a?(Field::FastIdentifier)) ? field : parse_field_name(field)
|
|
74
74
|
return nil if field_id.nil? || field_id.item.nil?
|
|
75
75
|
retrieve(field_id.item, field_id.derived? ? :derived : :field)
|
|
76
76
|
end
|
|
@@ -190,10 +190,11 @@ module Linzer
|
|
|
190
190
|
# @param method [Symbol] +:derived+ or +:field+
|
|
191
191
|
# @return [String, Integer, nil] the component value
|
|
192
192
|
def retrieve(name, method)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
# Fast path: no parameters means no special handling needed
|
|
194
|
+
return send(method, name) if name.parameters.empty?
|
|
195
|
+
|
|
196
|
+
valid_params = validate_parameters(name, method)
|
|
197
|
+
return nil if !valid_params
|
|
197
198
|
|
|
198
199
|
has_req = name.parameters["req"]
|
|
199
200
|
has_sf = name.parameters["sf"] || name.parameters.key?("key")
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
require_relative "adapter/abstract"
|
|
4
4
|
require_relative "adapter/generic/request"
|
|
5
5
|
require_relative "adapter/generic/response"
|
|
6
|
-
require_relative "adapter/rack/common"
|
|
7
|
-
require_relative "adapter/rack/request"
|
|
8
|
-
require_relative "adapter/rack/response"
|
|
9
6
|
require_relative "adapter/net_http/request"
|
|
10
7
|
require_relative "adapter/net_http/response"
|
|
11
8
|
|
data/lib/linzer/message/field.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Linzer
|
|
|
34
34
|
# @raise [Error] If the component identifier is invalid
|
|
35
35
|
def serialize
|
|
36
36
|
raise Error, "Invalid component identifier: '#{field_name}'!" unless item
|
|
37
|
-
Starry.serialize(@item)
|
|
37
|
+
@serialized || Starry.serialize(@item)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -54,6 +54,29 @@ module Linzer
|
|
|
54
54
|
|
|
55
55
|
Identifier.include Message::Field::IdentifierMethods
|
|
56
56
|
|
|
57
|
+
# Lightweight FieldId for simple components (no parameters).
|
|
58
|
+
# Bypasses Starry parsing entirely. Duck-types with Identifier
|
|
59
|
+
# for use in the adapter's [] method.
|
|
60
|
+
# @api private
|
|
61
|
+
class FastIdentifier
|
|
62
|
+
def initialize(serialized, item)
|
|
63
|
+
@field_name = serialized
|
|
64
|
+
@item = item
|
|
65
|
+
@serialized = serialized
|
|
66
|
+
freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
attr_reader :field_name, :item
|
|
70
|
+
|
|
71
|
+
def derived?
|
|
72
|
+
@item.value.start_with?("@")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def serialize
|
|
76
|
+
@serialized
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
57
80
|
class Identifier
|
|
58
81
|
class << self
|
|
59
82
|
# Serializes a single component identifier.
|
|
@@ -70,6 +93,35 @@ module Linzer
|
|
|
70
93
|
components.map(&method(:serialize))
|
|
71
94
|
end
|
|
72
95
|
|
|
96
|
+
# Serializes an array of component identifiers, returning both
|
|
97
|
+
# the serialized strings and the FieldId objects for reuse.
|
|
98
|
+
# @param components [Array<String>] Component names
|
|
99
|
+
# @return [Array(Array<String>, Array<Identifier>)] Serialized strings and FieldId objects
|
|
100
|
+
def serialize_components_with_field_ids(components)
|
|
101
|
+
serialized = Array.new(components.size)
|
|
102
|
+
field_ids = Array.new(components.size)
|
|
103
|
+
|
|
104
|
+
components.each_with_index do |c, i|
|
|
105
|
+
if c.include?(";") || c.include?('"')
|
|
106
|
+
# Complex component with parameters or already serialized:
|
|
107
|
+
# fall back to full Starry parsing
|
|
108
|
+
fid = new(field_name: c)
|
|
109
|
+
field_ids[i] = fid
|
|
110
|
+
serialized[i] = fid.serialize
|
|
111
|
+
else
|
|
112
|
+
# Simple component (e.g. "@method", "content-type"):
|
|
113
|
+
# build the Item and serialized string directly,
|
|
114
|
+
# bypassing Starry.parse_item + Starry.serialize
|
|
115
|
+
quoted = "\"#{c}\""
|
|
116
|
+
item = Starry::Item.new(c, {})
|
|
117
|
+
field_ids[i] = FastIdentifier.new(quoted, item)
|
|
118
|
+
serialized[i] = quoted
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
[serialized, field_ids]
|
|
123
|
+
end
|
|
124
|
+
|
|
73
125
|
# Deserializes component identifiers back to names.
|
|
74
126
|
# @param components [Array<String>] Serialized identifiers
|
|
75
127
|
# @return [Array<String>] Component names
|
|
@@ -12,8 +12,6 @@ module Linzer
|
|
|
12
12
|
module Wrapper
|
|
13
13
|
# Default adapter mappings for built-in HTTP library support.
|
|
14
14
|
@adapters = {
|
|
15
|
-
Rack::Request => Linzer::Message::Adapter::Rack::Request,
|
|
16
|
-
Rack::Response => Linzer::Message::Adapter::Rack::Response,
|
|
17
15
|
Net::HTTPRequest => Linzer::Message::Adapter::NetHTTP::Request,
|
|
18
16
|
Net::HTTPResponse => Linzer::Message::Adapter::NetHTTP::Response
|
|
19
17
|
}
|
data/lib/linzer/rack.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rack integration for Linzer.
|
|
4
|
+
#
|
|
5
|
+
# Require this file to enable Rack support. It loads the Rack adapter
|
|
6
|
+
# classes and registers them so that {Rack::Request} and {Rack::Response}
|
|
7
|
+
# objects can be used directly with the Linzer signing and verification API.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# require "linzer/rack"
|
|
11
|
+
#
|
|
12
|
+
# use Rack::Auth::Signature,
|
|
13
|
+
# except: "/login",
|
|
14
|
+
# default: :my_key
|
|
15
|
+
|
|
16
|
+
require "rack"
|
|
17
|
+
require "linzer"
|
|
18
|
+
require "rack/auth/signature"
|
|
19
|
+
require "linzer/message/adapter/rack/common"
|
|
20
|
+
require "linzer/message/adapter/rack/request"
|
|
21
|
+
require "linzer/message/adapter/rack/response"
|
|
22
|
+
|
|
23
|
+
Linzer::Message.register_adapter(Rack::Request, Linzer::Message::Adapter::Rack::Request)
|
|
24
|
+
Linzer::Message.register_adapter(Rack::Response, Linzer::Message::Adapter::Rack::Response)
|
data/lib/linzer/rsa_pss.rb
CHANGED
|
@@ -65,18 +65,18 @@ module Linzer
|
|
|
65
65
|
)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
private
|
|
69
|
+
|
|
68
70
|
# @return [Boolean] true if this key contains public key material
|
|
69
|
-
def
|
|
71
|
+
def compute_public?
|
|
70
72
|
has_pem_public?
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
# @return [Boolean] true if this key contains private key material
|
|
74
|
-
def
|
|
76
|
+
def compute_private?
|
|
75
77
|
has_pem_private?
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
private
|
|
79
|
-
|
|
80
80
|
# Returns OpenSSL options for PSS signature operations.
|
|
81
81
|
# @return [Hash] OpenSSL signature options
|
|
82
82
|
def signature_options
|
data/lib/linzer/signature.rb
CHANGED
|
@@ -26,12 +26,14 @@ module Linzer
|
|
|
26
26
|
# @see https://www.rfc-editor.org/rfc/rfc9421.html#section-4 RFC 9421 Section 4
|
|
27
27
|
class Signature
|
|
28
28
|
# @api private
|
|
29
|
-
# Use {.build} to create Signature instances.
|
|
30
|
-
def initialize(metadata, value, label, parameters = {})
|
|
31
|
-
@metadata
|
|
32
|
-
@value
|
|
33
|
-
@parameters
|
|
34
|
-
@label
|
|
29
|
+
# Use {.build} or {.from_components} to create Signature instances.
|
|
30
|
+
def initialize(metadata, value, label, parameters = {}, parsed_items: nil, headers: nil)
|
|
31
|
+
@metadata = metadata.clone.freeze
|
|
32
|
+
@value = value.clone.freeze
|
|
33
|
+
@parameters = parameters.clone.freeze
|
|
34
|
+
@label = label.clone.freeze
|
|
35
|
+
@parsed_items = parsed_items&.freeze
|
|
36
|
+
@headers = headers&.freeze
|
|
35
37
|
freeze
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -74,6 +76,20 @@ module Linzer
|
|
|
74
76
|
FieldId.deserialize_components(serialized_components)
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
# Builds FieldId objects for each covered component.
|
|
80
|
+
#
|
|
81
|
+
# Uses {parsed_items} when available to create {FastIdentifier} objects
|
|
82
|
+
# that bypass Starry re-parsing. Falls back to constructing full
|
|
83
|
+
# {FieldId} objects from the serialized strings.
|
|
84
|
+
#
|
|
85
|
+
# Returns a fresh array each time because some adapter methods may
|
|
86
|
+
# mutate item parameters during field lookup (e.g., deleting "req").
|
|
87
|
+
#
|
|
88
|
+
# @return [Array<FastIdentifier, FieldId>] FieldId objects for each component
|
|
89
|
+
def field_ids
|
|
90
|
+
build_field_ids
|
|
91
|
+
end
|
|
92
|
+
|
|
77
93
|
# Returns the signature creation timestamp.
|
|
78
94
|
#
|
|
79
95
|
# @return [Integer, nil] Unix timestamp when the signature was created,
|
|
@@ -132,20 +148,60 @@ module Linzer
|
|
|
132
148
|
# @example Attaching to a Net::HTTP request
|
|
133
149
|
# signature.to_h.each { |name, value| request[name] = value }
|
|
134
150
|
def to_h
|
|
151
|
+
return @headers if @headers
|
|
152
|
+
|
|
153
|
+
items = @parsed_items || serialized_components.map { |c| Starry.parse_item(c) }
|
|
135
154
|
{
|
|
136
155
|
"signature" => Starry.serialize({label => value}),
|
|
137
156
|
"signature-input" => Starry.serialize({
|
|
138
|
-
label => Starry::InnerList.new(
|
|
139
|
-
serialized_components.map { |c| Starry.parse_item(c) },
|
|
140
|
-
parameters
|
|
141
|
-
)
|
|
157
|
+
label => Starry::InnerList.new(items, parameters)
|
|
142
158
|
})
|
|
143
159
|
}
|
|
144
160
|
end
|
|
145
161
|
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def build_field_ids
|
|
165
|
+
if @parsed_items && @parsed_items.size == @metadata.size
|
|
166
|
+
@metadata.each_with_index.map do |serialized, i|
|
|
167
|
+
item = @parsed_items[i]
|
|
168
|
+
# Clone items that have parameters since the adapter's retrieve
|
|
169
|
+
# method may mutate parameters (e.g., deleting "req").
|
|
170
|
+
unless item.parameters.empty?
|
|
171
|
+
item = Starry::Item.new(item.value, item.parameters.dup)
|
|
172
|
+
end
|
|
173
|
+
Message::Field::FastIdentifier.new(serialized, item)
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
@metadata.map { |c| FieldId.new(field_name: c) }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
146
180
|
class << self
|
|
147
181
|
private :new
|
|
148
182
|
|
|
183
|
+
# Creates a Signature directly from its constituent parts.
|
|
184
|
+
#
|
|
185
|
+
# This avoids the serialize-then-parse round-trip when the caller
|
|
186
|
+
# (e.g. {Signer.sign}) already has all the data.
|
|
187
|
+
#
|
|
188
|
+
# @api private
|
|
189
|
+
# @param components [Array<String>] Serialized component identifiers
|
|
190
|
+
# @param raw_signature [String] The raw signature bytes
|
|
191
|
+
# @param label [String] The signature label
|
|
192
|
+
# @param parameters [Hash] Signature parameters (symbol keys)
|
|
193
|
+
# @param parsed_items [Array<Starry::Item>] Pre-parsed component items
|
|
194
|
+
# @param headers [Hash] Pre-serialized header strings
|
|
195
|
+
# @return [Signature] The constructed signature
|
|
196
|
+
def from_components(components:, raw_signature:, label:, parameters:, parsed_items:, headers:)
|
|
197
|
+
# Signature stores parameters with string keys (as produced by Starry
|
|
198
|
+
# parsing). Convert symbol keys from Signer to match.
|
|
199
|
+
string_params = {}
|
|
200
|
+
parameters.each { |k, v| string_params[k.to_s] = v }
|
|
201
|
+
new(components, raw_signature, label, string_params,
|
|
202
|
+
parsed_items: parsed_items, headers: headers)
|
|
203
|
+
end
|
|
204
|
+
|
|
149
205
|
# Builds a Signature from HTTP headers.
|
|
150
206
|
#
|
|
151
207
|
# Parses the `signature` and `signature-input` headers according to
|
|
@@ -184,19 +240,15 @@ module Linzer
|
|
|
184
240
|
reject_multiple_signatures if input.size > 1 && options[:label].nil?
|
|
185
241
|
label = options[:label] || input.keys.first
|
|
186
242
|
|
|
187
|
-
|
|
188
|
-
fail_with_signature_not_found label unless signature.key?(label)
|
|
189
|
-
|
|
190
|
-
raw_signature =
|
|
191
|
-
signature[label].value
|
|
192
|
-
.force_encoding(Encoding::ASCII_8BIT)
|
|
243
|
+
raw_signature = extract_raw_signature(headers["signature"], label)
|
|
193
244
|
|
|
194
245
|
fail_due_invalid_components unless input[label].value.respond_to?(:each)
|
|
195
246
|
|
|
196
|
-
|
|
247
|
+
parsed_items = input[label].value
|
|
248
|
+
components = serialize_parsed_items(parsed_items)
|
|
197
249
|
parameters = input[label].parameters
|
|
198
250
|
|
|
199
|
-
new(components, raw_signature, label, parameters)
|
|
251
|
+
new(components, raw_signature, label, parameters, parsed_items: parsed_items)
|
|
200
252
|
end
|
|
201
253
|
|
|
202
254
|
private
|
|
@@ -223,6 +275,79 @@ module Linzer
|
|
|
223
275
|
raise Error.new "Unexpected value for covered components."
|
|
224
276
|
end
|
|
225
277
|
|
|
278
|
+
# Extracts the raw signature bytes for a given label from the
|
|
279
|
+
# signature header without a full Starry dictionary parse.
|
|
280
|
+
#
|
|
281
|
+
# The signature header format is: label=:base64data:
|
|
282
|
+
# For multiple signatures: label1=:b64:, label2=:b64:
|
|
283
|
+
#
|
|
284
|
+
# Falls back to full Starry parsing for non-trivial cases.
|
|
285
|
+
#
|
|
286
|
+
# @param header_value [String] the raw signature header
|
|
287
|
+
# @param label [String] the signature label to extract
|
|
288
|
+
# @return [String] the decoded signature bytes (ASCII-8BIT)
|
|
289
|
+
# @raise [Error] if the label is not found or decoding fails
|
|
290
|
+
def extract_raw_signature(header_value, label)
|
|
291
|
+
value = header_value.is_a?(String) ? header_value : header_value.to_s
|
|
292
|
+
prefix = "#{label}=:"
|
|
293
|
+
|
|
294
|
+
# Fast path: single signature (most common case)
|
|
295
|
+
if value.start_with?(prefix)
|
|
296
|
+
end_idx = value.index(":", prefix.length)
|
|
297
|
+
if end_idx
|
|
298
|
+
encoded = value[prefix.length...end_idx]
|
|
299
|
+
return Base64.strict_decode64(encoded)
|
|
300
|
+
.force_encoding(Encoding::ASCII_8BIT)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Multi-signature or unusual format: find the right entry
|
|
305
|
+
# Split on comma-separated dictionary members
|
|
306
|
+
value.split(",").each do |entry|
|
|
307
|
+
entry = entry.strip
|
|
308
|
+
if entry.start_with?(prefix)
|
|
309
|
+
end_idx = entry.index(":", prefix.length)
|
|
310
|
+
if end_idx
|
|
311
|
+
encoded = entry[prefix.length...end_idx]
|
|
312
|
+
return Base64.strict_decode64(encoded)
|
|
313
|
+
.force_encoding(Encoding::ASCII_8BIT)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Label not found via fast path — fall back to Starry
|
|
319
|
+
signature = parse_structured_dictionary(
|
|
320
|
+
value.encode(Encoding::US_ASCII), "signature"
|
|
321
|
+
)
|
|
322
|
+
fail_with_signature_not_found label unless signature.key?(label)
|
|
323
|
+
signature[label].value.force_encoding(Encoding::ASCII_8BIT)
|
|
324
|
+
rescue ArgumentError
|
|
325
|
+
# Base64 decode failed — fall back to Starry
|
|
326
|
+
signature = parse_structured_dictionary(
|
|
327
|
+
value.encode(Encoding::US_ASCII), "signature"
|
|
328
|
+
)
|
|
329
|
+
fail_with_signature_not_found label unless signature.key?(label)
|
|
330
|
+
signature[label].value.force_encoding(Encoding::ASCII_8BIT)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Serializes parsed Starry items to their string representations
|
|
334
|
+
# without going through the generic Starry.serialize_item path.
|
|
335
|
+
#
|
|
336
|
+
# For simple items (no parameters): builds '"value"' directly.
|
|
337
|
+
# For items with parameters: falls back to Starry.serialize_item.
|
|
338
|
+
#
|
|
339
|
+
# @param items [Array<Starry::Item>] parsed items from signature-input
|
|
340
|
+
# @return [Array<String>] serialized component identifiers
|
|
341
|
+
def serialize_parsed_items(items)
|
|
342
|
+
items.map do |item|
|
|
343
|
+
if item.parameters.empty?
|
|
344
|
+
"\"#{item.value}\""
|
|
345
|
+
else
|
|
346
|
+
Starry.serialize_item(item)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
226
351
|
def parse_structured_dictionary(str, field_name = nil)
|
|
227
352
|
Starry.parse_dictionary(str)
|
|
228
353
|
rescue Starry::ParseError => _
|
data/lib/linzer/signer.rb
CHANGED
|
@@ -68,16 +68,31 @@ module Linzer
|
|
|
68
68
|
# tag: "my-app"
|
|
69
69
|
# )
|
|
70
70
|
def sign(key, message, components, options = {})
|
|
71
|
-
serialized_components =
|
|
72
|
-
|
|
71
|
+
serialized_components, field_ids =
|
|
72
|
+
FieldId.serialize_components_with_field_ids(Array(components))
|
|
73
|
+
validate key, message, serialized_components, field_ids: field_ids
|
|
74
|
+
|
|
75
|
+
# Reuse the already-parsed items from field_ids
|
|
76
|
+
parsed_items = field_ids.map(&:item)
|
|
73
77
|
|
|
74
78
|
parameters = populate_parameters(key, options)
|
|
75
|
-
signature_base = signature_base(message, serialized_components, parameters
|
|
79
|
+
signature_base = signature_base(message, serialized_components, parameters,
|
|
80
|
+
field_ids: field_ids)
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
raw_signature = key.sign(signature_base)
|
|
78
83
|
label = options[:label] || DEFAULT_LABEL
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
# Build the signature directly, bypassing the serialize -> parse round-trip
|
|
86
|
+
headers = serialize(raw_signature, serialized_components, parameters, label)
|
|
87
|
+
|
|
88
|
+
Linzer::Signature.from_components(
|
|
89
|
+
components: serialized_components,
|
|
90
|
+
raw_signature: raw_signature,
|
|
91
|
+
label: label,
|
|
92
|
+
parameters: parameters,
|
|
93
|
+
parsed_items: parsed_items,
|
|
94
|
+
headers: headers
|
|
95
|
+
)
|
|
81
96
|
end
|
|
82
97
|
|
|
83
98
|
# Returns the default signature label.
|
|
@@ -91,19 +106,22 @@ module Linzer
|
|
|
91
106
|
|
|
92
107
|
# Validates signing inputs.
|
|
93
108
|
# @raise [SigningError] If any input is invalid
|
|
94
|
-
def validate(key, message, components)
|
|
109
|
+
def validate(key, message, components, field_ids: nil)
|
|
95
110
|
msg = "Message cannot be signed with null %s"
|
|
96
111
|
raise SigningError, msg % "value" if message.nil?
|
|
97
112
|
raise SigningError, msg % "key" if key.nil?
|
|
98
113
|
raise SigningError, msg % "component" if components.nil?
|
|
99
114
|
|
|
100
115
|
begin
|
|
101
|
-
validate_components message, components
|
|
116
|
+
validate_components message, components, field_ids: field_ids
|
|
102
117
|
rescue Error => ex
|
|
103
118
|
raise SigningError, ex.message, cause: ex
|
|
104
119
|
end
|
|
105
120
|
end
|
|
106
121
|
|
|
122
|
+
RESERVED_OPTIONS = %i[created keyid label].freeze
|
|
123
|
+
private_constant :RESERVED_OPTIONS
|
|
124
|
+
|
|
107
125
|
# Builds the signature parameters hash from options and key.
|
|
108
126
|
# @return [Hash] The populated parameters
|
|
109
127
|
def populate_parameters(key, options)
|
|
@@ -114,22 +132,25 @@ module Linzer
|
|
|
114
132
|
key_id = options[:keyid] || (key.key_id if key.respond_to?(:key_id))
|
|
115
133
|
parameters[:keyid] = key_id unless key_id.nil?
|
|
116
134
|
|
|
117
|
-
|
|
135
|
+
options.each { |k, v| parameters[k] = v unless RESERVED_OPTIONS.include?(k) }
|
|
118
136
|
|
|
119
137
|
parameters
|
|
120
138
|
end
|
|
121
139
|
|
|
122
140
|
# Serializes the signature into HTTP header format.
|
|
141
|
+
#
|
|
142
|
+
# Uses direct string building instead of Starry.serialize to avoid
|
|
143
|
+
# the overhead of generic structured field serialization.
|
|
144
|
+
#
|
|
123
145
|
# @return [Hash] Hash with "signature" and "signature-input" keys
|
|
124
146
|
def serialize(signature, components, parameters, label)
|
|
147
|
+
sig_b64 = Base64.strict_encode64(signature)
|
|
148
|
+
input_params = HTTP::StructuredField.serialize_parameters(parameters)
|
|
149
|
+
components_str = components.join(" ")
|
|
150
|
+
|
|
125
151
|
{
|
|
126
|
-
"signature"
|
|
127
|
-
"signature-input" =>
|
|
128
|
-
Starry.serialize(label =>
|
|
129
|
-
Starry::InnerList.new(
|
|
130
|
-
components.map { |c| Starry.parse_item(c) },
|
|
131
|
-
parameters
|
|
132
|
-
))
|
|
152
|
+
"signature" => "#{label}=:#{sig_b64}:",
|
|
153
|
+
"signature-input" => "#{label}=(#{components_str})#{input_params}"
|
|
133
154
|
}
|
|
134
155
|
end
|
|
135
156
|
end
|
data/lib/linzer/verifier.rb
CHANGED
|
@@ -59,7 +59,12 @@ module Linzer
|
|
|
59
59
|
parameters = signature.parameters
|
|
60
60
|
serialized_components = signature.serialized_components
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
# Build fresh field_ids for signature_base (validate already
|
|
63
|
+
# consumed its own set, which may have been mutated by adapters).
|
|
64
|
+
field_ids = signature.field_ids
|
|
65
|
+
|
|
66
|
+
signature_base = signature_base(message, serialized_components, parameters,
|
|
67
|
+
field_ids: field_ids)
|
|
63
68
|
|
|
64
69
|
verify_or_fail key, signature.value, signature_base
|
|
65
70
|
end
|
|
@@ -78,10 +83,11 @@ module Linzer
|
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
raise VerifyError, "Signature raw value to cannot be null" if signature.value.nil?
|
|
81
|
-
raise VerifyError, "Components cannot be null" if signature.
|
|
86
|
+
raise VerifyError, "Components cannot be null" if signature.serialized_components.nil?
|
|
82
87
|
|
|
83
88
|
begin
|
|
84
|
-
validate_components message, signature.serialized_components
|
|
89
|
+
validate_components message, signature.serialized_components,
|
|
90
|
+
field_ids: signature.field_ids
|
|
85
91
|
rescue Error => ex
|
|
86
92
|
raise VerifyError, ex.message, cause: ex
|
|
87
93
|
end
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "starry"
|
|
4
4
|
require "openssl"
|
|
5
|
-
require "rack"
|
|
6
5
|
require "uri"
|
|
7
6
|
require "net/http"
|
|
8
7
|
|
|
@@ -26,6 +25,7 @@ require_relative "linzer/key/helper"
|
|
|
26
25
|
require_relative "linzer/signer"
|
|
27
26
|
require_relative "linzer/verifier"
|
|
28
27
|
require_relative "linzer/http"
|
|
28
|
+
require_relative "linzer/http/structured_field"
|
|
29
29
|
|
|
30
30
|
# Linzer is a Ruby library for HTTP Message Signatures as defined in RFC 9421.
|
|
31
31
|
#
|
|
@@ -157,5 +157,3 @@ module Linzer
|
|
|
157
157
|
# Used for serializing and deserializing component identifiers.
|
|
158
158
|
FieldId = Message::Field::Identifier
|
|
159
159
|
end
|
|
160
|
-
|
|
161
|
-
require_relative "rack/auth/signature"
|
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.
|
|
4
|
+
version: 0.8.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miguel Landaeta
|
|
@@ -23,26 +23,6 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.2'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rack
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '2.2'
|
|
33
|
-
- - "<"
|
|
34
|
-
- !ruby/object:Gem::Version
|
|
35
|
-
version: '4.0'
|
|
36
|
-
type: :runtime
|
|
37
|
-
prerelease: false
|
|
38
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
-
requirements:
|
|
40
|
-
- - ">="
|
|
41
|
-
- !ruby/object:Gem::Version
|
|
42
|
-
version: '2.2'
|
|
43
|
-
- - "<"
|
|
44
|
-
- !ruby/object:Gem::Version
|
|
45
|
-
version: '4.0'
|
|
46
26
|
- !ruby/object:Gem::Dependency
|
|
47
27
|
name: uri
|
|
48
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -63,26 +43,6 @@ dependencies:
|
|
|
63
43
|
- - ">="
|
|
64
44
|
- !ruby/object:Gem::Version
|
|
65
45
|
version: 1.0.2
|
|
66
|
-
- !ruby/object:Gem::Dependency
|
|
67
|
-
name: logger
|
|
68
|
-
requirement: !ruby/object:Gem::Requirement
|
|
69
|
-
requirements:
|
|
70
|
-
- - "~>"
|
|
71
|
-
- !ruby/object:Gem::Version
|
|
72
|
-
version: '1.7'
|
|
73
|
-
- - ">="
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: 1.7.0
|
|
76
|
-
type: :runtime
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - "~>"
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '1.7'
|
|
83
|
-
- - ">="
|
|
84
|
-
- !ruby/object:Gem::Version
|
|
85
|
-
version: 1.7.0
|
|
86
46
|
- !ruby/object:Gem::Dependency
|
|
87
47
|
name: forwardable
|
|
88
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -156,6 +116,10 @@ files:
|
|
|
156
116
|
- LICENSE.txt
|
|
157
117
|
- README.md
|
|
158
118
|
- Rakefile
|
|
119
|
+
- benchmarks/profile_sign_ed25519.rb
|
|
120
|
+
- benchmarks/profile_verify_ed25519.rb
|
|
121
|
+
- benchmarks/sign.rb
|
|
122
|
+
- benchmarks/verify.rb
|
|
159
123
|
- examples/sinatra/Gemfile
|
|
160
124
|
- examples/sinatra/config.ru
|
|
161
125
|
- examples/sinatra/http-signatures.yml
|
|
@@ -175,6 +139,7 @@ files:
|
|
|
175
139
|
- lib/linzer/http.rb
|
|
176
140
|
- lib/linzer/http/bootstrap.rb
|
|
177
141
|
- lib/linzer/http/signature_feature.rb
|
|
142
|
+
- lib/linzer/http/structured_field.rb
|
|
178
143
|
- lib/linzer/jws.rb
|
|
179
144
|
- lib/linzer/key.rb
|
|
180
145
|
- lib/linzer/key/helper.rb
|
|
@@ -197,6 +162,7 @@ files:
|
|
|
197
162
|
- lib/linzer/message/field/parser.rb
|
|
198
163
|
- lib/linzer/message/wrapper.rb
|
|
199
164
|
- lib/linzer/options.rb
|
|
165
|
+
- lib/linzer/rack.rb
|
|
200
166
|
- lib/linzer/rsa.rb
|
|
201
167
|
- lib/linzer/rsa_pss.rb
|
|
202
168
|
- lib/linzer/signature.rb
|