linzer 0.7.1 → 0.7.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d495888382dc17a4fae60d05b853dd0cbb0036119ae725f960471baa6ff0d4b
4
- data.tar.gz: a6860cf3dcfb586e1591b0b683c883bbf8a42de941f57ec6a2ba6f8574a5a2dd
3
+ metadata.gz: 13262f7f33737c2ad3aec4007e612001dc9d72c97f9a0636681f0ae695d98315
4
+ data.tar.gz: c9663a699455f8cab8cc2214c0af3da835a4e46542f6029dcddece40ad5b9a35
5
5
  SHA512:
6
- metadata.gz: c4d53b9d1102eaa3fc0241938afa86fd194a9ae780233bdabd171190ab9b83958193f5bc28b5bd78fb11a234c3a25c3145ee87e6ac6116f544cb04e73c67a5bd
7
- data.tar.gz: 40e9c1e6bf52d944cd1a82b67502a41ae2939852e723143f1b154bab0b984fb9db6d944ecdf56b6845b6419096f4cbcef01ac545b9b9df73fe8ccc6f0489ace9
6
+ metadata.gz: f4e7386cf5bb74bd20d2c74bd3fd8054c068ccc1b8704a088296ead0f61c5029cccd51744d974ef1123166a30068790590cafe6c4e3f27c55c67e0637a0073bb
7
+ data.tar.gz: 798032dd1a2c345f51fd23281bd9028a1ff31495f41e8d4c241d653d1857233e1c9c3bb327539c2296a399d0b5df4bbf02b9954e252ba486a022c80f79066785
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.3] - 2025-06-01
4
+
5
+ - Fix broken retrieval of header names from Rack responses.
6
+ Previously, this caused signatures attached to Rack response instances
7
+ to use incorrect header names, making them unverifiable.
8
+
9
+ - Add Linzer.signature_base method.
10
+
11
+ - Add initial support for JWS algorithms. See Linzer::JWS module for more details.
12
+ In this initial preview, only EdDSA algorithm (Ed25519) is supported).
13
+
14
+ - Add a simple integration test to verify signatures on HTTP responses.
15
+
16
+ ## [0.7.2] - 2025-05-21
17
+
18
+ - Add a few integration tests against CloudFlare test server.
19
+
20
+ - Fix bug when accessing headers in http adapter classes.
21
+ Pull request [#14](https://github.com/nomadium/linzer/pull/14)
22
+ by [oneiros](https://github.com/oneiros).
23
+
3
24
  ## [0.7.1] - 2025-05-18
4
25
 
5
26
  - Introduce specific exception classes for message signing errors
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/Rakefile CHANGED
@@ -7,4 +7,10 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "standard/rake"
9
9
 
10
+ desc "Run RSpec integration examples"
11
+ task :integration do
12
+ sh "bundle exec rspec -t integration spec/integration/**"
13
+ end
14
+
10
15
  task default: %i[spec standard]
16
+ task all: %i[integration default]
@@ -6,4 +6,4 @@ gem "sinatra", "~> 4.0"
6
6
  gem "rackup", "~> 2.1"
7
7
  gem "webrick", "~> 1.9"
8
8
  gem "rack-contrib", "~> 2.4"
9
- gem "linzer", "~> 0.7.0.beta2"
9
+ gem "linzer", "~> 0.7.2"
data/lib/linzer/common.rb CHANGED
@@ -13,6 +13,9 @@ module Linzer
13
13
  signature_base << "\"@signature-params\": #{signature_params}"
14
14
  signature_base
15
15
  end
16
+ module_function :signature_base
17
+
18
+ private
16
19
 
17
20
  def validate_components(message, components)
18
21
  if components.include?("@signature-params")
@@ -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/http.rb CHANGED
@@ -65,6 +65,7 @@ module Linzer
65
65
  end
66
66
 
67
67
  def build_headers(headers)
68
+ return headers if headers.transform_keys(&:downcase).key?("user-agent")
68
69
  headers.merge({"user-agent" => "Linzer/#{Linzer::VERSION}"})
69
70
  end
70
71
 
data/lib/linzer/jws.rb ADDED
@@ -0,0 +1,60 @@
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
+ raise Linzer::SigningError, "Private key is missing!" if !material.private?
31
+ algo = resolve_algorithm
32
+ algo.sign(data: data, signing_key: signing_key)
33
+ end
34
+
35
+ def verify(signature, data)
36
+ algo = resolve_algorithm
37
+ algo.verify(data: data, signature: signature, verification_key: verify_key)
38
+ end
39
+
40
+ private
41
+
42
+ def resolve_algorithm
43
+ case
44
+ when material.verify_key.is_a?(::Ed25519::VerifyKey)
45
+ JWT::JWA.resolve("EdDSA")
46
+ else
47
+ raise Linzer::Error, "Unknown/unsupported algorithm"
48
+ end
49
+ end
50
+
51
+ def verify_key
52
+ material.verify_key
53
+ end
54
+
55
+ def signing_key
56
+ material.signing_key
57
+ end
58
+ end
59
+ end
60
+ end
@@ -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
@@ -37,7 +37,7 @@ module Linzer
37
37
  end
38
38
  end
39
39
 
40
- def headers
40
+ def header(name)
41
41
  raise Linzer::Error, "Sub-classes are required to implement this method!"
42
42
  end
43
43
 
@@ -5,8 +5,8 @@ module Linzer
5
5
  module Adapter
6
6
  module HTTPGem
7
7
  class Request < Linzer::Message::Adapter::NetHTTP::Request
8
- def headers
9
- @operation.headers.to_h
8
+ def header(name)
9
+ @operation[name]
10
10
  end
11
11
 
12
12
  private
@@ -14,8 +14,8 @@ module Linzer
14
14
  freeze
15
15
  end
16
16
 
17
- def headers
18
- @operation.headers
17
+ def header(name)
18
+ @operation[name]
19
19
  end
20
20
 
21
21
  # XXX: this implementation is incomplete, e.g.: ;tr parameter is not supported yet
@@ -12,8 +12,8 @@ module Linzer
12
12
  freeze
13
13
  end
14
14
 
15
- def headers
16
- @operation.each_header.to_h
15
+ def header(name)
16
+ @operation[name]
17
17
  end
18
18
 
19
19
  def attach!(signature)
@@ -13,8 +13,8 @@ module Linzer
13
13
  freeze
14
14
  end
15
15
 
16
- def headers
17
- @operation.each_header.to_h
16
+ def header(name)
17
+ @operation[name]
18
18
  end
19
19
 
20
20
  # XXX: this implementation is incomplete, e.g.: ;tr parameter is not supported yet
@@ -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.new, err_msg, cause: ex
34
+ raise Linzer::Error, err_msg, cause: ex
35
+ # :nocov:
33
36
  end
34
37
 
35
38
  def rack_header_name(field_name)
@@ -44,19 +47,6 @@ 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
 
@@ -74,11 +64,9 @@ module Linzer
74
64
  if has_tr
75
65
  value = tr(name)
76
66
  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?
67
+ rack_header_name = rack_header_name(name.value.to_s)
68
+ value = @operation.env[rack_header_name] if request?
69
+ value = @operation.get_header(name.value.to_s) if response?
82
70
  end
83
71
  value.dup&.strip
84
72
  end
@@ -13,8 +13,8 @@ module Linzer
13
13
  freeze
14
14
  end
15
15
 
16
- def headers
17
- rack_request_headers(@operation)
16
+ def header(name)
17
+ @operation.get_header(rack_header_name(name))
18
18
  end
19
19
 
20
20
  def attach!(signature)
@@ -16,12 +16,14 @@ module Linzer
16
16
  freeze
17
17
  end
18
18
 
19
- def headers
20
- @operation.headers
19
+ def header(name)
20
+ @operation.get_header(name)
21
21
  end
22
22
 
23
23
  def attach!(signature)
24
- signature.to_h.each { |h, v| @operation[h] = v }
24
+ signature.to_h.each do |h, v|
25
+ @operation.set_header(h, v)
26
+ end
25
27
  @operation
26
28
  end
27
29
  end
@@ -15,7 +15,7 @@ module Linzer
15
15
  def_delegators :@adapter, :request?, :response?, :attached_request?
16
16
 
17
17
  # fields look up
18
- def_delegators :@adapter, :headers, :field?, :[]
18
+ def_delegators :@adapter, :header, :field?, :[]
19
19
 
20
20
  # to attach a signature to the underlying HTTP message
21
21
  def_delegators :@adapter, :attach!
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.3"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -9,6 +9,7 @@ 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"
@@ -35,6 +36,7 @@ module Linzer
35
36
 
36
37
  class << self
37
38
  include Key::Helper
39
+ include Helper
38
40
 
39
41
  def verify(pubkey, message, signature, no_older_than: nil)
40
42
  Linzer::Verifier.verify(pubkey, message, signature, no_older_than: no_older_than)
@@ -44,26 +46,8 @@ module Linzer
44
46
  Linzer::Signer.sign(key, message, components, options)
45
47
  end
46
48
 
47
- def sign!(request_or_response, **args)
48
- message = Message.new(request_or_response)
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 = Signature.build(message.headers.slice("signature", "signature-input"))
63
- keyid = signature.parameters["keyid"]
64
- raise Linzer::Error, "key not found" if !key && !keyid
65
- verify_key = block_given? ? (yield keyid) : key
66
- Linzer.verify(verify_key, message, signature, no_older_than: no_older_than)
49
+ def signature_base(message, components, parameters)
50
+ Linzer::Common.signature_base(message, components, parameters)
67
51
  end
68
52
  end
69
53
  end
@@ -67,7 +67,12 @@ module Rack
67
67
  signature_opts[:label] = label if label
68
68
 
69
69
  @message = Linzer::Message.new(request)
70
- signature = Linzer::Signature.build(@message.headers, **signature_opts)
70
+ signature_headers = {}
71
+ %w[signature-input signature].each do |name|
72
+ value = @message.header(name)
73
+ signature_headers[name] = value if value
74
+ end
75
+ signature = Linzer::Signature.build(signature_headers, **signature_opts)
71
76
  request.env["rack.signature"] = signature
72
77
  signature
73
78
  end
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.1
4
+ version: 0.7.3
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