linzer 0.7.9 → 0.8.0.beta2
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/.standard.yml +1 -0
- data/CHANGELOG.md +19 -0
- data/README.md +63 -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/faraday/http_signature/middleware.rb +25 -4
- data/lib/linzer/common.rb +53 -23
- data/lib/linzer/ed25519.rb +4 -2
- data/lib/linzer/helper.rb +34 -15
- data/lib/linzer/hmac.rb +14 -12
- data/lib/linzer/http/signature_feature.rb +15 -4
- data/lib/linzer/http/structured_field.rb +145 -0
- data/lib/linzer/http.rb +13 -7
- data/lib/linzer/jws.rb +4 -4
- data/lib/linzer/key.rb +20 -2
- data/lib/linzer/message/adapter/abstract.rb +36 -22
- data/lib/linzer/message/adapter/generic/request.rb +1 -0
- data/lib/linzer/message/adapter.rb +0 -3
- data/lib/linzer/message/field/parser.rb +5 -5
- data/lib/linzer/message/field.rb +55 -3
- data/lib/linzer/message/overlay.rb +143 -0
- data/lib/linzer/message/wrapper.rb +0 -2
- data/lib/linzer/message.rb +18 -0
- data/lib/linzer/rack.rb +24 -0
- data/lib/linzer/rsa_pss.rb +4 -4
- data/lib/linzer/signature/context.rb +80 -0
- data/lib/linzer/signature/profile/base.rb +43 -0
- data/lib/linzer/signature/profile/example.rb +39 -0
- data/lib/linzer/signature/profile/web_bot_auth.rb +201 -0
- data/lib/linzer/signature/profile.rb +70 -0
- data/lib/linzer/signature.rb +147 -32
- 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 +5 -4
- metadata +13 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a917fda2b4d4646e642ba3a52d11bd41a500b5bfed044142a6e0434e0ef1c94
|
|
4
|
+
data.tar.gz: 8d9296ba21595cba696532a41b20f123569d3b31de0730ba6e7e6f693e87e009
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f6b61e4a340594891976d77b651527b8e0d49ff9ab11d73c56f01b5baecc1ad3fdf8ad23d987652496929ef30495cb116ba04a53f1b7ef2d881e8c1c1cfa1c9
|
|
7
|
+
data.tar.gz: fb4800f527d14823e9ed76b776056fe20a60f35fc3e08c30ce15c05758648476f8489c2d8bb89851fe2c77eeff56891b96ec955663bf08b7b6326cdf4425c881
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0.beta2] - 2026-05-20
|
|
4
|
+
|
|
5
|
+
- Add Web Bot Auth support, implementing the current IETF draft
|
|
6
|
+
(draft-meunier-web-bot-auth-architecture-05).
|
|
7
|
+
Includes recommended signature parameter defaults, nonce generation,
|
|
8
|
+
and optional Signature-Agent header handling.
|
|
9
|
+
|
|
10
|
+
## [0.8.0.beta1] - 2026-05-07
|
|
11
|
+
|
|
12
|
+
- Optimize signature parsing, serialization, and validation performance
|
|
13
|
+
across signing and verifying flows, improving `sign!` throughput by
|
|
14
|
+
473% and `verify!` by 136% by minimizing Starry usage in hot paths
|
|
15
|
+
where possible.
|
|
16
|
+
|
|
17
|
+
- Add comprehensive benchmarking and profiling infrastructure across all
|
|
18
|
+
supported algorithms.
|
|
19
|
+
|
|
20
|
+
- Make Rack an optional dependency.
|
|
21
|
+
|
|
3
22
|
## [0.7.9] - 2026-04-30
|
|
4
23
|
|
|
5
24
|
(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
|
|
@@ -482,6 +490,61 @@ anything that to responds to `#to_i`, including an `ActiveSupport::Duration`.
|
|
|
482
490
|
If the signature is older than the allowed window, verification
|
|
483
491
|
fails with an error.
|
|
484
492
|
|
|
493
|
+
## Web Bot Auth
|
|
494
|
+
|
|
495
|
+
Linzer supports the Web Bot Auth authentication mechanism, which allows
|
|
496
|
+
automated clients to identify themselves using HTTP Message Signatures
|
|
497
|
+
(as defined in RFC 9421).
|
|
498
|
+
|
|
499
|
+
This is useful for distinguishing legitimate automated traffic from
|
|
500
|
+
anonymous or potentially abusive requests.
|
|
501
|
+
|
|
502
|
+
For more details on Web Bot Auth, refer to the
|
|
503
|
+
[relevant IETF drafts](https://datatracker.ietf.org/wg/webbotauth/documents/)
|
|
504
|
+
or to additional resources such as
|
|
505
|
+
[this Cloudflare article](https://blog.cloudflare.com/web-bot-auth/).
|
|
506
|
+
|
|
507
|
+
When enabled, as shown in the examples below, Linzer adds the required
|
|
508
|
+
signature headers to identify the client as an automated agent:
|
|
509
|
+
|
|
510
|
+
- Plain Linzer:
|
|
511
|
+
|
|
512
|
+
```ruby
|
|
513
|
+
|
|
514
|
+
Linzer.sign!(
|
|
515
|
+
request,
|
|
516
|
+
key: key,
|
|
517
|
+
label: "sig1",
|
|
518
|
+
profile: :web_bot_auth
|
|
519
|
+
# or override/set specific parameters like the following:
|
|
520
|
+
# profile: Linzer::Signing::Profile.web_bot_auth(agent: "https://...")
|
|
521
|
+
)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
- http.rb gem:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
require "linzer/http/signature_feature"
|
|
528
|
+
|
|
529
|
+
response = HTTP.headers(date: Time.now.utc.httpdate, foo: "bar")
|
|
530
|
+
.use(http_signature: {key: key, profile: :web_bot_auth}
|
|
531
|
+
.get(url)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
- Faraday:
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
require "linzer/faraday"
|
|
538
|
+
|
|
539
|
+
conn = Faraday.new(url: api_url) do |builder|
|
|
540
|
+
builder.request :http_signature, key: signing_key,
|
|
541
|
+
components: components,
|
|
542
|
+
profile: :web_bot_auth,
|
|
543
|
+
params: signature_params
|
|
544
|
+
end
|
|
545
|
+
response = conn.post("/task")
|
|
546
|
+
```
|
|
547
|
+
|
|
485
548
|
## Supported algorithms
|
|
486
549
|
|
|
487
550
|
Linzer currently supports the following signature algorithms:
|
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}"
|
data/benchmarks/sign.rb
ADDED
|
@@ -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
|
|
@@ -107,7 +107,16 @@ module Faraday
|
|
|
107
107
|
# @return [Boolean] when +true+ (default), raises
|
|
108
108
|
# {VerifyError} on verification failure; when +false+,
|
|
109
109
|
# sets +env[:http_signature_verified]+ to +false+ and continues
|
|
110
|
-
|
|
110
|
+
# @!attribute [rw] profile
|
|
111
|
+
# Optional HTTP Message Signatures signing profile.
|
|
112
|
+
#
|
|
113
|
+
# When set, the profile is passed to {Linzer.sign!} and may provide
|
|
114
|
+
# default covered components and signature parameters.
|
|
115
|
+
#
|
|
116
|
+
# @return [Symbol, Linzer::Signature::Profile::Base, nil]
|
|
117
|
+
# a registered profile name, a profile instance, or +nil+ to use
|
|
118
|
+
# the default signing behavior
|
|
119
|
+
class Options < Faraday::Options.new(:key, :sign_request, :sign_key, :components, :verify_response, :verify_key, :params, :strict, :profile)
|
|
111
120
|
# Returns the generic key.
|
|
112
121
|
# @return [Linzer::Key, nil]
|
|
113
122
|
def key
|
|
@@ -146,6 +155,13 @@ module Faraday
|
|
|
146
155
|
def params
|
|
147
156
|
Hash(self[:params])
|
|
148
157
|
end
|
|
158
|
+
|
|
159
|
+
# Returns the signing profile configuration.
|
|
160
|
+
#
|
|
161
|
+
# @return [Symbol, Linzer::Signature::Profile::Base, nil]
|
|
162
|
+
def profile
|
|
163
|
+
self[:profile]
|
|
164
|
+
end
|
|
149
165
|
end
|
|
150
166
|
|
|
151
167
|
# Creates a new middleware instance.
|
|
@@ -186,10 +202,15 @@ module Faraday
|
|
|
186
202
|
|
|
187
203
|
key = resolve_signing_key
|
|
188
204
|
request = Linzer::Faraday::Utils.create_request(env)
|
|
189
|
-
message = Linzer::Message.new(request)
|
|
190
205
|
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
Linzer.sign! request,
|
|
207
|
+
key: key,
|
|
208
|
+
components: options.components,
|
|
209
|
+
params: options.params,
|
|
210
|
+
profile: options.profile
|
|
211
|
+
|
|
212
|
+
signature_headers = request.headers.slice("signature", "signature-input")
|
|
213
|
+
env.request_headers.merge!(signature_headers)
|
|
193
214
|
env
|
|
194
215
|
rescue Linzer::Error => e
|
|
195
216
|
raise SigningError, e if options.strict?
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
45
|
+
buf << signature_params_line(serialized_components, parameters)
|
|
36
46
|
|
|
37
|
-
|
|
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
|
-
|
|
61
|
-
|
|
70
|
+
SERIALIZED_SIGNATURE_PARAMS = HTTP::StructuredField.serialize("@signature-params").freeze
|
|
71
|
+
private_constant :SERIALIZED_SIGNATURE_PARAMS
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
@@ -106,11 +136,11 @@ module Linzer
|
|
|
106
136
|
.flat_map
|
|
107
137
|
.with_index do |group, idx|
|
|
108
138
|
group
|
|
109
|
-
.map { |comp|
|
|
139
|
+
.map { |comp| HTTP::StructuredField.parse_item(idx.zero? ? comp[1..] : comp) }
|
|
110
140
|
.uniq { |comp| [comp.value, comp.parameters] }
|
|
111
141
|
end
|
|
112
142
|
|
|
113
|
-
raise Error
|
|
143
|
+
raise Error, duplicated if components.count != uniq_components.count
|
|
114
144
|
end
|
|
115
145
|
end
|
|
116
146
|
end
|
data/lib/linzer/ed25519.rb
CHANGED
|
@@ -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
|
|
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
|
|
57
|
+
def compute_private?
|
|
56
58
|
has_pem_private?
|
|
57
59
|
end
|
|
58
60
|
end
|