linzer 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b025e976012c79dc5153ebd4ca98f23df08a55bf0706c6d8427e0374b3cd7f5
4
- data.tar.gz: b864415aad8cc6c9cbee217a64531378b92edbb62fb58b67a036e5997fec4a30
3
+ metadata.gz: a9b2828e9333279ffabf8102d126f832ebece8e6b5ce26bec6897337bae773b4
4
+ data.tar.gz: 868c9763f3067b55eb8fc76dfb1b6c5e6529562a57ee08d41bbfb3f443311536
5
5
  SHA512:
6
- metadata.gz: 9d2eea3036d11c6b7f8969d0f30b71fa3715d62820acde93c7d512689aa7c91d281436f8b2ea91e47b906f62167a4611298d2fad64bcf241b39b18f4405ed7a9
7
- data.tar.gz: 6f85ffe146b8a52647969575dc80011f5fefdf299bdf082cd4f3b4de0fc605f60f8beaaa1cc4cf04843cf2b3a5f7d6441c7f6b3989d50ed559cca398bbc3d7b6
6
+ metadata.gz: ef676846ea6c038465971a4c5fa47b77763f74e37adc357b5c967c483a5d3c767f0e3d1b0e073c16621ac311cc07bd524fe422e0272e9ef6e0b88091e15120e7
7
+ data.tar.gz: 8047b4e3ea2778e53764bc625d04c4ba4435f2399992885406f46128a69502158598a0130184b49887ad576252220bc71bfdbe451910971ce7357a84374f2818
data/.standard.yml CHANGED
@@ -1,3 +1,11 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/testdouble/standard
3
3
  ruby_version: 2.6
4
+
5
+ ignore:
6
+ - 'lib/**/*':
7
+ - Layout/ExtraSpacing
8
+ - Layout/HashAlignment
9
+ - 'spec/**/*':
10
+ - Layout/ExtraSpacing
11
+ - Layout/HashAlignment
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2024-02-23
4
+
5
+ - Add signature signing functionality. RSASSA-PSS using SHA-512 is still the only
6
+ supported algorithm.
7
+
3
8
  ## [0.1.0] - 2024-02-18
4
9
 
5
10
  - Initial release
6
- - It barely passes unit tests to verify signatures with RSASSA-PSS Using SHA-512.
11
+ - It barely passes unit tests to verify signatures with RSASSA-PSS using SHA-512.
data/README.md CHANGED
@@ -14,11 +14,57 @@ Or just `gem install linzer`.
14
14
 
15
15
  ## Usage
16
16
 
17
- TODO: Write usage instructions here
17
+ ### To sign a HTTP message:
18
18
 
19
- For now just take a look at the unit tests.
19
+ ```ruby
20
+ key = Linzer::Key.new(material: OpenSSL::PKey::RSA.generate(2048), key_id: "my-test-key-rsa-pss")
21
+ # => #<struct Struct::Key material=#<OpenSSL::PKey::RSA:...
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", ...
25
+
26
+ fields = %w[date x-custom-header])
27
+ signature = Linzer.sign(key, message, fields)
28
+ # => #<Linzer::Signature:0x0000000111f77ad0 ...
29
+
30
+ puts signature.to_h
31
+ {"signature"=>
32
+ "sig1=:XQQnm4qyOdIa9yTebXzCQ4f7sXXnoe76D2g1gbFFc1DeqH...",
33
+ "signature-input"=>"sig1=(\"date\" \"x-custom-header\");created=1708690868;keyid=\"my-test-key-rsa-pss\""}
34
+ ```
35
+
36
+ ### To verify a valid signature:
37
+
38
+ ```ruby
39
+ pubkey = Linzer::Key.new(key_id: "some-key-rsa-pss", material: OpenSSL::PKey::RSA.new(test_key_rsa_pss))
40
+ # => #<struct Struct::Key material=#<OpenSSL::PKey::RSA:0x0000000106eade48 oid=rsaEncryption>, key_id="some-key-rsa-pss">
41
+
42
+ headers = {"signature-input" => "...", signature => "...", "date" => "Fri, 23 Feb 2024 13:18:15 GMT", "x-custom-header" => "bar"})
43
+
44
+ message = Linzer::Message.new(headers)
45
+ # => #<Linzer::Message:0x0000000111b592a0 @headers={"date"=>"Fri, 23 Feb 2024 13:18:15 GMT", ...
46
+
47
+ signature = Linzer::Signature.build(headers)
48
+ # => #<Linzer::Signature:0x0000000112396008 ...
49
+
50
+ Linzer.verify(pubkey, message, signature)
51
+ # => true
52
+ ```
53
+
54
+ ### What if an invalid signature if verified?
55
+
56
+ ```ruby
57
+ result = Linzer.verify(pubkey, message, signature)
58
+ linzer/lib/linzer/verifier.rb:14:in `verify': Failed to verify message: Invalid signature. (Linzer::Error)
59
+ ```
60
+
61
+ For now, to consult additional details, just take a look at source code and/or the unit tests.
62
+
63
+ Please note that is still early days, so only signatures RSASSA-PSS using SHA-512 like ones
64
+ described in the RFC are supported.
20
65
 
21
- It's still early days, so only signatures RSASSA-PSS Using SHA-512 like ones described in the RFC are supported. I'll be expanding the library to cover more functionality specified in the RFC in subsequent releases.
66
+ I'll be expanding the library to cover more functionality specified in the RFC
67
+ in subsequent releases.
22
68
 
23
69
 
24
70
  ## Development
@@ -3,8 +3,9 @@
3
3
  module Linzer
4
4
  class Message
5
5
  def initialize(request_data)
6
- @headers = Hash(request_data[:headers])
7
- @http = Hash(request_data[:http])
6
+ @headers = Hash(request_data[:headers].clone).freeze
7
+ @http = Hash(request_data[:http].clone).freeze
8
+ freeze
8
9
  end
9
10
 
10
11
  def empty?
@@ -15,16 +16,54 @@ module Linzer
15
16
  @headers.key?(header)
16
17
  end
17
18
 
19
+ def field?(f)
20
+ !!self[f]
21
+ end
22
+
18
23
  def [](field_name)
19
24
  return @headers[field_name] if !field_name.start_with?("@")
20
25
 
21
26
  case field_name
22
- when "@method" then @http["method"]
27
+ when "@method" then @http["method"]
23
28
  when "@authority" then @http["host"]
24
- when "@path" then @http["path"]
29
+ when "@path" then @http["path"]
25
30
  else # XXX: improve this and add support for all fields in the RFC
26
31
  raise Error.new "Unknown/unsupported field: \"#{field_name}\""
27
32
  end
28
33
  end
34
+
35
+ def signature_base(components, parameters)
36
+ validate_components components
37
+
38
+ signature_base = components.each_with_object(+"") do |comp, base|
39
+ base << "\"#{comp}\": #{self[comp]}\n"
40
+ end
41
+
42
+ signature_params =
43
+ Starry.serialize([Starry::InnerList.new(components, parameters)])
44
+
45
+ signature_base << "\"@signature-params\": #{signature_params}"
46
+ signature_base
47
+ end
48
+
49
+ class << self
50
+ def parse_structured_dictionary(str, field_name = nil)
51
+ Starry.parse_dictionary(str)
52
+ rescue Starry::ParseError => _
53
+ raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def validate_components(components)
60
+ if components.include?("@signature-params")
61
+ raise Error.new "Invalid component in signature input"
62
+ end
63
+ msg = "Cannot verify signature. Missing component in message: %s"
64
+ components.each { |c| raise Error.new msg % "\"#{c}\"" unless field? c }
65
+ msg = "Invalid signature. Duplicated component in signature input."
66
+ raise Error.new msg if components.size != components.uniq.size
67
+ end
29
68
  end
30
69
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Signature
5
+ def initialize(metadata, value, label, parameters = {})
6
+ @metadata = metadata.clone.freeze
7
+ @value = value.clone.freeze
8
+ @parameters = parameters.clone.freeze
9
+ @label = label.clone.freeze
10
+ freeze
11
+ end
12
+
13
+ attr_reader :metadata, :value, :parameters, :label
14
+ alias_method :components, :metadata
15
+ alias_method :bytes, :value
16
+
17
+ def to_h
18
+ {
19
+ "signature" => Starry.serialize({label => value}),
20
+ "signature-input" =>
21
+ Starry.serialize({label =>
22
+ Starry::InnerList.new(components, parameters)})
23
+ }
24
+ end
25
+
26
+ class << self
27
+ private :new
28
+
29
+ def build(headers, options = {})
30
+ validate headers
31
+
32
+ input = parse_field(headers, "signature-input")
33
+ reject_multiple_signatures if input.size > 1 && options[:label].nil?
34
+ label = options[:label] || input.keys.first
35
+
36
+ signature = parse_field(headers, "signature")
37
+ fail_with_signature_not_found label unless signature.key?(label)
38
+
39
+ raw_signature = signature[label].value
40
+
41
+ fail_due_invalid_components unless input[label].value.respond_to?(:each)
42
+
43
+ components = input[label].value.map(&:value)
44
+ parameters = input[label].parameters
45
+
46
+ new(components, raw_signature, label, parameters)
47
+ end
48
+
49
+ private
50
+
51
+ def validate(headers)
52
+ raise Error.new "Cannot build signature: Request headers cannot be null" if headers.nil?
53
+ raise Error.new "Cannot build signature: No request headers found" if headers.empty?
54
+ raise Error.new "Cannot build signature: No \"signature-input\" header found" unless headers.key?("signature-input")
55
+ raise Error.new "Cannot build signature: No \"signature\" header found" unless headers.key?("signature")
56
+ end
57
+
58
+ def reject_multiple_signatures
59
+ raise Error.new "Multiple signatures found but none was selected."
60
+ end
61
+
62
+ def fail_with_signature_not_found(label)
63
+ raise Error.new "Signature label not found: \"#{label}\""
64
+ end
65
+
66
+ def fail_due_invalid_components
67
+ raise Error.new "Unexpected value for covered components."
68
+ end
69
+
70
+ def parse_field(hsh, field_name)
71
+ Message.parse_structured_dictionary(hsh[field_name], field_name)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module Signer
5
+ DEFAULT_LABEL = "sig1"
6
+
7
+ class << self
8
+ def sign(key, message, components, options = {})
9
+ validate key, message, components
10
+
11
+ parameters = populate_parameters(key, options)
12
+ signature_base = message.signature_base(components, parameters)
13
+
14
+ signature = _sign(key, signature_base, options)
15
+ label = options[:label] || DEFAULT_LABEL
16
+
17
+ Linzer::Signature.build(serialize(signature, components, parameters, label))
18
+ end
19
+
20
+ def default_label
21
+ DEFAULT_LABEL
22
+ end
23
+
24
+ private
25
+
26
+ def validate(key, message, components)
27
+ raise Error.new "Message to sign cannot be null" if message.nil?
28
+ raise Error.new "Message cannot be signed with a null key" if key.nil?
29
+
30
+ if components.include?("@signature-params")
31
+ raise Error.new "Invalid component in signature input"
32
+ end
33
+
34
+ component_missing = 'Cannot sign message: component "%s" is not present in message'
35
+ components.each do |c|
36
+ raise Error.new component_missing % c unless message.field? c
37
+ end
38
+ end
39
+
40
+ def populate_parameters(key, options)
41
+ parameters = {}
42
+
43
+ parameters[:created] = options[:created] || Time.now.getutc.to_i
44
+
45
+ key_id = options[:keyid] || (key.key_id if key.respond_to?(:key_id))
46
+ parameters[:keyid] = key_id unless key_id.nil?
47
+
48
+ (options.keys - %i[created keyid label]).each { |k| parameters[k] = options[k] }
49
+
50
+ parameters
51
+ end
52
+
53
+ def _sign(key, data, options)
54
+ # signature = key.sign_pss("SHA512", signature_base, salt_length: 64, mgf1_hash: "SHA512")
55
+ key.sign("SHA512", data)
56
+ end
57
+
58
+ def serialize(signature, components, parameters, label)
59
+ {
60
+ "signature" => Starry.serialize({label => signature}),
61
+ "signature-input" =>
62
+ Starry.serialize({label =>
63
+ Starry::InnerList.new(components, parameters)})
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,101 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- class Verifier
5
- def initialize(pubkeys = nil)
6
- @pubkeys = Hash(pubkeys)
7
- end
8
-
9
- attr_reader :pubkeys
10
-
11
- # XXX: probably all this validation can be moved to the Message class
12
- def verify(message)
13
- validate message
14
-
15
- signature_input = parse_field(message, "signature-input")
16
- signature = parse_field(message, "signature")
17
-
18
- # XXX: this is a self-imposed limitation, fix later
19
- reject_multiple(signature)
20
-
21
- choosen_signature = signature.keys[0]
22
- if !signature_input.key?(choosen_signature)
23
- raise Error.new "Signature \"#{choosen_signature}\" is not found."
24
- end
25
-
26
- covered_components = signature_input[choosen_signature].to_a
27
- signature_parameters = signature_input[choosen_signature].parameters
28
-
29
- signature_value = signature[choosen_signature].value
30
- # XXX to-do: have a mechanism to inspect components and parameters
4
+ module Verifier
5
+ class << self
6
+ def verify(key, message, signature)
7
+ validate message, key, signature
31
8
 
32
- check_key_presence signature_parameters
33
- check_components message, covered_components
9
+ parameters = signature.parameters
10
+ components = signature.components
34
11
 
35
- signature_base = build_signature_base(message, signature_input)
12
+ signature_base = message.signature_base(components, parameters)
36
13
 
37
- # XXX to-do: get rid of this hard-coded SHA512 values, support more algs
38
- key = pubkeys[signature_parameters["keyid"]]
39
- if !key.verify_pss("SHA512", signature_value, signature_base, salt_length: :auto, mgf1_hash: "SHA512")
14
+ return true if _verify(key, signature.value, signature_base)
40
15
  raise Error.new "Failed to verify message: Invalid signature."
41
16
  end
42
17
 
43
- true
44
- end
45
-
46
- private
18
+ private
47
19
 
48
- def validate(message)
49
- raise Error.new "Message to verify cannot be null" if message.nil?
50
- raise Error.new "Message to verify cannot be empty" if message.empty?
51
- raise Error.new "Message signature cannot be incomplete" unless message.header?("signature-input")
52
- raise Error.new "Message has no signature to verify" unless message.header?("signature")
53
- end
20
+ def validate(message, key, signature)
21
+ raise Error.new "Message to verify cannot be null" if message.nil?
22
+ raise Error.new "Key to verify signature cannot be null" if key.nil?
23
+ raise Error.new "Signature to verify cannot be null" if signature.nil?
54
24
 
55
- def parse_field(message, field_name)
56
- Starry.parse_dictionary(message[field_name])
57
- rescue Starry::ParseError => _
58
- raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
59
- end
60
-
61
- def reject_multiple(hsh)
62
- msg = "Messages with more than 1 signatures are not supported"
63
- raise Error.new msg if hsh.keys.size > 1
64
- end
65
-
66
- def check_key_presence(parameters)
67
- msg = "Cannot verify signature. Key not found"
68
-
69
- key_id = parameters["keyid"]
70
- raise Error.new msg if key_id.nil?
71
- msg += ": \"#{key_id}\"" if !key_id.empty?
72
-
73
- raise Error.new msg unless pubkeys.key?(key_id)
74
- end
75
-
76
- def check_components(message, components)
77
- msg = "Cannot verify signature. Missing component in message: "
78
- components
79
- .map(&:value)
80
- .reject { |component| message[component] }
81
- .shift
82
- .tap do |component|
83
- if component
84
- msg += "\"#{component}\""
85
- raise Error.new msg
86
- end
25
+ if !signature.respond_to?(:value) || !signature.respond_to?(:components)
26
+ raise Error.new "Signature is invalid"
87
27
  end
88
- end
89
28
 
90
- def build_signature_base(message, signature_input)
91
- signature_base = +""
92
- signature_params = ""
93
- signature_input.each do |k, l|
94
- signature_params = l.to_s
95
- l.value.each { |c| signature_base << "\"#{c.value}\": #{message[c.value]}\n" }
29
+ raise Error.new "Signature raw value to cannot be null" if signature.value.nil?
30
+ raise Error.new "Components cannot be null" if signature.components.nil?
31
+ end
32
+
33
+ def _verify(key, signature, data)
34
+ # XXX to-do: get rid of this hard-coded SHA512 values, support more algs
35
+ return true if key.material.verify_pss("SHA512", signature, data, salt_length: :auto, mgf1_hash: "SHA512")
36
+ false
96
37
  end
97
- signature_base << "\"@signature-params\": #{signature_params}"
98
- signature_base
99
38
  end
100
39
  end
101
40
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -5,15 +5,29 @@ require "openssl"
5
5
 
6
6
  require_relative "linzer/version"
7
7
  require_relative "linzer/message"
8
+ require_relative "linzer/signature"
9
+ require_relative "linzer/signer"
8
10
  require_relative "linzer/verifier"
9
11
 
10
12
  module Linzer
11
13
  class Error < StandardError; end
12
14
 
15
+ Key = Struct.new("Key", :material, :key_id, keyword_init: true) do |clazz|
16
+ def sign(*args)
17
+ # XXX: probably this is going to grow in complexity and will need
18
+ # to be moved to its own class or dispatch to the signer
19
+ !material.nil? or raise Error.new "Cannot sign data, key material cannot be null."
20
+ material.sign(*args)
21
+ end
22
+ end
23
+
13
24
  class << self
14
- def verify(pubkeys, message)
15
- Linzer::Verifier.new(pubkeys)
16
- .verify(message)
25
+ def verify(pubkey, message, signature)
26
+ Linzer::Verifier.verify(pubkey, message, signature)
27
+ end
28
+
29
+ def sign(key, message, components, options = {})
30
+ Linzer::Signer.sign(key, message, components, options)
17
31
  end
18
32
  end
19
33
  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.1.0
4
+ version: 0.2.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-02-18 00:00:00.000000000 Z
11
+ date: 2024-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ed25519
@@ -60,6 +60,8 @@ files:
60
60
  - Rakefile
61
61
  - lib/linzer.rb
62
62
  - lib/linzer/message.rb
63
+ - lib/linzer/signature.rb
64
+ - lib/linzer/signer.rb
63
65
  - lib/linzer/verifier.rb
64
66
  - lib/linzer/version.rb
65
67
  - linzer.gemspec