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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a19b05f41aa933779f504056b56b00fc1a8a6cda19bb26065dce56177caf0c2
4
- data.tar.gz: 25d7d8b4afbd60a3e6e872064d92f9be22670466292e23a6ba8010ca546cdf23
3
+ metadata.gz: fd7b5e233ec42d94c7040f774097c612850eef9c5795a2c8d3251bc618c8df5f
4
+ data.tar.gz: 53cc818e2fc68fe1f6b3c89414a5b0d2393e1b8cfa8e4c9f3ba9076ce537f81c
5
5
  SHA512:
6
- metadata.gz: 527fd9a73a1605b706fc7ca597ce14f4f5c95be099907bf61832aa698b11fe51dbf25a5eaf70d48d7c6715c91c27bfa391482a88a67f0e932c5f60ff8f3f16df
7
- data.tar.gz: 2b30454801734973711afe81559ad18f09f6d08f473796241d56e1b13d5619971a697945703b85012e18ae969059ca5a3718e677fc71716151bd46e795eac8a7
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, e.g.:
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: {"key" => IO.read("app/config/pubkey.pem"), "alg" => "ed25519"}
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
@@ -6,4 +6,4 @@ gem "sinatra", "~> 4.0"
6
6
  gem "rackup", "~> 2.1"
7
7
  gem "webrick", "~> 1.9"
8
8
  gem "rack-contrib", "~> 2.4"
9
- gem "linzer", "~> 0.7.0.beta1"
9
+ gem "linzer", "~> 0.7.0.beta2"
@@ -1,26 +1,33 @@
1
1
  ---
2
- no_older_than: 6000
3
- created_required: true
4
- keyid_required: true
5
- # nonce_required: false
6
- # alg_required: false
7
- # tag_required: false
8
- # expires_required: false
9
- covered_components:
10
- - "@method"
11
- - "@request-target"
12
- - date
13
- known_keys:
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
- key: |
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: rsa.pem
30
+ path: pubkey_rsa.pem
31
+ baz:
32
+ alg: hmac-sha256
33
+ path: app.secret
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.7.0.beta1"
4
+ VERSION = "0.7.0.beta2"
5
5
  end
@@ -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
@@ -1,6 +1,6 @@
1
1
  require "linzer"
2
2
  require "logger"
3
- require "yaml"
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
- @message = Linzer::Message.new(request)
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 created?
78
- required = options[:created_required]
79
- if required
80
- params.key?("created") && Integer(params["created"])
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
- def alg?
106
- required = options[:alg_required]
107
- required ? params.key?("alg") : !required
108
- end
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 key
152
- build_key(params["keyid"] || :default)
153
- end
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
- def build_key(keyid)
156
- material = if keyid == :default
157
- options[:default_key]
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
- key_not_found = "Key not found. Signature cannot be verified."
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.beta1
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