linzer 0.1.0 → 0.3.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 +4 -4
- data/.standard.yml +8 -0
- data/CHANGELOG.md +11 -1
- data/README.md +48 -3
- data/lib/linzer/common.rb +27 -0
- data/lib/linzer/ecdsa.rb +20 -0
- data/lib/linzer/ed25519.rb +20 -0
- data/lib/linzer/hmac.rb +20 -0
- data/lib/linzer/key/helper.rb +77 -0
- data/lib/linzer/key.rb +40 -0
- data/lib/linzer/message.rb +18 -4
- data/lib/linzer/rsa.rb +29 -0
- data/lib/linzer/signature.rb +75 -0
- data/lib/linzer/signer.rb +60 -0
- data/lib/linzer/verifier.rb +23 -82
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +17 -3
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22298d26596b660ac67a7f039ed0d05cc41715a5413c4583a4703ce452e6548c
|
4
|
+
data.tar.gz: 735d31e3752eea02207baa7e093bd18a114281f4c493acf98cd7451d73e03fff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81428b963ffaa3f39e86ed28e52927923998aaeeb07f773a852bb01abe9272f812c8b0a593813293908845f57799c849163da00d4ae4ca2ef62d36687055ce81
|
7
|
+
data.tar.gz: 3f91ef995bd53bda69832e774ce383cf55a8ef903ffe615e8dbb1a586cf437b85a3d98f8082f39311a6757a3bd4f2657ac4b5d73c3270b4a4534ea571b9e8427
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,16 @@
|
|
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
|
+
|
8
|
+
## [0.2.0] - 2024-02-23
|
9
|
+
|
10
|
+
- Add signature signing functionality. RSASSA-PSS using SHA-512 is still the only
|
11
|
+
supported algorithm.
|
12
|
+
|
3
13
|
## [0.1.0] - 2024-02-18
|
4
14
|
|
5
15
|
- Initial release
|
6
|
-
- It barely passes unit tests to verify signatures with RSASSA-PSS
|
16
|
+
- It barely passes unit tests to verify signatures with RSASSA-PSS using SHA-512.
|
data/README.md
CHANGED
@@ -14,11 +14,56 @@ Or just `gem install linzer`.
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
-
|
17
|
+
### To sign a HTTP message:
|
18
18
|
|
19
|
-
|
19
|
+
```ruby
|
20
|
+
irb(main):001:0> key = Linzer.generate_ed25519_key
|
21
|
+
# => #<Linzer::Ed25519::Key:0x00000fe13e9bd208
|
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=:8rLY3nFtezwwsK+sqZEMe7wzbNHojZJGEnvp3suKichgwH...",
|
33
|
+
"signature-input"=>"sig1=(\"date\" \"x-custom-header\");created=1709075013;keyid=\"test-key-ed25519\""}
|
34
|
+
```
|
35
|
+
|
36
|
+
### To verify a valid signature:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
pubkey = Linzer.new_ed25519_public_key(test_ed25519_key_pub, "some-key-ed25519")
|
40
|
+
# => #<Linzer::Ed25519::Key:0x00000fe19b9384b0
|
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
|
+
lib/linzer/verifier.rb:34:in `verify_or_fail': 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 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.
|
20
64
|
|
21
|
-
|
65
|
+
I'll be expanding the library to cover more functionality specified in the RFC
|
66
|
+
in subsequent releases.
|
22
67
|
|
23
68
|
|
24
69
|
## Development
|
@@ -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
|
data/lib/linzer/ecdsa.rb
ADDED
@@ -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
|
data/lib/linzer/hmac.rb
ADDED
@@ -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
|
data/lib/linzer/message.rb
CHANGED
@@ -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
|
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,29 @@ 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"
|
27
|
+
when "@method" then @http["method"]
|
23
28
|
when "@authority" then @http["host"]
|
24
|
-
when "@path"
|
29
|
+
when "@path" then @http["path"]
|
30
|
+
when "@status" then @http["status"]
|
25
31
|
else # XXX: improve this and add support for all fields in the RFC
|
26
32
|
raise Error.new "Unknown/unsupported field: \"#{field_name}\""
|
27
33
|
end
|
28
34
|
end
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def parse_structured_dictionary(str, field_name = nil)
|
38
|
+
Starry.parse_dictionary(str)
|
39
|
+
rescue Starry::ParseError => _
|
40
|
+
raise Error.new "Cannot parse \"#{field_name}\" field. Bailing out!"
|
41
|
+
end
|
42
|
+
end
|
29
43
|
end
|
30
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
|
@@ -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,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Linzer
|
4
|
+
module Signer
|
5
|
+
DEFAULT_LABEL = "sig1"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
include Common
|
9
|
+
|
10
|
+
def sign(key, message, components, options = {})
|
11
|
+
validate key, message, components
|
12
|
+
|
13
|
+
parameters = populate_parameters(key, options)
|
14
|
+
signature_base = signature_base(message, components, parameters)
|
15
|
+
|
16
|
+
signature = key.sign(signature_base)
|
17
|
+
label = options[:label] || DEFAULT_LABEL
|
18
|
+
|
19
|
+
Linzer::Signature.build(serialize(signature, components, parameters, label))
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_label
|
23
|
+
DEFAULT_LABEL
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate(key, message, components)
|
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
|
+
|
34
|
+
validate_components message, components
|
35
|
+
end
|
36
|
+
|
37
|
+
def populate_parameters(key, options)
|
38
|
+
parameters = {}
|
39
|
+
|
40
|
+
parameters[:created] = options[:created] || Time.now.getutc.to_i
|
41
|
+
|
42
|
+
key_id = options[:keyid] || (key.key_id if key.respond_to?(:key_id))
|
43
|
+
parameters[:keyid] = key_id unless key_id.nil?
|
44
|
+
|
45
|
+
(options.keys - %i[created keyid label]).each { |k| parameters[k] = options[k] }
|
46
|
+
|
47
|
+
parameters
|
48
|
+
end
|
49
|
+
|
50
|
+
def serialize(signature, components, parameters, label)
|
51
|
+
{
|
52
|
+
"signature" => Starry.serialize({label => signature}),
|
53
|
+
"signature-input" =>
|
54
|
+
Starry.serialize({label =>
|
55
|
+
Starry::InnerList.new(components, parameters)})
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/linzer/verifier.rb
CHANGED
@@ -1,101 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Linzer
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
attr_reader :pubkeys
|
4
|
+
module Verifier
|
5
|
+
class << self
|
6
|
+
include Common
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
validate message
|
8
|
+
def verify(key, message, signature)
|
9
|
+
validate message, key, signature
|
14
10
|
|
15
|
-
|
16
|
-
|
11
|
+
parameters = signature.parameters
|
12
|
+
components = signature.components
|
17
13
|
|
18
|
-
|
19
|
-
reject_multiple(signature)
|
14
|
+
signature_base = signature_base(message, components, parameters)
|
20
15
|
|
21
|
-
|
22
|
-
if !signature_input.key?(choosen_signature)
|
23
|
-
raise Error.new "Signature \"#{choosen_signature}\" is not found."
|
16
|
+
verify_or_fail key, signature.value, signature_base
|
24
17
|
end
|
25
18
|
|
26
|
-
|
27
|
-
signature_parameters = signature_input[choosen_signature].parameters
|
19
|
+
private
|
28
20
|
|
29
|
-
|
30
|
-
|
21
|
+
def validate(message, key, signature)
|
22
|
+
raise Error.new "Message to verify cannot be null" if message.nil?
|
23
|
+
raise Error.new "Key to verify signature cannot be null" if key.nil?
|
24
|
+
raise Error.new "Signature to verify cannot be null" if signature.nil?
|
31
25
|
|
32
|
-
|
33
|
-
|
26
|
+
if !signature.respond_to?(:value) || !signature.respond_to?(:components)
|
27
|
+
raise Error.new "Signature is invalid"
|
28
|
+
end
|
34
29
|
|
35
|
-
|
30
|
+
raise Error.new "Signature raw value to cannot be null" if signature.value.nil?
|
31
|
+
raise Error.new "Components cannot be null" if signature.components.nil?
|
36
32
|
|
37
|
-
|
38
|
-
key = pubkeys[signature_parameters["keyid"]]
|
39
|
-
if !key.verify_pss("SHA512", signature_value, signature_base, salt_length: :auto, mgf1_hash: "SHA512")
|
40
|
-
raise Error.new "Failed to verify message: Invalid signature."
|
33
|
+
validate_components message, signature.components
|
41
34
|
end
|
42
35
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
private
|
47
|
-
|
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
|
54
|
-
|
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
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
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" }
|
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."
|
96
39
|
end
|
97
|
-
signature_base << "\"@signature-params\": #{signature_params}"
|
98
|
-
signature_base
|
99
40
|
end
|
100
41
|
end
|
101
42
|
end
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
@@ -4,16 +4,30 @@ 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"
|
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"
|
16
|
+
require_relative "linzer/signer"
|
8
17
|
require_relative "linzer/verifier"
|
9
18
|
|
10
19
|
module Linzer
|
11
20
|
class Error < StandardError; end
|
12
21
|
|
13
22
|
class << self
|
14
|
-
|
15
|
-
|
16
|
-
|
23
|
+
include Key::Helper
|
24
|
+
|
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.
|
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-
|
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,16 @@ 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
|
70
|
+
- lib/linzer/signature.rb
|
71
|
+
- lib/linzer/signer.rb
|
63
72
|
- lib/linzer/verifier.rb
|
64
73
|
- lib/linzer/version.rb
|
65
74
|
- linzer.gemspec
|