linzer 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|