linzer 0.7.9.beta2 → 0.7.9
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 +25 -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 +65 -9
- data/lib/linzer/message/adapter/faraday/request.rb +63 -0
- data/lib/linzer/message/adapter/faraday/response.rb +46 -0
- data/lib/linzer/message/adapter/generic/request.rb +27 -20
- data/lib/linzer/message/adapter/generic/response.rb +11 -8
- data/lib/linzer/message/adapter/http_gem/common.rb +16 -8
- 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/adapter/rack/request.rb +11 -8
- data/lib/linzer/message/adapter/rack/response.rb +11 -8
- 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/linzer.rb +0 -1
- data/lib/rack/auth/signature/helpers.rb +72 -0
- metadata +7 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91f1e3986fae03eb9bf01d4dc8e9c919cd7072ea3ae0f8168099261da0da8c11
|
|
4
|
+
data.tar.gz: fb07600d19e8e2198e7cbcfb910516d9300e3aa89d94e81e3bc3ebd6a9d007e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aed32987b8ea9fc398ef1d263a94ce3df3ec86e8cd331d2e9b23383c0c6de1db3f0bdaa621990eb1809c210b0ae3f38b045abd2e8e48229a9ec8129cf4213c65
|
|
7
|
+
data.tar.gz: 26a21c99d8e383af31395f5ebfa5581b0fedf126bb1bb806db3cb1eb0f0d89306dde3af7bf54100cd0251235f54e5cd4f852f8d61007b74fe079716b4447986a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.9] - 2026-04-30
|
|
4
|
+
|
|
5
|
+
(No changes since the last beta release, this new stable release just
|
|
6
|
+
bundles all the features/changes introduced during 0.7.9 beta releases)
|
|
7
|
+
|
|
8
|
+
- Add support for http gem 6.x while maintaining compatibility with 5.x.
|
|
9
|
+
Handles API differences introduced in 6.0.
|
|
10
|
+
|
|
11
|
+
- Improve README clarity and align it with current behavior and production
|
|
12
|
+
usage.
|
|
13
|
+
|
|
14
|
+
- Enforce `expires` signature parameter validation in Verifier.
|
|
15
|
+
|
|
16
|
+
- Add Faraday middleware and adapters for HTTP message signatures.
|
|
17
|
+
|
|
18
|
+
- Add support for multiple signatures on a single HTTP message
|
|
19
|
+
This fixes a bug on multi-signature header overwrite when calling sign!
|
|
20
|
+
multiple times.
|
|
21
|
+
|
|
22
|
+
## [0.7.9.beta3] - 2026-04-27
|
|
23
|
+
|
|
24
|
+
- Enforce `expires` signature parameter validation in Verifier.
|
|
25
|
+
|
|
26
|
+
- Add Faraday middleware and adapters for HTTP message signatures.
|
|
27
|
+
|
|
3
28
|
## [0.7.9.beta2] - 2026-04-19
|
|
4
29
|
|
|
5
30
|
- 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)
|