linzer 0.4.1 → 0.5.1

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: e05d05474d794c882d28a6fb83949147497218f141ae1e40e6c07e750407b421
4
- data.tar.gz: 58b26e247acd0ca13e9029ace69ebb7459f1639b7908733e07ac88566ac255d1
3
+ metadata.gz: 1955734eb1a8115753448b579b0cbe84f7d1ad247677ae20dd49495841b4aef3
4
+ data.tar.gz: abf9b2fbc465f10210c77f3d29e10c7e15af26a5badba90516f0b96050e3a548
5
5
  SHA512:
6
- metadata.gz: 3af97f2888d5c4bd40900c490945590077604b93c954819c9308d2ab8fe767c491a08d2cbe5054aaced580a4da568a1d1cf11f82048048532feb5150dd59dfea
7
- data.tar.gz: b4a47fd541623baef9582cc0b361975b504cc7824fd926f47f56f7f692f6a6d8dec0d8e4afdb5e64967d6e1d7a2107656165b95f383af5e902bddd26f0efe387
6
+ metadata.gz: 5263ec042dd91dc5bc434f4bbccc8f17d0ec52c751b7b9adec613c87efa180175cc7e5b4f47da007e6e61298a2b153f9ca05250ba3d55786c2d5609725233903
7
+ data.tar.gz: 45ad9720b910f441bf36f9766ec10eaaab64fb7a36f420b947a5170d9cae0d18d540f380996eba2265b6faa0df0b9537fb1c8b2b2e58c94bab45abcaccb19112
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2024-04-01
4
+
5
+ - Add support for additional derived components:
6
+ @target-uri, @scheme, @request-target, @query and @query-param.
7
+
8
+ ## [0.5.0] - 2024-03-30
9
+
10
+ - Build Linzer::Message instances from Rack request and response objects
11
+ instead of unspecified/ad-hoc hashes with HTTP request and
12
+ response parameters.
13
+
14
+ - Update README examples.
15
+
3
16
  ## [0.4.1] - 2024-03-25
4
17
 
5
18
  - Fix one-off error on ECDSA P-256 and P-384 curve signature generation.
data/README.md CHANGED
@@ -1,4 +1,11 @@
1
- # Linzer
1
+ # Linzer [![Latest Version][gem-badge]][gem-link] [![License: MIT][license-image]][license-link] [![CI Status][ci-image]][ci-link]
2
+
3
+ [gem-badge]: https://badge.fury.io/rb/linzer.svg
4
+ [gem-link]: https://rubygems.org/gems/linzer
5
+ [license-image]: https://img.shields.io/badge/license-MIT-blue.svg
6
+ [license-link]: https://github.com/nomadium/linzer/blob/master/LICENSE.txt
7
+ [ci-image]: https://github.com/nomadium/linzer/actions/workflows/main.yml/badge.svg?branch=master
8
+ [ci-link]: https://github.com/nomadium/linzer/actions/workflows/main.yml
2
9
 
3
10
  Linzer is a Ruby library for [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421.html).
4
11
 
@@ -17,35 +24,97 @@ Or just `gem install linzer`.
17
24
  ### To sign a HTTP message:
18
25
 
19
26
  ```ruby
20
- irb(main):001:0> key = Linzer.generate_ed25519_key
27
+ key = Linzer.generate_ed25519_key
21
28
  # => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
22
29
 
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", ...
30
+ headers = {
31
+ "date" => "Fri, 23 Feb 2024 17:57:23 GMT",
32
+ "x-custom-header" => "foo"
33
+ }
34
+
35
+ request = Linzer.new_request(:post, "/some_uri", {}, headers)
36
+ # => #<Rack::Request:0x0000000104c1c8c0
37
+ # @env={"HTTP_DATE"=>"Fri, 23 Feb 2024 17:57:23 GMT", "HTTP_X_CUSTOM..."
38
+ # @params=nil>
39
+
40
+ message = Linzer::Message.new(request)
41
+ # => #<Linzer::Message:0x0000000104afa960
42
+ # @operation=#<Rack::Request:0x00000001049754a0
43
+ # @env={"HTTP_DATE"=>"Fri, 23 Feb 2024 17:57:23 GMT", "HTTP_X_CUSTOM..."
44
+ # @params=nil>>
45
+
46
+ fields = %w[date x-custom-header @method @path]
25
47
 
26
- fields = %w[date x-custom-header]
27
48
  signature = Linzer.sign(key, message, fields)
28
49
  # => #<Linzer::Signature:0x0000000111f77ad0 ...
29
50
 
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\""}
51
+ pp signature.to_h
52
+ # => {"signature"=>"sig1=:Cv1TUCxUpX+5SVa7pH0Xh...",
53
+ # "signature-input"=>"sig1=(\"date\" \"x-custom-header\" ..."}
54
+ ```
55
+
56
+ ### Use the message signature with any HTTP client:
57
+
58
+ ```ruby
59
+ require "net/http"
60
+
61
+ http = Net::HTTP.new("localhost", 9292)
62
+ http.set_debug_output($stderr)
63
+ response = http.post("/some_uri", "data", headers.merge(signature.to_h))
64
+ # opening connection to localhost:9292...
65
+ # opened
66
+ # <- "POST /some_uri HTTP/1.1\r\n
67
+ # <- Date: Fri, 23 Feb 2024 17:57:23 GMT\r\n
68
+ # <- X-Custom-Header: foo\r\n
69
+ # <- Signature: sig1=:Cv1TUCxUpX+5SVa7pH0X...
70
+ # <- Signature-Input: sig1=(\"date\" \"x-custom-header\" \"@method\"...
71
+ # <- Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\n
72
+ # <- Accept: */*\r\n
73
+ # <- User-Agent: Ruby\r\n
74
+ # <- Connection: close\r\n
75
+ # <- Host: localhost:9292
76
+ # <- Content-Length: 4\r\n
77
+ # <- Content-Type: application/x-www-form-urlencoded\r\n\r\n"
78
+ # <- "data"
79
+ #
80
+ # -> "HTTP/1.1 200 OK\r\n"
81
+ # -> "Content-Type: text/html;charset=utf-8\r\n"
82
+ # -> "Content-Length: 0\r\n"
83
+ # -> "X-Xss-Protection: 1; mode=block\r\n"
84
+ # -> "X-Content-Type-Options: nosniff\r\n"
85
+ # -> "X-Frame-Options: SAMEORIGIN\r\n"
86
+ # -> "Server: WEBrick/1.8.1 (Ruby/3.2.0/2022-12-25)\r\n"
87
+ # -> "Date: Thu, 28 Mar 2024 17:19:21 GMT\r\n"
88
+ # -> "Connection: close\r\n"
89
+ # -> "\r\n"
90
+ # reading 0 bytes...
91
+ # -> ""
92
+ # read 0 bytes
93
+ # Conn close
94
+ # => #<Net::HTTPOK 200 OK readbody=true>
34
95
  ```
35
96
 
36
97
  ### To verify a valid signature:
37
98
 
38
99
  ```ruby
39
- pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
100
+ test_ed25519_key_pub = Base64.strict_encode64(key.material.verify_key.to_bytes)
101
+ # => "EUra7KsJ8B/lSZJVhDaopMycmZ6T7KtJqKVNJTHKIw0="
102
+
103
+ raw_pubkey = Base64.strict_decode64(test_ed25519_key_pub)
104
+ # => "\xB1rM\xFFR\x1F\xDDw\x00\x89\..."
105
+
106
+ pubkey = Linzer.new_ed25519_public_key(raw_pubkey, "some-key-ed25519")
40
107
  # => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
41
108
 
42
- headers = {"signature-input" => "...", signature => "...", "date" => "Fri, 23 Feb 2024 13:18:15 GMT", "x-custom-header" => "bar"})
109
+ # if you have to, there is a helper method to build a request object on the server side
110
+ # although any standard Ruby web server or framework (Sinatra, Rails, etc) should expose
111
+ # a request object and this should not be required for most cases.
112
+ #
113
+ # request = Linzer.new_request(:post, "/some_uri", {}, headers)
43
114
 
44
- message = Linzer::Message.new(headers)
45
- # => #<Linzer::Message:0x0000000111b592a0 @headers={"date"=>"Fri, 23 Feb 2024 13:18:15 GMT", ...
115
+ message = Linzer::Message.new(request)
46
116
 
47
- signature = Linzer::Signature.build(headers)
48
- # => #<Linzer::Signature:0x0000000112396008 ...
117
+ signature = Linzer::Signature.build(message.headers)
49
118
 
50
119
  Linzer.verify(pubkey, message, signature)
51
120
  # => true
@@ -55,10 +124,35 @@ Linzer.verify(pubkey, message, signature)
55
124
 
56
125
  ```ruby
57
126
  result = Linzer.verify(pubkey, message, signature)
58
- lib/linzer/verifier.rb:34:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
127
+ lib/linzer/verifier.rb:38:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
128
+ ```
129
+
130
+ ### HTTP responses are also supported
131
+
132
+ HTTP responses can also be signed and verified in the same way as requests.
133
+
134
+ ```ruby
135
+ headers = {
136
+ "date" => "Sat, 30 Mar 2024 21:40:13 GMT",
137
+ "x-response-custom" => "bar"
138
+ }
139
+
140
+ response = Linzer.new_response("request body", 200, headers)
141
+ # or just use the response object exposed by your HTTP framework
142
+
143
+ message = Linzer::Message.new(response)
144
+ fields = %w[@status date x-response-custom]
145
+
146
+ signature = Linzer.sign(key, message, fields)
147
+
148
+ pp signature.to_h
149
+ # => {"signature"=>
150
+ # "sig1=:tCldwXqbISktyABrmbhszo...",
151
+ # "signature-input"=>"sig1=(\"@status\" \"date\" ..."}
152
+
59
153
  ```
60
154
 
61
- For now, to consult additional details, just take a look at source code and/or the unit tests.
155
+ For now, to consult additional details just take a look at source code and/or the unit tests.
62
156
 
63
157
  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
158
 
@@ -2,37 +2,56 @@
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)
22
25
  !!self[f]
23
26
  end
24
27
 
28
+ DERIVED_COMPONENT = {
29
+ "@method" => :request_method,
30
+ "@authority" => :authority,
31
+ "@path" => :path_info,
32
+ "@status" => :status,
33
+ "@target-uri" => :url,
34
+ "@scheme" => :scheme,
35
+ "@request-target" => :fullpath,
36
+ "@query" => :query_string
37
+ }.freeze
38
+
25
39
  def [](field_name)
26
- return @headers[field_name] if !field_name.start_with?("@")
40
+ if !field_name.start_with?("@")
41
+ return @operation.env[Request.rack_header_name(field_name)] if request?
42
+ return @operation.headers[field_name] # if response?
43
+ end
44
+
45
+ method = DERIVED_COMPONENT[field_name]
27
46
 
28
47
  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"]
33
- else # XXX: improve this and add support for all fields in the RFC
34
- raise Error.new "Unknown/unsupported field: \"#{field_name}\""
48
+ when "@query"
49
+ return "?#{@operation.public_send(method)}"
50
+ when /\A(?<field>(?<prefix>@query-param)(?<rest>;name=.+)\Z)/
51
+ return parse_query_param Regexp.last_match
35
52
  end
53
+
54
+ method ? @operation.public_send(method) : nil
36
55
  end
37
56
 
38
57
  class << self
@@ -42,5 +61,17 @@ module Linzer
42
61
  raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
43
62
  end
44
63
  end
64
+
65
+ private
66
+
67
+ def parse_query_param(match_data)
68
+ raw_item = '"%s"%s' % [match_data[:prefix], match_data[:rest]]
69
+ parsed_item = Starry.parse_item(raw_item)
70
+ fail unless parsed_item.value == "@query-param"
71
+ param_name = URI.decode_uri_component(parsed_item.parameters["name"])
72
+ URI.encode_uri_component(@operation.params.fetch(param_name))
73
+ rescue => _
74
+ nil
75
+ end
45
76
  end
46
77
  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.1"
4
+ VERSION = "0.5.1"
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.1
4
+ version: 0.5.1
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-25 00:00:00.000000000 Z
11
+ date: 2024-04-01 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