linzer 0.2.0 → 0.3.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: a9b2828e9333279ffabf8102d126f832ebece8e6b5ce26bec6897337bae773b4
4
- data.tar.gz: 868c9763f3067b55eb8fc76dfb1b6c5e6529562a57ee08d41bbfb3f443311536
3
+ metadata.gz: 22298d26596b660ac67a7f039ed0d05cc41715a5413c4583a4703ce452e6548c
4
+ data.tar.gz: 735d31e3752eea02207baa7e093bd18a114281f4c493acf98cd7451d73e03fff
5
5
  SHA512:
6
- metadata.gz: ef676846ea6c038465971a4c5fa47b77763f74e37adc357b5c967c483a5d3c767f0e3d1b0e073c16621ac311cc07bd524fe422e0272e9ef6e0b88091e15120e7
7
- data.tar.gz: 8047b4e3ea2778e53764bc625d04c4ba4435f2399992885406f46128a69502158598a0130184b49887ad576252220bc71bfdbe451910971ce7357a84374f2818
6
+ metadata.gz: 81428b963ffaa3f39e86ed28e52927923998aaeeb07f773a852bb01abe9272f812c8b0a593813293908845f57799c849163da00d4ae4ca2ef62d36687055ce81
7
+ data.tar.gz: 3f91ef995bd53bda69832e774ce383cf55a8ef903ffe615e8dbb1a586cf437b85a3d98f8082f39311a6757a3bd4f2657ac4b5d73c3270b4a4534ea571b9e8427
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-02-28
4
+
5
+ - Add support for the following algorithms: Ed25519, HMAC-SHA256 and
6
+ ECDSA (P-256 and P-384 curves).
7
+
3
8
  ## [0.2.0] - 2024-02-23
4
9
 
5
10
  - Add signature signing functionality. RSASSA-PSS using SHA-512 is still the only
data/README.md CHANGED
@@ -17,27 +17,27 @@ Or just `gem install linzer`.
17
17
  ### To sign a HTTP message:
18
18
 
19
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:...
20
+ irb(main):001:0> key = Linzer.generate_ed25519_key
21
+ # => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
22
22
 
23
23
  message = Linzer::Message.new(headers: {"date" => "Fri, 23 Feb 2024 17:57:23 GMT", "x-custom-header" => "foo"})
24
24
  # => #<Linzer::Message:0x0000000111b592a0 @headers={"date"=>"Fri, 23 Feb 2024 17:57:23 GMT", ...
25
25
 
26
- fields = %w[date x-custom-header])
26
+ fields = %w[date x-custom-header]
27
27
  signature = Linzer.sign(key, message, fields)
28
28
  # => #<Linzer::Signature:0x0000000111f77ad0 ...
29
29
 
30
30
  puts signature.to_h
31
31
  {"signature"=>
32
- "sig1=:XQQnm4qyOdIa9yTebXzCQ4f7sXXnoe76D2g1gbFFc1DeqH...",
33
- "signature-input"=>"sig1=(\"date\" \"x-custom-header\");created=1708690868;keyid=\"my-test-key-rsa-pss\""}
32
+ "sig1=:8rLY3nFtezwwsK+sqZEMe7wzbNHojZJGEnvp3suKichgwH...",
33
+ "signature-input"=>"sig1=(\"date\" \"x-custom-header\");created=1709075013;keyid=\"test-key-ed25519\""}
34
34
  ```
35
35
 
36
36
  ### To verify a valid signature:
37
37
 
38
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">
39
+ pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
40
+ # => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
41
41
 
42
42
  headers = {"signature-input" => "...", signature => "...", "date" => "Fri, 23 Feb 2024 13:18:15 GMT", "x-custom-header" => "bar"})
43
43
 
@@ -55,13 +55,12 @@ Linzer.verify(pubkey, message, signature)
55
55
 
56
56
  ```ruby
57
57
  result = Linzer.verify(pubkey, message, signature)
58
- linzer/lib/linzer/verifier.rb:14:in `verify': Failed to verify message: Invalid signature. (Linzer::Error)
58
+ lib/linzer/verifier.rb:34:in `verify_or_fail': Failed to verify message: Invalid signature. (Linzer::Error)
59
59
  ```
60
60
 
61
61
  For now, to consult additional details, just take a look at source code and/or the unit tests.
62
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.
63
+ 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 curve. ECDSA P-384 curve was also added but not tested yet.
65
64
 
66
65
  I'll be expanding the library to cover more functionality specified in the RFC
67
66
  in subsequent releases.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module Common
5
+ def signature_base(message, components, parameters)
6
+ signature_base = components.each_with_object(+"") do |component, base|
7
+ base << "\"#{component}\": #{message[component]}\n"
8
+ end
9
+
10
+ signature_params =
11
+ Starry.serialize([Starry::InnerList.new(components, parameters)])
12
+
13
+ signature_base << "\"@signature-params\": #{signature_params}"
14
+ signature_base
15
+ end
16
+
17
+ def validate_components(message, components)
18
+ if components.include?("@signature-params")
19
+ raise Error.new "Invalid component in signature input"
20
+ end
21
+ msg = "Cannot verify signature. Missing component in message: %s"
22
+ components.each { |c| raise Error.new msg % "\"#{c}\"" unless message.field?(c) }
23
+ msg = "Invalid signature. Duplicated component in signature input."
24
+ raise Error.new msg if components.size != components.uniq.size
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module ECDSA
5
+ class Key < Linzer::Key
6
+ def validate
7
+ super
8
+ validate_digest
9
+ end
10
+
11
+ def sign(data)
12
+ material.sign(@params[:digest], data)
13
+ end
14
+
15
+ def verify(signature, data)
16
+ material.verify(@params[:digest], signature, data)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ed25519"
4
+
5
+ module Linzer
6
+ module Ed25519
7
+ class Key < Linzer::Key
8
+ def sign(data)
9
+ material.sign(data)
10
+ end
11
+
12
+ def verify(signature, data)
13
+ verify_key = material.is_a?(::Ed25519::SigningKey) ? material.verify_key : material
14
+ verify_key.verify(signature, data)
15
+ rescue ::Ed25519::VerifyError
16
+ false
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module HMAC
5
+ class Key < Linzer::Key
6
+ def validate
7
+ super
8
+ validate_digest
9
+ end
10
+
11
+ def sign(data)
12
+ OpenSSL::HMAC.digest(@params[:digest], material, data)
13
+ end
14
+
15
+ def verify(signature, data)
16
+ signature == sign(data)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Key
5
+ module Helper
6
+ def generate_rsa_pss_sha512_key(size, key_id = nil)
7
+ material = OpenSSL::PKey::RSA.generate(size)
8
+ Linzer::RSA::Key.new(material, id: key_id, digest: "SHA512")
9
+ end
10
+
11
+ def new_rsa_pss_sha512_key(material, key_id = nil)
12
+ key = OpenSSL::PKey.read(material)
13
+ Linzer::RSA::Key.new(key, id: key_id, digest: "SHA512")
14
+ end
15
+
16
+ def new_rsa_pss_sha512_public_key(material, key_id = nil)
17
+ key = OpenSSL::PKey::RSA.new(material)
18
+ Linzer::RSA::Key.new(key, id: key_id, digest: "SHA512")
19
+ end
20
+
21
+ # XXX: to-do
22
+ # Linzer::RSA::Key
23
+ # def new_rsa_v1_5_sha256_key
24
+ # def generate_rsa_v1_5_sha256_key
25
+
26
+ def generate_hmac_sha256_key(key_id = nil)
27
+ material = OpenSSL::Random.random_bytes(64)
28
+ Linzer::HMAC::Key.new(material, id: key_id, digest: "SHA256")
29
+ end
30
+
31
+ def new_hmac_sha256_key(material, key_id = nil)
32
+ Linzer::HMAC::Key.new(material, id: key_id, digest: "SHA256")
33
+ end
34
+
35
+ def generate_ed25519_key(key_id = nil)
36
+ material = ::Ed25519::SigningKey.generate
37
+ Linzer::Ed25519::Key.new(material, id: key_id)
38
+ end
39
+
40
+ def new_ed25519_key(material, key_id = nil)
41
+ key = ::Ed25519::SigningKey.new(material)
42
+ Linzer::Ed25519::Key.new(key, id: key_id)
43
+ end
44
+
45
+ def new_ed25519_public_key(material, key_id = nil)
46
+ key = ::Ed25519::VerifyKey.new(material)
47
+ Linzer::Ed25519::Key.new(key, id: key_id)
48
+ end
49
+
50
+ # https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A
51
+ # Table 6: Equivalent curves defined by SECG, ANSI, and NIST
52
+ # secp256r1 | prime256v1 | NIST P-256
53
+ def generate_ecdsa_p256_sha256_key(key_id = nil)
54
+ material = OpenSSL::PKey::EC.generate("prime256v1")
55
+ Linzer::ECDSA::Key.new(material, id: key_id, digest: "SHA256")
56
+ end
57
+
58
+ def new_ecdsa_p256_sha256_key(material, key_id = nil)
59
+ key = OpenSSL::PKey::EC.new(material)
60
+ Linzer::ECDSA::Key.new(key, id: key_id, digest: "SHA256")
61
+ end
62
+
63
+ # https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A
64
+ # Table 6: Equivalent curves defined by SECG, ANSI, and NIST
65
+ # secp384r1 | | NIST P-384
66
+ def generate_ecdsa_p384_sha256_key(key_id = nil)
67
+ material = OpenSSL::PKey::EC.generate("secp384r1")
68
+ Linzer::ECDSA::Key.new(material, id: key_id, digest: "SHA384")
69
+ end
70
+
71
+ def new_ecdsa_p384_sha384_key(material, key_id = nil)
72
+ key = OpenSSL::PKey::EC.new(material)
73
+ Linzer::ECDSA::Key.new(key, id: key_id, digest: "SHA384")
74
+ end
75
+ end
76
+ end
77
+ end
data/lib/linzer/key.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Key
5
+ def initialize(material, params = {})
6
+ @material = material
7
+ @params = Hash(params).clone.freeze
8
+ validate
9
+ freeze
10
+ end
11
+
12
+ attr_reader :material
13
+
14
+ def key_id
15
+ @params[:id]
16
+ end
17
+
18
+ def sign(*args)
19
+ abstract_error = "Cannot sign data, \"#{self.class}\" is an abstract class."
20
+ raise Error.new abstract_error
21
+ end
22
+
23
+ def verify(*args)
24
+ abstract_error = "Cannot verify signature, \"#{self.class}\" is an abstract class."
25
+ raise Error.new abstract_error
26
+ end
27
+
28
+ private
29
+
30
+ def validate
31
+ !material.nil? or raise Error.new "Invalid key. No key material provided."
32
+ end
33
+
34
+ def validate_digest
35
+ no_digest = !@params.key?(:digest) || @params[:digest].nil? || String(@params[:digest]).empty?
36
+ no_digest_error = "Invalid key definition, no digest algorithm was selected."
37
+ raise Linzer::Error.new no_digest_error if no_digest
38
+ end
39
+ end
40
+ end
@@ -27,25 +27,12 @@ module Linzer
27
27
  when "@method" then @http["method"]
28
28
  when "@authority" then @http["host"]
29
29
  when "@path" then @http["path"]
30
+ when "@status" then @http["status"]
30
31
  else # XXX: improve this and add support for all fields in the RFC
31
32
  raise Error.new "Unknown/unsupported field: \"#{field_name}\""
32
33
  end
33
34
  end
34
35
 
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
36
  class << self
50
37
  def parse_structured_dictionary(str, field_name = nil)
51
38
  Starry.parse_dictionary(str)
@@ -53,17 +40,5 @@ module Linzer
53
40
  raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
54
41
  end
55
42
  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
68
43
  end
69
44
  end
data/lib/linzer/rsa.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ module RSA
5
+ class Key < Linzer::Key
6
+ def validate
7
+ super
8
+ validate_digest
9
+ end
10
+
11
+ def sign(data)
12
+ # XXX: should check if the key is usable for signing
13
+ @material.sign(@params[:digest], data)
14
+ end
15
+
16
+ def verify(signature, data)
17
+ # XXX: should check if the key is usable for verifying
18
+ return true if @material.verify_pss(
19
+ @params[:digest],
20
+ signature,
21
+ data,
22
+ salt_length: @params[:salt_length] || :auto,
23
+ mgf1_hash: @params[:digest]
24
+ )
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/linzer/signer.rb CHANGED
@@ -5,13 +5,15 @@ module Linzer
5
5
  DEFAULT_LABEL = "sig1"
6
6
 
7
7
  class << self
8
+ include Common
9
+
8
10
  def sign(key, message, components, options = {})
9
11
  validate key, message, components
10
12
 
11
13
  parameters = populate_parameters(key, options)
12
- signature_base = message.signature_base(components, parameters)
14
+ signature_base = signature_base(message, components, parameters)
13
15
 
14
- signature = _sign(key, signature_base, options)
16
+ signature = key.sign(signature_base)
15
17
  label = options[:label] || DEFAULT_LABEL
16
18
 
17
19
  Linzer::Signature.build(serialize(signature, components, parameters, label))
@@ -24,17 +26,12 @@ module Linzer
24
26
  private
25
27
 
26
28
  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
29
+ msg = "Message cannot be signed with null %s"
30
+ raise Error.new msg % "value" if message.nil?
31
+ raise Error.new msg % "key" if key.nil?
32
+ raise Error.new msg % "component" if components.nil?
33
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
34
+ validate_components message, components
38
35
  end
39
36
 
40
37
  def populate_parameters(key, options)
@@ -50,11 +47,6 @@ module Linzer
50
47
  parameters
51
48
  end
52
49
 
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
50
  def serialize(signature, components, parameters, label)
59
51
  {
60
52
  "signature" => Starry.serialize({label => signature}),
@@ -3,16 +3,17 @@
3
3
  module Linzer
4
4
  module Verifier
5
5
  class << self
6
+ include Common
7
+
6
8
  def verify(key, message, signature)
7
9
  validate message, key, signature
8
10
 
9
11
  parameters = signature.parameters
10
12
  components = signature.components
11
13
 
12
- signature_base = message.signature_base(components, parameters)
14
+ signature_base = signature_base(message, components, parameters)
13
15
 
14
- return true if _verify(key, signature.value, signature_base)
15
- raise Error.new "Failed to verify message: Invalid signature."
16
+ verify_or_fail key, signature.value, signature_base
16
17
  end
17
18
 
18
19
  private
@@ -28,12 +29,13 @@ module Linzer
28
29
 
29
30
  raise Error.new "Signature raw value to cannot be null" if signature.value.nil?
30
31
  raise Error.new "Components cannot be null" if signature.components.nil?
32
+
33
+ validate_components message, signature.components
31
34
  end
32
35
 
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
36
+ def verify_or_fail(key, signature, data)
37
+ return true if key.verify(signature, data)
38
+ raise Error.new "Failed to verify message: Invalid signature."
37
39
  end
38
40
  end
39
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linzer
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/linzer.rb CHANGED
@@ -4,24 +4,24 @@ require "starry"
4
4
  require "openssl"
5
5
 
6
6
  require_relative "linzer/version"
7
+ require_relative "linzer/common"
7
8
  require_relative "linzer/message"
8
9
  require_relative "linzer/signature"
10
+ require_relative "linzer/key"
11
+ require_relative "linzer/rsa"
12
+ require_relative "linzer/hmac"
13
+ require_relative "linzer/ed25519"
14
+ require_relative "linzer/ecdsa"
15
+ require_relative "linzer/key/helper"
9
16
  require_relative "linzer/signer"
10
17
  require_relative "linzer/verifier"
11
18
 
12
19
  module Linzer
13
20
  class Error < StandardError; end
14
21
 
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
-
24
22
  class << self
23
+ include Key::Helper
24
+
25
25
  def verify(pubkey, message, signature)
26
26
  Linzer::Verifier.verify(pubkey, message, signature)
27
27
  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.2.0
4
+ version: 0.3.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-23 00:00:00.000000000 Z
11
+ date: 2024-02-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ed25519
@@ -59,7 +59,14 @@ files:
59
59
  - README.md
60
60
  - Rakefile
61
61
  - lib/linzer.rb
62
+ - lib/linzer/common.rb
63
+ - lib/linzer/ecdsa.rb
64
+ - lib/linzer/ed25519.rb
65
+ - lib/linzer/hmac.rb
66
+ - lib/linzer/key.rb
67
+ - lib/linzer/key/helper.rb
62
68
  - lib/linzer/message.rb
69
+ - lib/linzer/rsa.rb
63
70
  - lib/linzer/signature.rb
64
71
  - lib/linzer/signer.rb
65
72
  - lib/linzer/verifier.rb