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 +4 -4
- data/.standard.yml +1 -0
- data/CHANGELOG.md +11 -0
- data/README.md +37 -0
- data/examples/sinatra/Gemfile +9 -0
- data/examples/sinatra/config.ru +11 -0
- data/examples/sinatra/http-signatures.yml +33 -0
- data/examples/sinatra/myapp.rb +21 -0
- data/lib/linzer/hmac.rb +13 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +1 -0
- data/lib/rack/auth/signature/helpers.rb +132 -0
- data/lib/rack/auth/signature.rb +115 -0
- metadata +29 -3
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/.standard.yml
CHANGED
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,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
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.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
|
@@ -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.
|
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:
|
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.
|
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: []
|