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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91f1e3986fae03eb9bf01d4dc8e9c919cd7072ea3ae0f8168099261da0da8c11
4
- data.tar.gz: fb07600d19e8e2198e7cbcfb910516d9300e3aa89d94e81e3bc3ebd6a9d007e8
3
+ metadata.gz: 0330e72cbbfb7cc56b9fa124a4f60a18bb7fbdab4eb98a072598c8fc3680cf76
4
+ data.tar.gz: 321630c979fc736cc64284eb535b22752e50a42c8bbfae399cbf0db56efecdd3
5
5
  SHA512:
6
- metadata.gz: aed32987b8ea9fc398ef1d263a94ce3df3ec86e8cd331d2e9b23383c0c6de1db3f0bdaa621990eb1809c210b0ae3f38b045abd2e8e48229a9ec8129cf4213c65
7
- data.tar.gz: 26a21c99d8e383af31395f5ebfa5581b0fedf126bb1bb806db3cb1eb0f0d89306dde3af7bf54100cd0251235f54e5cd4f852f8d61007b74fe079716b4447986a
6
+ metadata.gz: e6e99bf6a409b960d0432f702b286f7b71b06a34b61ff242d2d75eebc9c125bc02c6daa573efc5af69dfeb7c2f788811fb91d2f6f6decd1a869db26e6a5462e6
7
+ data.tar.gz: 9c76877de5ba5164f677aad7be9ffb3f905f2fd874523b9f2aaff287abd0ebdd8d8c7272da22eddfcacb56a7eb325392e52e1b2a3b551e8940627b146190a895
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.0.beta1] - 2026-05-07
4
+
5
+ - Optimize signature parsing, serialization, and validation performance
6
+ across signing and verifying flows, improving `sign!` throughput by
7
+ 473% and `verify!` by 136% by minimizing Starry usage in hot paths
8
+ where possible.
9
+
10
+ - Add comprehensive benchmarking and profiling infrastructure across all
11
+ supported algorithms.
12
+
13
+ - Make Rack an optional dependency.
14
+
3
15
  ## [0.7.9] - 2026-04-30
4
16
 
5
17
  (No changes since the last beta release, this new stable release just
data/README.md CHANGED
@@ -45,6 +45,8 @@ sections.
45
45
  Add the middleware to your Rack application:
46
46
 
47
47
  ```ruby
48
+ require "linzer/rack"
49
+
48
50
  # config.ru
49
51
  use Rack::Auth::Signature,
50
52
  except: "/login",
@@ -64,6 +66,8 @@ for all endpoints except /login.
64
66
  For more complex setups, you can load configuration from a file, e.g.:
65
67
 
66
68
  ```ruby
69
+ require "linzer/rack"
70
+
67
71
  # config.ru
68
72
  use Rack::Auth::Signature,
69
73
  except: "/login",
@@ -75,6 +79,8 @@ use Rack::Auth::Signature,
75
79
  In a Rails application, add the middleware in your configuration:
76
80
 
77
81
  ```ruby
82
+ require "linzer/rack"
83
+
78
84
  # config/application.rb
79
85
  config.middleware.use Rack::Auth::Signature,
80
86
  except: "/login",
@@ -255,6 +261,8 @@ For production use, prefer a full-featured HTTP client).
255
261
  You can sign responses using the same API as for requests, e.g.:
256
262
 
257
263
  ```ruby
264
+ require "linzer/rack"
265
+
258
266
  put "/baz" do
259
267
  ...
260
268
  response
data/Rakefile CHANGED
@@ -12,5 +12,11 @@ task :integration do
12
12
  sh "bundle exec rspec -t integration spec/integration/**"
13
13
  end
14
14
 
15
+ desc "Run benchmarks"
16
+ task :benchmarks do
17
+ sh "bundle exec ruby benchmarks/sign.rb"
18
+ sh "bundle exec ruby benchmarks/verify.rb"
19
+ end
20
+
15
21
  task default: %i[spec standard]
16
22
  task all: %i[integration default]
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Profile: Linzer.sign! with Ed25519 keys
5
+ #
6
+ # Uses StackProf (sampling CPU profiler) to identify bottlenecks in the
7
+ # signing hot path. Produces both a summary report and a flamegraph-ready
8
+ # dump file.
9
+ #
10
+ # Run with:
11
+ # ruby benchmarks/profile_sign_ed25519.rb
12
+ #
13
+ # For flamegraph (requires stackprof gem):
14
+ # stackprof --flamegraph tmp/stackprof-sign-ed25519.dump > tmp/flamegraph.json
15
+ # stackprof --flamegraph-viewer tmp/flamegraph.json
16
+
17
+ require "bundler/setup"
18
+ require "linzer"
19
+ require "net/http"
20
+ require "stackprof"
21
+ require "fileutils"
22
+
23
+ ITERATIONS = Integer(ENV.fetch("ITERATIONS", 5_000))
24
+ OUTPUT_DIR = "tmp"
25
+ DUMP_FILE = File.join(OUTPUT_DIR, "stackprof-sign-ed25519.dump")
26
+
27
+ FileUtils.mkdir_p(OUTPUT_DIR)
28
+
29
+ key = Linzer.generate_ed25519_key("bench-ed25519")
30
+ uri = URI("https://example.com/api/resource")
31
+ components = %w[@method @path @authority content-type]
32
+
33
+ puts "StackProf profile \u2014 Linzer.sign! (Ed25519)"
34
+ puts "Ruby #{RUBY_VERSION} / #{RUBY_PLATFORM}"
35
+ puts "Iterations: #{ITERATIONS}"
36
+ puts
37
+
38
+ # Warm up (JIT, autoloads, caches)
39
+ 20.times do
40
+ req = Net::HTTP::Post.new(uri)
41
+ req["content-type"] = "application/json"
42
+ req.body = '{"hello":"world"}'
43
+ Linzer.sign!(req, key: key, components: components)
44
+ end
45
+
46
+ StackProf.run(
47
+ mode: :cpu,
48
+ interval: 100, # sample every 100 \u00b5s
49
+ out: DUMP_FILE,
50
+ raw: true # needed for flamegraph output
51
+ ) do
52
+ ITERATIONS.times do
53
+ request = Net::HTTP::Post.new(uri)
54
+ request["content-type"] = "application/json"
55
+ request.body = '{"hello":"world"}'
56
+
57
+ Linzer.sign!(request, key: key, components: components)
58
+ end
59
+ end
60
+
61
+ puts "=" * 78
62
+ puts "METHOD-LEVEL REPORT (top 30 by self time)"
63
+ puts "=" * 78
64
+ puts
65
+
66
+ report = StackProf::Report.new(Marshal.load(File.binread(DUMP_FILE)))
67
+ report.print_text(false, 30)
68
+
69
+ puts
70
+ puts "=" * 78
71
+ puts "METHOD-LEVEL REPORT (top 30 by total time)"
72
+ puts "=" * 78
73
+ puts
74
+
75
+ report.print_text(true, 30)
76
+
77
+ # Also print source-annotated hotspots for the top methods
78
+ puts
79
+ puts "=" * 78
80
+ puts "SOURCE ANNOTATIONS FOR TOP METHODS"
81
+ puts "=" * 78
82
+
83
+ top_frames = report.data[:frames]
84
+ .sort_by { |_id, f| -(f[:samples] || 0) }
85
+ .first(10)
86
+
87
+ top_frames.each do |_frame_id, frame|
88
+ name = frame[:name]
89
+ file = frame[:file]
90
+ next unless file && File.exist?(file)
91
+ puts
92
+ puts "-" * 78
93
+ puts "#{name} (#{file}:#{frame[:line]})"
94
+ puts " self: #{frame[:samples]} total: #{frame[:total_samples]}"
95
+ puts "-" * 78
96
+ report.print_method(name)
97
+ end
98
+
99
+ puts
100
+ puts "Dump written to: #{DUMP_FILE}"
101
+ puts "To generate a flamegraph:"
102
+ puts " stackprof --flamegraph #{DUMP_FILE} > tmp/flamegraph.json"
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Profile: Linzer.verify! with Ed25519 keys
5
+ #
6
+ # Uses StackProf (sampling CPU profiler) to identify bottlenecks in the
7
+ # verification hot path.
8
+ #
9
+ # Run with:
10
+ # ruby benchmarks/profile_verify_ed25519.rb
11
+
12
+ require "bundler/setup"
13
+ require "linzer"
14
+ require "net/http"
15
+ require "stackprof"
16
+ require "fileutils"
17
+
18
+ ITERATIONS = Integer(ENV.fetch("ITERATIONS", 5_000))
19
+ OUTPUT_DIR = "tmp"
20
+ DUMP_FILE = File.join(OUTPUT_DIR, "stackprof-verify-ed25519.dump")
21
+
22
+ FileUtils.mkdir_p(OUTPUT_DIR)
23
+
24
+ key = Linzer.generate_ed25519_key("bench-ed25519")
25
+ uri = URI("https://example.com/api/resource")
26
+ components = %w[@method @path @authority content-type]
27
+
28
+ # Sign a request once
29
+ request = Net::HTTP::Post.new(uri)
30
+ request["content-type"] = "application/json"
31
+ request.body = '{"hello":"world"}'
32
+ Linzer.sign!(request, key: key, components: components)
33
+
34
+ sig_header = request["signature"]
35
+ input_header = request["signature-input"]
36
+
37
+ puts "StackProf profile \u2014 Linzer.verify! (Ed25519)"
38
+ puts "Ruby #{RUBY_VERSION} / #{RUBY_PLATFORM}"
39
+ puts "Iterations: #{ITERATIONS}"
40
+ puts
41
+
42
+ # Warm up
43
+ 20.times do
44
+ req = Net::HTTP::Post.new(uri)
45
+ req["content-type"] = "application/json"
46
+ req.body = '{"hello":"world"}'
47
+ req["signature"] = sig_header
48
+ req["signature-input"] = input_header
49
+ Linzer.verify!(req, key: key)
50
+ end
51
+
52
+ StackProf.run(
53
+ mode: :cpu,
54
+ interval: 100,
55
+ out: DUMP_FILE,
56
+ raw: true
57
+ ) do
58
+ ITERATIONS.times do
59
+ req = Net::HTTP::Post.new(uri)
60
+ req["content-type"] = "application/json"
61
+ req.body = '{"hello":"world"}'
62
+ req["signature"] = sig_header
63
+ req["signature-input"] = input_header
64
+ Linzer.verify!(req, key: key)
65
+ end
66
+ end
67
+
68
+ puts "=" * 78
69
+ puts "METHOD-LEVEL REPORT (top 30 by self time)"
70
+ puts "=" * 78
71
+ puts
72
+
73
+ report = StackProf::Report.new(Marshal.load(File.binread(DUMP_FILE)))
74
+ report.print_text(false, 30)
75
+
76
+ puts
77
+ puts "=" * 78
78
+ puts "METHOD-LEVEL REPORT (top 30 by total time)"
79
+ puts "=" * 78
80
+ puts
81
+
82
+ report.print_text(true, 30)
83
+
84
+ # Source-annotated hotspots for the top methods
85
+ puts
86
+ puts "=" * 78
87
+ puts "SOURCE ANNOTATIONS FOR TOP METHODS"
88
+ puts "=" * 78
89
+
90
+ top_frames = report.data[:frames]
91
+ .sort_by { |_id, f| -(f[:samples] || 0) }
92
+ .first(10)
93
+
94
+ top_frames.each do |_frame_id, frame|
95
+ name = frame[:name]
96
+ file = frame[:file]
97
+ next unless file && File.exist?(file)
98
+ puts
99
+ puts "-" * 78
100
+ puts "#{name} (#{file}:#{frame[:line]})"
101
+ puts " self: #{frame[:samples]} total: #{frame[:total_samples]}"
102
+ puts "-" * 78
103
+ report.print_method(name)
104
+ end
105
+
106
+ puts
107
+ puts "Dump written to: #{DUMP_FILE}"
@@ -0,0 +1,55 @@
1
+ require "benchmark_driver"
2
+ require "openssl"
3
+
4
+ BENCH_ALGS = %i[
5
+ ed25519
6
+ ecdsa_p256_sha256
7
+ ecdsa_p384_sha384
8
+ rsa_v1_5_sha256
9
+ rsa_pss_sha512
10
+ hmac_sha256
11
+ jws_ed25519
12
+ ]
13
+
14
+ puts "Linzer.sign! benchmark — supported algorithms"
15
+ puts `git show HEAD | head -1 | cut -b 1-15`
16
+ puts "Ruby #{RUBY_VERSION} / #{RUBY_PLATFORM}"
17
+ puts "OpenSSL #{OpenSSL::VERSION}"
18
+ puts Time.now.utc
19
+
20
+ BENCH_ALGS.each do |alg|
21
+ next if alg == :rsa_pss_sha512 && RUBY_VERSION < "3.1.0"
22
+ Benchmark.driver do |x|
23
+ x.prelude <<~RUBY
24
+ require "bundler/setup"
25
+ require "linzer"
26
+ require "linzer/jws"
27
+
28
+ def generate_linzer_key(alg)
29
+ case alg
30
+ when :ed25519, :ecdsa_p256_sha256, :ecdsa_p384_sha384, :hmac_sha256
31
+ Linzer.public_send(:"generate_#{alg}_key", "bench-#{alg}")
32
+ when :rsa_v1_5_sha256, :rsa_pss_sha512
33
+ Linzer.public_send(:"generate_#{alg}_key", 2048, "bench-#{alg}")
34
+ when :jws_ed25519
35
+ Linzer.generate_jws_key(algorithm: "EdDSA")
36
+ else
37
+ raise ArgumentError, "Unknown algorithm!"
38
+ end
39
+ end
40
+
41
+ key = generate_linzer_key(:"#{alg}")
42
+ uri = URI("https://example.com/api/resource")
43
+ components = %w[@method @path @authority content-type]
44
+ RUBY
45
+
46
+ puts "\n#{alg}:\n#{"#" * 70}\n\n"
47
+
48
+ x.report "[#{alg}] sign!", %{
49
+ request = Net::HTTP::Post.new(uri)
50
+ request["content-type"] = "application/json"
51
+ request.body = '{"hello":"world"}'
52
+ Linzer.sign!(request, key: key, components: components)
53
+ }
54
+ end
55
+ end
@@ -0,0 +1,68 @@
1
+ require "benchmark_driver"
2
+ require "openssl"
3
+
4
+ BENCH_ALGS = %i[
5
+ ed25519
6
+ ecdsa_p256_sha256
7
+ ecdsa_p384_sha384
8
+ rsa_v1_5_sha256
9
+ rsa_pss_sha512
10
+ hmac_sha256
11
+ jws_ed25519
12
+ ]
13
+
14
+ puts "Linzer.verify! benchmark — supported algorithms"
15
+ puts `git show HEAD | head -1 | cut -b 1-15`
16
+ puts "Ruby #{RUBY_VERSION} / #{RUBY_PLATFORM}"
17
+ puts "OpenSSL #{OpenSSL::VERSION}"
18
+ puts Time.now.utc
19
+
20
+ BENCH_ALGS.each do |alg|
21
+ next if alg == :rsa_pss_sha512 && RUBY_VERSION < "3.1.0"
22
+ Benchmark.driver do |x|
23
+ x.prelude <<~RUBY
24
+ require "bundler/setup"
25
+ require "linzer"
26
+ require "linzer/jws"
27
+
28
+ def generate_linzer_key(alg)
29
+ case alg
30
+ when :ed25519, :ecdsa_p256_sha256, :ecdsa_p384_sha384, :hmac_sha256
31
+ Linzer.public_send(:"generate_#{alg}_key", "bench-#{alg}")
32
+ when :rsa_v1_5_sha256, :rsa_pss_sha512
33
+ Linzer.public_send(:"generate_#{alg}_key", 2048, "bench-#{alg}")
34
+ when :jws_ed25519
35
+ Linzer.generate_jws_key(algorithm: "EdDSA")
36
+ else
37
+ raise ArgumentError, "Unknown algorithm!"
38
+ end
39
+ end
40
+
41
+ key = generate_linzer_key(:"#{alg}")
42
+ uri = URI("https://example.com/api/resource")
43
+ components = %w[@method @path @authority content-type]
44
+
45
+ request = Net::HTTP::Post.new(uri)
46
+ request["content-type"] = "application/json"
47
+ request.body = '{"hello":"world"}'
48
+
49
+ Linzer.sign!(request, key: key, components: components)
50
+
51
+ # Capture the signed headers so we can rebuild identical requests
52
+ sig_header = request["signature"]
53
+ input_header = request["signature-input"]
54
+ RUBY
55
+
56
+ puts "\n#{alg}:\n#{"#" * 70}\n\n"
57
+
58
+ x.report "[#{alg}] verify!", %{
59
+ req = Net::HTTP::Post.new(uri)
60
+ req["content-type"] = "application/json"
61
+ req.body = '{"hello":"world"}'
62
+ req["signature"] = sig_header
63
+ req["signature-input"] = input_header
64
+
65
+ Linzer.verify!(req, key: key)
66
+ }
67
+ end
68
+ end
data/lib/linzer/common.rb CHANGED
@@ -26,15 +26,25 @@ module Linzer
26
26
  # # "@path": /foo
27
27
  # # "content-type": application/json
28
28
  # # "@signature-params": ("@method" "@path" "content-type");created=1618884473
29
- def signature_base(message, serialized_components, parameters)
30
- signature_base =
31
- serialized_components.each_with_object(+"") do |component, base|
32
- base << "%s\n" % signature_base_line(component, message)
29
+ def signature_base(message, serialized_components, parameters, field_ids: nil)
30
+ buf = +""
31
+
32
+ if field_ids
33
+ i = 0
34
+ len = serialized_components.size
35
+ while i < len
36
+ buf << serialized_components[i] << ": " << String(message[field_ids[i]]) << "\n"
37
+ i += 1
38
+ end
39
+ else
40
+ serialized_components.each do |component|
41
+ buf << signature_base_line(component, message) << "\n"
33
42
  end
43
+ end
34
44
 
35
- signature_base << signature_params_line(serialized_components, parameters)
45
+ buf << signature_params_line(serialized_components, parameters)
36
46
 
37
- signature_base
47
+ buf
38
48
  end
39
49
  module_function :signature_base
40
50
 
@@ -57,13 +67,14 @@ module Linzer
57
67
  # @param serialized_components [Array<String>] The covered components
58
68
  # @param parameters [Hash] Signature parameters
59
69
  # @return [String] The formatted @signature-params line
60
- def signature_params_line(serialized_components, parameters)
61
- identifiers = serialized_components.map { |c| Starry.parse_item(c) }
70
+ SERIALIZED_SIGNATURE_PARAMS = Starry.serialize("@signature-params").freeze
71
+ private_constant :SERIALIZED_SIGNATURE_PARAMS
62
72
 
63
- signature_params =
64
- Starry.serialize([Starry::InnerList.new(identifiers, parameters)])
73
+ def signature_params_line(serialized_components, parameters)
74
+ params_str = HTTP::StructuredField.serialize_parameters(parameters)
75
+ components_str = serialized_components.join(" ")
65
76
 
66
- "%s: %s" % [Starry.serialize("@signature-params"), signature_params]
77
+ "#{SERIALIZED_SIGNATURE_PARAMS}: (#{components_str})#{params_str}"
67
78
  end
68
79
  module_function :signature_params_line
69
80
 
@@ -76,18 +87,30 @@ module Linzer
76
87
  # @raise [Error] If @signature-params is in the components
77
88
  # @raise [Error] If any component is missing from the message
78
89
  # @raise [Error] If any component is duplicated
79
- def validate_components(message, components)
80
- if components.include?('"@signature-params"') ||
81
- components.any? { |c| c.start_with?('"@signature-params"') }
82
- raise Error.new "Invalid component in signature input"
83
- end
90
+ def validate_components(message, components, field_ids: nil)
91
+ has_params = false
92
+ missing = "Cannot verify signature. Missing component in message"
93
+ invalid = "Invalid component in signature input"
84
94
 
85
- msg = "Cannot verify signature. Missing component in message: %s"
86
- components.each do |c|
87
- raise Error.new msg % "\"#{c}\"" unless message.field?(c)
95
+ if field_ids
96
+ i = 0
97
+ len = components.size
98
+ while i < len
99
+ c = components[i]
100
+ raise Error, invalid if c.include?("@signature-params")
101
+ has_params = true if !has_params && c.include?(";")
102
+ raise Error, "#{missing}: \"#{c}\"" unless message.field?(field_ids[i])
103
+ i += 1
104
+ end
105
+ else
106
+ components.each do |c|
107
+ raise Error, invalid if c.include?("@signature-params")
108
+ has_params = true if !has_params && c.include?(";")
109
+ raise Error, "#{missing}: \"#{c}\"" unless message.field?(c)
110
+ end
88
111
  end
89
112
 
90
- validate_uniqueness components
113
+ validate_uniqueness(components) if has_params || components.size != components.uniq.size
91
114
  end
92
115
 
93
116
  # Validates that there are no duplicate components.
@@ -98,7 +121,14 @@ module Linzer
98
121
  # @param components [Array<String>] Component identifiers to check
99
122
  # @raise [Error] If any component appears more than once
100
123
  def validate_uniqueness(components)
101
- msg = "Invalid signature. Duplicated component in signature input."
124
+ duplicated = "Invalid signature. Duplicated component in signature input."
125
+
126
+ # String-level duplicates are always invalid
127
+ raise Error, duplicated if components.size != components.uniq.size
128
+
129
+ # If any component has parameters, also check for semantic duplicates
130
+ # (e.g. ;bs;req vs ;req;bs are semantically equal but different strings)
131
+ return unless components.any? { |c| c.include?(";") }
102
132
 
103
133
  uniq_components =
104
134
  components
@@ -110,7 +140,7 @@ module Linzer
110
140
  .uniq { |comp| [comp.value, comp.parameters] }
111
141
  end
112
142
 
113
- raise Error.new msg if components.count != uniq_components.count
143
+ raise Error, duplicated if components.count != uniq_components.count
114
144
  end
115
145
  end
116
146
  end
@@ -46,13 +46,15 @@ module Linzer
46
46
  material.verify(nil, signature, data)
47
47
  end
48
48
 
49
+ private
50
+
49
51
  # @return [Boolean] true if this key contains public key material
50
- def public?
52
+ def compute_public?
51
53
  has_pem_public?
52
54
  end
53
55
 
54
56
  # @return [Boolean] true if this key contains private key material
55
- def private?
57
+ def compute_private?
56
58
  has_pem_private?
57
59
  end
58
60
  end
data/lib/linzer/hmac.rb CHANGED
@@ -54,18 +54,6 @@ module Linzer
54
54
  OpenSSL.secure_compare(signature, sign(data))
55
55
  end
56
56
 
57
- # HMAC keys can always sign (they contain the secret).
58
- # @return [Boolean] true if key material is present
59
- def private?
60
- !material.nil?
61
- end
62
-
63
- # HMAC keys are symmetric, not public/private.
64
- # @return [Boolean] always false for HMAC keys
65
- def public?
66
- false
67
- end
68
-
69
57
  # Returns a safe string representation that doesn't leak the secret.
70
58
  #
71
59
  # The key material is intentionally excluded from the output to prevent
@@ -82,6 +70,20 @@ module Linzer
82
70
  oid = Digest::SHA2.hexdigest(object_id.to_s)[48..63]
83
71
  "#<%s:0x%s %s>" % [self.class, oid, vars.join(", ")]
84
72
  end
73
+
74
+ private
75
+
76
+ # HMAC keys can always sign (they contain the secret).
77
+ # @return [Boolean] true if key material is present
78
+ def compute_private?
79
+ !material.nil?
80
+ end
81
+
82
+ # HMAC keys are symmetric, not public/private.
83
+ # @return [Boolean] always false for HMAC keys
84
+ def compute_public?
85
+ false
86
+ end
85
87
  end
86
88
  end
87
89
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module HTTP
5
+ # Utilities for serializing HTTP Structured Fields as defined in RFC 8941.
6
+ #
7
+ # This module currently provides helpers for serializing HTTP Message
8
+ # Signature parameters as used by RFC 9421.
9
+ #
10
+ # @see https://www.rfc-editor.org/rfc/rfc8941 RFC 8941
11
+ # @see https://www.rfc-editor.org/rfc/rfc9421 RFC 9421
12
+ module StructuredField
13
+ # Serializes signature parameters to the RFC 8941 string format.
14
+ #
15
+ # Integers are bare, strings are double-quoted. This covers all
16
+ # parameter types used in RFC 9421 signatures (created, expires,
17
+ # keyid, nonce, alg, tag).
18
+ #
19
+ # @example Serialize signature parameters
20
+ # StructuredField.serialize_parameters(
21
+ # created: 1700000000,
22
+ # keyid: "my-key"
23
+ # )
24
+ # # => ';created=1700000000;keyid="my-key"'
25
+ #
26
+ # @param parameters [Hash{Symbol,String => Object}]
27
+ # The parameters to serialize.
28
+ #
29
+ # @return [String]
30
+ # The serialized structured field parameter string.
31
+ #
32
+ def self.serialize_parameters(parameters)
33
+ params_str = +""
34
+ parameters.each do |key, value|
35
+ params_str << case value
36
+ when Integer
37
+ ";#{key}=#{value}"
38
+ when String
39
+ ";#{key}=\"#{value}\""
40
+ else
41
+ ";#{key}=#{value}"
42
+ end
43
+ end
44
+ params_str
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/linzer/jws.rb CHANGED
@@ -101,18 +101,18 @@ module Linzer
101
101
  algo.verify(data: data, signature: signature, verification_key: verify_key)
102
102
  end
103
103
 
104
+ private
105
+
104
106
  # @return [Boolean] true if this key can verify signatures
105
- def public?
107
+ def compute_public?
106
108
  !!verify_key
107
109
  end
108
110
 
109
111
  # @return [Boolean] true if this key can create signatures
110
- def private?
112
+ def compute_private?
111
113
  !!signing_key
112
114
  end
113
115
 
114
- private
115
-
116
116
  # Resolves the appropriate JWT algorithm implementation.
117
117
  # @return [JWT::JWA::SigningAlgorithm] The algorithm implementation
118
118
  # @raise [Error] If the algorithm cannot be determined