linzer 0.6.5 → 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: 6d5b1a0dfcd7d402e3a5bf2e22102271e3273e1f7092e4dad09ae7078e04dcf3
4
- data.tar.gz: 197933c6728e55678e7f29e5161b57fecd0ca39bacb586260412674a538cd8b1
3
+ metadata.gz: fd7b5e233ec42d94c7040f774097c612850eef9c5795a2c8d3251bc618c8df5f
4
+ data.tar.gz: 53cc818e2fc68fe1f6b3c89414a5b0d2393e1b8cfa8e4c9f3ba9076ce537f81c
5
5
  SHA512:
6
- metadata.gz: f6da1cd0829f06c15f1a5cf66af191836294ffeea666e15f969f28c7171bf2b8e887b1fb5aa405fbfc7c840719ce82cc6cdae3b7238f987bb00f6c71e238c930
7
- data.tar.gz: 047a03b94451dcffe5982165bc14acc7c6419fe4d8ff9f72e2d4510a77fae87c5960fabdaa0ff129d89724b0185a37454e3a6d9ec6ea903192eadc013e1abb64
6
+ metadata.gz: 93e5fb0d4fd0cd534691102a02efa8ae14589d12ee3636f86d10d19e57104b1bb122f950c9191c5fb40264fc453a31d33aa1369fac0fe34451e74108ce9c6a5c
7
+ data.tar.gz: e5b12b53e46f7958c560cb579111c14eab85da6a047effcdb599d39f955fe38b89175faef1937a963890c8d8254763c54314ea95c4eff70815fc0c0b8c396551
data/.standard.yml CHANGED
@@ -8,3 +8,4 @@ ignore:
8
8
  - Layout/HashAlignment
9
9
  - Layout/ArgumentAlignment
10
10
  - Style/EmptyCaseCondition
11
+ - Style/RescueModifier
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
10
+ ## [0.7.0.beta1] - 2025-04-12
11
+
12
+ - Introduce Rack::Auth::Signature middleware.
13
+
3
14
  ## [0.6.5] - 2025-04-09
4
15
 
5
16
  - Add support for RSA (RSASSA-PKCS1-V1_5) and improve RSASSA-PSS handling.
data/README.md CHANGED
@@ -21,6 +21,43 @@ Or just `gem install linzer`.
21
21
 
22
22
  ## Usage
23
23
 
24
+ ### TL;DR: I just want to protect my application!!
25
+
26
+ Add the following middleware to you run Rack application and configure it
27
+ as needed, e.g.:
28
+
29
+ ```ruby
30
+ # config.ru
31
+ use Rack::Auth::Signature, except: "/login",
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"}
34
+ ```
35
+
36
+ or on more complex scenarios:
37
+
38
+ ```ruby
39
+ # config.ru
40
+ use Rack::Auth::Signature, except: "/login",
41
+ config_path: "app/configuration/http-signatures.yml"
42
+ ```
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
+
52
+ And that's it, all routes in the example app (except `/login`) above will
53
+ require a valid signature created with the respective private key held by a
54
+ client. For more details on what configuration options are available, take a
55
+ look at
56
+ [examples/sinatra/http-signatures.yml](https://github.com/nomadium/linzer/tree/master/examples/sinatra/http-signatures.yml) to get started and/or
57
+ [lib/rack/auth/signature.rb](https://github.com/nomadium/linzer/tree/master/lib/rack/auth/signature.rb) for full implementation details.
58
+
59
+ To learn about more specific scenarios or use cases, keep reading on below.
60
+
24
61
  ### To sign a HTTP message:
25
62
 
26
63
  ```ruby
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sinatra", "~> 4.0"
6
+ gem "rackup", "~> 2.1"
7
+ gem "webrick", "~> 1.9"
8
+ gem "rack-contrib", "~> 2.4"
9
+ gem "linzer", "~> 0.7.0.beta2"
@@ -0,0 +1,11 @@
1
+ require "rack"
2
+ require "rack/contrib"
3
+ require "linzer"
4
+ require_relative "myapp"
5
+
6
+ set :root, File.dirname(__FILE__)
7
+
8
+ use Rack::Auth::Signature, except: "/",
9
+ config_path: "http-signatures.yml"
10
+
11
+ run Sinatra::Application
@@ -0,0 +1,33 @@
1
+ ---
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:
22
+ foo:
23
+ alg: ed25519
24
+ material: |
25
+ -----BEGIN PUBLIC KEY-----
26
+ MCowBQYDK2VwAyEAMEH9bSanwgAWE5qxUEaXjK6qei8z2hiHT0nlr7ljG0Y=
27
+ -----END PUBLIC KEY-----
28
+ bar:
29
+ alg: rsa-pss-sha512
30
+ path: pubkey_rsa.pem
31
+ baz:
32
+ alg: hmac-sha256
33
+ path: app.secret
@@ -0,0 +1,21 @@
1
+ # myapp.rb
2
+ require "sinatra"
3
+
4
+ get "/" do
5
+ "Hello world!"
6
+ end
7
+
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
+ #
20
+ "secure area"
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.6.5"
4
+ VERSION = "0.7.0.beta2"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -21,6 +21,7 @@ require_relative "linzer/ecdsa"
21
21
  require_relative "linzer/key/helper"
22
22
  require_relative "linzer/signer"
23
23
  require_relative "linzer/verifier"
24
+ require_relative "rack/auth/signature"
24
25
 
25
26
  module Linzer
26
27
  class Error < StandardError; 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
@@ -0,0 +1,115 @@
1
+ require "linzer"
2
+ require "logger"
3
+ require_relative "signature/helpers"
4
+
5
+ # Rack::Auth::Signature implements HTTP Message Signatures, as per RFC 9421.
6
+ #
7
+ # Initialize with the Rack application that you want protecting.
8
+ # A hash with options and a block can be passed to customize, enhance
9
+ # or disable security checks applied to incoming requests.
10
+ #
11
+ module Rack
12
+ module Auth
13
+ class Signature
14
+ include Helpers
15
+
16
+ def initialize(app, options = {}, &block)
17
+ @app = app
18
+ @options = load_options(Hash(options))
19
+ instance_eval(&block) if block
20
+ end
21
+
22
+ def call(env)
23
+ @request = Rack::Request.new(env)
24
+
25
+ if excluded? || allowed?
26
+ @app.call(env)
27
+ else
28
+ response = options[:signatures][:error_response].values
29
+ Rack::Response.new(*response).finish
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def excluded?
36
+ return false if !request
37
+ Array(options[:except]).include?(request.path_info)
38
+ end
39
+
40
+ def allowed?
41
+ has_signature? && acceptable? && verifiable?
42
+ end
43
+
44
+ attr_reader :request, :options
45
+
46
+ def params
47
+ @signature.parameters || {}
48
+ end
49
+
50
+ def logger
51
+ @logger ||= request.logger || ::Logger.new($stderr)
52
+ end
53
+
54
+ def has_signature?
55
+ @signature = build_signature
56
+ (@signature.to_h.keys & %w[signature signature-input]).size == 2
57
+ rescue => ex
58
+ logger.error ex.message
59
+ false
60
+ end
61
+
62
+ def build_signature
63
+ signature_opts = {}
64
+ label = options[:signatures][:default_label]
65
+ signature_opts[:label] = label if label
66
+
67
+ @message = Linzer::Message.new(request)
68
+ signature = Linzer::Signature.build(@message.headers, **signature_opts)
69
+ request.env["rack.signature"] = signature
70
+ signature
71
+ end
72
+
73
+ def has_required_params?
74
+ created? && expires? && keyid? && nonce? && alg? && tag?
75
+ rescue => ex
76
+ logger.error ex.message
77
+ false
78
+ end
79
+
80
+ def has_required_components?
81
+ components = @signature.components || []
82
+ covered_components = options[:signatures][:covered_components]
83
+ warning = "Insufficient coverage by signature. Consult S 7.2.1. in RFC"
84
+ logger.warn warning if covered_components.empty?
85
+ (covered_components || []).all? { |c| components.include?(c) }
86
+ end
87
+
88
+ def acceptable?
89
+ has_required_params? && has_required_components?
90
+ end
91
+
92
+ def verifiable?
93
+ verify_opts = build_and_check_verify_opts || {}
94
+ Linzer.verify(key, @message, @signature, **verify_opts)
95
+ rescue => ex
96
+ logger.error ex.message
97
+ false
98
+ end
99
+
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
105
+
106
+ if reject_older
107
+ age = Integer(reject_older)
108
+ verify_opts[:no_older_than] = age
109
+ end
110
+
111
+ verify_opts
112
+ end
113
+ end
114
+ end
115
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linzer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.7.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miguel Landaeta
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: openssl
@@ -103,6 +103,26 @@ dependencies:
103
103
  - - ">="
104
104
  - !ruby/object:Gem::Version
105
105
  version: 3.1.2
106
+ - !ruby/object:Gem::Dependency
107
+ name: logger
108
+ requirement: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '1.7'
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 1.7.0
116
+ type: :runtime
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '1.7'
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 1.7.0
106
126
  email:
107
127
  - miguel@miguel.cc
108
128
  executables: []
@@ -116,6 +136,10 @@ files:
116
136
  - LICENSE.txt
117
137
  - README.md
118
138
  - Rakefile
139
+ - examples/sinatra/Gemfile
140
+ - examples/sinatra/config.ru
141
+ - examples/sinatra/http-signatures.yml
142
+ - examples/sinatra/myapp.rb
119
143
  - lib/linzer.rb
120
144
  - lib/linzer/common.rb
121
145
  - lib/linzer/ecdsa.rb
@@ -132,6 +156,8 @@ files:
132
156
  - lib/linzer/signer.rb
133
157
  - lib/linzer/verifier.rb
134
158
  - lib/linzer/version.rb
159
+ - lib/rack/auth/signature.rb
160
+ - lib/rack/auth/signature/helpers.rb
135
161
  homepage: https://github.com/nomadium/linzer
136
162
  licenses:
137
163
  - MIT
@@ -153,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
179
  - !ruby/object:Gem::Version
154
180
  version: '0'
155
181
  requirements: []
156
- rubygems_version: 3.6.2
182
+ rubygems_version: 3.6.7
157
183
  specification_version: 4
158
184
  summary: An implementation of HTTP Messages Signatures (RFC9421)
159
185
  test_files: []