linzer 0.7.2 → 0.7.4
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 +24 -0
- data/README.md +4 -0
- data/examples/sinatra/Gemfile +1 -1
- data/lib/linzer/common.rb +27 -8
- data/lib/linzer/ecdsa.rb +2 -0
- data/lib/linzer/ed25519.rb +10 -0
- data/lib/linzer/helper.rb +32 -0
- data/lib/linzer/hmac.rb +8 -0
- data/lib/linzer/jws.rb +65 -0
- data/lib/linzer/key/helper.rb +8 -0
- data/lib/linzer/key.rb +29 -3
- data/lib/linzer/message/adapter/abstract.rb +16 -21
- data/lib/linzer/message/adapter/http_gem/request.rb +1 -1
- data/lib/linzer/message/adapter/net_http/request.rb +8 -8
- data/lib/linzer/message/adapter/net_http/response.rb +16 -6
- data/lib/linzer/message/adapter/rack/common.rb +15 -25
- data/lib/linzer/message/adapter/rack/response.rb +2 -2
- data/lib/linzer/message/field/parser.rb +50 -0
- data/lib/linzer/message/field.rb +56 -0
- data/lib/linzer/rsa.rb +4 -5
- data/lib/linzer/rsa_pss.rb +12 -5
- data/lib/linzer/signature.rb +13 -6
- data/lib/linzer/signer.rb +9 -5
- data/lib/linzer/verifier.rb +3 -3
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +10 -26
- data/lib/rack/auth/signature/helpers.rb +8 -1
- data/lib/rack/auth/signature.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ca1f4d456be7ceedda5bbe7cfd205e696689da336408324c6ec06ed10440d07
|
4
|
+
data.tar.gz: 1db3b82c5644c65b6037cb844bea963aa803dd4bf74c93673aac8473dc647d38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce767d2e1c8df88b72321e8fbb513a9dadadb6436a4cefe6caa1a873f82cd4332b4f676ca0f355170156175077af2f675dabb22fbcb16ec7984950ca826a307c
|
7
|
+
data.tar.gz: affedd0f107f8ae2e9073ceb007896e5de9b45d2b50086bec7ff9dc6e771d208cffa7edb1e12ff414771a13eecc2597f2559b0d9c68a1a2ddb1010732afa2663
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,29 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.7.4] - 2025-06-30
|
4
|
+
|
5
|
+
- Add validation routines on key instances before performing
|
6
|
+
signing and verifying operations. This will catch obvious errors
|
7
|
+
like trying to sign a request with a public key.
|
8
|
+
|
9
|
+
- Fix bug with incorrect signature generation and verification on
|
10
|
+
messages with HTTP field identifiers including parameters. Those
|
11
|
+
fields were not serialized correctly and messages were being signed
|
12
|
+
with incorrect signature base.
|
13
|
+
|
14
|
+
## [0.7.3] - 2025-06-01
|
15
|
+
|
16
|
+
- Fix broken retrieval of header names from Rack responses.
|
17
|
+
Previously, this caused signatures attached to Rack response instances
|
18
|
+
to use incorrect header names, making them unverifiable.
|
19
|
+
|
20
|
+
- Add Linzer.signature_base method.
|
21
|
+
|
22
|
+
- Add initial support for JWS algorithms. See Linzer::JWS module for more details.
|
23
|
+
In this initial preview, only EdDSA algorithm (Ed25519) is supported).
|
24
|
+
|
25
|
+
- Add a simple integration test to verify signatures on HTTP responses.
|
26
|
+
|
3
27
|
## [0.7.2] - 2025-05-21
|
4
28
|
|
5
29
|
- Add a few integration tests against CloudFlare test server.
|
data/README.md
CHANGED
@@ -356,6 +356,10 @@ in subsequent releases.
|
|
356
356
|
|
357
357
|
linzer is built in [Continuous Integration](https://github.com/nomadium/linzer/actions/workflows/main.yml) on Ruby 3.0+.
|
358
358
|
|
359
|
+
## Security
|
360
|
+
|
361
|
+
This gem is provided “as is” without any warranties. It has not been audited for security vulnerabilities. Users are advised to review the code and assess its suitability for their use case, particularly in production environments.
|
362
|
+
|
359
363
|
## Development
|
360
364
|
|
361
365
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/examples/sinatra/Gemfile
CHANGED
data/lib/linzer/common.rb
CHANGED
@@ -2,20 +2,39 @@
|
|
2
2
|
|
3
3
|
module Linzer
|
4
4
|
module Common
|
5
|
-
def signature_base(message,
|
6
|
-
signature_base =
|
7
|
-
|
8
|
-
|
5
|
+
def signature_base(message, serialized_components, parameters)
|
6
|
+
signature_base =
|
7
|
+
serialized_components.each_with_object(+"") do |component, base|
|
8
|
+
base << "%s\n" % signature_base_line(component, message)
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
Starry.serialize([Starry::InnerList.new(components, parameters)])
|
11
|
+
signature_base << signature_params_line(serialized_components, parameters)
|
12
12
|
|
13
|
-
signature_base << "\"@signature-params\": #{signature_params}"
|
14
13
|
signature_base
|
15
14
|
end
|
15
|
+
module_function :signature_base
|
16
|
+
|
17
|
+
def signature_base_line(component, message)
|
18
|
+
field_id = FieldId.new(field_name: component)
|
19
|
+
"%s: %s" % [field_id.serialize, message[field_id]]
|
20
|
+
end
|
21
|
+
module_function :signature_base_line
|
22
|
+
|
23
|
+
def signature_params_line(serialized_components, parameters)
|
24
|
+
identifiers = serialized_components.map { |c| Starry.parse_item(c) }
|
25
|
+
|
26
|
+
signature_params =
|
27
|
+
Starry.serialize([Starry::InnerList.new(identifiers, parameters)])
|
28
|
+
|
29
|
+
"%s: %s" % [Starry.serialize("@signature-params"), signature_params]
|
30
|
+
end
|
31
|
+
module_function :signature_params_line
|
32
|
+
|
33
|
+
private
|
16
34
|
|
17
35
|
def validate_components(message, components)
|
18
|
-
if components.include?("@signature-params")
|
36
|
+
if components.include?('"@signature-params"') ||
|
37
|
+
components.any? { |c| c.start_with?('"@signature-params"') }
|
19
38
|
raise Error.new "Invalid component in signature input"
|
20
39
|
end
|
21
40
|
|
data/lib/linzer/ecdsa.rb
CHANGED
@@ -9,10 +9,12 @@ module Linzer
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def sign(data)
|
12
|
+
validate_signing_key
|
12
13
|
decode_der_signature(material.sign(@params[:digest], data))
|
13
14
|
end
|
14
15
|
|
15
16
|
def verify(signature, data)
|
17
|
+
validate_verify_key
|
16
18
|
material.verify(@params[:digest], der_signature(signature), data)
|
17
19
|
end
|
18
20
|
|
data/lib/linzer/ed25519.rb
CHANGED
@@ -4,12 +4,22 @@ module Linzer
|
|
4
4
|
module Ed25519
|
5
5
|
class Key < Linzer::Key
|
6
6
|
def sign(data)
|
7
|
+
validate_signing_key
|
7
8
|
material.sign(nil, data)
|
8
9
|
end
|
9
10
|
|
10
11
|
def verify(signature, data)
|
12
|
+
validate_verify_key
|
11
13
|
material.verify(nil, signature, data)
|
12
14
|
end
|
15
|
+
|
16
|
+
def public?
|
17
|
+
has_pem_public?
|
18
|
+
end
|
19
|
+
|
20
|
+
def private?
|
21
|
+
has_pem_private?
|
22
|
+
end
|
13
23
|
end
|
14
24
|
end
|
15
25
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
module Helper
|
5
|
+
def sign!(request_or_response, **args)
|
6
|
+
message = Message.new(request_or_response)
|
7
|
+
options = {}
|
8
|
+
|
9
|
+
label = args[:label]
|
10
|
+
options[:label] = label if label
|
11
|
+
options.merge!(args.fetch(:params, {}))
|
12
|
+
|
13
|
+
key = args.fetch(:key)
|
14
|
+
signature = Linzer::Signer.sign(key, message, args.fetch(:components), options)
|
15
|
+
message.attach!(signature)
|
16
|
+
end
|
17
|
+
|
18
|
+
def verify!(request_or_response, key: nil, no_older_than: 900)
|
19
|
+
message = Message.new(request_or_response)
|
20
|
+
signature_headers = {}
|
21
|
+
%w[signature-input signature].each do |name|
|
22
|
+
value = message.header(name)
|
23
|
+
signature_headers[name] = value if value
|
24
|
+
end
|
25
|
+
signature = Signature.build(signature_headers)
|
26
|
+
keyid = signature.parameters["keyid"]
|
27
|
+
raise Linzer::Error, "key not found" if !key && !keyid
|
28
|
+
verify_key = block_given? ? (yield keyid) : key
|
29
|
+
Linzer.verify(verify_key, message, signature, no_older_than: no_older_than)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/linzer/hmac.rb
CHANGED
data/lib/linzer/jws.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jwt"
|
4
|
+
require "jwt/eddsa"
|
5
|
+
require "ed25519"
|
6
|
+
|
7
|
+
module Linzer
|
8
|
+
module JWS
|
9
|
+
def jwk_import(key, params = {})
|
10
|
+
material = JWT::JWK.import(key)
|
11
|
+
Linzer::JWS::Key.new(material, params)
|
12
|
+
end
|
13
|
+
module_function :jwk_import
|
14
|
+
|
15
|
+
def generate_key(algorithm:)
|
16
|
+
case String(algorithm)
|
17
|
+
when "EdDSA"
|
18
|
+
ed25519_keypair = ::Ed25519::SigningKey.generate
|
19
|
+
material = JWT::JWK.new(ed25519_keypair)
|
20
|
+
Linzer::JWS::Key.new(material)
|
21
|
+
else
|
22
|
+
err_msg = "Algorithm '#{algorithm}' is unsupported or not implemented yet."
|
23
|
+
raise Linzer::Error, err_msg
|
24
|
+
end
|
25
|
+
end
|
26
|
+
module_function :generate_key
|
27
|
+
|
28
|
+
class Key < Linzer::Key
|
29
|
+
def sign(data)
|
30
|
+
validate_signing_key
|
31
|
+
algo = resolve_algorithm
|
32
|
+
algo.sign(data: data, signing_key: signing_key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def verify(signature, data)
|
36
|
+
validate_verify_key
|
37
|
+
algo = resolve_algorithm
|
38
|
+
algo.verify(data: data, signature: signature, verification_key: verify_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def public?
|
42
|
+
!!verify_key
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def resolve_algorithm
|
48
|
+
case
|
49
|
+
when material.verify_key.is_a?(::Ed25519::VerifyKey)
|
50
|
+
JWT::JWA.resolve("EdDSA")
|
51
|
+
else
|
52
|
+
raise Linzer::Error, "Unknown/unsupported algorithm"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def verify_key
|
57
|
+
material.verify_key
|
58
|
+
end
|
59
|
+
|
60
|
+
def signing_key
|
61
|
+
material.signing_key
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/linzer/key/helper.rb
CHANGED
@@ -85,6 +85,14 @@ module Linzer
|
|
85
85
|
key = OpenSSL::PKey::EC.new(material)
|
86
86
|
Linzer::ECDSA::Key.new(key, id: key_id, digest: "SHA384")
|
87
87
|
end
|
88
|
+
|
89
|
+
def generate_jws_key(algorithm:)
|
90
|
+
Linzer::JWS.generate_key(algorithm: algorithm)
|
91
|
+
end
|
92
|
+
|
93
|
+
def jwk_import(key, params = {})
|
94
|
+
Linzer::JWS.jwk_import(key, params)
|
95
|
+
end
|
88
96
|
end
|
89
97
|
end
|
90
98
|
end
|
data/lib/linzer/key.rb
CHANGED
@@ -17,12 +17,20 @@ module Linzer
|
|
17
17
|
|
18
18
|
def sign(*args)
|
19
19
|
abstract_error = "Cannot sign data, \"#{self.class}\" is an abstract class."
|
20
|
-
raise Error
|
20
|
+
raise Error, abstract_error
|
21
21
|
end
|
22
22
|
|
23
23
|
def verify(*args)
|
24
24
|
abstract_error = "Cannot verify signature, \"#{self.class}\" is an abstract class."
|
25
|
-
raise Error
|
25
|
+
raise Error, abstract_error
|
26
|
+
end
|
27
|
+
|
28
|
+
def public?
|
29
|
+
material.public?
|
30
|
+
end
|
31
|
+
|
32
|
+
def private?
|
33
|
+
material.private?
|
26
34
|
end
|
27
35
|
|
28
36
|
private
|
@@ -34,7 +42,25 @@ module Linzer
|
|
34
42
|
def validate_digest
|
35
43
|
no_digest = !@params.key?(:digest) || @params[:digest].nil? || String(@params[:digest]).empty?
|
36
44
|
no_digest_error = "Invalid key definition, no digest algorithm was selected."
|
37
|
-
raise
|
45
|
+
raise Error, no_digest_error if no_digest
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_signing_key
|
49
|
+
raise SigningError, "Private key is needed!" unless private?
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_verify_key
|
53
|
+
raise VerifyError, "Public key is needed!" unless public?
|
54
|
+
end
|
55
|
+
|
56
|
+
def has_pem_public?
|
57
|
+
material.public_to_pem.match?(/^-----BEGIN PUBLIC KEY-----/)
|
58
|
+
end
|
59
|
+
|
60
|
+
def has_pem_private?
|
61
|
+
material.private_to_pem.match?(/^-----BEGIN PRIVATE KEY-----/)
|
62
|
+
rescue
|
63
|
+
false
|
38
64
|
end
|
39
65
|
end
|
40
66
|
end
|
@@ -26,15 +26,10 @@ module Linzer
|
|
26
26
|
!!self[f]
|
27
27
|
end
|
28
28
|
|
29
|
-
def [](
|
30
|
-
|
31
|
-
return nil if
|
32
|
-
|
33
|
-
if field_name.start_with?("@")
|
34
|
-
retrieve(name, :derived)
|
35
|
-
else
|
36
|
-
retrieve(name, :field)
|
37
|
-
end
|
29
|
+
def [](field)
|
30
|
+
field_id = field.is_a?(FieldId) ? field : parse_field_name(field)
|
31
|
+
return nil if field_id.nil? || field_id.item.nil?
|
32
|
+
retrieve(field_id.item, field_id.derived? ? :derived : :field)
|
38
33
|
end
|
39
34
|
|
40
35
|
def header(name)
|
@@ -48,13 +43,16 @@ module Linzer
|
|
48
43
|
private
|
49
44
|
|
50
45
|
def parse_field_name(field_name)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
46
|
+
field_id = FieldId.new(field_name: field_name)
|
47
|
+
component = field_id.item
|
48
|
+
|
49
|
+
return nil if component.nil?
|
50
|
+
|
51
|
+
# 2.2.9
|
52
|
+
invalid = "@status component identifier is invalid in a request message"
|
53
|
+
raise Error, invalid if request? && component.value == "@status"
|
54
|
+
|
55
|
+
field_id
|
58
56
|
end
|
59
57
|
|
60
58
|
def validate_attached_request(message)
|
@@ -73,7 +71,7 @@ module Linzer
|
|
73
71
|
value = name.value
|
74
72
|
|
75
73
|
# Section 2.2.8 of RFC 9421
|
76
|
-
return nil if has_name && value !=
|
74
|
+
return nil if has_name && value != "@query-param"
|
77
75
|
|
78
76
|
# No derived values come from trailers section
|
79
77
|
return nil if method == :derived && name.parameters["tr"]
|
@@ -141,10 +139,7 @@ module Linzer
|
|
141
139
|
end
|
142
140
|
|
143
141
|
def req(field, method)
|
144
|
-
|
145
|
-
when :derived then @attached_request["@#{field}"]
|
146
|
-
when :field then @attached_request[field.to_s]
|
147
|
-
end
|
142
|
+
attached_request? ? @attached_request[String(field)] : nil
|
148
143
|
end
|
149
144
|
end
|
150
145
|
end
|
@@ -25,14 +25,14 @@ module Linzer
|
|
25
25
|
|
26
26
|
def derived(name)
|
27
27
|
case name.value
|
28
|
-
when
|
29
|
-
when
|
30
|
-
when
|
31
|
-
when
|
32
|
-
when
|
33
|
-
when
|
34
|
-
when
|
35
|
-
when
|
28
|
+
when "@method" then @operation.method
|
29
|
+
when "@target-uri" then @operation.uri.to_s
|
30
|
+
when "@authority" then @operation.uri.authority.downcase
|
31
|
+
when "@scheme" then @operation.uri.scheme.downcase
|
32
|
+
when "@request-target" then @operation.uri.request_uri
|
33
|
+
when "@path" then @operation.uri.path
|
34
|
+
when "@query" then "?%s" % String(@operation.uri.query)
|
35
|
+
when "@query-param" then query_param(name)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -17,16 +17,26 @@ module Linzer
|
|
17
17
|
@operation[name]
|
18
18
|
end
|
19
19
|
|
20
|
-
# XXX: this implementation is incomplete, e.g.: ;tr parameter is not supported yet
|
21
|
-
def [](field_name)
|
22
|
-
return @operation.code.to_i if field_name == "@status"
|
23
|
-
@operation[field_name]
|
24
|
-
end
|
25
|
-
|
26
20
|
def attach!(signature)
|
27
21
|
signature.to_h.each { |h, v| @operation[h] = v }
|
28
22
|
@operation
|
29
23
|
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def derived(name)
|
28
|
+
case name.value
|
29
|
+
when "@status" then @operation.code.to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# XXX: this implementation is incomplete, e.g.: ;bs parameter is not supported yet
|
34
|
+
def field(name)
|
35
|
+
has_tr = name.parameters["tr"]
|
36
|
+
return nil if has_tr # Net::HTTP doesn't support trailers
|
37
|
+
value = @operation[name.value.to_s]
|
38
|
+
value.dup&.strip
|
39
|
+
end
|
30
40
|
end
|
31
41
|
end
|
32
42
|
end
|
@@ -6,14 +6,14 @@ module Linzer
|
|
6
6
|
module Rack
|
7
7
|
module Common
|
8
8
|
DERIVED_COMPONENT = {
|
9
|
-
method:
|
10
|
-
authority:
|
11
|
-
path:
|
12
|
-
status:
|
13
|
-
"target-uri"
|
14
|
-
scheme:
|
15
|
-
"request-target"
|
16
|
-
query:
|
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
17
|
}.freeze
|
18
18
|
private_constant :DERIVED_COMPONENT
|
19
19
|
|
@@ -28,8 +28,11 @@ module Linzer
|
|
28
28
|
raise ArgumentError.new, "Blank header name." if name.empty?
|
29
29
|
name.to_str
|
30
30
|
rescue => ex
|
31
|
+
# :nocov:
|
32
|
+
# XXX: this block of code seems to be unreachable
|
31
33
|
err_msg = "Invalid header name: '#{name}'"
|
32
|
-
raise Linzer::Error
|
34
|
+
raise Linzer::Error, err_msg, cause: ex
|
35
|
+
# :nocov:
|
33
36
|
end
|
34
37
|
|
35
38
|
def rack_header_name(field_name)
|
@@ -44,25 +47,12 @@ module Linzer
|
|
44
47
|
end
|
45
48
|
end
|
46
49
|
|
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
50
|
def derived(name)
|
61
51
|
method = DERIVED_COMPONENT[name.value]
|
62
52
|
|
63
53
|
value = case name.value
|
64
|
-
when
|
65
|
-
when
|
54
|
+
when "@query" then derive(@operation, method)
|
55
|
+
when "@query-param" then query_param(name)
|
66
56
|
end
|
67
57
|
|
68
58
|
return nil if !method && !value
|
@@ -76,7 +66,7 @@ module Linzer
|
|
76
66
|
else
|
77
67
|
rack_header_name = rack_header_name(name.value.to_s)
|
78
68
|
value = @operation.env[rack_header_name] if request?
|
79
|
-
value = @operation.get_header(
|
69
|
+
value = @operation.get_header(name.value.to_s) if response?
|
80
70
|
end
|
81
71
|
value.dup&.strip
|
82
72
|
end
|
@@ -17,12 +17,12 @@ module Linzer
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def header(name)
|
20
|
-
@operation.get_header(
|
20
|
+
@operation.get_header(name)
|
21
21
|
end
|
22
22
|
|
23
23
|
def attach!(signature)
|
24
24
|
signature.to_h.each do |h, v|
|
25
|
-
@operation.set_header(
|
25
|
+
@operation.set_header(h, v)
|
26
26
|
end
|
27
27
|
@operation
|
28
28
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
class Field
|
6
|
+
class Identifier
|
7
|
+
module Parser
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def parse(field_name)
|
11
|
+
case
|
12
|
+
when field_name.match?(/";/), field_name.start_with?('"')
|
13
|
+
Starry.parse_item(field_name)
|
14
|
+
when field_name.match?(/;/)
|
15
|
+
parse_unserialized_input(field_name)
|
16
|
+
when field_name.start_with?("@"), field_name.match?(/^[a-z]/)
|
17
|
+
Starry.parse_item(Starry.serialize(field_name))
|
18
|
+
else
|
19
|
+
raise Error, "Invalid component identifier: '#{field_name}'!"
|
20
|
+
end
|
21
|
+
rescue Starry::ParseError => ex
|
22
|
+
parse_error = "Failed to parse component identifier: '#{field_name}'!"
|
23
|
+
raise Error, parse_error, cause: ex
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def parse_unserialized_input(field_name)
|
29
|
+
field, *raw_params = field_name.split(";")
|
30
|
+
item = Starry.parse_item(Starry.serialize(field))
|
31
|
+
item.parameters = collect_parameters(raw_params)
|
32
|
+
item
|
33
|
+
end
|
34
|
+
|
35
|
+
def collect_parameters(str)
|
36
|
+
params = str.map do |param|
|
37
|
+
if (tokens = param.split("=")) == [param] # e.g.: ";bs"
|
38
|
+
{param => true}
|
39
|
+
else
|
40
|
+
Hash[*tokens.first(2)] # e.g.: ";key=\"foo\""
|
41
|
+
.transform_values! { |v| Starry.parse_item(v).value }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
params.reduce({}, :merge)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
class Message
|
5
|
+
class Field
|
6
|
+
module IdentifierMethods
|
7
|
+
def initialize(field_name:)
|
8
|
+
@item = Identifier::Parser.parse(field_name) rescue nil
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :item
|
13
|
+
|
14
|
+
def derived?
|
15
|
+
item&.value&.start_with?("@")
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
raise Error, "Invalid component identifier: '#{field_name}'!" unless item
|
20
|
+
Starry.serialize(@item)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Excluded from coverage as obviously both branches cannot be covered
|
25
|
+
# on a single test run.
|
26
|
+
# :nocov:
|
27
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2.0")
|
28
|
+
class Identifier < Struct.new(:field_name, keyword_init: true); end
|
29
|
+
else
|
30
|
+
class Identifier < Data.define(:field_name); end
|
31
|
+
end
|
32
|
+
# :nocov:
|
33
|
+
|
34
|
+
Identifier.include Message::Field::IdentifierMethods
|
35
|
+
|
36
|
+
class Identifier
|
37
|
+
class << self
|
38
|
+
def serialize(component)
|
39
|
+
new(field_name: component).serialize
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize_components(components)
|
43
|
+
components.map(&method(:serialize))
|
44
|
+
end
|
45
|
+
|
46
|
+
def deserialize_components(components)
|
47
|
+
components.map do |c|
|
48
|
+
item = Starry.parse_item(c)
|
49
|
+
item.parameters.empty? ? item.value : Starry.serialize(item)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/linzer/rsa.rb
CHANGED
@@ -9,14 +9,13 @@ module Linzer
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def sign(data)
|
12
|
-
|
13
|
-
|
12
|
+
validate_signing_key
|
13
|
+
material.sign(@params[:digest], data)
|
14
14
|
end
|
15
15
|
|
16
16
|
def verify(signature, data)
|
17
|
-
|
18
|
-
|
19
|
-
false
|
17
|
+
validate_verify_key
|
18
|
+
material.verify(@params[:digest], signature, data)
|
20
19
|
end
|
21
20
|
end
|
22
21
|
end
|
data/lib/linzer/rsa_pss.rb
CHANGED
@@ -11,19 +11,26 @@ module Linzer
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def sign(data)
|
14
|
-
|
15
|
-
|
14
|
+
validate_signing_key
|
15
|
+
material.sign(@params[:digest], data, signature_options)
|
16
16
|
end
|
17
17
|
|
18
18
|
def verify(signature, data)
|
19
|
-
|
20
|
-
|
19
|
+
validate_verify_key
|
20
|
+
material.verify(
|
21
21
|
@params[:digest],
|
22
22
|
signature,
|
23
23
|
data,
|
24
24
|
signature_options
|
25
25
|
)
|
26
|
-
|
26
|
+
end
|
27
|
+
|
28
|
+
def public?
|
29
|
+
has_pem_public?
|
30
|
+
end
|
31
|
+
|
32
|
+
def private?
|
33
|
+
has_pem_private?
|
27
34
|
end
|
28
35
|
|
29
36
|
private
|
data/lib/linzer/signature.rb
CHANGED
@@ -11,9 +11,13 @@ module Linzer
|
|
11
11
|
end
|
12
12
|
|
13
13
|
attr_reader :metadata, :value, :parameters, :label
|
14
|
-
alias_method :
|
14
|
+
alias_method :serialized_components, :metadata
|
15
15
|
alias_method :bytes, :value
|
16
16
|
|
17
|
+
def components
|
18
|
+
FieldId.deserialize_components(serialized_components)
|
19
|
+
end
|
20
|
+
|
17
21
|
def created
|
18
22
|
Integer(parameters["created"])
|
19
23
|
rescue
|
@@ -28,10 +32,13 @@ module Linzer
|
|
28
32
|
|
29
33
|
def to_h
|
30
34
|
{
|
31
|
-
"signature"
|
32
|
-
"signature-input" =>
|
33
|
-
Starry.
|
34
|
-
Starry
|
35
|
+
"signature" => Starry.serialize({label => value}),
|
36
|
+
"signature-input" => Starry.serialize({
|
37
|
+
label => Starry::InnerList.new(
|
38
|
+
serialized_components.map { |c| Starry.parse_item(c) },
|
39
|
+
parameters
|
40
|
+
)
|
41
|
+
})
|
35
42
|
}
|
36
43
|
end
|
37
44
|
|
@@ -56,7 +63,7 @@ module Linzer
|
|
56
63
|
|
57
64
|
fail_due_invalid_components unless input[label].value.respond_to?(:each)
|
58
65
|
|
59
|
-
components = input[label].value.map(
|
66
|
+
components = input[label].value.map { |c| Starry.serialize_item(c) }
|
60
67
|
parameters = input[label].parameters
|
61
68
|
|
62
69
|
new(components, raw_signature, label, parameters)
|
data/lib/linzer/signer.rb
CHANGED
@@ -8,15 +8,16 @@ module Linzer
|
|
8
8
|
include Common
|
9
9
|
|
10
10
|
def sign(key, message, components, options = {})
|
11
|
-
|
11
|
+
serialized_components = FieldId.serialize_components(Array(components))
|
12
|
+
validate key, message, serialized_components
|
12
13
|
|
13
14
|
parameters = populate_parameters(key, options)
|
14
|
-
signature_base = signature_base(message,
|
15
|
+
signature_base = signature_base(message, serialized_components, parameters)
|
15
16
|
|
16
17
|
signature = key.sign(signature_base)
|
17
18
|
label = options[:label] || DEFAULT_LABEL
|
18
19
|
|
19
|
-
Linzer::Signature.build(serialize(signature,
|
20
|
+
Linzer::Signature.build(serialize(signature, serialized_components, parameters, label))
|
20
21
|
end
|
21
22
|
|
22
23
|
def default_label
|
@@ -55,8 +56,11 @@ module Linzer
|
|
55
56
|
{
|
56
57
|
"signature" => Starry.serialize({label => signature}),
|
57
58
|
"signature-input" =>
|
58
|
-
Starry.serialize(
|
59
|
-
Starry::InnerList.new(
|
59
|
+
Starry.serialize(label =>
|
60
|
+
Starry::InnerList.new(
|
61
|
+
components.map { |c| Starry.parse_item(c) },
|
62
|
+
parameters
|
63
|
+
))
|
60
64
|
}
|
61
65
|
end
|
62
66
|
end
|
data/lib/linzer/verifier.rb
CHANGED
@@ -9,9 +9,9 @@ module Linzer
|
|
9
9
|
validate message, key, signature, no_older_than: no_older_than
|
10
10
|
|
11
11
|
parameters = signature.parameters
|
12
|
-
|
12
|
+
serialized_components = signature.serialized_components
|
13
13
|
|
14
|
-
signature_base = signature_base(message,
|
14
|
+
signature_base = signature_base(message, serialized_components, parameters)
|
15
15
|
|
16
16
|
verify_or_fail key, signature.value, signature_base
|
17
17
|
end
|
@@ -31,7 +31,7 @@ module Linzer
|
|
31
31
|
raise VerifyError, "Components cannot be null" if signature.components.nil?
|
32
32
|
|
33
33
|
begin
|
34
|
-
validate_components message, signature.
|
34
|
+
validate_components message, signature.serialized_components
|
35
35
|
rescue Error => ex
|
36
36
|
raise VerifyError, ex.message, cause: ex
|
37
37
|
end
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
@@ -9,10 +9,13 @@ require "net/http"
|
|
9
9
|
|
10
10
|
require_relative "linzer/version"
|
11
11
|
require_relative "linzer/common"
|
12
|
+
require_relative "linzer/helper"
|
12
13
|
require_relative "linzer/options"
|
13
14
|
require_relative "linzer/message"
|
14
15
|
require_relative "linzer/message/adapter"
|
15
16
|
require_relative "linzer/message/wrapper"
|
17
|
+
require_relative "linzer/message/field"
|
18
|
+
require_relative "linzer/message/field/parser"
|
16
19
|
require_relative "linzer/signature"
|
17
20
|
require_relative "linzer/key"
|
18
21
|
require_relative "linzer/rsa"
|
@@ -24,7 +27,6 @@ require_relative "linzer/key/helper"
|
|
24
27
|
require_relative "linzer/signer"
|
25
28
|
require_relative "linzer/verifier"
|
26
29
|
require_relative "linzer/http"
|
27
|
-
require_relative "rack/auth/signature"
|
28
30
|
|
29
31
|
module Linzer
|
30
32
|
class Error < StandardError; end
|
@@ -35,6 +37,7 @@ module Linzer
|
|
35
37
|
|
36
38
|
class << self
|
37
39
|
include Key::Helper
|
40
|
+
include Helper
|
38
41
|
|
39
42
|
def verify(pubkey, message, signature, no_older_than: nil)
|
40
43
|
Linzer::Verifier.verify(pubkey, message, signature, no_older_than: no_older_than)
|
@@ -44,31 +47,12 @@ module Linzer
|
|
44
47
|
Linzer::Signer.sign(key, message, components, options)
|
45
48
|
end
|
46
49
|
|
47
|
-
def
|
48
|
-
message
|
49
|
-
options = {}
|
50
|
-
|
51
|
-
label = args[:label]
|
52
|
-
options[:label] = label if label
|
53
|
-
options.merge!(args.fetch(:params, {}))
|
54
|
-
|
55
|
-
key = args.fetch(:key)
|
56
|
-
signature = Linzer::Signer.sign(key, message, args.fetch(:components), options)
|
57
|
-
message.attach!(signature)
|
58
|
-
end
|
59
|
-
|
60
|
-
def verify!(request_or_response, key: nil, no_older_than: 900)
|
61
|
-
message = Message.new(request_or_response)
|
62
|
-
signature_headers = {}
|
63
|
-
%w[signature-input signature].each do |name|
|
64
|
-
value = message.header(name)
|
65
|
-
signature_headers[name] = value if value
|
66
|
-
end
|
67
|
-
signature = Signature.build(signature_headers)
|
68
|
-
keyid = signature.parameters["keyid"]
|
69
|
-
raise Linzer::Error, "key not found" if !key && !keyid
|
70
|
-
verify_key = block_given? ? (yield keyid) : key
|
71
|
-
Linzer.verify(verify_key, message, signature, no_older_than: no_older_than)
|
50
|
+
def signature_base(message, components, parameters)
|
51
|
+
Linzer::Common.signature_base(message, components, parameters)
|
72
52
|
end
|
73
53
|
end
|
54
|
+
|
55
|
+
FieldId = Message::Field::Identifier
|
74
56
|
end
|
57
|
+
|
58
|
+
require_relative "rack/auth/signature"
|
@@ -36,6 +36,11 @@ module Rack
|
|
36
36
|
end
|
37
37
|
|
38
38
|
module Configuration
|
39
|
+
def default_covered_components
|
40
|
+
Linzer::Options::DEFAULT[:covered_components]
|
41
|
+
end
|
42
|
+
module_function :default_covered_components
|
43
|
+
|
39
44
|
DEFAULT_OPTIONS = {
|
40
45
|
signatures: {
|
41
46
|
reject_older_than: 900,
|
@@ -45,7 +50,9 @@ module Rack
|
|
45
50
|
tag_required: false,
|
46
51
|
expires_required: false,
|
47
52
|
keyid_required: false,
|
48
|
-
covered_components:
|
53
|
+
covered_components:
|
54
|
+
Linzer::FieldId
|
55
|
+
.serialize_components(default_covered_components),
|
49
56
|
error_response: {body: [], status: 401, headers: {}}
|
50
57
|
},
|
51
58
|
keys: {}
|
data/lib/rack/auth/signature.rb
CHANGED
@@ -85,7 +85,7 @@ module Rack
|
|
85
85
|
end
|
86
86
|
|
87
87
|
def has_required_components?
|
88
|
-
components = @signature.
|
88
|
+
components = @signature.serialized_components || []
|
89
89
|
covered_components = options[:signatures][:covered_components]
|
90
90
|
warning = "Insufficient coverage by signature. Consult S 7.2.1. in RFC"
|
91
91
|
logger.warn warning if covered_components.empty?
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: linzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miguel Landaeta
|
@@ -192,10 +192,12 @@ files:
|
|
192
192
|
- lib/linzer/common.rb
|
193
193
|
- lib/linzer/ecdsa.rb
|
194
194
|
- lib/linzer/ed25519.rb
|
195
|
+
- lib/linzer/helper.rb
|
195
196
|
- lib/linzer/hmac.rb
|
196
197
|
- lib/linzer/http.rb
|
197
198
|
- lib/linzer/http/bootstrap.rb
|
198
199
|
- lib/linzer/http/signature_feature.rb
|
200
|
+
- lib/linzer/jws.rb
|
199
201
|
- lib/linzer/key.rb
|
200
202
|
- lib/linzer/key/helper.rb
|
201
203
|
- lib/linzer/message.rb
|
@@ -208,6 +210,8 @@ files:
|
|
208
210
|
- lib/linzer/message/adapter/rack/common.rb
|
209
211
|
- lib/linzer/message/adapter/rack/request.rb
|
210
212
|
- lib/linzer/message/adapter/rack/response.rb
|
213
|
+
- lib/linzer/message/field.rb
|
214
|
+
- lib/linzer/message/field/parser.rb
|
211
215
|
- lib/linzer/message/wrapper.rb
|
212
216
|
- lib/linzer/options.rb
|
213
217
|
- lib/linzer/rsa.rb
|
@@ -239,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
239
243
|
- !ruby/object:Gem::Version
|
240
244
|
version: '0'
|
241
245
|
requirements: []
|
242
|
-
rubygems_version: 3.6.
|
246
|
+
rubygems_version: 3.6.8
|
243
247
|
specification_version: 4
|
244
248
|
summary: An implementation of HTTP Messages Signatures (RFC9421)
|
245
249
|
test_files: []
|