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 +4 -4
- data/.standard.yml +8 -0
- data/CHANGELOG.md +6 -1
- data/README.md +49 -3
- data/lib/linzer/message.rb +43 -4
- data/lib/linzer/signature.rb +75 -0
- data/lib/linzer/signer.rb +68 -0
- data/lib/linzer/verifier.rb +23 -84
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +17 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9b2828e9333279ffabf8102d126f832ebece8e6b5ce26bec6897337bae773b4
|
4
|
+
data.tar.gz: 868c9763f3067b55eb8fc76dfb1b6c5e6529562a57ee08d41bbfb3f443311536
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef676846ea6c038465971a4c5fa47b77763f74e37adc357b5c967c483a5d3c767f0e3d1b0e073c16621ac311cc07bd524fe422e0272e9ef6e0b88091e15120e7
|
7
|
+
data.tar.gz: 8047b4e3ea2778e53764bc625d04c4ba4435f2399992885406f46128a69502158598a0130184b49887ad576252220bc71bfdbe451910971ce7357a84374f2818
|
data/.standard.yml
CHANGED
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
|
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
|
-
|
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:...
|
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
|
-
|
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
|
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,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"
|
27
|
+
when "@method" then @http["method"]
|
23
28
|
when "@authority" then @http["host"]
|
24
|
-
when "@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
|
data/lib/linzer/verifier.rb
CHANGED
@@ -1,101 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Linzer
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
33
|
-
|
9
|
+
parameters = signature.parameters
|
10
|
+
components = signature.components
|
34
11
|
|
35
|
-
|
12
|
+
signature_base = message.signature_base(components, parameters)
|
36
13
|
|
37
|
-
|
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
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
18
|
+
private
|
47
19
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
data/lib/linzer/version.rb
CHANGED
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(
|
15
|
-
Linzer::Verifier.
|
16
|
-
|
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.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-
|
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
|