melos 0.0.2 → 0.1.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/README.md +63 -11
- data/docs/validation.md +168 -0
- data/lib/melos/capabilities.rb +15 -0
- data/lib/melos/client.rb +85 -0
- data/lib/melos/crypto.rb +84 -15
- data/lib/melos/epoch_secrets.rb +113 -0
- data/lib/melos/group/validation.rb +237 -0
- data/lib/melos/group.rb +594 -0
- data/lib/melos/key_package_bundle.rb +59 -0
- data/lib/melos/private_tree.rb +51 -0
- data/lib/melos/ratchet_tree.rb +407 -0
- data/lib/melos/secret_tree.rb +72 -89
- data/lib/melos/struct/base.rb +46 -20
- data/lib/melos/struct/structs.rb +162 -87
- data/lib/melos/tree.rb +4 -120
- data/lib/melos/vec.rb +20 -35
- data/lib/melos/version.rb +1 -1
- data/lib/melos.rb +9 -2
- metadata +13 -6
- data/lib/melos/key_schedule.rb +0 -62
- data/lib/melos/struct/ratchet_tree.rb +0 -324
data/lib/melos/struct/base.rb
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
module Melos::Struct; end
|
|
2
2
|
|
|
3
3
|
class Melos::Struct::Base
|
|
4
|
-
|
|
4
|
+
# external: values this struct's select fields depend on but which belong to an
|
|
5
|
+
# enclosing struct (e.g. FramedContent.content_type for FramedContentAuthData).
|
|
6
|
+
# They are seeded into the parse context and set as instance variables, but are
|
|
7
|
+
# not part of STRUCT and so are never serialized.
|
|
8
|
+
def initialize(stream, **external)
|
|
5
9
|
stream = StringIO.new(stream) if stream.is_a?(String)
|
|
6
|
-
context = deserialize(stream)
|
|
10
|
+
context = deserialize(stream, external)
|
|
7
11
|
set_instance_vars(context)
|
|
8
12
|
self
|
|
9
13
|
end
|
|
10
14
|
|
|
15
|
+
# build an instance from its field values. Structs with defaults or fields that
|
|
16
|
+
# depend on an enclosing struct define their own create.
|
|
17
|
+
def self.create(**fields)
|
|
18
|
+
unknown = fields.keys - self::STRUCT.map(&:first)
|
|
19
|
+
raise ArgumentError.new("#{name}: unknown fields #{unknown.join(', ')}") unless unknown.empty?
|
|
20
|
+
instance = allocate
|
|
21
|
+
fields.each { |field, value| instance.instance_variable_set("@#{field}", value) }
|
|
22
|
+
instance
|
|
23
|
+
end
|
|
24
|
+
|
|
11
25
|
def raw
|
|
12
26
|
buf = ''
|
|
27
|
+
context = field_context
|
|
13
28
|
self.class::STRUCT.each do |elem|
|
|
29
|
+
value = self.instance_variable_get("@#{elem[0]}")
|
|
14
30
|
case elem[1]
|
|
15
31
|
when :select
|
|
16
|
-
|
|
17
|
-
buf += serialize_select_elem(value, elem[3])
|
|
32
|
+
buf += serialize_select_elem(elem[0], value, elem[2], elem[3], context)
|
|
18
33
|
else
|
|
19
|
-
value = self.instance_variable_get("@#{elem[0]}")
|
|
20
34
|
buf += serialize_elem(value, elem[1], elem[2])
|
|
21
35
|
end
|
|
22
36
|
end
|
|
@@ -36,15 +50,19 @@ class Melos::Struct::Base
|
|
|
36
50
|
end
|
|
37
51
|
|
|
38
52
|
private
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
# external is a Hash of values seeded into the context before parsing (see #initialize)
|
|
54
|
+
def deserialize(stream, external = {})
|
|
55
|
+
context = external.to_a
|
|
41
56
|
self.class::STRUCT.each do |elem|
|
|
42
57
|
case elem[1]
|
|
43
58
|
when :select
|
|
44
59
|
value = deserialize_select_elem_with_context(stream, context.to_h, elem[2], elem[3], elem[4])
|
|
45
60
|
context << [elem[0], value]
|
|
46
|
-
when :
|
|
47
|
-
|
|
61
|
+
when :class
|
|
62
|
+
# [:name, :class, Klass, ->(ctx){ {external_name: value} }]
|
|
63
|
+
# the optional 4th element derives external context for the child struct from this struct's context
|
|
64
|
+
external_for_child = elem[3] ? elem[3].(context.to_h) : {}
|
|
65
|
+
value = elem[2].new(stream, **external_for_child)
|
|
48
66
|
context << [elem[0], value]
|
|
49
67
|
else
|
|
50
68
|
value = deserialize_elem(stream, elem[1], elem[2])
|
|
@@ -71,9 +89,9 @@ class Melos::Struct::Base
|
|
|
71
89
|
when :uint64
|
|
72
90
|
value = stream.read(8).unpack1('Q>')
|
|
73
91
|
when :vec
|
|
74
|
-
value = Melos::Vec.
|
|
92
|
+
value = Melos::Vec.parse_stream(stream)
|
|
75
93
|
when :vec_of_type
|
|
76
|
-
data = Melos::Vec.
|
|
94
|
+
data = Melos::Vec.parse_stream(stream)
|
|
77
95
|
value = []
|
|
78
96
|
data_stream = StringIO.new(data)
|
|
79
97
|
while (!data_stream.eof?)
|
|
@@ -83,7 +101,7 @@ class Melos::Struct::Base
|
|
|
83
101
|
when :class
|
|
84
102
|
value = type_param.send(:new, stream)
|
|
85
103
|
when :classes
|
|
86
|
-
data = Melos::Vec.
|
|
104
|
+
data = Melos::Vec.parse_stream(stream)
|
|
87
105
|
value = []
|
|
88
106
|
data_stream = StringIO.new(data)
|
|
89
107
|
while (!data_stream.eof?)
|
|
@@ -119,13 +137,13 @@ class Melos::Struct::Base
|
|
|
119
137
|
when :uint64
|
|
120
138
|
[value].pack('Q>')
|
|
121
139
|
when :vec
|
|
122
|
-
Melos::Vec.
|
|
140
|
+
Melos::Vec.string_to_vec(value)
|
|
123
141
|
when :vec_of_type
|
|
124
|
-
Melos::Vec.
|
|
125
|
-
when :class
|
|
142
|
+
Melos::Vec.string_to_vec(value.map { serialize_elem(_1, type_param, nil) }.join)
|
|
143
|
+
when :class
|
|
126
144
|
value.raw
|
|
127
145
|
when :classes
|
|
128
|
-
Melos::Vec.
|
|
146
|
+
Melos::Vec.string_to_vec(value.map(&:raw).join)
|
|
129
147
|
when :optional
|
|
130
148
|
if value.nil?
|
|
131
149
|
[0].pack('C')
|
|
@@ -140,11 +158,19 @@ class Melos::Struct::Base
|
|
|
140
158
|
end
|
|
141
159
|
end
|
|
142
160
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
# every instance variable as a Hash, the same shape as the parse context,
|
|
162
|
+
# so that select predicates can be evaluated at serialization time
|
|
163
|
+
def field_context
|
|
164
|
+
instance_variables.to_h { [_1.to_s.delete_prefix('@').to_sym, instance_variable_get(_1)] }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# a select field is serialized only when its predicate holds; it must then be present
|
|
168
|
+
def serialize_select_elem(name, value, predicate, type, context)
|
|
169
|
+
if predicate.(context)
|
|
170
|
+
raise ArgumentError.new("#{self.class.name}##{name} is required by its select predicate but is nil") if value.nil?
|
|
147
171
|
serialize_elem(value, type, nil)
|
|
172
|
+
else
|
|
173
|
+
''
|
|
148
174
|
end
|
|
149
175
|
end
|
|
150
176
|
end
|
data/lib/melos/struct/structs.rb
CHANGED
|
@@ -33,6 +33,13 @@ class Melos::Struct::Credential < Melos::Struct::Base
|
|
|
33
33
|
[:identity, :select, ->(ctx){ctx[:credential_type] == Melos::Constants::CredentialType::BASIC}, :vec],
|
|
34
34
|
[:certificates, :select, ->(ctx){ctx[:credential_type] == Melos::Constants::CredentialType::X509}, :classes, Melos::Struct::Certificate]
|
|
35
35
|
]
|
|
36
|
+
|
|
37
|
+
def self.create_basic_credential(identity)
|
|
38
|
+
new_instance = self.allocate
|
|
39
|
+
new_instance.instance_variable_set(:@credential_type, Melos::Constants::CredentialType::BASIC)
|
|
40
|
+
new_instance.instance_variable_set(:@identity, identity)
|
|
41
|
+
new_instance
|
|
42
|
+
end
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
## 6.3.2
|
|
@@ -84,6 +91,26 @@ class Melos::Struct::Capabilities < Melos::Struct::Base
|
|
|
84
91
|
[:proposals, :vec], # vec of ProposalTypes (uint16)
|
|
85
92
|
[:credentials, :vec] # vec of CredentialTypes (uint16)
|
|
86
93
|
]
|
|
94
|
+
|
|
95
|
+
def self.import(capabilities)
|
|
96
|
+
new_instance = self.allocate
|
|
97
|
+
new_instance.instance_variable_set(:@versions, capabilities.versions.pack('S>*'))
|
|
98
|
+
new_instance.instance_variable_set(:@cipher_suites, capabilities.cipher_suites.pack('S>*'))
|
|
99
|
+
new_instance.instance_variable_set(:@extensions, capabilities.extensions.pack('S>*'))
|
|
100
|
+
new_instance.instance_variable_set(:@proposals, capabilities.proposals.pack('S>*'))
|
|
101
|
+
new_instance.instance_variable_set(:@credentials, capabilities.credentials.pack('S>*'))
|
|
102
|
+
new_instance
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def export
|
|
106
|
+
cap = Melos::Capabilities.new
|
|
107
|
+
cap.versions = versions.unpack('S>*')
|
|
108
|
+
cap.cipher_suites = cipher_suites.unpack('S>*')
|
|
109
|
+
cap.extensions = extensions.unpack('S>*')
|
|
110
|
+
cap.proposals = proposals.unpack('S>*')
|
|
111
|
+
cap.credentials = credentials.unpack('S>*')
|
|
112
|
+
cap
|
|
113
|
+
end
|
|
87
114
|
end
|
|
88
115
|
|
|
89
116
|
class Melos::Struct::Lifetime < Melos::Struct::Base
|
|
@@ -92,6 +119,13 @@ class Melos::Struct::Lifetime < Melos::Struct::Base
|
|
|
92
119
|
[:not_before, :uint64],
|
|
93
120
|
[:not_after, :uint64]
|
|
94
121
|
]
|
|
122
|
+
|
|
123
|
+
def self.create(not_before:, not_after:)
|
|
124
|
+
new_instance = self.allocate
|
|
125
|
+
new_instance.instance_variable_set(:@not_before, not_before)
|
|
126
|
+
new_instance.instance_variable_set(:@not_after, not_after)
|
|
127
|
+
new_instance
|
|
128
|
+
end
|
|
95
129
|
end
|
|
96
130
|
|
|
97
131
|
class Melos::Struct::Extension < Melos::Struct::Base
|
|
@@ -100,6 +134,13 @@ class Melos::Struct::Extension < Melos::Struct::Base
|
|
|
100
134
|
[:extension_type, :uint16], # ExtensionType = uint16
|
|
101
135
|
[:extension_data, :vec]
|
|
102
136
|
]
|
|
137
|
+
|
|
138
|
+
# 13.4: any list of extensions MUST NOT have more than one extension of a given type
|
|
139
|
+
def self.check_unique_types!(extensions, where)
|
|
140
|
+
duplicates = extensions.map(&:extension_type).tally.select { |_type, count| count > 1 }.keys
|
|
141
|
+
return if duplicates.empty?
|
|
142
|
+
raise ArgumentError.new("#{where} has more than one extension of type #{duplicates.map { '0x%04x' % _1 }.join(', ')}")
|
|
143
|
+
end
|
|
103
144
|
end
|
|
104
145
|
|
|
105
146
|
class Melos::Struct::LeafNode < Melos::Struct::Base
|
|
@@ -118,19 +159,19 @@ class Melos::Struct::LeafNode < Melos::Struct::Base
|
|
|
118
159
|
|
|
119
160
|
def leaf_node_tbs(group_id, leaf_index)
|
|
120
161
|
buf = ''
|
|
121
|
-
buf += Melos::Vec.
|
|
122
|
-
buf += Melos::Vec.
|
|
162
|
+
buf += Melos::Vec.string_to_vec(encryption_key)
|
|
163
|
+
buf += Melos::Vec.string_to_vec(signature_key)
|
|
123
164
|
buf += credential.raw
|
|
124
165
|
buf += capabilities.raw
|
|
125
166
|
buf += [leaf_node_source].pack('C')
|
|
126
167
|
if leaf_node_source == Melos::Constants::LeafNodeSource::KEY_PACKAGE
|
|
127
168
|
buf += lifetime.raw
|
|
128
169
|
elsif leaf_node_source == Melos::Constants::LeafNodeSource::COMMIT
|
|
129
|
-
buf += Melos::Vec.
|
|
170
|
+
buf += Melos::Vec.string_to_vec(parent_hash)
|
|
130
171
|
end
|
|
131
|
-
buf += Melos::Vec.
|
|
172
|
+
buf += Melos::Vec.string_to_vec(extensions.map(&:raw).join)
|
|
132
173
|
if leaf_node_source == Melos::Constants::LeafNodeSource::UPDATE || leaf_node_source == Melos::Constants::LeafNodeSource::COMMIT
|
|
133
|
-
buf += Melos::Vec.
|
|
174
|
+
buf += Melos::Vec.string_to_vec(group_id)
|
|
134
175
|
buf += [leaf_index].pack('L>') # uint32
|
|
135
176
|
end
|
|
136
177
|
buf
|
|
@@ -344,6 +385,31 @@ class Melos::Struct::KeyPackage < Melos::Struct::Base
|
|
|
344
385
|
def ref(suite)
|
|
345
386
|
Melos::Crypto.make_keypackage_ref(suite, self.raw)
|
|
346
387
|
end
|
|
388
|
+
|
|
389
|
+
def self.create(cipher_suite:, init_key:, leaf_node:)
|
|
390
|
+
new_instance = self.allocate
|
|
391
|
+
new_instance.instance_variable_set(:@version, 1)
|
|
392
|
+
new_instance.instance_variable_set(:@cipher_suite, cipher_suite)
|
|
393
|
+
new_instance.instance_variable_set(:@init_key, init_key)
|
|
394
|
+
new_instance.instance_variable_set(:@leaf_node, leaf_node)
|
|
395
|
+
new_instance.instance_variable_set(:@extensions, leaf_node.extensions)
|
|
396
|
+
new_instance.instance_variable_set(:@signature, '')
|
|
397
|
+
new_instance
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def keypackage_tbs
|
|
401
|
+
[version].pack('S>') + [cipher_suite].pack('S>') + Melos::Vec.string_to_vec(init_key) + leaf_node.raw + Melos::Vec.string_to_vec(extensions.map(&:raw).join)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def sign(signature_private_key)
|
|
405
|
+
suite = Melos::Crypto::CipherSuite.new(@cipher_suite)
|
|
406
|
+
@signature = Melos::Crypto.sign_with_label(suite, signature_private_key, "KeyPackageTBS", keypackage_tbs)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def verify()
|
|
410
|
+
suite = Melos::Crypto::CipherSuite.new(@cipher_suite)
|
|
411
|
+
Melos::Crypto.verify_with_label(suite, leaf_node.signature_key, "KeyPackageTBS", keypackage_tbs, signature)
|
|
412
|
+
end
|
|
347
413
|
end
|
|
348
414
|
|
|
349
415
|
class Melos::Struct::KeyPackageTBS < Melos::Struct::Base
|
|
@@ -373,7 +439,7 @@ end
|
|
|
373
439
|
## 12.1.1 - 12.1.7
|
|
374
440
|
|
|
375
441
|
class Melos::Struct::Add < Melos::Struct::Base
|
|
376
|
-
|
|
442
|
+
attr_accessor :key_package
|
|
377
443
|
STRUCT = [
|
|
378
444
|
[:key_package, :class, Melos::Struct::KeyPackage]
|
|
379
445
|
]
|
|
@@ -387,7 +453,7 @@ class Melos::Struct::Update < Melos::Struct::Base
|
|
|
387
453
|
end
|
|
388
454
|
|
|
389
455
|
class Melos::Struct::Remove < Melos::Struct::Base
|
|
390
|
-
|
|
456
|
+
attr_accessor :removed
|
|
391
457
|
STRUCT = [
|
|
392
458
|
[:removed, :uint32]
|
|
393
459
|
]
|
|
@@ -437,7 +503,7 @@ end
|
|
|
437
503
|
## 12.1
|
|
438
504
|
|
|
439
505
|
class Melos::Struct::Proposal < Melos::Struct::Base
|
|
440
|
-
|
|
506
|
+
attr_accessor :proposal_type, :add, :update, :remove, :psk, :reinit, :external_init, :group_context_extensions
|
|
441
507
|
STRUCT = [
|
|
442
508
|
[:proposal_type, :uint16],
|
|
443
509
|
[:add, :select, ->(ctx){ctx[:proposal_type] == Melos::Constants::ProposalType::ADD}, :class, Melos::Struct::Add],
|
|
@@ -463,6 +529,14 @@ class Melos::Struct::ProposalOrRef < Melos::Struct::Base
|
|
|
463
529
|
[:proposal, :select, ->(ctx){ctx[:type] == Melos::Constants::ProposalOrRefType::PROPOSAL}, :class, Melos::Struct::Proposal],
|
|
464
530
|
[:reference, :select, ->(ctx){ctx[:type] == Melos::Constants::ProposalOrRefType::REFERENCE}, :vec] # ProposalRef is a HashReference, which is a :vec
|
|
465
531
|
]
|
|
532
|
+
|
|
533
|
+
def self.create(type:, proposal:, reference:)
|
|
534
|
+
new_instance = self.allocate
|
|
535
|
+
new_instance.instance_variable_set(:@type, type)
|
|
536
|
+
new_instance.instance_variable_set(:@proposal, proposal)
|
|
537
|
+
new_instance.instance_variable_set(:@reference, reference)
|
|
538
|
+
new_instance
|
|
539
|
+
end
|
|
466
540
|
end
|
|
467
541
|
|
|
468
542
|
class Melos::Struct::Commit < Melos::Struct::Base
|
|
@@ -471,6 +545,13 @@ class Melos::Struct::Commit < Melos::Struct::Base
|
|
|
471
545
|
[:proposals, :classes, Melos::Struct::ProposalOrRef],
|
|
472
546
|
[:path, :optional, Melos::Struct::UpdatePath]
|
|
473
547
|
]
|
|
548
|
+
|
|
549
|
+
def self.create(proposals:, path:)
|
|
550
|
+
new_instance = self.allocate
|
|
551
|
+
new_instance.instance_variable_set(:@proposals, proposals)
|
|
552
|
+
new_instance.instance_variable_set(:@path, path)
|
|
553
|
+
new_instance
|
|
554
|
+
end
|
|
474
555
|
end
|
|
475
556
|
|
|
476
557
|
## 12.4.3
|
|
@@ -494,8 +575,8 @@ class Melos::Struct::GroupInfo < Melos::Struct::Base
|
|
|
494
575
|
)
|
|
495
576
|
end
|
|
496
577
|
|
|
497
|
-
def sign(suite,
|
|
498
|
-
Melos::Crypto.sign_with_label(suite, signer_private_key, "GroupInfoTBS", group_info_tbs.raw)
|
|
578
|
+
def sign(suite, signer_private_key)
|
|
579
|
+
@signature = Melos::Crypto.sign_with_label(suite, signer_private_key, "GroupInfoTBS", group_info_tbs.raw)
|
|
499
580
|
end
|
|
500
581
|
|
|
501
582
|
def verify(suite, signer_public_key)
|
|
@@ -575,8 +656,9 @@ class Melos::Struct::Node < Melos::Struct::Base
|
|
|
575
656
|
[:parent_node, :select, ->(ctx){ctx[:node_type] == Melos::Constants::NodeType::PARENT}, :class, Melos::Struct::ParentNode] # parent
|
|
576
657
|
]
|
|
577
658
|
|
|
659
|
+
# the parent_hash this node carries: nil for a leaf whose source is key_package or update (7.2)
|
|
578
660
|
def parent_hash_in_node
|
|
579
|
-
@
|
|
661
|
+
@parent_node ? @parent_node.parent_hash : @leaf_node.parent_hash
|
|
580
662
|
end
|
|
581
663
|
|
|
582
664
|
def public_encryption_key
|
|
@@ -630,36 +712,17 @@ class Melos::Struct::Sender < Melos::Struct::Base
|
|
|
630
712
|
end
|
|
631
713
|
end
|
|
632
714
|
|
|
715
|
+
# The select in FramedContentAuthData depends on FramedContent.content_type, a field of
|
|
716
|
+
# the enclosing struct, so the parent passes it in as external context:
|
|
717
|
+
# [:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content].content_type} }]
|
|
633
718
|
class Melos::Struct::FramedContentAuthData < Melos::Struct::Base
|
|
634
719
|
attr_reader :signature, :confirmation_tag, :content_type
|
|
635
720
|
|
|
636
721
|
STRUCT = [
|
|
637
|
-
[:signature, :vec]
|
|
722
|
+
[:signature, :vec],
|
|
723
|
+
[:confirmation_tag, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::COMMIT}, :vec] # MAC is opaque <V>
|
|
638
724
|
]
|
|
639
725
|
|
|
640
|
-
# initialize from stream
|
|
641
|
-
def self.new_with_content_type(stream, content_type)
|
|
642
|
-
instance = self.allocate
|
|
643
|
-
context = instance.send(:deserialize, stream)
|
|
644
|
-
# custom part based on instance variable
|
|
645
|
-
if content_type == Melos::Constants::ContentType::COMMIT # commit
|
|
646
|
-
# read MAC(opaque <V>) confirmation_tag
|
|
647
|
-
value = Melos::Vec.parse_stringio(stream)
|
|
648
|
-
context << [:confirmation_tag, value]
|
|
649
|
-
end
|
|
650
|
-
context << [:content_type, content_type]
|
|
651
|
-
instance.send(:set_instance_vars, context)
|
|
652
|
-
instance
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
def raw
|
|
656
|
-
if @content_type == Melos::Constants::ContentType::COMMIT
|
|
657
|
-
Melos::Vec.from_string(@signature) + Melos::Vec.from_string(@confirmation_tag)
|
|
658
|
-
else
|
|
659
|
-
Melos::Vec.from_string(@signature)
|
|
660
|
-
end
|
|
661
|
-
end
|
|
662
|
-
|
|
663
726
|
def self.create(signature:, content_type:, confirmation_tag:)
|
|
664
727
|
instance = self.allocate
|
|
665
728
|
instance.instance_variable_set(:@signature, signature)
|
|
@@ -669,6 +732,12 @@ class Melos::Struct::FramedContentAuthData < Melos::Struct::Base
|
|
|
669
732
|
end
|
|
670
733
|
instance
|
|
671
734
|
end
|
|
735
|
+
|
|
736
|
+
# a commit's confirmation tag is computed after signing, once the next epoch's key schedule is known (8.2)
|
|
737
|
+
def confirmation_tag=(tag)
|
|
738
|
+
raise ArgumentError.new('only a commit carries a confirmation tag') unless @content_type == Melos::Constants::ContentType::COMMIT
|
|
739
|
+
@confirmation_tag = tag
|
|
740
|
+
end
|
|
672
741
|
end
|
|
673
742
|
|
|
674
743
|
class Melos::Struct::FramedContent < Melos::Struct::Base
|
|
@@ -719,7 +788,7 @@ class Melos::Struct::AuthenticatedContent < Melos::Struct::Base
|
|
|
719
788
|
STRUCT = [
|
|
720
789
|
[:wire_format, :uint16],
|
|
721
790
|
[:content, :class, Melos::Struct::FramedContent],
|
|
722
|
-
[:auth, :
|
|
791
|
+
[:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content].content_type} }]
|
|
723
792
|
]
|
|
724
793
|
|
|
725
794
|
def content_tbm
|
|
@@ -750,15 +819,15 @@ class Melos::Struct::AuthenticatedContent < Melos::Struct::Base
|
|
|
750
819
|
new_instance
|
|
751
820
|
end
|
|
752
821
|
|
|
753
|
-
# populate auth with
|
|
754
|
-
def sign(suite, signature_private_key, group_context)
|
|
822
|
+
# populate auth with the signature (6.1); a commit's confirmation_tag may be given here or set on auth later
|
|
823
|
+
def sign(suite, signature_private_key, group_context, confirmation_tag: nil)
|
|
755
824
|
raise ArgumentError.new('Application data cannot be sent as a PublicMessage') if wire_format == Melos::Constants::WireFormat::MLS_PUBLIC_MESSAGE && content.content_type == Melos::Constants::ContentType::APPLICATION
|
|
756
825
|
content_tbs = content.content_tbs(Melos::Constants::Version::MLS10, wire_format, group_context)
|
|
757
826
|
signature = Melos::Crypto.sign_with_label(suite, signature_private_key, "FramedContentTBS", content_tbs)
|
|
758
827
|
@auth = Melos::Struct::FramedContentAuthData.create(
|
|
759
828
|
signature: signature,
|
|
760
829
|
content_type: content.content_type,
|
|
761
|
-
confirmation_tag:
|
|
830
|
+
confirmation_tag: confirmation_tag
|
|
762
831
|
)
|
|
763
832
|
end
|
|
764
833
|
end
|
|
@@ -769,7 +838,7 @@ class Melos::Struct::PublicMessage < Melos::Struct::Base
|
|
|
769
838
|
attr_reader :content, :auth, :membership_tag
|
|
770
839
|
STRUCT = [
|
|
771
840
|
[:content, :class, Melos::Struct::FramedContent],
|
|
772
|
-
[:auth, :
|
|
841
|
+
[:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content].content_type} }],
|
|
773
842
|
[:membership_tag, :select, ->(ctx){ctx[:content].sender.sender_type == Melos::Constants::SenderType::MEMBER}, :vec] # MAC is opaque <V>
|
|
774
843
|
]
|
|
775
844
|
|
|
@@ -834,7 +903,7 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
|
|
|
834
903
|
end
|
|
835
904
|
|
|
836
905
|
def self.sender_data_aad_impl(gid, ep, ct)
|
|
837
|
-
Melos::Vec.
|
|
906
|
+
Melos::Vec.string_to_vec(gid) + [ep].pack('Q>') + [ct].pack('C')
|
|
838
907
|
end
|
|
839
908
|
|
|
840
909
|
def private_content_aad
|
|
@@ -842,17 +911,21 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
|
|
|
842
911
|
end
|
|
843
912
|
|
|
844
913
|
def self.private_content_aad_impl(gid, ep, ct, ad)
|
|
845
|
-
Melos::Vec.
|
|
914
|
+
Melos::Vec.string_to_vec(gid) + [ep].pack('Q>') + [ct].pack('C') + Melos::Vec.string_to_vec(ad)
|
|
846
915
|
end
|
|
847
916
|
|
|
848
917
|
def self.protect(authenticated_content, suite, secret_tree, sender_data_secret, padding_size)
|
|
849
918
|
leaf_index = authenticated_content.content.sender.leaf_index
|
|
850
919
|
content_type = authenticated_content.content.content_type
|
|
851
920
|
reuse_guard = SecureRandom.random_bytes(4)
|
|
852
|
-
key, nonce, generation =
|
|
921
|
+
key, nonce, generation = secret_tree.ratchet_and_get(content_type, leaf_index)
|
|
853
922
|
new_nonce = apply_nonce_reuse_guard(nonce, reuse_guard)
|
|
854
923
|
|
|
855
|
-
private_message_content_plain =
|
|
924
|
+
private_message_content_plain = Melos::Struct::PrivateMessageContent.create(
|
|
925
|
+
framed_content: authenticated_content.content,
|
|
926
|
+
auth: authenticated_content.auth,
|
|
927
|
+
padding_size: padding_size
|
|
928
|
+
).raw
|
|
856
929
|
aad = private_content_aad_impl(
|
|
857
930
|
authenticated_content.content.group_id,
|
|
858
931
|
authenticated_content.content.epoch,
|
|
@@ -885,10 +958,12 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
|
|
|
885
958
|
|
|
886
959
|
def unprotect(suite, secret_tree, sender_data_secret)
|
|
887
960
|
sender_data = decrypt_sender_data(suite, sender_data_secret)
|
|
888
|
-
key, nonce, _ =
|
|
961
|
+
key, nonce, _ = secret_tree.ratchet_until_and_get(content_type, sender_data.leaf_index, sender_data.generation)
|
|
889
962
|
new_nonce = self.class.apply_nonce_reuse_guard(nonce, sender_data.reuse_guard)
|
|
890
963
|
stream = StringIO.new(Melos::Crypto.aead_decrypt(suite, key, new_nonce, private_content_aad, ciphertext))
|
|
891
|
-
pmc = Melos::Struct::PrivateMessageContent.
|
|
964
|
+
pmc = Melos::Struct::PrivateMessageContent.new(stream, content_type: content_type)
|
|
965
|
+
# 6.3.1: the padding MUST be all zero bytes, otherwise the message is malformed
|
|
966
|
+
raise ArgumentError.new('PrivateMessage padding is not all zero') unless pmc.padding_zero?
|
|
892
967
|
|
|
893
968
|
fc = Melos::Struct::FramedContent.create(
|
|
894
969
|
group_id: group_id,
|
|
@@ -922,56 +997,44 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
|
|
|
922
997
|
nonce_arr.pack('C*')
|
|
923
998
|
end
|
|
924
999
|
|
|
925
|
-
def self.serialize_private_message_content(framed_content, framed_content_auth_data, padding_size)
|
|
926
|
-
buf = ''
|
|
927
|
-
case framed_content.content_type
|
|
928
|
-
when Melos::Constants::ContentType::APPLICATION
|
|
929
|
-
buf += Melos::Vec.from_string(framed_content.application_data)
|
|
930
|
-
when Melos::Constants::ContentType::PROPOSAL
|
|
931
|
-
buf += framed_content.proposal.raw
|
|
932
|
-
when Melos::Constants::ContentType::COMMIT
|
|
933
|
-
buf += framed_content.commit.raw
|
|
934
|
-
end
|
|
935
|
-
buf += Melos::Vec.from_string(framed_content_auth_data.signature)
|
|
936
|
-
if framed_content.content_type == Melos::Constants::ContentType::COMMIT
|
|
937
|
-
buf += Melos::Vec.from_string(framed_content_auth_data.confirmation_tag)
|
|
938
|
-
end
|
|
939
|
-
buf += Melos::Crypto::Util.zero_vector(padding_size)
|
|
940
|
-
buf
|
|
941
|
-
end
|
|
942
1000
|
end
|
|
943
1001
|
|
|
1002
|
+
# The selects in PrivateMessageContent depend on PrivateMessage.content_type, a field of
|
|
1003
|
+
# the enclosing struct, so the caller passes it in:
|
|
1004
|
+
# Melos::Struct::PrivateMessageContent.new(stream, content_type: content_type)
|
|
944
1005
|
class Melos::Struct::PrivateMessageContent < Melos::Struct::Base
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1006
|
+
attr_reader :application_data, :proposal, :commit, :auth, :padding, :content_type
|
|
1007
|
+
STRUCT = [
|
|
1008
|
+
[:application_data, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::APPLICATION}, :vec],
|
|
1009
|
+
[:proposal, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::PROPOSAL}, :class, Melos::Struct::Proposal],
|
|
1010
|
+
[:commit, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::COMMIT}, :class, Melos::Struct::Commit],
|
|
1011
|
+
[:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content_type]} }],
|
|
1012
|
+
[:padding, :padding] # the rest of the stream; zero bytes
|
|
1013
|
+
]
|
|
948
1014
|
|
|
949
|
-
def self.
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
case content_type
|
|
1015
|
+
def self.create(framed_content:, auth:, padding_size:)
|
|
1016
|
+
new_instance = self.allocate
|
|
1017
|
+
new_instance.instance_variable_set(:@content_type, framed_content.content_type)
|
|
1018
|
+
case framed_content.content_type
|
|
954
1019
|
when Melos::Constants::ContentType::APPLICATION
|
|
955
|
-
|
|
956
|
-
context << [:application_data, value]
|
|
1020
|
+
new_instance.instance_variable_set(:@application_data, framed_content.application_data)
|
|
957
1021
|
when Melos::Constants::ContentType::PROPOSAL
|
|
958
|
-
|
|
959
|
-
context << [:proposal, value]
|
|
1022
|
+
new_instance.instance_variable_set(:@proposal, framed_content.proposal)
|
|
960
1023
|
when Melos::Constants::ContentType::COMMIT
|
|
961
|
-
|
|
962
|
-
context << [:commit, value]
|
|
1024
|
+
new_instance.instance_variable_set(:@commit, framed_content.commit)
|
|
963
1025
|
end
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
context << [:padding, stream.read()]
|
|
968
|
-
instance.send(:set_instance_vars, context)
|
|
969
|
-
instance
|
|
1026
|
+
new_instance.instance_variable_set(:@auth, auth)
|
|
1027
|
+
new_instance.instance_variable_set(:@padding, Melos::Crypto::Util.zero_vector(padding_size))
|
|
1028
|
+
new_instance
|
|
970
1029
|
end
|
|
971
1030
|
|
|
972
1031
|
def content
|
|
973
1032
|
@application_data || @proposal || @commit
|
|
974
1033
|
end
|
|
1034
|
+
|
|
1035
|
+
def padding_zero?
|
|
1036
|
+
@padding.each_byte.all?(&:zero?)
|
|
1037
|
+
end
|
|
975
1038
|
end
|
|
976
1039
|
|
|
977
1040
|
## 8.2
|
|
@@ -1012,9 +1075,21 @@ class Melos::Struct::MLSMessage < Melos::Struct::Base
|
|
|
1012
1075
|
[:key_package, :select, ->(ctx){ctx[:wire_format] == Melos::Constants::WireFormat::MLS_KEY_PACKAGE}, :class, Melos::Struct::KeyPackage]
|
|
1013
1076
|
]
|
|
1014
1077
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1078
|
+
# wrap exactly one of the message types; the wire format follows from which one is given
|
|
1079
|
+
def self.create(public_message: nil, private_message: nil, welcome: nil, group_info: nil, key_package: nil)
|
|
1080
|
+
given = {
|
|
1081
|
+
public_message: [Melos::Constants::WireFormat::MLS_PUBLIC_MESSAGE, public_message],
|
|
1082
|
+
private_message: [Melos::Constants::WireFormat::MLS_PRIVATE_MESSAGE, private_message],
|
|
1083
|
+
welcome: [Melos::Constants::WireFormat::MLS_WELCOME, welcome],
|
|
1084
|
+
group_info: [Melos::Constants::WireFormat::MLS_GROUP_INFO, group_info],
|
|
1085
|
+
key_package: [Melos::Constants::WireFormat::MLS_KEY_PACKAGE, key_package]
|
|
1086
|
+
}.reject { |_, (_, value)| value.nil? }
|
|
1087
|
+
raise ArgumentError.new('exactly one message must be given') unless given.size == 1
|
|
1088
|
+
field, (wire_format, value) = given.first
|
|
1089
|
+
instance = allocate
|
|
1090
|
+
instance.instance_variable_set(:@version, Melos::Constants::Version::MLS10)
|
|
1091
|
+
instance.instance_variable_set(:@wire_format, wire_format)
|
|
1092
|
+
instance.instance_variable_set("@#{field}", value)
|
|
1093
|
+
instance
|
|
1019
1094
|
end
|
|
1020
1095
|
end
|