linzer 0.7.9.beta3 → 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 +31 -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 +36 -9
- data/lib/linzer/message/adapter/faraday/response.rb +11 -9
- data/lib/linzer/message/adapter/generic/request.rb +11 -8
- data/lib/linzer/message/adapter/generic/response.rb +11 -8
- data/lib/linzer/message/adapter/http_gem/common.rb +11 -8
- data/lib/linzer/message/adapter/rack/request.rb +11 -8
- data/lib/linzer/message/adapter/rack/response.rb +11 -8
- 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 -4
- metadata +7 -61
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
|
|
@@ -10,8 +10,8 @@ module Linzer
|
|
|
10
10
|
# implements field retrieval, header access, and signature attachment
|
|
11
11
|
# for a specific HTTP message type.
|
|
12
12
|
#
|
|
13
|
-
# @abstract Subclass and implement {#header}, {#
|
|
14
|
-
#
|
|
13
|
+
# @abstract Subclass and implement {#header}, {#derived}, and {#field}
|
|
14
|
+
# to create a new adapter.
|
|
15
15
|
#
|
|
16
16
|
# @see Rack::Request Rack request adapter
|
|
17
17
|
# @see Rack::Response Rack response adapter
|
|
@@ -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
|
|
@@ -84,13 +84,39 @@ module Linzer
|
|
|
84
84
|
raise Linzer::Error, "Sub-classes are required to implement this method!"
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Checks whether the request contains HTTP Message Signature headers.
|
|
88
|
+
#
|
|
89
|
+
# Returns true if either the "signature-input" or "signature" header
|
|
90
|
+
# is present.
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if the request includes HTTP Message Signature headers
|
|
93
|
+
def has_signature?
|
|
94
|
+
!!header("signature-input") || !!header("signature")
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
# Attaches a signature to the underlying HTTP message.
|
|
88
98
|
#
|
|
89
|
-
# @abstract Subclasses must implement this method.
|
|
90
99
|
# @param signature [Signature] The signature to attach
|
|
91
100
|
# @return [Object] The underlying HTTP message
|
|
92
101
|
def attach!(signature)
|
|
93
|
-
|
|
102
|
+
signature_headers = signature.to_h
|
|
103
|
+
|
|
104
|
+
unless has_signature?
|
|
105
|
+
signature_headers.each { |h, v| set_header!(h, v) }
|
|
106
|
+
return @operation
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
signature_headers.each do |hdr, value|
|
|
110
|
+
merged = Starry.parse_dictionary(String(header(hdr)))
|
|
111
|
+
merged.merge!(Starry.parse_dictionary(value))
|
|
112
|
+
set_header!(hdr, Starry.serialize_dictionary(merged))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@operation
|
|
116
|
+
rescue Starry::ParseError => e
|
|
117
|
+
raise Error,
|
|
118
|
+
"Cannot attach signature, invalid signature header(s)!",
|
|
119
|
+
cause: e
|
|
94
120
|
end
|
|
95
121
|
|
|
96
122
|
private
|
|
@@ -164,10 +190,11 @@ module Linzer
|
|
|
164
190
|
# @param method [Symbol] +:derived+ or +:field+
|
|
165
191
|
# @return [String, Integer, nil] the component value
|
|
166
192
|
def retrieve(name, method)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
171
198
|
|
|
172
199
|
has_req = name.parameters["req"]
|
|
173
200
|
has_sf = name.parameters["sf"] || name.parameters.key?("key")
|
|
@@ -16,15 +16,6 @@ module Linzer
|
|
|
16
16
|
# @see Generic::Response
|
|
17
17
|
# @see https://github.com/lostisland/faraday faraday gem
|
|
18
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
19
|
private
|
|
29
20
|
|
|
30
21
|
# Resolves a derived component value from the response.
|
|
@@ -37,6 +28,17 @@ module Linzer
|
|
|
37
28
|
when "@status" then @operation.status.to_i
|
|
38
29
|
end
|
|
39
30
|
end
|
|
31
|
+
|
|
32
|
+
# Sets a header on the underlying HTTP message.
|
|
33
|
+
#
|
|
34
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
35
|
+
#
|
|
36
|
+
# @param header [String] the header name
|
|
37
|
+
# @param value [String] the header value
|
|
38
|
+
# @return [String] the value assigned to the header
|
|
39
|
+
def set_header!(header, value)
|
|
40
|
+
@operation.headers[header] = value
|
|
41
|
+
end
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
end
|
|
@@ -40,14 +40,6 @@ module Linzer
|
|
|
40
40
|
@operation[name]
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Attaches a signature to the request.
|
|
44
|
-
# @param signature [Signature] The signature to attach
|
|
45
|
-
# @return [Object] The underlying request object
|
|
46
|
-
def attach!(signature)
|
|
47
|
-
signature.to_h.each { |h, v| @operation[h] = v }
|
|
48
|
-
@operation
|
|
49
|
-
end
|
|
50
|
-
|
|
51
43
|
private
|
|
52
44
|
|
|
53
45
|
def derived(name)
|
|
@@ -68,6 +60,17 @@ module Linzer
|
|
|
68
60
|
end
|
|
69
61
|
end
|
|
70
62
|
|
|
63
|
+
# Sets a header on the underlying HTTP message.
|
|
64
|
+
#
|
|
65
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
66
|
+
#
|
|
67
|
+
# @param header [String] the header name
|
|
68
|
+
# @param value [String] the header value
|
|
69
|
+
# @return [String] the value assigned to the header
|
|
70
|
+
def set_header!(header, value)
|
|
71
|
+
@operation[header] = value
|
|
72
|
+
end
|
|
73
|
+
|
|
71
74
|
def query_param(uri_query, name)
|
|
72
75
|
param_name = name.parameters["name"]
|
|
73
76
|
return nil if !param_name
|
|
@@ -31,16 +31,19 @@ module Linzer
|
|
|
31
31
|
@operation[name]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Attaches a signature to the response.
|
|
35
|
-
# @param signature [Signature] The signature to attach
|
|
36
|
-
# @return [Object] The underlying response object
|
|
37
|
-
def attach!(signature)
|
|
38
|
-
signature.to_h.each { |h, v| @operation[h] = v }
|
|
39
|
-
@operation
|
|
40
|
-
end
|
|
41
|
-
|
|
42
34
|
private
|
|
43
35
|
|
|
36
|
+
# Sets a header on the underlying HTTP message.
|
|
37
|
+
#
|
|
38
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
39
|
+
#
|
|
40
|
+
# @param header [String] the header name
|
|
41
|
+
# @param value [String] the header value
|
|
42
|
+
# @return [String] the value assigned to the header
|
|
43
|
+
def set_header!(header, value)
|
|
44
|
+
@operation[header] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
44
47
|
def derived(name)
|
|
45
48
|
raise Linzer::Error, "Sub-classes are required to implement this method!"
|
|
46
49
|
end
|
|
@@ -20,14 +20,6 @@ module Linzer
|
|
|
20
20
|
@operation.headers[name]
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Attaches a signature to the response.
|
|
24
|
-
# @param signature [Signature] The signature to attach
|
|
25
|
-
# @return [Object] The underlying response object
|
|
26
|
-
def attach!(signature)
|
|
27
|
-
signature.to_h.each { |h, v| @operation.headers[h] = v }
|
|
28
|
-
@operation
|
|
29
|
-
end
|
|
30
|
-
|
|
31
23
|
private
|
|
32
24
|
|
|
33
25
|
# Retrieves an HTTP field value from the request or response headers.
|
|
@@ -41,6 +33,17 @@ module Linzer
|
|
|
41
33
|
value = @operation.headers[name.value.to_s]
|
|
42
34
|
value.dup&.strip
|
|
43
35
|
end
|
|
36
|
+
|
|
37
|
+
# Sets a header on the underlying HTTP message.
|
|
38
|
+
#
|
|
39
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
40
|
+
#
|
|
41
|
+
# @param header [String] the header name
|
|
42
|
+
# @param value [String] the header value
|
|
43
|
+
# @return [String] the value assigned to the header
|
|
44
|
+
def set_header!(header, value)
|
|
45
|
+
@operation.headers[header] = value
|
|
46
|
+
end
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
end
|
|
@@ -27,14 +27,17 @@ module Linzer
|
|
|
27
27
|
@operation.get_header(rack_header_name(name))
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Sets a header on the underlying HTTP message.
|
|
33
|
+
#
|
|
34
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
35
|
+
#
|
|
36
|
+
# @param header [String] the header name
|
|
37
|
+
# @param value [String] the header value
|
|
38
|
+
# @return [String] the value assigned to the header
|
|
39
|
+
def set_header!(header, value)
|
|
40
|
+
@operation.set_header(rack_header_name(header), value)
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
end
|
|
@@ -28,14 +28,17 @@ module Linzer
|
|
|
28
28
|
@operation.get_header(name)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Sets a header on the underlying HTTP message.
|
|
34
|
+
#
|
|
35
|
+
# If a header with the given name already exists, its value is overwritten.
|
|
36
|
+
#
|
|
37
|
+
# @param header [String] the header name
|
|
38
|
+
# @param value [String] the header value
|
|
39
|
+
# @return [String] the value assigned to the header
|
|
40
|
+
def set_header!(header, value)
|
|
41
|
+
@operation.set_header(header, value)
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|
|
@@ -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 => _
|