linzer 0.7.0.beta1 → 0.7.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/CHANGELOG.md +7 -0
- data/README.md +12 -2
- data/examples/sinatra/Gemfile +1 -1
- data/examples/sinatra/http-signatures.yml +25 -18
- data/examples/sinatra/myapp.rb +11 -0
- data/lib/linzer/hmac.rb +13 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/rack/auth/signature/helpers.rb +132 -0
- data/lib/rack/auth/signature.rb +25 -116
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd7b5e233ec42d94c7040f774097c612850eef9c5795a2c8d3251bc618c8df5f
|
4
|
+
data.tar.gz: 53cc818e2fc68fe1f6b3c89414a5b0d2393e1b8cfa8e4c9f3ba9076ce537f81c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93e5fb0d4fd0cd534691102a02efa8ae14589d12ee3636f86d10d19e57104b1bb122f950c9191c5fb40264fc453a31d33aa1369fac0fe34451e74108ce9c6a5c
|
7
|
+
data.tar.gz: e5b12b53e46f7958c560cb579111c14eab85da6a047effcdb599d39f955fe38b89175faef1937a963890c8d8254763c54314ea95c4eff70815fc0c0b8c396551
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.7.0.beta2] - 2025-04-13
|
4
|
+
|
5
|
+
- Refactor and improve Rack::Auth::Signature code organization.
|
6
|
+
- Do not expose secret material on HMAC SHA-256 key when #inspect method is used.
|
7
|
+
- Update Rack::Auth::Signature configuration file options.
|
8
|
+
- Validate and test Rack::Auth::Signature with example Rails and Sinatra apps.
|
9
|
+
|
3
10
|
## [0.7.0.beta1] - 2025-04-12
|
4
11
|
|
5
12
|
- Introduce Rack::Auth::Signature middleware.
|
data/README.md
CHANGED
@@ -23,12 +23,14 @@ Or just `gem install linzer`.
|
|
23
23
|
|
24
24
|
### TL;DR: I just want to protect my application!!
|
25
25
|
|
26
|
-
Add the following middleware to you run Rack application
|
26
|
+
Add the following middleware to you run Rack application and configure it
|
27
|
+
as needed, e.g.:
|
27
28
|
|
28
29
|
```ruby
|
29
30
|
# config.ru
|
30
31
|
use Rack::Auth::Signature, except: "/login",
|
31
|
-
default_key: {
|
32
|
+
default_key: {material: Base64.strict_decode64(ENV["MYAPP_KEY"]), alg: "hmac-sha256"}
|
33
|
+
# or: default_key: {material: IO.read("app/config/pubkey.pem"), "ed25519"}
|
32
34
|
```
|
33
35
|
|
34
36
|
or on more complex scenarios:
|
@@ -39,6 +41,14 @@ use Rack::Auth::Signature, except: "/login",
|
|
39
41
|
config_path: "app/configuration/http-signatures.yml"
|
40
42
|
```
|
41
43
|
|
44
|
+
or with a typical Rails application:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# config/application.rb
|
48
|
+
config.middleware.use Rack::Auth::Signature, except: "/login",
|
49
|
+
config_path: "http-signatures.yml"
|
50
|
+
```
|
51
|
+
|
42
52
|
And that's it, all routes in the example app (except `/login`) above will
|
43
53
|
require a valid signature created with the respective private key held by a
|
44
54
|
client. For more details on what configuration options are available, take a
|
data/examples/sinatra/Gemfile
CHANGED
@@ -1,26 +1,33 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
- "@
|
12
|
-
-
|
13
|
-
|
2
|
+
signatures:
|
3
|
+
reject_older_than: 6000 # seconds
|
4
|
+
created_required: true
|
5
|
+
keyid_required: true
|
6
|
+
# nonce_required: false
|
7
|
+
# alg_required: false
|
8
|
+
# tag_required: false
|
9
|
+
# expires_required: false
|
10
|
+
covered_components:
|
11
|
+
- "@method"
|
12
|
+
- "@request-target"
|
13
|
+
- date
|
14
|
+
# In most cases is not needed to configure a label but it
|
15
|
+
# could useful in the event of receiving a signature
|
16
|
+
# header with more than 1 signature. Currently, linzer signatures
|
17
|
+
# middleware will only validate 1 signature per request and multiple
|
18
|
+
# signatures validation at the same time are not supported.
|
19
|
+
# If you need this, feel free to open an issue and explain your use case.
|
20
|
+
# default_label: "mylabel"
|
21
|
+
keys:
|
14
22
|
foo:
|
15
23
|
alg: ed25519
|
16
|
-
|
24
|
+
material: |
|
17
25
|
-----BEGIN PUBLIC KEY-----
|
18
26
|
MCowBQYDK2VwAyEAMEH9bSanwgAWE5qxUEaXjK6qei8z2hiHT0nlr7ljG0Y=
|
19
27
|
-----END PUBLIC KEY-----
|
20
28
|
bar:
|
21
|
-
alg: hmac-sha256
|
22
|
-
key: !binary |-
|
23
|
-
KuOR/Q8U1Crgp0WsFqW+5ZuC+KbN41dIlXWp71UhPEeJcRfuxZEy6XAUE9nf0yl4jt55yRASBnD0kQKucO6SfA==
|
24
|
-
baz:
|
25
29
|
alg: rsa-pss-sha512
|
26
|
-
path:
|
30
|
+
path: pubkey_rsa.pem
|
31
|
+
baz:
|
32
|
+
alg: hmac-sha256
|
33
|
+
path: app.secret
|
data/examples/sinatra/myapp.rb
CHANGED
@@ -6,5 +6,16 @@ get "/" do
|
|
6
6
|
end
|
7
7
|
|
8
8
|
get "/protected" do
|
9
|
+
# if signature is valid, rack will expose it in the request object,
|
10
|
+
# so application specific checks can be performed, e.g.:
|
11
|
+
#
|
12
|
+
# halt if !request.env["rack.signature"].parameters["nonce"]
|
13
|
+
# halt if redis.exists?(request.env["rack.signature"].parameters["nonce"])
|
14
|
+
# halt unless request.env["rack.signature"].parameters["tag"] == "myapp"
|
15
|
+
#
|
16
|
+
# Note that these examples are deliberately simple for illustration
|
17
|
+
# purposes since such validations would make more sense to be
|
18
|
+
# encapsulated in helper methods called in a before { ... } block.
|
19
|
+
#
|
9
20
|
"secure area"
|
10
21
|
end
|
data/lib/linzer/hmac.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "digest"
|
4
|
+
|
3
5
|
module Linzer
|
4
6
|
module HMAC
|
5
7
|
class Key < Linzer::Key
|
@@ -15,6 +17,17 @@ module Linzer
|
|
15
17
|
def verify(signature, data)
|
16
18
|
signature == sign(data)
|
17
19
|
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
vars =
|
23
|
+
instance_variables
|
24
|
+
.reject { |v| v == :@material } # don't leak secret unneccesarily
|
25
|
+
.map do |n|
|
26
|
+
"#{n}=#{instance_variable_get(n).inspect}"
|
27
|
+
end
|
28
|
+
oid = Digest::SHA2.hexdigest(object_id.to_s)[48..63]
|
29
|
+
"#<%s:0x%s %s>" % [self.class, oid, vars.join(", ")]
|
30
|
+
end
|
18
31
|
end
|
19
32
|
end
|
20
33
|
end
|
data/lib/linzer/version.rb
CHANGED
@@ -0,0 +1,132 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Auth
|
5
|
+
class Signature
|
6
|
+
module Helpers
|
7
|
+
module Parameters
|
8
|
+
private
|
9
|
+
|
10
|
+
def created?
|
11
|
+
!options[:signatures][:created_required] || !!Integer(params.fetch("created"))
|
12
|
+
end
|
13
|
+
|
14
|
+
def expires?
|
15
|
+
return true if !options[:signatures][:expires_required]
|
16
|
+
Integer(params.fetch("expires")) > Time.now.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def keyid?
|
20
|
+
!options[:signatures][:keyid_required] || String(params.fetch("keyid"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def nonce?
|
24
|
+
!options[:signatures][:nonce_required] || String(params.fetch("nonce"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def alg?
|
28
|
+
!options[:signatures][:alg_required] || String(params.fetch("alg"))
|
29
|
+
end
|
30
|
+
|
31
|
+
def tag?
|
32
|
+
!options[:signatures][:tag_required] || String(params.fetch("tag"))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module Configuration
|
37
|
+
DEFAULT_OPTIONS = {
|
38
|
+
signatures: {
|
39
|
+
reject_older_than: 900,
|
40
|
+
created_required: true,
|
41
|
+
nonce_required: false,
|
42
|
+
alg_required: false,
|
43
|
+
tag_required: false,
|
44
|
+
expires_required: false,
|
45
|
+
keyid_required: false,
|
46
|
+
covered_components: %w[@method @request-target @authority date],
|
47
|
+
error_response: {body: [], status: 401, headers: {}}
|
48
|
+
},
|
49
|
+
keys: {}
|
50
|
+
}
|
51
|
+
|
52
|
+
private_constant :DEFAULT_OPTIONS
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def load_options(options)
|
57
|
+
options_from_file = load_options_from_config_file(options)
|
58
|
+
{
|
59
|
+
except: options[:except] || options_from_file[:except],
|
60
|
+
default_key: options[:default_key] || options_from_file[:default_key],
|
61
|
+
signatures:
|
62
|
+
DEFAULT_OPTIONS[:signatures]
|
63
|
+
.merge(options_from_file[:signatures].to_h)
|
64
|
+
.merge(options[:signatures].to_h),
|
65
|
+
keys:
|
66
|
+
DEFAULT_OPTIONS[:keys]
|
67
|
+
.merge(options_from_file[:keys].to_h)
|
68
|
+
.merge(options[:keys].to_h)
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def load_options_from_config_file(options)
|
73
|
+
config_path = options[:config_path]
|
74
|
+
YAML.safe_load_file(config_path, symbolize_names: true)
|
75
|
+
rescue
|
76
|
+
{}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module Key
|
81
|
+
private
|
82
|
+
|
83
|
+
def key
|
84
|
+
build_key(params["keyid"])
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_key(keyid)
|
88
|
+
key_data = if keyid.nil? ||
|
89
|
+
(!options[:keys].key?(keyid.to_sym) && options[:default_key])
|
90
|
+
options[:default_key].to_h
|
91
|
+
else
|
92
|
+
options[:keys][keyid.to_sym] || {}
|
93
|
+
end
|
94
|
+
|
95
|
+
if key_data.key?(:path) && !key_data.key?(:material)
|
96
|
+
key_data[:material] = IO.read(key_data[:path]) rescue nil
|
97
|
+
end
|
98
|
+
|
99
|
+
if !key_data || key_data.empty? || !key_data[:material]
|
100
|
+
key_not_found = "Key not found. Signature cannot be verified."
|
101
|
+
raise Linzer::Error.new key_not_found
|
102
|
+
end
|
103
|
+
|
104
|
+
alg = @signature.parameters["alg"] || key_data[:alg] || :unknown
|
105
|
+
instantiate_key(keyid || :default, alg, key_data)
|
106
|
+
end
|
107
|
+
|
108
|
+
def instantiate_key(keyid, alg, key_data)
|
109
|
+
key_methods = {
|
110
|
+
"rsa-pss-sha512" => :new_rsa_pss_sha512_key,
|
111
|
+
"rsa-v1_5-sha256" => :new_rsa_v1_5_sha256_key,
|
112
|
+
"hmac-sha256" => :new_hmac_sha256_key,
|
113
|
+
"ecdsa-p256-sha256" => :new_ecdsa_p256_sha256_key,
|
114
|
+
"ecdsa-p384-sha384" => :new_ecdsa_p384_sha384_key,
|
115
|
+
"ed25519" => :new_ed25519_public_key
|
116
|
+
}
|
117
|
+
method = key_methods[alg]
|
118
|
+
|
119
|
+
alg_error = "Unsupported or unknown signature algorithm"
|
120
|
+
raise Linzer::Error.new alg_error unless method
|
121
|
+
|
122
|
+
Linzer.public_send(method, key_data[:material], keyid.to_s)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
include Parameters
|
127
|
+
include Configuration
|
128
|
+
include Key
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/rack/auth/signature.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "linzer"
|
2
2
|
require "logger"
|
3
|
-
|
3
|
+
require_relative "signature/helpers"
|
4
4
|
|
5
5
|
# Rack::Auth::Signature implements HTTP Message Signatures, as per RFC 9421.
|
6
6
|
#
|
@@ -11,9 +11,11 @@ require "yaml"
|
|
11
11
|
module Rack
|
12
12
|
module Auth
|
13
13
|
class Signature
|
14
|
+
include Helpers
|
15
|
+
|
14
16
|
def initialize(app, options = {}, &block)
|
15
17
|
@app = app
|
16
|
-
@options = load_options(options)
|
18
|
+
@options = load_options(Hash(options))
|
17
19
|
instance_eval(&block) if block
|
18
20
|
end
|
19
21
|
|
@@ -23,7 +25,7 @@ module Rack
|
|
23
25
|
if excluded? || allowed?
|
24
26
|
@app.call(env)
|
25
27
|
else
|
26
|
-
response = options[:error_response].values
|
28
|
+
response = options[:signatures][:error_response].values
|
27
29
|
Rack::Response.new(*response).finish
|
28
30
|
end
|
29
31
|
end
|
@@ -39,21 +41,6 @@ module Rack
|
|
39
41
|
has_signature? && acceptable? && verifiable?
|
40
42
|
end
|
41
43
|
|
42
|
-
DEFAULT_OPTIONS = {
|
43
|
-
no_older_than: 900,
|
44
|
-
created_required: true,
|
45
|
-
nonce_required: false,
|
46
|
-
alg_required: false,
|
47
|
-
tag_required: false,
|
48
|
-
expires_required: false,
|
49
|
-
keyid_required: false,
|
50
|
-
known_keys: {},
|
51
|
-
covered_components: %w[@method @request-target @authority date],
|
52
|
-
error_response: {body: [], status: 401, headers: {}}
|
53
|
-
}
|
54
|
-
|
55
|
-
private_constant :DEFAULT_OPTIONS
|
56
|
-
|
57
44
|
attr_reader :request, :options
|
58
45
|
|
59
46
|
def params
|
@@ -65,51 +52,22 @@ module Rack
|
|
65
52
|
end
|
66
53
|
|
67
54
|
def has_signature?
|
68
|
-
@
|
69
|
-
@signature = Linzer::Signature.build(@message.headers)
|
70
|
-
request.env["rack.signature"] = @signature
|
55
|
+
@signature = build_signature
|
71
56
|
(@signature.to_h.keys & %w[signature signature-input]).size == 2
|
72
57
|
rescue => ex
|
73
58
|
logger.error ex.message
|
74
59
|
false
|
75
60
|
end
|
76
61
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
else
|
82
|
-
!required
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def expires?
|
87
|
-
required = options[:expires_required]
|
88
|
-
if required
|
89
|
-
params.key?("expires") && Integer(params["expires"]) > Time.now.to_i
|
90
|
-
else
|
91
|
-
!required
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def keyid?
|
96
|
-
required = options[:keyid_required]
|
97
|
-
required ? params.key?("keyid") : !required
|
98
|
-
end
|
99
|
-
|
100
|
-
def nonce?
|
101
|
-
required = options[:nonce_required]
|
102
|
-
required ? params.key?("nonce") : !required
|
103
|
-
end
|
62
|
+
def build_signature
|
63
|
+
signature_opts = {}
|
64
|
+
label = options[:signatures][:default_label]
|
65
|
+
signature_opts[:label] = label if label
|
104
66
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
def tag?
|
111
|
-
required = options[:tag_required]
|
112
|
-
required ? params.key?("tag") : !required
|
67
|
+
@message = Linzer::Message.new(request)
|
68
|
+
signature = Linzer::Signature.build(@message.headers, **signature_opts)
|
69
|
+
request.env["rack.signature"] = signature
|
70
|
+
signature
|
113
71
|
end
|
114
72
|
|
115
73
|
def has_required_params?
|
@@ -121,7 +79,7 @@ module Rack
|
|
121
79
|
|
122
80
|
def has_required_components?
|
123
81
|
components = @signature.components || []
|
124
|
-
covered_components = options[:covered_components]
|
82
|
+
covered_components = options[:signatures][:covered_components]
|
125
83
|
warning = "Insufficient coverage by signature. Consult S 7.2.1. in RFC"
|
126
84
|
logger.warn warning if covered_components.empty?
|
127
85
|
(covered_components || []).all? { |c| components.include?(c) }
|
@@ -132,74 +90,25 @@ module Rack
|
|
132
90
|
end
|
133
91
|
|
134
92
|
def verifiable?
|
135
|
-
verify_opts = {}
|
136
|
-
|
137
|
-
warning = "Risk of signature replay! (Consult S 7.2.2. in RFC)"
|
138
|
-
logger.warn warning unless options[:no_older_than]
|
139
|
-
|
140
|
-
if options[:no_older_than]
|
141
|
-
age = Integer(options[:no_older_than])
|
142
|
-
verify_opts[:no_older_than] = age
|
143
|
-
end
|
144
|
-
|
93
|
+
verify_opts = build_and_check_verify_opts || {}
|
145
94
|
Linzer.verify(key, @message, @signature, **verify_opts)
|
146
95
|
rescue => ex
|
147
96
|
logger.error ex.message
|
148
97
|
false
|
149
98
|
end
|
150
99
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
100
|
+
def build_and_check_verify_opts
|
101
|
+
verify_opts = {}
|
102
|
+
reject_older = options[:signatures][:reject_older_than]
|
103
|
+
warning = "Risk of signature replay! (Consult S 7.2.2. in RFC)"
|
104
|
+
logger.warn warning unless reject_older
|
154
105
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
else
|
159
|
-
key_data = options[:known_keys][keyid] || {}
|
160
|
-
if !key_data.key?("key") && key_data.key?("path")
|
161
|
-
key_data["key"] = IO.read(key_data["path"]) rescue nil
|
162
|
-
end
|
163
|
-
key_data
|
106
|
+
if reject_older
|
107
|
+
age = Integer(reject_older)
|
108
|
+
verify_opts[:no_older_than] = age
|
164
109
|
end
|
165
110
|
|
166
|
-
|
167
|
-
raise Linzer::Error.new key_not_found if !material || !material["key"]
|
168
|
-
|
169
|
-
alg = @signature.parameters["alg"] || material["alg"] || :unknown
|
170
|
-
instantiate_key(keyid, alg, material)
|
171
|
-
end
|
172
|
-
|
173
|
-
def instantiate_key(keyid, alg, material)
|
174
|
-
key_methods = {
|
175
|
-
"rsa-pss-sha512" => :new_rsa_pss_sha512_key,
|
176
|
-
"rsa-v1_5-sha256" => :new_rsa_v1_5_sha256_key,
|
177
|
-
"hmac-sha256" => :new_hmac_sha256_key,
|
178
|
-
"ecdsa-p256-sha256" => :new_ecdsa_p256_sha256_key,
|
179
|
-
"ecdsa-p384-sha384" => :new_ecdsa_p384_sha384_key,
|
180
|
-
"ed25519" => :new_ed25519_public_key
|
181
|
-
}
|
182
|
-
method = key_methods[alg]
|
183
|
-
|
184
|
-
alg_error = "Unsupported or unknown signature algorithm"
|
185
|
-
raise Linzer::Error.new alg_error unless method
|
186
|
-
|
187
|
-
Linzer.public_send(method, material["key"], keyid.to_s)
|
188
|
-
end
|
189
|
-
|
190
|
-
def load_options(options)
|
191
|
-
DEFAULT_OPTIONS
|
192
|
-
.merge(load_options_from_config_file(options))
|
193
|
-
.merge(Hash(options)) || {}
|
194
|
-
end
|
195
|
-
|
196
|
-
def load_options_from_config_file(options)
|
197
|
-
config_path = options[:config_path]
|
198
|
-
YAML
|
199
|
-
.safe_load_file(config_path)
|
200
|
-
.transform_keys(&:to_sym)
|
201
|
-
rescue
|
202
|
-
{}
|
111
|
+
verify_opts
|
203
112
|
end
|
204
113
|
end
|
205
114
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: linzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.0.
|
4
|
+
version: 0.7.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miguel Landaeta
|
@@ -157,6 +157,7 @@ files:
|
|
157
157
|
- lib/linzer/verifier.rb
|
158
158
|
- lib/linzer/version.rb
|
159
159
|
- lib/rack/auth/signature.rb
|
160
|
+
- lib/rack/auth/signature/helpers.rb
|
160
161
|
homepage: https://github.com/nomadium/linzer
|
161
162
|
licenses:
|
162
163
|
- MIT
|