linzer 0.7.9.beta2 → 0.7.9.beta3
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 +6 -0
- data/README.md +96 -12
- data/lib/faraday/http_signature/middleware.rb +296 -0
- data/lib/faraday/http_signature.rb +36 -0
- data/lib/linzer/faraday/utils.rb +29 -0
- data/lib/linzer/faraday.rb +29 -0
- data/lib/linzer/http.rb +43 -1
- data/lib/linzer/message/adapter/abstract.rb +35 -5
- data/lib/linzer/message/adapter/faraday/request.rb +63 -0
- data/lib/linzer/message/adapter/faraday/response.rb +44 -0
- data/lib/linzer/message/adapter/generic/request.rb +16 -12
- data/lib/linzer/message/adapter/http_gem/common.rb +5 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +8 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +7 -0
- data/lib/linzer/message/adapter/net_http/request.rb +8 -0
- data/lib/linzer/message/adapter/net_http/response.rb +7 -0
- data/lib/linzer/message/adapter/rack/common.rb +37 -0
- data/lib/linzer/message/field/parser.rb +15 -0
- data/lib/linzer/message/wrapper.rb +12 -2
- data/lib/linzer/signature.rb +20 -0
- data/lib/linzer/verifier.rb +8 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/rack/auth/signature/helpers.rb +72 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d39f9d5f600453197afb744fc030afc2325f40d580540f9fbbca2ba81e0275e2
|
|
4
|
+
data.tar.gz: 0ab05a81ade047eb116e0527c8647bce22008a04684b6dae5bdd37a9441b0fa8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52adbd0af86067b700fd60e2666675134bf58a8ed611b60087ecd3c74da64b50f83232271e4a8a4c2af6162558fd880598fb15af6cd201f9621ff4cd976bbdc5
|
|
7
|
+
data.tar.gz: b351287249fb050bb06bc08a5312b24bf9876dc555fd7c2ee69edbab15e0b43bec5d0cbc3ab852c1766c2eede159a1bd8c6c2ba963558e308b2ec7e73ad469d5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.9.beta3] - 2026-04-27
|
|
4
|
+
|
|
5
|
+
- Enforce `expires` signature parameter validation in Verifier.
|
|
6
|
+
|
|
7
|
+
- Add Faraday middleware and adapters for HTTP message signatures.
|
|
8
|
+
|
|
3
9
|
## [0.7.9.beta2] - 2026-04-19
|
|
4
10
|
|
|
5
11
|
- Add support for http gem 6.x while maintaining compatibility with 5.x.
|
data/README.md
CHANGED
|
@@ -30,6 +30,18 @@ Or just `gem install linzer`.
|
|
|
30
30
|
|
|
31
31
|
### Quick start
|
|
32
32
|
|
|
33
|
+
- To sign/verify HTTP requests and responses, see the
|
|
34
|
+
[Signing Requests](#signing-http-requests-and-responses) and
|
|
35
|
+
[Verifying HTTP signatures](#verifying-http-signatures) or
|
|
36
|
+
[Verifying responses (client-side)](#verifying-responses-client-side)
|
|
37
|
+
sections.
|
|
38
|
+
|
|
39
|
+
- For a more hands-off approach to enforcing request authentication
|
|
40
|
+
with HTTP signatures in Rack applications (such as Rails),
|
|
41
|
+
see the examples in the next section.
|
|
42
|
+
|
|
43
|
+
### Rack middleware
|
|
44
|
+
|
|
33
45
|
Add the middleware to your Rack application:
|
|
34
46
|
|
|
35
47
|
```ruby
|
|
@@ -47,9 +59,6 @@ use Rack::Auth::Signature,
|
|
|
47
59
|
In this example, the middleware requires a valid HTTP Message Signature
|
|
48
60
|
for all endpoints except /login.
|
|
49
61
|
|
|
50
|
-
To learn how to sign requests, see the
|
|
51
|
-
[Signing Requests](#signing-http-requests-and-responses) section.
|
|
52
|
-
|
|
53
62
|
#### Using a configuration file
|
|
54
63
|
|
|
55
64
|
For more complex setups, you can load configuration from a file, e.g.:
|
|
@@ -80,10 +89,13 @@ without a valid signature will be rejected.
|
|
|
80
89
|
|
|
81
90
|
#### Next steps
|
|
82
91
|
|
|
92
|
+
- See how to [sign HTTP messages](#signing-http-requests-and-responses)
|
|
93
|
+
or [verify HTTP requests and responses](#verifying-responses-client-side).
|
|
94
|
+
|
|
83
95
|
- See a full configuration example:
|
|
84
96
|
[examples/sinatra/http-signatures.yml](https://github.com/nomadium/linzer/tree/master/examples/sinatra/http-signatures.yml)
|
|
85
97
|
|
|
86
|
-
- Browse the middleware implementation for all options:
|
|
98
|
+
- Browse the Rack middleware implementation for all options:
|
|
87
99
|
[lib/rack/auth/signature.rb](https://github.com/nomadium/linzer/tree/master/lib/rack/auth/signature.rb)
|
|
88
100
|
|
|
89
101
|
- For more specific scenarios and use cases, continue below.
|
|
@@ -97,6 +109,7 @@ method, path, headers, etc).
|
|
|
97
109
|
Choose your client:
|
|
98
110
|
|
|
99
111
|
- Use the [http gem](https://github.com/httprb/http) → recommended (simplest)
|
|
112
|
+
- Use Faraday and the provided middleware → also recommended (very simple)
|
|
100
113
|
- Use `Net::HTTP` → lower-level control
|
|
101
114
|
- Use `Linzer::HTTP` → quick experiments / debugging
|
|
102
115
|
|
|
@@ -121,6 +134,35 @@ response.body.to_s
|
|
|
121
134
|
=> "protected content..."
|
|
122
135
|
```
|
|
123
136
|
|
|
137
|
+
#### Using Faraday
|
|
138
|
+
|
|
139
|
+
Linzer ships Faraday middleware for signing outbound requests and verifying
|
|
140
|
+
signed responses.
|
|
141
|
+
|
|
142
|
+
To sign requests:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
require "linzer/faraday"
|
|
146
|
+
|
|
147
|
+
api_url = "https://example.com/api/service"
|
|
148
|
+
components = %w[@target-uri @authority date cache-control]
|
|
149
|
+
signature_params = {alg: "rsa-pss-sha512", keyid: "test-key-rsa-pss",
|
|
150
|
+
expires: Time.now.to_i + 300}
|
|
151
|
+
|
|
152
|
+
conn = Faraday.new(url: api_url) do |builder|
|
|
153
|
+
builder.headers["Cache-control"] = "no-cache"
|
|
154
|
+
builder.headers["Date"] = Time.now.httpdate
|
|
155
|
+
builder.request :http_signature, key: signing_key,
|
|
156
|
+
components: components,
|
|
157
|
+
params: signature_params
|
|
158
|
+
end
|
|
159
|
+
response = conn.post("/task")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This signs the request automatically before dispatch. In this example,
|
|
163
|
+
`Date` and `Cache-Control` are included in the signature to protect
|
|
164
|
+
freshness-related metadata from modification.
|
|
165
|
+
|
|
124
166
|
#### Using `Net::HTTP` (manual control)
|
|
125
167
|
|
|
126
168
|
```ruby
|
|
@@ -293,8 +335,8 @@ end
|
|
|
293
335
|
|
|
294
336
|
#### Dynamic key lookup
|
|
295
337
|
|
|
296
|
-
In many cases, the verification key depends on the `keyid` parameter
|
|
297
|
-
the signature.
|
|
338
|
+
In many cases, the verification key depends on the `keyid` parameter
|
|
339
|
+
provided in the signature.
|
|
298
340
|
|
|
299
341
|
You can supply a block to resolve keys dynamically:
|
|
300
342
|
|
|
@@ -331,14 +373,48 @@ result = Linzer.verify!(response, key: pubkey, no_older_than: 600)
|
|
|
331
373
|
# => true
|
|
332
374
|
```
|
|
333
375
|
|
|
376
|
+
Or if you are using Faraday, response verification can be handled by
|
|
377
|
+
middleware as well:
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
require "linzer/faraday"
|
|
381
|
+
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
conn = Faraday.new(url: api_url) do |builder|
|
|
385
|
+
builder.response :http_signature, key: verify_key
|
|
386
|
+
end
|
|
387
|
+
response = conn.post("/task")
|
|
388
|
+
|
|
389
|
+
response.env[:http_signature_verified]
|
|
390
|
+
# => true
|
|
391
|
+
|
|
392
|
+
response.env[:http_signature]
|
|
393
|
+
# => #<Linzer::Signature ...>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
After verification, the middleware stores:
|
|
397
|
+
|
|
398
|
+
- `env[:http_signature_verified]` — whether verification succeeded
|
|
399
|
+
|
|
400
|
+
- `env[:http_signature]` — the parsed `Linzer::Signature` object (when valid)
|
|
401
|
+
|
|
402
|
+
Verification failures can optionally raise instead of returning status;
|
|
403
|
+
see middleware options below.
|
|
404
|
+
|
|
334
405
|
### Using a custom HTTP library
|
|
335
406
|
|
|
336
|
-
If you
|
|
337
|
-
|
|
407
|
+
If you are using another HTTP library, you may not need a custom Linzer
|
|
408
|
+
adapter at all. Because Linzer integrates with Faraday middleware, you can
|
|
409
|
+
often use Faraday together with the appropriate Faraday adapter for your
|
|
410
|
+
HTTP client of choice.
|
|
411
|
+
|
|
412
|
+
If you need tighter integration or are not using Faraday, you can also
|
|
413
|
+
implement a native Linzer adapter directly.
|
|
338
414
|
|
|
339
|
-
In most cases, implementing an adapter just means mapping your library
|
|
340
|
-
request/response objects to the small interface Linzer expects,
|
|
341
|
-
|
|
415
|
+
In most cases, implementing an adapter just means mapping your library's
|
|
416
|
+
request/response objects to the small interface Linzer expects, then
|
|
417
|
+
registering it.
|
|
342
418
|
|
|
343
419
|
To do this:
|
|
344
420
|
|
|
@@ -383,7 +459,7 @@ pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
|
|
|
383
459
|
|
|
384
460
|
message = Linzer::Message.new(request)
|
|
385
461
|
|
|
386
|
-
signature = Linzer::Signature.build(
|
|
462
|
+
signature = Linzer::Signature.build(request.headers)
|
|
387
463
|
|
|
388
464
|
Linzer.verify(pubkey, message, signature)
|
|
389
465
|
# => true
|
|
@@ -433,6 +509,14 @@ For deeper details or edge cases, the source code and unit tests are also a good
|
|
|
433
509
|
|
|
434
510
|
linzer is built in [Continuous Integration](https://github.com/nomadium/linzer/actions/workflows/main.yml) on Ruby 3.0+.
|
|
435
511
|
|
|
512
|
+
> [!NOTE]
|
|
513
|
+
>
|
|
514
|
+
> Ruby 3.0 is supported and tested in CI, but RSA-based signature algorithms
|
|
515
|
+
> (RSA-PSS and RSA PKCS#1 v1.5) may not work correctly due to its older
|
|
516
|
+
> OpenSSL bindings. If you need RSA algorithms, use Ruby 3.1 or later.
|
|
517
|
+
> Ruby 3.0 has been EOL since March 2024 — users are advised to upgrade
|
|
518
|
+
> to a supported Ruby release.
|
|
519
|
+
|
|
436
520
|
## Security
|
|
437
521
|
|
|
438
522
|
This gem is provided “as is” without any warranties. It has not
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
module HttpSignature
|
|
5
|
+
# Raised when HTTP response signature verification fails in strict mode.
|
|
6
|
+
#
|
|
7
|
+
# Inherits from {Faraday::Error} so that standard Faraday error handling
|
|
8
|
+
# (e.g. +rescue Faraday::Error+) catches verification failures.
|
|
9
|
+
# The original {Linzer::VerifyError} is preserved as {#wrapped_exception}
|
|
10
|
+
# and the {Faraday::Response} is available via {#response}.
|
|
11
|
+
#
|
|
12
|
+
# @example Catching a verification failure
|
|
13
|
+
# begin
|
|
14
|
+
# response = conn.get("/")
|
|
15
|
+
# rescue Faraday::HttpSignature::VerifyError => e
|
|
16
|
+
# e.message # => "Failed to verify message: Invalid signature."
|
|
17
|
+
# e.response # => the Faraday::Response object
|
|
18
|
+
# e.wrapped_exception # => the original Linzer::VerifyError
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @see Middleware
|
|
22
|
+
class VerifyError < Faraday::Error; end
|
|
23
|
+
|
|
24
|
+
# Raised when HTTP request signature creation fails.
|
|
25
|
+
#
|
|
26
|
+
# Inherits from {Faraday::Error} so that standard Faraday error handling
|
|
27
|
+
# (e.g. +rescue Faraday::Error+) catches verification failures.
|
|
28
|
+
# The original {Linzer::Error} is preserved as {#wrapped_exception}.
|
|
29
|
+
#
|
|
30
|
+
# @example Catching a signing failure
|
|
31
|
+
# begin
|
|
32
|
+
# response = conn.post("/")
|
|
33
|
+
# rescue Faraday::HttpSignature::SigningError => e
|
|
34
|
+
# e.message # => "Failed to sign message: Missing component."
|
|
35
|
+
# e.wrapped_exception # => the original Linzer::Error
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# @see Middleware
|
|
39
|
+
class SigningError < Faraday::Error; end
|
|
40
|
+
|
|
41
|
+
# Faraday middleware for HTTP message signing and verification (RFC 9421).
|
|
42
|
+
#
|
|
43
|
+
# When registered via +request+, signs outgoing requests (default).
|
|
44
|
+
# When registered via +response+, verifies incoming response signatures.
|
|
45
|
+
# When registered via +use+, signs requests by default; pass
|
|
46
|
+
# +verify_response: true+ to also verify responses.
|
|
47
|
+
#
|
|
48
|
+
# == Verification result metadata
|
|
49
|
+
#
|
|
50
|
+
# After response verification, the middleware stores results in
|
|
51
|
+
# +env[:http_signature_verified]+ (+true+ or +false+) and
|
|
52
|
+
# +env[:http_signature]+ (the {Linzer::Signature} on success).
|
|
53
|
+
# These are accessible via +response.env[:http_signature_verified]+.
|
|
54
|
+
#
|
|
55
|
+
# @example Sign requests
|
|
56
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
57
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Verify responses
|
|
61
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
62
|
+
# f.response :http_signature, verify_key: server_pubkey
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# @example Lenient verification (no exception on failure)
|
|
66
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
67
|
+
# f.response :http_signature, verify_key: server_pubkey, strict: false
|
|
68
|
+
# end
|
|
69
|
+
# response = conn.get("/")
|
|
70
|
+
# response.env[:http_signature_verified] # => true or false
|
|
71
|
+
#
|
|
72
|
+
# @see https://datatracker.ietf.org/doc/html/rfc9421 RFC 9421
|
|
73
|
+
class Middleware < Faraday::Middleware
|
|
74
|
+
# Default options for the base middleware class (used by +use+
|
|
75
|
+
# and +request+ registrations). Signs requests, does not verify
|
|
76
|
+
# responses, strict mode enabled.
|
|
77
|
+
DEFAULT_OPTIONS = {
|
|
78
|
+
sign_request: true,
|
|
79
|
+
verify_response: false,
|
|
80
|
+
strict: true
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
# Configuration options for the HTTP signature middleware.
|
|
84
|
+
#
|
|
85
|
+
# @!attribute [rw] key
|
|
86
|
+
# @return [Linzer::Key, nil] generic key used for signing or
|
|
87
|
+
# verification when only one mode is active
|
|
88
|
+
# @!attribute [rw] sign_request
|
|
89
|
+
# @return [Boolean] whether to sign outgoing requests
|
|
90
|
+
# (defaults to +true+)
|
|
91
|
+
# @!attribute [rw] sign_key
|
|
92
|
+
# @return [Linzer::Key, nil] explicit key for signing; required
|
|
93
|
+
# when both signing and verification are enabled
|
|
94
|
+
# @!attribute [rw] components
|
|
95
|
+
# @return [Array<String>] HTTP message components to include in
|
|
96
|
+
# the signature (e.g. +["@method", "@path", "content-type"]+)
|
|
97
|
+
# @!attribute [rw] verify_response
|
|
98
|
+
# @return [Boolean] whether to verify incoming response signatures
|
|
99
|
+
# (defaults to +false+)
|
|
100
|
+
# @!attribute [rw] verify_key
|
|
101
|
+
# @return [Linzer::Key, nil] explicit key for verification; required
|
|
102
|
+
# when both signing and verification are enabled
|
|
103
|
+
# @!attribute [rw] params
|
|
104
|
+
# @return [Hash] additional signature parameters
|
|
105
|
+
# (e.g. +{ tag: "my_tag" }+)
|
|
106
|
+
# @!attribute [rw] strict
|
|
107
|
+
# @return [Boolean] when +true+ (default), raises
|
|
108
|
+
# {VerifyError} on verification failure; when +false+,
|
|
109
|
+
# sets +env[:http_signature_verified]+ to +false+ and continues
|
|
110
|
+
class Options < Faraday::Options.new(:key, :sign_request, :sign_key, :components, :verify_response, :verify_key, :params, :strict)
|
|
111
|
+
# Returns the generic key.
|
|
112
|
+
# @return [Linzer::Key, nil]
|
|
113
|
+
def key
|
|
114
|
+
self[:key]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Whether outgoing requests should be signed.
|
|
118
|
+
# Defaults to +true+ (returns +true+ when unset).
|
|
119
|
+
# @return [Boolean]
|
|
120
|
+
def sign_request?
|
|
121
|
+
self[:sign_request] != false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Whether incoming responses should be verified.
|
|
125
|
+
# Defaults to +false+ (returns +false+ when unset).
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def verify_response?
|
|
128
|
+
self[:verify_response]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Whether verification failures should raise an exception.
|
|
132
|
+
# Defaults to +true+ (returns +true+ when unset).
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def strict?
|
|
135
|
+
self[:strict] != false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns the list of HTTP message components to sign.
|
|
139
|
+
# @return [Array<String>]
|
|
140
|
+
def components
|
|
141
|
+
Array(self[:components])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns additional signature parameters.
|
|
145
|
+
# @return [Hash]
|
|
146
|
+
def params
|
|
147
|
+
Hash(self[:params])
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Creates a new middleware instance.
|
|
152
|
+
#
|
|
153
|
+
# Merges class-level {DEFAULT_OPTIONS} with the user-provided options
|
|
154
|
+
# so that subclasses ({Request}, {Response}) can override defaults.
|
|
155
|
+
#
|
|
156
|
+
# @param app [#call] the next middleware or adapter in the stack
|
|
157
|
+
# @param options [Hash, nil] middleware options
|
|
158
|
+
# @option options [Linzer::Key] :key generic key for signing or verification
|
|
159
|
+
# @option options [Linzer::Key] :sign_key explicit signing key
|
|
160
|
+
# @option options [Linzer::Key] :verify_key explicit verification key
|
|
161
|
+
# @option options [Array<String>] :components components to sign
|
|
162
|
+
# @option options [Hash] :params additional signature parameters
|
|
163
|
+
# @option options [Boolean] :sign_request (+true+) whether to sign requests
|
|
164
|
+
# @option options [Boolean] :verify_response (+false+) whether to verify responses
|
|
165
|
+
# @option options [Boolean] :strict (+true+) raise on verification failure
|
|
166
|
+
def initialize(app, options = nil)
|
|
167
|
+
super(app)
|
|
168
|
+
defaults = self.class::DEFAULT_OPTIONS
|
|
169
|
+
merged = defaults.merge(Hash(options))
|
|
170
|
+
@options = Options.from(merged)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Signs the outgoing request when {Options#sign_request?} is +true+.
|
|
174
|
+
#
|
|
175
|
+
# Resolves the signing key, builds a {Linzer::Message} from the
|
|
176
|
+
# Faraday environment, generates a signature over the configured
|
|
177
|
+
# components, and merges the +signature+ and +signature-input+
|
|
178
|
+
# headers into the request.
|
|
179
|
+
#
|
|
180
|
+
# @param env [Faraday::Env] the middleware environment
|
|
181
|
+
# @return [Faraday::Env, nil] the modified env, or +nil+ if signing
|
|
182
|
+
# is disabled
|
|
183
|
+
# @raise [Linzer::Error] if no valid signing key is available
|
|
184
|
+
def on_request(env)
|
|
185
|
+
return unless options.sign_request?
|
|
186
|
+
|
|
187
|
+
key = resolve_signing_key
|
|
188
|
+
request = Linzer::Faraday::Utils.create_request(env)
|
|
189
|
+
message = Linzer::Message.new(request)
|
|
190
|
+
|
|
191
|
+
signature = Linzer.sign(key, message, options.components, options.params)
|
|
192
|
+
env.request_headers.merge!(signature.to_h)
|
|
193
|
+
env
|
|
194
|
+
rescue Linzer::Error => e
|
|
195
|
+
raise SigningError, e if options.strict?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Verifies the response signature when {Options#verify_response?} is +true+.
|
|
199
|
+
#
|
|
200
|
+
# On success, sets +env[:http_signature_verified]+ to +true+ and
|
|
201
|
+
# +env[:http_signature]+ to the verified {Linzer::Signature}.
|
|
202
|
+
#
|
|
203
|
+
# On failure in strict mode (default), raises {VerifyError}.
|
|
204
|
+
# In lenient mode (+strict: false+), sets
|
|
205
|
+
# +env[:http_signature_verified]+ to +false+ and allows the response
|
|
206
|
+
# to continue through the middleware stack.
|
|
207
|
+
#
|
|
208
|
+
# @param env [Faraday::Env] the middleware environment
|
|
209
|
+
# @return [Faraday::Env, nil] the modified env, or +nil+ if verifying
|
|
210
|
+
# is disabled
|
|
211
|
+
# @raise [VerifyError] if verification fails and +strict+ is +true+
|
|
212
|
+
# @raise [Linzer::Error] if no valid verification key is available
|
|
213
|
+
def on_complete(env)
|
|
214
|
+
env[:http_signature_verified] = false
|
|
215
|
+
return unless options.verify_response?
|
|
216
|
+
|
|
217
|
+
key = resolve_verify_key
|
|
218
|
+
response = ::Faraday::Response.new(env)
|
|
219
|
+
message = Linzer::Message.new(response)
|
|
220
|
+
signature = Linzer::Signature.build(response.headers)
|
|
221
|
+
|
|
222
|
+
Linzer.verify(key, message, signature)
|
|
223
|
+
env[:http_signature_verified] = true
|
|
224
|
+
env[:http_signature] = signature
|
|
225
|
+
env
|
|
226
|
+
rescue Linzer::Error => e
|
|
227
|
+
raise VerifyError.new(e, response: response) if options.strict?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# Resolves the key to use for signing requests.
|
|
233
|
+
#
|
|
234
|
+
# Prefers {Options#sign_key}. Falls back to the generic {Options#key}
|
|
235
|
+
# when only one mode (sign or verify) is active. When both modes are
|
|
236
|
+
# active, the generic key is ambiguous and +sign_key+ must be set
|
|
237
|
+
# explicitly.
|
|
238
|
+
#
|
|
239
|
+
# @return [Linzer::Key] the resolved signing key
|
|
240
|
+
# @raise [Linzer::Error] if no key is available or the key is invalid
|
|
241
|
+
def resolve_signing_key
|
|
242
|
+
key = options.sign_key
|
|
243
|
+
key ||= options.key unless options.sign_request? && options.verify_response?
|
|
244
|
+
raise Linzer::Error, "No signing key provided!" if !key
|
|
245
|
+
raise Linzer::Error, "Invalid key!" if !key.is_a?(Linzer::Key)
|
|
246
|
+
|
|
247
|
+
key
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Resolves the key to use for verifying response signatures.
|
|
251
|
+
#
|
|
252
|
+
# Prefers {Options#verify_key}. Falls back to the generic {Options#key}
|
|
253
|
+
# when only one mode (sign or verify) is active. When both modes are
|
|
254
|
+
# active, the generic key is ambiguous and +verify_key+ must be set
|
|
255
|
+
# explicitly.
|
|
256
|
+
#
|
|
257
|
+
# @return [Linzer::Key] the resolved verification key
|
|
258
|
+
# @raise [Linzer::Error] if no key is available or the key is invalid
|
|
259
|
+
def resolve_verify_key
|
|
260
|
+
key = options.verify_key
|
|
261
|
+
key ||= options.key unless options.sign_request? && options.verify_response?
|
|
262
|
+
raise Linzer::Error, "No verification key provided!" if !key
|
|
263
|
+
raise Linzer::Error, "Invalid key!" if !key.is_a?(Linzer::Key)
|
|
264
|
+
|
|
265
|
+
key
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Subclass registered under {Faraday::Request}.
|
|
269
|
+
#
|
|
270
|
+
# Inherits the base {DEFAULT_OPTIONS} which sign requests by default
|
|
271
|
+
# and do not verify responses.
|
|
272
|
+
#
|
|
273
|
+
# @example
|
|
274
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
275
|
+
class Request < self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Subclass registered under {Faraday::Response}.
|
|
279
|
+
#
|
|
280
|
+
# Overrides {DEFAULT_OPTIONS} to verify responses by default and
|
|
281
|
+
# not sign requests.
|
|
282
|
+
#
|
|
283
|
+
# @example
|
|
284
|
+
# f.response :http_signature, verify_key: server_pubkey
|
|
285
|
+
class Response < self
|
|
286
|
+
# Default options for the response subclass. Verifies responses
|
|
287
|
+
# and does not sign requests.
|
|
288
|
+
DEFAULT_OPTIONS = {
|
|
289
|
+
sign_request: false,
|
|
290
|
+
verify_response: true,
|
|
291
|
+
strict: true
|
|
292
|
+
}.freeze
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require_relative "http_signature/middleware"
|
|
5
|
+
|
|
6
|
+
module Faraday
|
|
7
|
+
# Faraday middleware for signing and verifying HTTP messages
|
|
8
|
+
# as defined in RFC 9421.
|
|
9
|
+
#
|
|
10
|
+
# Three registration points are provided so the middleware can be added
|
|
11
|
+
# via +request+, +response+ or +use+, each with appropriate defaults:
|
|
12
|
+
#
|
|
13
|
+
# @example Sign outgoing requests
|
|
14
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
15
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Verify incoming responses
|
|
19
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
20
|
+
# f.response :http_signature, verify_key: server_pubkey
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Sign requests and verify responses
|
|
24
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
25
|
+
# f.use :http_signature, sign_key: my_key, verify_key: server_pubkey,
|
|
26
|
+
# verify_response: true, components: %w[@method @path]
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @see Faraday::HttpSignature::Middleware
|
|
30
|
+
# @see https://datatracker.ietf.org/doc/html/rfc9421 RFC 9421 - HTTP Message Signatures
|
|
31
|
+
module HttpSignature
|
|
32
|
+
Faraday::Request.register_middleware(http_signature: Faraday::HttpSignature::Middleware::Request)
|
|
33
|
+
Faraday::Response.register_middleware(http_signature: Faraday::HttpSignature::Middleware::Response)
|
|
34
|
+
Faraday::Middleware.register_middleware(http_signature: Faraday::HttpSignature::Middleware)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
# Faraday integration for Linzer.
|
|
5
|
+
#
|
|
6
|
+
# @see file:lib/linzer/faraday.rb
|
|
7
|
+
module Faraday
|
|
8
|
+
# Utility methods for converting Faraday middleware objects into
|
|
9
|
+
# types compatible with Linzer adapters.
|
|
10
|
+
module Utils
|
|
11
|
+
# Creates a {::Faraday::Request} from a middleware environment.
|
|
12
|
+
#
|
|
13
|
+
# Builds a minimal request suitable for use with
|
|
14
|
+
# {Linzer::Message::Adapter::Faraday::Request}, preserving the
|
|
15
|
+
# original HTTP method, URL, and headers from the environment.
|
|
16
|
+
#
|
|
17
|
+
# @param env [::Faraday::Env] the middleware environment
|
|
18
|
+
# @return [::Faraday::Request] a new request object
|
|
19
|
+
def self.create_request(env)
|
|
20
|
+
::Faraday::Request.create(env.method) do |req|
|
|
21
|
+
req.params = ::Faraday::Utils::ParamsHash.new
|
|
22
|
+
req.headers = ::Faraday::Utils::Headers.new(env.request_headers.dup)
|
|
23
|
+
req.options = ::Faraday::ConnectionOptions.from(nil).request
|
|
24
|
+
req.url env.url
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Faraday integration for Linzer.
|
|
4
|
+
#
|
|
5
|
+
# Require this file to automatically register Faraday message adapters
|
|
6
|
+
# and the HTTP signature (RFC 9421) middleware.
|
|
7
|
+
#
|
|
8
|
+
# This sets up:
|
|
9
|
+
# - {Linzer::Message::Adapter::Faraday::Request} for {::Faraday::Request}
|
|
10
|
+
# - {Linzer::Message::Adapter::Faraday::Response} for {::Faraday::Response}
|
|
11
|
+
# - {Faraday::HttpSignature::Middleware} registered as +:http_signature+
|
|
12
|
+
# on +Faraday::Request+, +Faraday::Response+ and +Faraday::Middleware+
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# require "linzer/faraday"
|
|
16
|
+
#
|
|
17
|
+
# conn = Faraday.new(url: "https://example.com") do |f|
|
|
18
|
+
# f.request :http_signature, key: my_key, components: %w[@method @path]
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
require "faraday"
|
|
22
|
+
require "linzer"
|
|
23
|
+
require "faraday/http_signature"
|
|
24
|
+
require "linzer/message/adapter/faraday/request"
|
|
25
|
+
require "linzer/message/adapter/faraday/response"
|
|
26
|
+
require "linzer/faraday/utils"
|
|
27
|
+
|
|
28
|
+
Linzer::Message.register_adapter(Faraday::Request, Linzer::Message::Adapter::Faraday::Request)
|
|
29
|
+
Linzer::Message.register_adapter(Faraday::Response, Linzer::Message::Adapter::Faraday::Response)
|
data/lib/linzer/http.rb
CHANGED
|
@@ -78,6 +78,14 @@ module Linzer
|
|
|
78
78
|
private
|
|
79
79
|
|
|
80
80
|
# Executes a signed HTTP request.
|
|
81
|
+
#
|
|
82
|
+
# Validates inputs, builds and signs the request, then sends it.
|
|
83
|
+
#
|
|
84
|
+
# @param verb [Symbol] the HTTP method (e.g. +:get+, +:post+)
|
|
85
|
+
# @param uri [String] the request URI
|
|
86
|
+
# @param options [Hash] request options
|
|
87
|
+
# @return [Net::HTTPResponse] the response
|
|
88
|
+
# @raise [Linzer::Error] if the verb or key is invalid
|
|
81
89
|
def request(verb, uri, options = {})
|
|
82
90
|
validate_verb(verb)
|
|
83
91
|
|
|
@@ -100,10 +108,16 @@ module Linzer
|
|
|
100
108
|
do_request(http, uri, verb, options[:data], signature, headers)
|
|
101
109
|
end
|
|
102
110
|
|
|
111
|
+
# Returns the default covered components for signing.
|
|
112
|
+
# @return [Array<String>]
|
|
103
113
|
def default_components
|
|
104
114
|
Linzer::Options::DEFAULT[:covered_components]
|
|
105
115
|
end
|
|
106
116
|
|
|
117
|
+
# Validates that the HTTP verb is recognized.
|
|
118
|
+
#
|
|
119
|
+
# @param verb [Symbol] the HTTP method
|
|
120
|
+
# @raise [Linzer::Error] if the verb is unknown
|
|
107
121
|
def validate_verb(verb)
|
|
108
122
|
method_name = verb.to_s.upcase
|
|
109
123
|
if !known_http_methods.include?(method_name)
|
|
@@ -111,17 +125,32 @@ module Linzer
|
|
|
111
125
|
end
|
|
112
126
|
end
|
|
113
127
|
|
|
128
|
+
# Validates that the signing key is present and usable.
|
|
129
|
+
#
|
|
130
|
+
# @param key [Linzer::Key] the signing key
|
|
131
|
+
# @return [Linzer::Key] the validated key
|
|
132
|
+
# @raise [Linzer::Error] if the key is nil or does not respond to +#sign+
|
|
114
133
|
def validate_key(key)
|
|
115
134
|
raise Linzer::Error, "Key can not be nil!" if !key
|
|
116
135
|
raise Linzer::Error, "Key object is invalid!" if !key.respond_to?(:sign)
|
|
117
136
|
key
|
|
118
137
|
end
|
|
119
138
|
|
|
139
|
+
# Builds request headers, adding a default User-Agent if not present.
|
|
140
|
+
#
|
|
141
|
+
# @param headers [Hash] user-provided headers
|
|
142
|
+
# @return [Hash] headers with User-Agent ensured
|
|
120
143
|
def build_headers(headers)
|
|
121
144
|
return headers if headers.transform_keys(&:downcase).key?("user-agent")
|
|
122
145
|
headers.merge({"user-agent" => "Linzer/#{Linzer::VERSION}"})
|
|
123
146
|
end
|
|
124
147
|
|
|
148
|
+
# Builds a Net::HTTP request object for the given method and URI.
|
|
149
|
+
#
|
|
150
|
+
# @param method [Symbol] the HTTP method
|
|
151
|
+
# @param uri [String] the request URI
|
|
152
|
+
# @param headers [Hash] request headers to set
|
|
153
|
+
# @return [Net::HTTPRequest] the constructed request
|
|
125
154
|
def build_request(method, uri, headers)
|
|
126
155
|
request_class = Net::HTTP.const_get(method.to_s.capitalize)
|
|
127
156
|
request = request_class.new(URI(uri))
|
|
@@ -129,7 +158,10 @@ module Linzer
|
|
|
129
158
|
request
|
|
130
159
|
end
|
|
131
160
|
|
|
132
|
-
#
|
|
161
|
+
# Checks if the HTTP method typically carries a request body.
|
|
162
|
+
#
|
|
163
|
+
# @param verb [Symbol] the HTTP method
|
|
164
|
+
# @return [Boolean] +true+ for POST, PUT, PATCH, and WebDAV write methods
|
|
133
165
|
def with_body?(verb)
|
|
134
166
|
# common HTTP
|
|
135
167
|
return false if %i[get head options trace delete].include?(verb)
|
|
@@ -142,6 +174,16 @@ module Linzer
|
|
|
142
174
|
true
|
|
143
175
|
end
|
|
144
176
|
|
|
177
|
+
# Sends the HTTP request with the signature headers attached.
|
|
178
|
+
#
|
|
179
|
+
# @param http [Net::HTTP] the HTTP connection
|
|
180
|
+
# @param uri [String] the request URI
|
|
181
|
+
# @param verb [Symbol] the HTTP method
|
|
182
|
+
# @param data [String, nil] the request body
|
|
183
|
+
# @param signature [Linzer::Signature] the generated signature
|
|
184
|
+
# @param headers [Hash] request headers
|
|
185
|
+
# @return [Net::HTTPResponse] the response
|
|
186
|
+
# @raise [Linzer::Error] if a body is required but not provided
|
|
145
187
|
def do_request(http, uri, verb, data, signature, headers)
|
|
146
188
|
if with_body?(verb)
|
|
147
189
|
if !data
|