linzer 0.6.4 → 0.7.0.beta1

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: bff7f24f79244ceffa58e5b005b45b2384b34f2d23eda4c0a613060c4f708fa4
4
- data.tar.gz: bfee805d74cf0e1fc3d3588ec6eac8a1de5b96a16021198d016f0be9fb48e371
3
+ metadata.gz: 9a19b05f41aa933779f504056b56b00fc1a8a6cda19bb26065dce56177caf0c2
4
+ data.tar.gz: 25d7d8b4afbd60a3e6e872064d92f9be22670466292e23a6ba8010ca546cdf23
5
5
  SHA512:
6
- metadata.gz: d62f773a0d20b09bb97987180f251a70b4db5c29eb4ecf2112e486c9169e418085823c813fa68e4063347d421328f171604c3f82bdf1b6ffae0385bfc41fd8ce
7
- data.tar.gz: 0e53c41f9485a8028f11f67e368ca0ec60a82f8e795973d258fb96a57cfdcfe085308f4cd2eb0ac55fe8b5ee39c1a4f8d56d3d9d09fa57ee720eda17831e66ae
6
+ metadata.gz: 527fd9a73a1605b706fc7ca597ce14f4f5c95be099907bf61832aa698b11fe51dbf25a5eaf70d48d7c6715c91c27bfa391482a88a67f0e932c5f60ff8f3f16df
7
+ data.tar.gz: 2b30454801734973711afe81559ad18f09f6d08f473796241d56e1b13d5619971a697945703b85012e18ae969059ca5a3718e677fc71716151bd46e795eac8a7
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,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0.beta1] - 2025-04-12
4
+
5
+ - Introduce Rack::Auth::Signature middleware.
6
+
7
+ ## [0.6.5] - 2025-04-09
8
+
9
+ - Add support for RSA (RSASSA-PKCS1-V1_5) and improve RSASSA-PSS handling.
10
+ Pull request [#10](https://github.com/nomadium/linzer/pull/10)
11
+ by [oneiros](https://github.com/oneiros).
12
+
3
13
  ## [0.6.4] - 2025-04-04
4
14
 
5
15
  - Allow validating the `created` parameter to mitigate the
data/README.md CHANGED
@@ -21,6 +21,33 @@ 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, e.g.:
27
+
28
+ ```ruby
29
+ # config.ru
30
+ use Rack::Auth::Signature, except: "/login",
31
+ default_key: {"key" => IO.read("app/config/pubkey.pem"), "alg" => "ed25519"}
32
+ ```
33
+
34
+ or on more complex scenarios:
35
+
36
+ ```ruby
37
+ # config.ru
38
+ use Rack::Auth::Signature, except: "/login",
39
+ config_path: "app/configuration/http-signatures.yml"
40
+ ```
41
+
42
+ And that's it, all routes in the example app (except `/login`) above will
43
+ require a valid signature created with the respective private key held by a
44
+ client. For more details on what configuration options are available, take a
45
+ look at
46
+ [examples/sinatra/http-signatures.yml](https://github.com/nomadium/linzer/tree/master/examples/sinatra/http-signatures.yml) to get started and/or
47
+ [lib/rack/auth/signature.rb](https://github.com/nomadium/linzer/tree/master/lib/rack/auth/signature.rb) for full implementation details.
48
+
49
+ To learn about more specific scenarios or use cases, keep reading on below.
50
+
24
51
  ### To sign a HTTP message:
25
52
 
26
53
  ```ruby
@@ -160,7 +187,7 @@ pp signature.to_h
160
187
 
161
188
  For now, to consult additional details just take a look at source code and/or the unit tests.
162
189
 
163
- Please note that is still early days and extensive testing is still ongoing. For now only the following algorithms are supported: RSASSA-PSS using SHA-512, HMAC-SHA256, Ed25519 and ECDSA (P-256 and P-384 curves).
190
+ Please note that is still early days and extensive testing is still ongoing. For now the following algorithms are supported: RSASSA-PSS using SHA-512, RSASSA-PKCS1-v1_5 using SHA-256, HMAC-SHA256, Ed25519 and ECDSA (P-256 and P-384 curves). JSON Web Signature (JWS) algorithms mentioned in the RFC are not supported yet.
164
191
 
165
192
  I'll be expanding the library to cover more functionality specified in the RFC
166
193
  in subsequent releases.
@@ -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.beta1"
@@ -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,26 @@
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:
14
+ foo:
15
+ alg: ed25519
16
+ key: |
17
+ -----BEGIN PUBLIC KEY-----
18
+ MCowBQYDK2VwAyEAMEH9bSanwgAWE5qxUEaXjK6qei8z2hiHT0nlr7ljG0Y=
19
+ -----END PUBLIC KEY-----
20
+ bar:
21
+ alg: hmac-sha256
22
+ key: !binary |-
23
+ KuOR/Q8U1Crgp0WsFqW+5ZuC+KbN41dIlXWp71UhPEeJcRfuxZEy6XAUE9nf0yl4jt55yRASBnD0kQKucO6SfA==
24
+ baz:
25
+ alg: rsa-pss-sha512
26
+ path: rsa.pem
@@ -0,0 +1,10 @@
1
+ # myapp.rb
2
+ require "sinatra"
3
+
4
+ get "/" do
5
+ "Hello world!"
6
+ end
7
+
8
+ get "/protected" do
9
+ "secure area"
10
+ end
@@ -4,24 +4,38 @@ module Linzer
4
4
  class Key
5
5
  module Helper
6
6
  def generate_rsa_pss_sha512_key(size, key_id = nil)
7
- material = OpenSSL::PKey::RSA.generate(size)
8
- Linzer::RSA::Key.new(material, id: key_id, digest: "SHA512")
7
+ material = OpenSSL::PKey.generate_key("RSASSA-PSS")
8
+ Linzer::RSAPSS::Key.new(material, id: key_id, digest: "SHA512")
9
9
  end
10
10
 
11
11
  def new_rsa_pss_sha512_key(material, key_id = nil)
12
12
  key = OpenSSL::PKey.read(material)
13
- Linzer::RSA::Key.new(key, id: key_id, digest: "SHA512")
13
+ Linzer::RSAPSS::Key.new(key, id: key_id, digest: "SHA512")
14
14
  end
15
15
 
16
+ # XXX: investigate: was this method made redundant after:
17
+ # https://github.com/nomadium/linzer/pull/10
16
18
  def new_rsa_pss_sha512_public_key(material, key_id = nil)
17
19
  key = OpenSSL::PKey::RSA.new(material)
18
- Linzer::RSA::Key.new(key, id: key_id, digest: "SHA512")
20
+ Linzer::RSAPSS::Key.new(key, id: key_id, digest: "SHA512")
19
21
  end
20
22
 
21
- # XXX: to-do
22
- # Linzer::RSA::Key
23
- # def new_rsa_v1_5_sha256_key
24
- # def generate_rsa_v1_5_sha256_key
23
+ def generate_rsa_v1_5_sha256_key(size, key_id = nil)
24
+ material = OpenSSL::PKey::RSA.generate(size)
25
+ Linzer::RSA::Key.new(material, id: key_id, digest: "SHA256")
26
+ end
27
+
28
+ def new_rsa_v1_5_sha256_key(material, key_id = nil)
29
+ key = OpenSSL::PKey.read(material)
30
+ Linzer::RSA::Key.new(key, id: key_id, digest: "SHA256")
31
+ end
32
+
33
+ # XXX: investigate: was this method made redundant after:
34
+ # https://github.com/nomadium/linzer/pull/10
35
+ def new_rsa_v1_5_sha256_public_key(material, key_id = nil)
36
+ key = OpenSSL::PKey.read(material)
37
+ Linzer::RSA::Key.new(key, id: key_id, digest: "SHA256")
38
+ end
25
39
 
26
40
  def generate_hmac_sha256_key(key_id = nil)
27
41
  material = OpenSSL::Random.random_bytes(64)
data/lib/linzer/rsa.rb CHANGED
@@ -15,13 +15,7 @@ module Linzer
15
15
 
16
16
  def verify(signature, data)
17
17
  # XXX: should check if the key is usable for verifying
18
- return true if @material.verify_pss(
19
- @params[:digest],
20
- signature,
21
- data,
22
- salt_length: @params[:salt_length] || :auto,
23
- mgf1_hash: @params[:digest]
24
- )
18
+ return true if @material.verify(@params[:digest], signature, data)
25
19
  false
26
20
  end
27
21
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module RSAPSS
5
+ SALT_LENGTH = 64
6
+
7
+ class Key < Linzer::Key
8
+ def validate
9
+ super
10
+ validate_digest
11
+ end
12
+
13
+ def sign(data)
14
+ # XXX: should check if the key is usable for signing
15
+ @material.sign(@params[:digest], data, signature_options)
16
+ end
17
+
18
+ def verify(signature, data)
19
+ # XXX: should check if the key is usable for verifying
20
+ return true if @material.verify(
21
+ @params[:digest],
22
+ signature,
23
+ data,
24
+ signature_options
25
+ )
26
+ false
27
+ end
28
+
29
+ private
30
+
31
+ def signature_options
32
+ {
33
+ rsa_padding_mode: "pss",
34
+ rsa_pss_saltlen: @params[:salt_length] || SALT_LENGTH,
35
+ rsa_mgf1_md: @params[:digest]
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.6.4"
4
+ VERSION = "0.7.0.beta1"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -14,12 +14,14 @@ require_relative "linzer/message"
14
14
  require_relative "linzer/signature"
15
15
  require_relative "linzer/key"
16
16
  require_relative "linzer/rsa"
17
+ require_relative "linzer/rsa_pss"
17
18
  require_relative "linzer/hmac"
18
19
  require_relative "linzer/ed25519"
19
20
  require_relative "linzer/ecdsa"
20
21
  require_relative "linzer/key/helper"
21
22
  require_relative "linzer/signer"
22
23
  require_relative "linzer/verifier"
24
+ require_relative "rack/auth/signature"
23
25
 
24
26
  module Linzer
25
27
  class Error < StandardError; end
@@ -0,0 +1,206 @@
1
+ require "linzer"
2
+ require "logger"
3
+ require "yaml"
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
+ def initialize(app, options = {}, &block)
15
+ @app = app
16
+ @options = load_options(options)
17
+ instance_eval(&block) if block
18
+ end
19
+
20
+ def call(env)
21
+ @request = Rack::Request.new(env)
22
+
23
+ if excluded? || allowed?
24
+ @app.call(env)
25
+ else
26
+ response = options[:error_response].values
27
+ Rack::Response.new(*response).finish
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def excluded?
34
+ return false if !request
35
+ Array(options[:except]).include?(request.path_info)
36
+ end
37
+
38
+ def allowed?
39
+ has_signature? && acceptable? && verifiable?
40
+ end
41
+
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
+ attr_reader :request, :options
58
+
59
+ def params
60
+ @signature.parameters || {}
61
+ end
62
+
63
+ def logger
64
+ @logger ||= request.logger || ::Logger.new($stderr)
65
+ end
66
+
67
+ def has_signature?
68
+ @message = Linzer::Message.new(request)
69
+ @signature = Linzer::Signature.build(@message.headers)
70
+ request.env["rack.signature"] = @signature
71
+ (@signature.to_h.keys & %w[signature signature-input]).size == 2
72
+ rescue => ex
73
+ logger.error ex.message
74
+ false
75
+ end
76
+
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
104
+
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
113
+ end
114
+
115
+ def has_required_params?
116
+ created? && expires? && keyid? && nonce? && alg? && tag?
117
+ rescue => ex
118
+ logger.error ex.message
119
+ false
120
+ end
121
+
122
+ def has_required_components?
123
+ components = @signature.components || []
124
+ covered_components = options[:covered_components]
125
+ warning = "Insufficient coverage by signature. Consult S 7.2.1. in RFC"
126
+ logger.warn warning if covered_components.empty?
127
+ (covered_components || []).all? { |c| components.include?(c) }
128
+ end
129
+
130
+ def acceptable?
131
+ has_required_params? && has_required_components?
132
+ end
133
+
134
+ 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
+
145
+ Linzer.verify(key, @message, @signature, **verify_opts)
146
+ rescue => ex
147
+ logger.error ex.message
148
+ false
149
+ end
150
+
151
+ def key
152
+ build_key(params["keyid"] || :default)
153
+ end
154
+
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
164
+ end
165
+
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
+ {}
203
+ end
204
+ end
205
+ end
206
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linzer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.7.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miguel Landaeta
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-04-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: openssl
@@ -104,7 +103,26 @@ dependencies:
104
103
  - - ">="
105
104
  - !ruby/object:Gem::Version
106
105
  version: 3.1.2
107
- description:
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
108
126
  email:
109
127
  - miguel@miguel.cc
110
128
  executables: []
@@ -118,6 +136,10 @@ files:
118
136
  - LICENSE.txt
119
137
  - README.md
120
138
  - Rakefile
139
+ - examples/sinatra/Gemfile
140
+ - examples/sinatra/config.ru
141
+ - examples/sinatra/http-signatures.yml
142
+ - examples/sinatra/myapp.rb
121
143
  - lib/linzer.rb
122
144
  - lib/linzer/common.rb
123
145
  - lib/linzer/ecdsa.rb
@@ -129,10 +151,12 @@ files:
129
151
  - lib/linzer/request.rb
130
152
  - lib/linzer/response.rb
131
153
  - lib/linzer/rsa.rb
154
+ - lib/linzer/rsa_pss.rb
132
155
  - lib/linzer/signature.rb
133
156
  - lib/linzer/signer.rb
134
157
  - lib/linzer/verifier.rb
135
158
  - lib/linzer/version.rb
159
+ - lib/rack/auth/signature.rb
136
160
  homepage: https://github.com/nomadium/linzer
137
161
  licenses:
138
162
  - MIT
@@ -140,7 +164,6 @@ metadata:
140
164
  homepage_uri: https://github.com/nomadium/linzer
141
165
  source_code_uri: https://github.com/nomadium/linzer
142
166
  changelog_uri: https://github.com/nomadium/linzer/blob/master/CHANGELOG.md
143
- post_install_message:
144
167
  rdoc_options: []
145
168
  require_paths:
146
169
  - lib
@@ -155,8 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
178
  - !ruby/object:Gem::Version
156
179
  version: '0'
157
180
  requirements: []
158
- rubygems_version: 3.4.3
159
- signing_key:
181
+ rubygems_version: 3.6.7
160
182
  specification_version: 4
161
183
  summary: An implementation of HTTP Messages Signatures (RFC9421)
162
184
  test_files: []