linzer 0.1.0 → 0.2.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: 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