linzer 0.7.9.beta1 → 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 +12 -0
- data/README.md +283 -114
- data/flake.lock +10 -34
- data/flake.nix +17 -5
- 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 +48 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +13 -7
- data/lib/linzer/message/adapter/http_gem/response.rb +11 -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 +8 -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,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [0.7.9.beta2] - 2026-04-19
|
|
10
|
+
|
|
11
|
+
- Add support for http gem 6.x while maintaining compatibility with 5.x.
|
|
12
|
+
Handles API differences introduced in 6.0.
|
|
13
|
+
- Improve README clarity and align it with current behavior and production usage.
|
|
14
|
+
|
|
3
15
|
## [0.7.9.beta1] - 2026-03-03
|
|
4
16
|
|
|
5
17
|
(Beta release to test gem release automation; no functional changes)
|
data/README.md
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
[rubydoc-badge]: https://img.shields.io/badge/docs-RubyDoc.info-blue
|
|
10
10
|
[rubydoc-link]: https://www.rubydoc.info/gems/linzer
|
|
11
11
|
|
|
12
|
-
Linzer is a Ruby library for [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421.html)
|
|
12
|
+
Linzer is a Ruby library for [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421.html),
|
|
13
|
+
allowing you to sign and verify HTTP requests and responses with
|
|
14
|
+
standard-compliant cryptographic signatures.
|
|
15
|
+
|
|
16
|
+
Useful for APIs, webhooks, and services that need to verify request
|
|
17
|
+
authenticity or prevent tampering.
|
|
13
18
|
|
|
14
19
|
## Install
|
|
15
20
|
|
|
@@ -23,48 +28,92 @@ Or just `gem install linzer`.
|
|
|
23
28
|
|
|
24
29
|
## Usage
|
|
25
30
|
|
|
26
|
-
###
|
|
31
|
+
### Quick start
|
|
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
|
|
27
44
|
|
|
28
|
-
Add the
|
|
29
|
-
as needed, e.g.:
|
|
45
|
+
Add the middleware to your Rack application:
|
|
30
46
|
|
|
31
47
|
```ruby
|
|
32
48
|
# config.ru
|
|
33
|
-
use Rack::Auth::Signature,
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
use Rack::Auth::Signature,
|
|
50
|
+
except: "/login",
|
|
51
|
+
default_key: {
|
|
52
|
+
material: Base64.strict_decode64(ENV["MYAPP_KEY"]),
|
|
53
|
+
alg: "hmac-sha256"
|
|
54
|
+
}
|
|
55
|
+
# or using a public/private key pair:
|
|
56
|
+
# default_key: { material: IO.read("app/config/pubkey.pem"), alg: "ed25519" }
|
|
36
57
|
```
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
In this example, the middleware requires a valid HTTP Message Signature
|
|
60
|
+
for all endpoints except /login.
|
|
61
|
+
|
|
62
|
+
#### Using a configuration file
|
|
63
|
+
|
|
64
|
+
For more complex setups, you can load configuration from a file, e.g.:
|
|
39
65
|
|
|
40
66
|
```ruby
|
|
41
67
|
# config.ru
|
|
42
|
-
use Rack::Auth::Signature,
|
|
68
|
+
use Rack::Auth::Signature,
|
|
69
|
+
except: "/login",
|
|
43
70
|
config_path: "app/configuration/http-signatures.yml"
|
|
44
71
|
```
|
|
45
72
|
|
|
46
|
-
|
|
73
|
+
#### Rails
|
|
74
|
+
|
|
75
|
+
In a Rails application, add the middleware in your configuration:
|
|
47
76
|
|
|
48
77
|
```ruby
|
|
49
78
|
# config/application.rb
|
|
50
|
-
config.middleware.use Rack::Auth::Signature,
|
|
79
|
+
config.middleware.use Rack::Auth::Signature,
|
|
80
|
+
except: "/login",
|
|
51
81
|
config_path: "http-signatures.yml"
|
|
52
82
|
```
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
#### What this does?
|
|
85
|
+
|
|
86
|
+
Once enabled, all protected routes will require a valid signature
|
|
87
|
+
generated by a client using the corresponding private key. Requests
|
|
88
|
+
without a valid signature will be rejected.
|
|
89
|
+
|
|
90
|
+
#### Next steps
|
|
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
|
+
|
|
95
|
+
- See a full configuration example:
|
|
96
|
+
[examples/sinatra/http-signatures.yml](https://github.com/nomadium/linzer/tree/master/examples/sinatra/http-signatures.yml)
|
|
97
|
+
|
|
98
|
+
- Browse the Rack middleware implementation for all options:
|
|
99
|
+
[lib/rack/auth/signature.rb](https://github.com/nomadium/linzer/tree/master/lib/rack/auth/signature.rb)
|
|
100
|
+
|
|
101
|
+
- For more specific scenarios and use cases, continue below.
|
|
102
|
+
|
|
103
|
+
### Signing HTTP requests and responses
|
|
60
104
|
|
|
61
|
-
|
|
105
|
+
Linzer signs HTTP requests by adding the required `Signature` and
|
|
106
|
+
`Signature-Input` headers based on selected request components (e.g.
|
|
107
|
+
method, path, headers, etc).
|
|
62
108
|
|
|
63
|
-
|
|
109
|
+
Choose your client:
|
|
64
110
|
|
|
65
|
-
|
|
111
|
+
- Use the [http gem](https://github.com/httprb/http) → recommended (simplest)
|
|
112
|
+
- Use Faraday and the provided middleware → also recommended (very simple)
|
|
113
|
+
- Use `Net::HTTP` → lower-level control
|
|
114
|
+
- Use `Linzer::HTTP` → quick experiments / debugging
|
|
66
115
|
|
|
67
|
-
####
|
|
116
|
+
#### Using [http gem](https://github.com/httprb/http)
|
|
68
117
|
|
|
69
118
|
```ruby
|
|
70
119
|
# first require http signatures feature class ready to be used with http gem:
|
|
@@ -85,7 +134,36 @@ response.body.to_s
|
|
|
85
134
|
=> "protected content..."
|
|
86
135
|
```
|
|
87
136
|
|
|
88
|
-
####
|
|
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
|
+
|
|
166
|
+
#### Using `Net::HTTP` (manual control)
|
|
89
167
|
|
|
90
168
|
```ruby
|
|
91
169
|
key = Linzer.generate_ed25519_key
|
|
@@ -111,7 +189,7 @@ request["signature-input"]
|
|
|
111
189
|
# => "sig1=(\"@method\" \"@request-target\" \"date\" ..."}
|
|
112
190
|
```
|
|
113
191
|
|
|
114
|
-
Then
|
|
192
|
+
Then send the request:
|
|
115
193
|
|
|
116
194
|
```ruby
|
|
117
195
|
require "net/http"
|
|
@@ -152,10 +230,7 @@ response = http.request(request)
|
|
|
152
230
|
# => #<Net::HTTPOK 200 OK readbody=true>
|
|
153
231
|
```
|
|
154
232
|
|
|
155
|
-
####
|
|
156
|
-
|
|
157
|
-
(This client is probably not suitable for production use but could be useful
|
|
158
|
-
enough to get started. It's build on top of Net::HTTP.)
|
|
233
|
+
#### Using the built-in client
|
|
159
234
|
|
|
160
235
|
```ruby
|
|
161
236
|
key = Linzer.generate_rsa_pss_sha512_key(4096)
|
|
@@ -172,13 +247,62 @@ response =
|
|
|
172
247
|
=> #<Net::HTTPOK 200 OK readbody=true>
|
|
173
248
|
```
|
|
174
249
|
|
|
175
|
-
|
|
250
|
+
(This client is intended for testing and exploration.
|
|
251
|
+
For production use, prefer a full-featured HTTP client).
|
|
176
252
|
|
|
177
|
-
|
|
178
|
-
[as shown above](#tldr-i-just-want-to-protect-my-application).
|
|
253
|
+
#### Signing HTTP responses (server-side)
|
|
179
254
|
|
|
180
|
-
|
|
181
|
-
|
|
255
|
+
You can sign responses using the same API as for requests, e.g.:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
put "/baz" do
|
|
259
|
+
...
|
|
260
|
+
response
|
|
261
|
+
# => #<Sinatra::Response:0x0000000109ac40b8 ...
|
|
262
|
+
response.headers["x-custom-app-header"] = "..."
|
|
263
|
+
Linzer.sign!(response,
|
|
264
|
+
key: my_key,
|
|
265
|
+
components: %w[@status content-type content-digest x-custom-app-header],
|
|
266
|
+
label: "sig1",
|
|
267
|
+
params: {
|
|
268
|
+
created: Time.now.to_i
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
response["signature"]
|
|
272
|
+
# => "sig1=:2TPCzD4l48bg6LMcVXdV9u..."
|
|
273
|
+
response["signature-input"]
|
|
274
|
+
# => "sig1=(\"@status\" \"content-type\" \"content-digest\"..."
|
|
275
|
+
...
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Verifying HTTP signatures
|
|
280
|
+
|
|
281
|
+
Linzer verifies incoming requests (or responses) by checking:
|
|
282
|
+
|
|
283
|
+
- the signature is valid for the given key
|
|
284
|
+
- the signed components match the actual request
|
|
285
|
+
- any signature parameters (e.g. created, expires) are valid
|
|
286
|
+
|
|
287
|
+
If verification fails, an exception is raised explaining the reason.
|
|
288
|
+
|
|
289
|
+
#### Recommended: Rack middleware
|
|
290
|
+
|
|
291
|
+
The easiest way to verify incoming requests is via middleware:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
use Rack::Auth::Signature, except: "/login"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
This automatically:
|
|
298
|
+
|
|
299
|
+
- verifies all incoming requests
|
|
300
|
+
- rejects invalid or unsigned requests
|
|
301
|
+
- integrates cleanly with Rack-based frameworks (Rails, Sinatra, etc.)
|
|
302
|
+
|
|
303
|
+
#### Manual verification (controller / route level)
|
|
304
|
+
|
|
305
|
+
If you need more control, you can verify incoming requests manually:
|
|
182
306
|
|
|
183
307
|
```ruby
|
|
184
308
|
post "/foo" do
|
|
@@ -190,18 +314,31 @@ post "/foo" do
|
|
|
190
314
|
# "PATH_INFO" => "/api",
|
|
191
315
|
# ...
|
|
192
316
|
|
|
193
|
-
result = Linzer.verify!(request, key: some_client_key)
|
|
317
|
+
result = Linzer.verify!(request, key: some_client_key) rescue false
|
|
194
318
|
# => true
|
|
195
319
|
...
|
|
320
|
+
# proceed with trusted request
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
If the signature is missing or invalid, verify! will raise an exception.
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
head "/bar" do
|
|
328
|
+
begin
|
|
329
|
+
Linzer.verify!(request, key: key)
|
|
330
|
+
rescue Linzer::VerifyError => e
|
|
331
|
+
halt 401, e.message
|
|
332
|
+
end
|
|
196
333
|
end
|
|
197
334
|
```
|
|
198
335
|
|
|
199
|
-
|
|
200
|
-
|
|
336
|
+
#### Dynamic key lookup
|
|
337
|
+
|
|
338
|
+
In many cases, the verification key depends on the `keyid` parameter
|
|
339
|
+
provided in the signature.
|
|
201
340
|
|
|
202
|
-
|
|
203
|
-
a block with the `keyid` parameter extracted from the signature (if any) as argument.
|
|
204
|
-
This can be useful to retrieve key data from databases/caches on the server side, e.g.:
|
|
341
|
+
You can supply a block to resolve keys dynamically:
|
|
205
342
|
|
|
206
343
|
```ruby
|
|
207
344
|
get "/bar" do
|
|
@@ -211,14 +348,22 @@ get "/bar" do
|
|
|
211
348
|
end
|
|
212
349
|
# => true
|
|
213
350
|
...
|
|
351
|
+
# request is now verified
|
|
214
352
|
end
|
|
215
353
|
```
|
|
216
354
|
|
|
217
|
-
|
|
355
|
+
This is useful when:
|
|
356
|
+
|
|
357
|
+
- you have multiple clients
|
|
358
|
+
- keys are stored in a database or external service
|
|
359
|
+
- keys rotate over time
|
|
360
|
+
|
|
361
|
+
#### Verifying responses (client-side)
|
|
218
362
|
|
|
219
|
-
|
|
363
|
+
As expected, signed responses are verified using the same API shown previously:
|
|
220
364
|
|
|
221
365
|
```ruby
|
|
366
|
+
...
|
|
222
367
|
response
|
|
223
368
|
# => #<Net::HTTPOK 200 OK readbody=true>
|
|
224
369
|
response.body
|
|
@@ -228,43 +373,60 @@ result = Linzer.verify!(response, key: pubkey, no_older_than: 600)
|
|
|
228
373
|
# => true
|
|
229
374
|
```
|
|
230
375
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Again, the same principle used to sign outgoing requests, the same method is used,
|
|
234
|
-
see example below:
|
|
376
|
+
Or if you are using Faraday, response verification can be handled by
|
|
377
|
+
middleware as well:
|
|
235
378
|
|
|
236
379
|
```ruby
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
key: my_key,
|
|
244
|
-
components: %w[@status content-type content-digest x-custom-app-header],
|
|
245
|
-
label: "sig1",
|
|
246
|
-
params: {
|
|
247
|
-
created: Time.now.to_i
|
|
248
|
-
}
|
|
249
|
-
)
|
|
250
|
-
response["signature"]
|
|
251
|
-
# => "sig1=:2TPCzD4l48bg6LMcVXdV9u..."
|
|
252
|
-
response["signature-input"]
|
|
253
|
-
# => "sig1=(\"@status\" \"content-type\" \"content-digest\"..."
|
|
254
|
-
...
|
|
380
|
+
require "linzer/faraday"
|
|
381
|
+
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
conn = Faraday.new(url: api_url) do |builder|
|
|
385
|
+
builder.response :http_signature, key: verify_key
|
|
255
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 ...>
|
|
256
394
|
```
|
|
257
395
|
|
|
258
|
-
|
|
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
|
+
|
|
405
|
+
### Using a custom HTTP library
|
|
406
|
+
|
|
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.
|
|
259
411
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
[example adapter for http gem response](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter/http_gem/response.rb)
|
|
263
|
-
included with this gem or the ones
|
|
264
|
-
[provided out of the box](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter).
|
|
412
|
+
If you need tighter integration or are not using Faraday, you can also
|
|
413
|
+
implement a native Linzer adapter directly.
|
|
265
414
|
|
|
266
|
-
|
|
267
|
-
|
|
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.
|
|
418
|
+
|
|
419
|
+
To do this:
|
|
420
|
+
|
|
421
|
+
- implement a simple adapter for your request/response objects
|
|
422
|
+
- register it with `Linzer::Message`
|
|
423
|
+
|
|
424
|
+
You can use the existing adapters as references:
|
|
425
|
+
|
|
426
|
+
- [HTTP gem response adapter](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter/http_gem/response.rb)
|
|
427
|
+
- [Built-in adapters](https://github.com/nomadium/linzer/blob/master/lib/linzer/message/adapter)
|
|
428
|
+
|
|
429
|
+
Example of how to register an adapter before using a custom HTTP library:
|
|
268
430
|
|
|
269
431
|
```ruby
|
|
270
432
|
Linzer::Message.register_adapter(HTTP::Response, Linzer::Message::Adapter::HTTPGem::Response)
|
|
@@ -278,89 +440,96 @@ response["signature-input"]
|
|
|
278
440
|
result = Linzer.verify!(response, key: my_key)
|
|
279
441
|
# => true
|
|
280
442
|
```
|
|
281
|
-
---
|
|
282
443
|
|
|
283
|
-
|
|
284
|
-
control on how the signing and verification routines are performed, Linzer allows
|
|
285
|
-
to manipulate instances of internal HTTP messages (requests & responses, see
|
|
286
|
-
`Linzer::Message` class and available adapters), signature objects
|
|
287
|
-
(`Linzer::Signature`) and how to register additional message adapters for any
|
|
288
|
-
HTTP ruby library not supported out of the box by this gem.
|
|
444
|
+
### Advanced verification
|
|
289
445
|
|
|
290
|
-
|
|
446
|
+
For low-level control over signing and verification, Linzer
|
|
447
|
+
exposes internal message and signature objects. This allows
|
|
448
|
+
you to work directly with `Linzer::Message` and `Linzer::Signature`,
|
|
449
|
+
or integrate custom HTTP adapters if needed.
|
|
291
450
|
|
|
292
|
-
####
|
|
451
|
+
#### Verifying a signature manually
|
|
293
452
|
|
|
294
453
|
```ruby
|
|
295
454
|
test_ed25519_key_pub = key.material.public_to_pem
|
|
296
|
-
# => "-----BEGIN PUBLIC KEY-----\
|
|
455
|
+
# => "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAK1ZrC4JqC356pRs..."
|
|
297
456
|
|
|
298
457
|
pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
|
|
299
458
|
# => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
|
|
300
459
|
|
|
301
460
|
message = Linzer::Message.new(request)
|
|
302
461
|
|
|
303
|
-
signature = Linzer::Signature.build(
|
|
462
|
+
signature = Linzer::Signature.build(request.headers)
|
|
304
463
|
|
|
305
464
|
Linzer.verify(pubkey, message, signature)
|
|
306
465
|
# => true
|
|
307
466
|
```
|
|
308
467
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```ruby
|
|
312
|
-
Linzer.verify(pubkey, message, signature, no_older_than: 500)
|
|
313
|
-
```
|
|
468
|
+
#### Preventing replay attacks
|
|
314
469
|
|
|
315
|
-
|
|
316
|
-
|
|
470
|
+
To reduce the risk of replay attacks (e.g. reusing a captured
|
|
471
|
+
valid request), you can validate the `created` timestamp in the signature.
|
|
317
472
|
|
|
318
|
-
|
|
473
|
+
Linzer supports this via the `no_older_than` option:
|
|
319
474
|
|
|
320
475
|
```ruby
|
|
321
|
-
|
|
322
|
-
lib/linzer/verifier.rb:38:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
|
|
476
|
+
Linzer.verify(pubkey, message, signature, no_older_than: 500)
|
|
323
477
|
```
|
|
324
478
|
|
|
325
|
-
|
|
479
|
+
`no_older_than` expects a number of seconds, but you can pass
|
|
480
|
+
anything that to responds to `#to_i`, including an `ActiveSupport::Duration`.
|
|
326
481
|
|
|
327
|
-
|
|
482
|
+
If the signature is older than the allowed window, verification
|
|
483
|
+
fails with an error.
|
|
328
484
|
|
|
329
|
-
|
|
330
|
-
headers = {
|
|
331
|
-
"date" => "Sat, 30 Mar 2024 21:40:13 GMT",
|
|
332
|
-
"x-response-custom" => "bar"
|
|
333
|
-
}
|
|
485
|
+
## Supported algorithms
|
|
334
486
|
|
|
335
|
-
|
|
336
|
-
# or just use the response object exposed by your HTTP framework
|
|
487
|
+
Linzer currently supports the following signature algorithms:
|
|
337
488
|
|
|
338
|
-
|
|
339
|
-
|
|
489
|
+
- RSASSA-PSS (SHA-512)
|
|
490
|
+
- RSASSA-PKCS1-v1_5 (SHA-256)
|
|
491
|
+
- HMAC-SHA256
|
|
492
|
+
- Ed25519
|
|
493
|
+
- ECDSA (P-256 and P-384 curves).
|
|
340
494
|
|
|
341
|
-
|
|
495
|
+
Of the JSON Web Signature (JWS) algorithms mentioned in RFC 9421,
|
|
496
|
+
only Ed25519 is currently supported. Support for additional
|
|
497
|
+
algorithms is planned and should be straightforward to add.
|
|
342
498
|
|
|
343
|
-
|
|
344
|
-
# => {"signature"=>
|
|
345
|
-
# "sig1=:tCldwXqbISktyABrmbhszo...",
|
|
346
|
-
# "signature-input"=>"sig1=(\"@status\" \"date\" ..."}
|
|
499
|
+
The goal is to support as much of the RFC as possible before the 1.0 release.
|
|
347
500
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
For now, to consult additional details just take a look at source code and/or the unit tests.
|
|
501
|
+
## Documentation
|
|
351
502
|
|
|
352
|
-
|
|
503
|
+
The codebase is well-documented, and the Ruby API documentation is
|
|
504
|
+
available on [Rubydoc](https://www.rubydoc.info/gems/linzer).
|
|
353
505
|
|
|
354
|
-
|
|
355
|
-
in subsequent releases.
|
|
506
|
+
For deeper details or edge cases, the source code and unit tests are also a good reference.
|
|
356
507
|
|
|
357
508
|
## Ruby version compatibility
|
|
358
509
|
|
|
359
510
|
linzer is built in [Continuous Integration](https://github.com/nomadium/linzer/actions/workflows/main.yml) on Ruby 3.0+.
|
|
360
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
|
+
|
|
361
520
|
## Security
|
|
362
521
|
|
|
363
|
-
This gem is provided “as is” without any warranties. It has not
|
|
522
|
+
This gem is provided “as is” without any warranties. It has not
|
|
523
|
+
been independently audited for security vulnerabilities. Users
|
|
524
|
+
are advised to review the code and assess its suitability for their
|
|
525
|
+
use case, particularly in production environments.
|
|
526
|
+
|
|
527
|
+
Despite this, Linzer is already used in production by other projects
|
|
528
|
+
with security-sensitive requirements, including
|
|
529
|
+
[Mastodon](https://github.com/mastodon/mastodon)
|
|
530
|
+
([since version 4.5.0](https://docs.joinmastodon.org/spec/security/#http-message-signatures)).
|
|
531
|
+
This does not constitute a security guarantee or endorsement,
|
|
532
|
+
but it may be useful context when evaluating adoption.
|
|
364
533
|
|
|
365
534
|
## Development
|
|
366
535
|
|
data/flake.lock
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
"nodes": {
|
|
3
3
|
"bundix": {
|
|
4
4
|
"inputs": {
|
|
5
|
-
"nixpkgs":
|
|
5
|
+
"nixpkgs": [
|
|
6
|
+
"nixpkgs"
|
|
7
|
+
]
|
|
6
8
|
},
|
|
7
9
|
"locked": {
|
|
8
10
|
"lastModified": 1762235257,
|
|
@@ -20,25 +22,11 @@
|
|
|
20
22
|
},
|
|
21
23
|
"nixpkgs": {
|
|
22
24
|
"locked": {
|
|
23
|
-
"lastModified":
|
|
24
|
-
"narHash": "sha256-
|
|
25
|
+
"lastModified": 1775710090,
|
|
26
|
+
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
|
25
27
|
"owner": "NixOS",
|
|
26
28
|
"repo": "nixpkgs",
|
|
27
|
-
"rev": "
|
|
28
|
-
"type": "github"
|
|
29
|
-
},
|
|
30
|
-
"original": {
|
|
31
|
-
"id": "nixpkgs",
|
|
32
|
-
"type": "indirect"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"nixpkgs_2": {
|
|
36
|
-
"locked": {
|
|
37
|
-
"lastModified": 1770115704,
|
|
38
|
-
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
|
39
|
-
"owner": "NixOS",
|
|
40
|
-
"repo": "nixpkgs",
|
|
41
|
-
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
|
29
|
+
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
|
42
30
|
"type": "github"
|
|
43
31
|
},
|
|
44
32
|
"original": {
|
|
@@ -48,31 +36,19 @@
|
|
|
48
36
|
"type": "github"
|
|
49
37
|
}
|
|
50
38
|
},
|
|
51
|
-
"nixpkgs_3": {
|
|
52
|
-
"locked": {
|
|
53
|
-
"lastModified": 1678875422,
|
|
54
|
-
"narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
|
|
55
|
-
"owner": "NixOS",
|
|
56
|
-
"repo": "nixpkgs",
|
|
57
|
-
"rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
|
|
58
|
-
"type": "github"
|
|
59
|
-
},
|
|
60
|
-
"original": {
|
|
61
|
-
"id": "nixpkgs",
|
|
62
|
-
"type": "indirect"
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
39
|
"root": {
|
|
66
40
|
"inputs": {
|
|
67
41
|
"bundix": "bundix",
|
|
68
|
-
"nixpkgs": "
|
|
42
|
+
"nixpkgs": "nixpkgs",
|
|
69
43
|
"ruby-nix": "ruby-nix",
|
|
70
44
|
"systems": "systems"
|
|
71
45
|
}
|
|
72
46
|
},
|
|
73
47
|
"ruby-nix": {
|
|
74
48
|
"inputs": {
|
|
75
|
-
"nixpkgs":
|
|
49
|
+
"nixpkgs": [
|
|
50
|
+
"nixpkgs"
|
|
51
|
+
]
|
|
76
52
|
},
|
|
77
53
|
"locked": {
|
|
78
54
|
"lastModified": 1755059052,
|