linzer 0.4.0 → 0.5.0
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 +13 -0
- data/README.md +99 -15
- data/lib/linzer/ecdsa.rb +15 -7
- data/lib/linzer/message.rb +20 -14
- data/lib/linzer/request.rb +93 -0
- data/lib/linzer/response.rb +9 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +8 -0
- data/linzer.gemspec +1 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 026db131a5602b1f62b4f930086a0612b99f17a3aa6aa0eb649baa7bc8ba574b
|
4
|
+
data.tar.gz: 8f29e81e3083257c4731c9e408f4a66a95125fdb4d0678fd8d16cc410637d694
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6edb3a943f2bcf408ed865fe8bec013c3b13e609ea791981474a84bb21936b2e5fa61d86e64f9e9917c0a3e440e6246d443761c81cd94c094898c78423137789
|
7
|
+
data.tar.gz: 82074df18649f3f85af7c187762dbcf90b602c9748c9f6dba4695db42ee53ce24c5a546589f96011ab9d47564e1f9d2436a35616b54678e24f4222cd12f97837
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] - 2024-03-30
|
4
|
+
|
5
|
+
- Build Linzer::Message instances from Rack request and response objects
|
6
|
+
instead of unspecified/ad-hoc hashes with HTTP request and
|
7
|
+
response parameters.
|
8
|
+
|
9
|
+
- Update README examples.
|
10
|
+
|
11
|
+
## [0.4.1] - 2024-03-25
|
12
|
+
|
13
|
+
- Fix one-off error on ECDSA P-256 and P-384 curve signature generation.
|
14
|
+
In some cases, an invalid signature of 63 or 95 bytes could be generated.
|
15
|
+
|
3
16
|
## [0.4.0] - 2024-03-16
|
4
17
|
|
5
18
|
- Add support for capitalized HTTP header names.
|
data/README.md
CHANGED
@@ -17,35 +17,94 @@ Or just `gem install linzer`.
|
|
17
17
|
### To sign a HTTP message:
|
18
18
|
|
19
19
|
```ruby
|
20
|
-
|
20
|
+
key = Linzer.generate_ed25519_key
|
21
21
|
# => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
headers = {
|
24
|
+
"date" => "Fri, 23 Feb 2024 17:57:23 GMT",
|
25
|
+
"x-custom-header" => "foo"
|
26
|
+
}
|
27
|
+
|
28
|
+
request = Linzer.new_request(:post, "/some_uri", {}, headers)
|
29
|
+
# => #<Rack::Request:0x0000000104c1c8c0
|
30
|
+
# @env={"HTTP_DATE"=>"Fri, 23 Feb 2024 17:57:23 GMT", "HTTP_X_CUSTOM..."
|
31
|
+
# @params=nil>
|
32
|
+
|
33
|
+
message = Linzer::Message.new(request)
|
34
|
+
# => #<Linzer::Message:0x0000000104afa960
|
35
|
+
# @operation=#<Rack::Request:0x00000001049754a0
|
36
|
+
# @env={"HTTP_DATE"=>"Fri, 23 Feb 2024 17:57:23 GMT", "HTTP_X_CUSTOM..."
|
37
|
+
# @params=nil>>
|
38
|
+
|
39
|
+
fields = %w[date x-custom-header @method @path]
|
25
40
|
|
26
|
-
fields = %w[date x-custom-header]
|
27
41
|
signature = Linzer.sign(key, message, fields)
|
28
42
|
# => #<Linzer::Signature:0x0000000111f77ad0 ...
|
29
43
|
|
30
|
-
|
31
|
-
{"signature"=>
|
32
|
-
"sig1
|
33
|
-
|
44
|
+
pp signature.to_h
|
45
|
+
# => {"signature"=>"sig1=:Cv1TUCxUpX+5SVa7pH0Xh...",
|
46
|
+
# "signature-input"=>"sig1=(\"date\" \"x-custom-header\" ..."}
|
47
|
+
```
|
48
|
+
|
49
|
+
### Use the message signature with any HTTP client:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
require "net/http"
|
53
|
+
|
54
|
+
http = Net::HTTP.new("localhost", 9292)
|
55
|
+
http.set_debug_output($stderr)
|
56
|
+
response = http.post("/some_uri", "data", headers.merge(signature.to_h))
|
57
|
+
# opening connection to localhost:9292...
|
58
|
+
# opened
|
59
|
+
# <- "POST /some_uri HTTP/1.1\r\n
|
60
|
+
# <- Date: Fri, 23 Feb 2024 17:57:23 GMT\r\n
|
61
|
+
# <- X-Custom-Header: foo\r\n
|
62
|
+
# <- Signature: sig1=:Cv1TUCxUpX+5SVa7pH0X...
|
63
|
+
# <- Signature-Input: sig1=(\"date\" \"x-custom-header\" \"@method\"...
|
64
|
+
# <- Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\n
|
65
|
+
# <- Accept: */*\r\n
|
66
|
+
# <- User-Agent: Ruby\r\n
|
67
|
+
# <- Connection: close\r\n
|
68
|
+
# <- Host: localhost:9292
|
69
|
+
# <- Content-Length: 4\r\n
|
70
|
+
# <- Content-Type: application/x-www-form-urlencoded\r\n\r\n"
|
71
|
+
# <- "data"
|
72
|
+
#
|
73
|
+
# -> "HTTP/1.1 200 OK\r\n"
|
74
|
+
# -> "Content-Type: text/html;charset=utf-8\r\n"
|
75
|
+
# -> "Content-Length: 0\r\n"
|
76
|
+
# -> "X-Xss-Protection: 1; mode=block\r\n"
|
77
|
+
# -> "X-Content-Type-Options: nosniff\r\n"
|
78
|
+
# -> "X-Frame-Options: SAMEORIGIN\r\n"
|
79
|
+
# -> "Server: WEBrick/1.8.1 (Ruby/3.2.0/2022-12-25)\r\n"
|
80
|
+
# -> "Date: Thu, 28 Mar 2024 17:19:21 GMT\r\n"
|
81
|
+
# -> "Connection: close\r\n"
|
82
|
+
# -> "\r\n"
|
83
|
+
# reading 0 bytes...
|
84
|
+
# -> ""
|
85
|
+
# read 0 bytes
|
86
|
+
# Conn close
|
87
|
+
# => #<Net::HTTPOK 200 OK readbody=true>
|
34
88
|
```
|
35
89
|
|
36
90
|
### To verify a valid signature:
|
37
91
|
|
38
92
|
```ruby
|
93
|
+
test_ed25519_key_pub = Base64.strict_encode64(key.material.verify_key.to_bytes)
|
94
|
+
# => "EUra7KsJ8B/lSZJVhDaopMycmZ6T7KtJqKVNJTHKIw0="
|
95
|
+
|
39
96
|
pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
|
40
97
|
# => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
|
41
98
|
|
42
|
-
|
99
|
+
# if you have to, there is a helper method to build a request object on the server side
|
100
|
+
# although any standard Ruby web server or framework (Sinatra, Rails, etc) should expose
|
101
|
+
# a request object and this should not be required for most cases.
|
102
|
+
#
|
103
|
+
# request = Linzer.new_request(:post, "/some_uri", {}, headers)
|
43
104
|
|
44
|
-
message = Linzer::Message.new(
|
45
|
-
# => #<Linzer::Message:0x0000000111b592a0 @headers={"date"=>"Fri, 23 Feb 2024 13:18:15 GMT", ...
|
105
|
+
message = Linzer::Message.new(request)
|
46
106
|
|
47
|
-
signature = Linzer::Signature.build(headers)
|
48
|
-
# => #<Linzer::Signature:0x0000000112396008 ...
|
107
|
+
signature = Linzer::Signature.build(message.headers)
|
49
108
|
|
50
109
|
Linzer.verify(pubkey, message, signature)
|
51
110
|
# => true
|
@@ -55,10 +114,35 @@ Linzer.verify(pubkey, message, signature)
|
|
55
114
|
|
56
115
|
```ruby
|
57
116
|
result = Linzer.verify(pubkey, message, signature)
|
58
|
-
lib/linzer/verifier.rb:
|
117
|
+
lib/linzer/verifier.rb:38:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
|
118
|
+
```
|
119
|
+
|
120
|
+
### HTTP responses are also supported
|
121
|
+
|
122
|
+
HTTP responses can also be signed and verified in the same way as requests.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
headers = {
|
126
|
+
"date" => "Sat, 30 Mar 2024 21:40:13 GMT",
|
127
|
+
"x-response-custom" => "bar"
|
128
|
+
}
|
129
|
+
|
130
|
+
response = Linzer.new_response("request body", 200, headers)
|
131
|
+
# or just use the response object exposed by your HTTP framework
|
132
|
+
|
133
|
+
message = Linzer::Message.new(response)
|
134
|
+
fields = %w[@status date x-response-custom]
|
135
|
+
|
136
|
+
signature = Linzer.sign(key, message, fields)
|
137
|
+
|
138
|
+
pp signature.to_h
|
139
|
+
# => {"signature"=>
|
140
|
+
# "sig1=:tCldwXqbISktyABrmbhszo...",
|
141
|
+
# "signature-input"=>"sig1=(\"@status\" \"date\" ..."}
|
142
|
+
|
59
143
|
```
|
60
144
|
|
61
|
-
For now, to consult additional details
|
145
|
+
For now, to consult additional details just take a look at source code and/or the unit tests.
|
62
146
|
|
63
147
|
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).
|
64
148
|
|
data/lib/linzer/ecdsa.rb
CHANGED
@@ -25,12 +25,12 @@ module Linzer
|
|
25
25
|
case digest
|
26
26
|
when "SHA256"
|
27
27
|
raise Linzer::Error.new(msg) if sig.length != 64
|
28
|
-
r_bn = OpenSSL::BN.new(sig[0..31].unpack1("
|
29
|
-
s_bn = OpenSSL::BN.new(sig[32..63].unpack1("
|
28
|
+
r_bn = OpenSSL::BN.new(sig[0..31].unpack1("H64").to_i(16))
|
29
|
+
s_bn = OpenSSL::BN.new(sig[32..63].unpack1("H64").to_i(16))
|
30
30
|
when "SHA384"
|
31
31
|
raise Linzer::Error.new(msg) if sig.length != 96
|
32
|
-
r_bn = OpenSSL::BN.new(sig[0..47].unpack1("
|
33
|
-
s_bn = OpenSSL::BN.new(sig[48..95].unpack1("
|
32
|
+
r_bn = OpenSSL::BN.new(sig[0..47].unpack1("H96").to_i(16))
|
33
|
+
s_bn = OpenSSL::BN.new(sig[48..95].unpack1("H96").to_i(16))
|
34
34
|
else
|
35
35
|
msg = "Cannot verify signature, unsupported digest algorithm: '%s'" % digest
|
36
36
|
raise Linzer::Error.new(msg)
|
@@ -44,13 +44,21 @@ module Linzer
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def decode_der_signature(der_sig)
|
47
|
+
digest = @params[:digest]
|
48
|
+
msg = "Unsupported digest algorithm: '%s'" % digest
|
47
49
|
OpenSSL::ASN1
|
48
50
|
.decode(der_sig)
|
49
51
|
.value
|
50
|
-
.map
|
51
|
-
|
52
|
+
.map do |n|
|
53
|
+
case digest
|
54
|
+
when "SHA256" then "%.64x" % n.value
|
55
|
+
when "SHA384" then "%.96x" % n.value
|
56
|
+
else raise Linzer::Error.new(msg)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
.map { |s| [s].pack("H#{s.length}") }
|
52
60
|
.reduce(:<<)
|
53
|
-
.
|
61
|
+
.encode(Encoding::ASCII_8BIT)
|
54
62
|
end
|
55
63
|
end
|
56
64
|
end
|
data/lib/linzer/message.rb
CHANGED
@@ -2,20 +2,23 @@
|
|
2
2
|
|
3
3
|
module Linzer
|
4
4
|
class Message
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@headers = Hash(request_data.fetch(:headers, {})
|
8
|
-
.transform_keys(&:downcase)
|
9
|
-
.clone).freeze
|
5
|
+
def initialize(operation)
|
6
|
+
@operation = operation
|
10
7
|
freeze
|
11
8
|
end
|
12
9
|
|
13
|
-
def
|
14
|
-
@
|
10
|
+
def request?
|
11
|
+
@operation.is_a?(Rack::Request) || @operation.respond_to?(:request_method)
|
15
12
|
end
|
16
13
|
|
17
|
-
def
|
18
|
-
@
|
14
|
+
def response?
|
15
|
+
@operation.is_a?(Rack::Response) || @operation.respond_to?(:status)
|
16
|
+
end
|
17
|
+
|
18
|
+
def headers
|
19
|
+
return @operation.headers if response? || @operation.respond_to?(:headers)
|
20
|
+
|
21
|
+
Request.headers(@operation)
|
19
22
|
end
|
20
23
|
|
21
24
|
def field?(f)
|
@@ -23,13 +26,16 @@ module Linzer
|
|
23
26
|
end
|
24
27
|
|
25
28
|
def [](field_name)
|
26
|
-
|
29
|
+
if !field_name.start_with?("@")
|
30
|
+
return @operation.env[Request.rack_header_name(field_name)] if request?
|
31
|
+
return @operation.headers[field_name] # if response?
|
32
|
+
end
|
27
33
|
|
28
34
|
case field_name
|
29
|
-
when "@method" then @
|
30
|
-
when "@authority" then @
|
31
|
-
when "@path" then @
|
32
|
-
when "@status" then @
|
35
|
+
when "@method" then @operation.request_method
|
36
|
+
when "@authority" then @operation.authority
|
37
|
+
when "@path" then @operation.path_info
|
38
|
+
when "@status" then @operation.status
|
33
39
|
else # XXX: improve this and add support for all fields in the RFC
|
34
40
|
raise Error.new "Unknown/unsupported field: \"#{field_name}\""
|
35
41
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
module Request
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def build(verb, uri = "/", params = {}, headers = {})
|
8
|
+
validate verb, uri, params, headers
|
9
|
+
|
10
|
+
# XXX: to-do: handle rack request params?
|
11
|
+
request_method = Rack.const_get(verb.upcase)
|
12
|
+
args = {
|
13
|
+
"REQUEST_METHOD" => request_method,
|
14
|
+
"PATH_INFO" => uri.to_str
|
15
|
+
}
|
16
|
+
|
17
|
+
Rack::Request.new(build_rack_env(headers).merge(args))
|
18
|
+
end
|
19
|
+
|
20
|
+
def rack_header_name(field_name)
|
21
|
+
validate_header_name field_name
|
22
|
+
|
23
|
+
rack_name = field_name.upcase.tr("-", "_")
|
24
|
+
case field_name.downcase
|
25
|
+
when "content-type", "content-length"
|
26
|
+
rack_name
|
27
|
+
else
|
28
|
+
"HTTP_#{rack_name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def headers(rack_request)
|
33
|
+
rack_request
|
34
|
+
.each_header
|
35
|
+
.to_h
|
36
|
+
.select do |k, _|
|
37
|
+
k.start_with?("HTTP_") || %w[CONTENT_TYPE CONTENT_LENGTH].include?(k)
|
38
|
+
end
|
39
|
+
.transform_keys { |k| k.downcase.tr("_", "-") }
|
40
|
+
.transform_keys do |k|
|
41
|
+
%w[content-type content-length].include?(k) ? k : k.gsub(/^http-/, "")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def validate(verb, uri, params, headers)
|
48
|
+
validate_verb verb
|
49
|
+
validate_uri uri
|
50
|
+
validate_arg_hash headers: headers
|
51
|
+
validate_arg_hash params: params
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_verb(verb)
|
55
|
+
Rack.const_get(verb.upcase)
|
56
|
+
rescue => ex
|
57
|
+
unknown_method = "Unknown/invalid HTTP request method"
|
58
|
+
raise Error.new, unknown_method, cause: ex
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_uri(uri)
|
62
|
+
uri.to_str
|
63
|
+
rescue => ex
|
64
|
+
invalid_uri = "Invalid URI"
|
65
|
+
raise Error.new, invalid_uri, cause: ex
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate_arg_hash(hsh)
|
69
|
+
arg_name = hsh.keys.first
|
70
|
+
hsh[arg_name].to_hash
|
71
|
+
rescue => ex
|
72
|
+
err_msg = "invalid \"#{arg_name}\" parameter, cannot be converted to hash."
|
73
|
+
raise Error.new, "Cannot build request: #{err_msg}", cause: ex
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_header_name(name)
|
77
|
+
raise ArgumentError.new, "Blank header name." if name.empty?
|
78
|
+
name.to_str
|
79
|
+
rescue => ex
|
80
|
+
err_msg = "Invalid header name: '#{name}'"
|
81
|
+
raise Error.new, err_msg, cause: ex
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_rack_env(headers)
|
85
|
+
headers
|
86
|
+
.to_hash
|
87
|
+
.transform_keys { |k| k.upcase.tr("-", "_") }
|
88
|
+
.transform_keys do |k|
|
89
|
+
%w[CONTENT_TYPE CONTENT_LENGTH].include?(k) ? k : "HTTP_#{k}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
@@ -2,9 +2,12 @@
|
|
2
2
|
|
3
3
|
require "starry"
|
4
4
|
require "openssl"
|
5
|
+
require "rack"
|
5
6
|
|
6
7
|
require_relative "linzer/version"
|
7
8
|
require_relative "linzer/common"
|
9
|
+
require_relative "linzer/request"
|
10
|
+
require_relative "linzer/response"
|
8
11
|
require_relative "linzer/message"
|
9
12
|
require_relative "linzer/signature"
|
10
13
|
require_relative "linzer/key"
|
@@ -21,6 +24,11 @@ module Linzer
|
|
21
24
|
|
22
25
|
class << self
|
23
26
|
include Key::Helper
|
27
|
+
include Response
|
28
|
+
|
29
|
+
def new_request(verb, uri = "/", params = {}, headers = {})
|
30
|
+
Linzer::Request.build(verb, uri, params, headers)
|
31
|
+
end
|
24
32
|
|
25
33
|
def verify(pubkey, message, signature)
|
26
34
|
Linzer::Verifier.verify(pubkey, message, signature)
|
data/linzer.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: linzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miguel Landaeta
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-03-
|
11
|
+
date: 2024-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ed25519
|
@@ -44,6 +44,20 @@ dependencies:
|
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '0.1'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rack
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
47
61
|
description:
|
48
62
|
email:
|
49
63
|
- miguel@miguel.cc
|
@@ -66,6 +80,8 @@ files:
|
|
66
80
|
- lib/linzer/key.rb
|
67
81
|
- lib/linzer/key/helper.rb
|
68
82
|
- lib/linzer/message.rb
|
83
|
+
- lib/linzer/request.rb
|
84
|
+
- lib/linzer/response.rb
|
69
85
|
- lib/linzer/rsa.rb
|
70
86
|
- lib/linzer/signature.rb
|
71
87
|
- lib/linzer/signer.rb
|