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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94cad96e720cc1235948c2a2ef20b1056f444b4a456364c8551cfda414c539fb
4
- data.tar.gz: fbd2f500e2c64e010fa380ac525e2afeef1a1fc7de055655263138a1a6afab27
3
+ metadata.gz: 026db131a5602b1f62b4f930086a0612b99f17a3aa6aa0eb649baa7bc8ba574b
4
+ data.tar.gz: 8f29e81e3083257c4731c9e408f4a66a95125fdb4d0678fd8d16cc410637d694
5
5
  SHA512:
6
- metadata.gz: f187e852f367e8d6428eb8cb61919693c67f0650a6b723e20211b1eabc47d0834c0b1facaaf89790e0d2e7335fe65752c09b0b136734e9d3214578b54fac794c
7
- data.tar.gz: a5fa3a4eb24c9362918197d0828347095a354ed2151f3f08e6582d42854a0fd19460163b16523148823f82ada8a1aa95a4d467553490b909b39cba52a0ae9166
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
- irb(main):001:0> key = Linzer.generate_ed25519_key
20
+ key = Linzer.generate_ed25519_key
21
21
  # => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
22
22
 
23
- message = Linzer::Message.new(headers: {"date" => "Fri, 23 Feb 2024 17:57:23 GMT", "x-custom-header" => "foo"})
24
- # => #<Linzer::Message:0x0000000111b592a0 @headers={"date"=>"Fri, 23 Feb 2024 17:57:23 GMT", ...
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
- puts signature.to_h
31
- {"signature"=>
32
- "sig1=:8rLY3nFtezwwsK+sqZEMe7wzbNHojZJGEnvp3suKichgwH...",
33
- "signature-input"=>"sig1=(\"date\" \"x-custom-header\");created=1709075013;keyid=\"test-key-ed25519\""}
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
- headers = {"signature-input" => "...", signature => "...", "date" => "Fri, 23 Feb 2024 13:18:15 GMT", "x-custom-header" => "bar"})
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(headers)
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:34:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
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, just take a look at source code and/or the unit tests.
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("H*").to_i(16))
29
- s_bn = OpenSSL::BN.new(sig[32..63].unpack1("H*").to_i(16))
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("H*").to_i(16))
33
- s_bn = OpenSSL::BN.new(sig[48..95].unpack1("H*").to_i(16))
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 { |n| n.value.to_s(16) }
51
- .map { |s| [s].pack("H*") }
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
- .force_encoding(Encoding::ASCII_8BIT)
61
+ .encode(Encoding::ASCII_8BIT)
54
62
  end
55
63
  end
56
64
  end
@@ -2,20 +2,23 @@
2
2
 
3
3
  module Linzer
4
4
  class Message
5
- def initialize(request_data)
6
- @http = Hash(request_data[:http].clone).freeze
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 empty?
14
- @headers.empty?
10
+ def request?
11
+ @operation.is_a?(Rack::Request) || @operation.respond_to?(:request_method)
15
12
  end
16
13
 
17
- def header?(header)
18
- @headers.key?(header)
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
- return @headers[field_name] if !field_name.start_with?("@")
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 @http["method"]
30
- when "@authority" then @http["host"]
31
- when "@path" then @http["path"]
32
- when "@status" then @http["status"]
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module Response
5
+ def new_response(body = nil, status = 200, headers = {})
6
+ Rack::Response.new(body, status, headers)
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_runtime_dependency "ed25519", "~> 1.3", ">= 1.3.0"
33
33
  spec.add_runtime_dependency "starry", "~> 0.1"
34
+ spec.add_runtime_dependency "rack", "~> 3.0"
34
35
  end
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.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-16 00:00:00.000000000 Z
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