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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require_relative 'crypto'
|
|
2
|
+
require_relative 'secret_tree'
|
|
3
|
+
|
|
4
|
+
# The key schedule (RFC 9420 Section 8) for one epoch: the epoch_secret and the secrets
|
|
5
|
+
# derived from it (Table 4), the joiner and PSK secrets that produced it (needed to build
|
|
6
|
+
# a Welcome, 12.4.3.1), and the epoch's secret tree (Section 9).
|
|
7
|
+
#
|
|
8
|
+
# A new epoch comes from #next with the commit secret, PSK secret and new GroupContext,
|
|
9
|
+
# which uses this epoch's init_secret as Section 8 describes.
|
|
10
|
+
class Melos::EpochSecrets
|
|
11
|
+
attr_reader :suite, :epoch_secret, :joiner_secret, :psk_secret, :n_leaves
|
|
12
|
+
|
|
13
|
+
# Table 4, plus init_secret which is derived the same way
|
|
14
|
+
DERIVED_SECRETS = {
|
|
15
|
+
sender_data_secret: 'sender data',
|
|
16
|
+
encryption_secret: 'encryption',
|
|
17
|
+
exporter_secret: 'exporter',
|
|
18
|
+
external_secret: 'external',
|
|
19
|
+
confirmation_key: 'confirm',
|
|
20
|
+
membership_key: 'membership',
|
|
21
|
+
resumption_psk: 'resumption',
|
|
22
|
+
epoch_authenticator: 'authentication',
|
|
23
|
+
init_secret: 'init'
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
DERIVED_SECRETS.each do |name, label|
|
|
27
|
+
define_method(name) do
|
|
28
|
+
@derived[name] ||= Melos::Crypto.derive_secret(@suite, require_epoch_secret(name), label)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# derived: secrets known without an epoch_secret (see .from_init_secret and .from_components)
|
|
33
|
+
def initialize(suite, epoch_secret: nil, joiner_secret: nil, psk_secret: nil, n_leaves: nil, derived: {})
|
|
34
|
+
unknown = derived.keys - DERIVED_SECRETS.keys
|
|
35
|
+
raise ArgumentError.new("unknown secrets: #{unknown.join(', ')}") unless unknown.empty?
|
|
36
|
+
@suite = suite
|
|
37
|
+
@epoch_secret = epoch_secret
|
|
38
|
+
@joiner_secret = joiner_secret
|
|
39
|
+
@psk_secret = psk_secret
|
|
40
|
+
@n_leaves = n_leaves
|
|
41
|
+
@derived = derived.dup
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.from_epoch_secret(suite, epoch_secret, n_leaves: nil)
|
|
45
|
+
new(suite, epoch_secret: epoch_secret, n_leaves: n_leaves)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 8: epoch_secret = ExpandWithLabel(Extract(joiner_secret, psk_secret), "epoch", GroupContext)
|
|
49
|
+
# This is how a new member joins an epoch from a Welcome (12.4.3.1).
|
|
50
|
+
def self.from_joiner(suite, joiner_secret, psk_secret, group_context, n_leaves: nil)
|
|
51
|
+
member_secret = Melos::Crypto.kdf_extract(suite, joiner_secret, psk_secret)
|
|
52
|
+
epoch_secret = Melos::Crypto.expand_with_label(suite, member_secret, "epoch", group_context.raw, suite.kdf.n_h)
|
|
53
|
+
new(suite, epoch_secret: epoch_secret, joiner_secret: joiner_secret, psk_secret: psk_secret, n_leaves: n_leaves)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 8: the state before an epoch exists, when only init_secret is known (a fresh random
|
|
57
|
+
# value for a new group). Only #next is meaningful on it.
|
|
58
|
+
def self.from_init_secret(suite, init_secret)
|
|
59
|
+
new(suite, derived: { init_secret: init_secret })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# an epoch given by some of its derived secrets rather than by epoch_secret; for test vectors
|
|
63
|
+
def self.from_components(suite, n_leaves: nil, **secrets)
|
|
64
|
+
new(suite, n_leaves: n_leaves, derived: secrets)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 8: welcome_secret = DeriveSecret(Extract(joiner_secret, psk_secret), "welcome")
|
|
68
|
+
def self.welcome_secret(suite, joiner_secret, psk_secret)
|
|
69
|
+
Melos::Crypto.derive_secret(suite, Melos::Crypto.kdf_extract(suite, joiner_secret, psk_secret), "welcome")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 12.4.3.1: the key and nonce that encrypt the GroupInfo in a Welcome
|
|
73
|
+
def self.welcome_key_and_nonce(suite, joiner_secret, psk_secret)
|
|
74
|
+
ws = welcome_secret(suite, joiner_secret, psk_secret)
|
|
75
|
+
[
|
|
76
|
+
Melos::Crypto.expand_with_label(suite, ws, "key", "", suite.hpke.n_k),
|
|
77
|
+
Melos::Crypto.expand_with_label(suite, ws, "nonce", "", suite.hpke.n_n)
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def welcome_secret
|
|
82
|
+
self.class.welcome_secret(@suite, require_joiner_secret(:welcome_secret), @psk_secret)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def welcome_key_and_nonce
|
|
86
|
+
self.class.welcome_key_and_nonce(@suite, require_joiner_secret(:welcome_key_and_nonce), @psk_secret)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# 8: the next epoch. joiner_secret = ExpandWithLabel(Extract(init_secret, commit_secret), "joiner", GroupContext)
|
|
90
|
+
# n_leaves: leaf count of the ratchet tree in the new epoch, for its secret tree
|
|
91
|
+
def next(commit_secret, psk_secret, group_context, n_leaves: @n_leaves)
|
|
92
|
+
joiner_secret = Melos::Crypto.expand_with_label(
|
|
93
|
+
@suite, Melos::Crypto.kdf_extract(@suite, init_secret, commit_secret), "joiner", group_context.raw, @suite.kdf.n_h
|
|
94
|
+
)
|
|
95
|
+
self.class.from_joiner(@suite, joiner_secret, psk_secret, group_context, n_leaves: n_leaves)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 9: this epoch's secret tree, built on first use
|
|
99
|
+
def secret_tree
|
|
100
|
+
raise ArgumentError.new('n_leaves is needed to build the secret tree') if @n_leaves.nil?
|
|
101
|
+
@secret_tree ||= Melos::SecretTree.new(@suite, @n_leaves, encryption_secret)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def require_epoch_secret(name)
|
|
107
|
+
@epoch_secret or raise ArgumentError.new("#{name} is not available: this epoch has no epoch_secret")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def require_joiner_secret(name)
|
|
111
|
+
@joiner_secret or raise ArgumentError.new("#{name} is not available: this epoch has no joiner_secret")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Validation of the pieces that enter a group: proposal lists (12.2), UpdatePaths (12.4.2),
|
|
2
|
+
# KeyPackages (10.1) and LeafNodes (7.3). All methods raise ArgumentError on failure.
|
|
3
|
+
class Melos::Group
|
|
4
|
+
# 7.2: the proposal and extension types every client implements; they are not listed in Capabilities
|
|
5
|
+
DEFAULT_PROPOSAL_TYPES = (0x0001..0x0007).to_a.freeze
|
|
6
|
+
DEFAULT_EXTENSION_TYPES = (0x0001..0x0005).to_a.freeze
|
|
7
|
+
|
|
8
|
+
# 7.2: applications MUST define a maximum acceptable leaf node lifetime (seconds) and pass it as
|
|
9
|
+
# max_leaf_node_lifetime. There is no default limit: interop test vectors use the full uint64 range.
|
|
10
|
+
DEFAULT_MAX_LEAF_NODE_LIFETIME = nil
|
|
11
|
+
|
|
12
|
+
# 5.3.1: credentials are validated by the application's Authentication Service. Without one,
|
|
13
|
+
# only basic credentials are accepted, since nothing else can be checked here.
|
|
14
|
+
DEFAULT_CREDENTIAL_VALIDATOR = ->(credential, _leaf_node) { credential.credential_type == Melos::Constants::CredentialType::BASIC }
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# 12.2: the rules a regular commit's proposal list must satisfy as a whole; proposals is [proposal, sender] pairs.
|
|
19
|
+
# "The same client" is taken to mean the same signature key, which the RFC leaves to the application.
|
|
20
|
+
def validate_proposal_list!(proposals, committer)
|
|
21
|
+
of_type = ->(type) { proposals.select { |proposal, _sender| proposal.proposal_type == type } }
|
|
22
|
+
updates = of_type.(Melos::Constants::ProposalType::UPDATE)
|
|
23
|
+
removes = of_type.(Melos::Constants::ProposalType::REMOVE)
|
|
24
|
+
adds = of_type.(Melos::Constants::ProposalType::ADD)
|
|
25
|
+
psks = of_type.(Melos::Constants::ProposalType::PSK)
|
|
26
|
+
gces = of_type.(Melos::Constants::ProposalType::GROUP_CONTEXT_EXTENSIONS)
|
|
27
|
+
|
|
28
|
+
raise ArgumentError.new('commit contains an Update from the committer') if updates.any? { |_proposal, sender| sender == committer }
|
|
29
|
+
removed = removes.map { |proposal, _sender| proposal.remove.removed }
|
|
30
|
+
raise ArgumentError.new('commit removes its own sender') if removed.include?(committer)
|
|
31
|
+
touched = updates.map { |_proposal, sender| sender } + removed
|
|
32
|
+
repeated = touched.tally.select { |_leaf, count| count > 1 }.keys
|
|
33
|
+
raise ArgumentError.new("more than one Update or Remove for leaf #{repeated.join(', ')}") unless repeated.empty?
|
|
34
|
+
|
|
35
|
+
added_keys = adds.map { |proposal, _sender| proposal.add.key_package.leaf_node.signature_key }
|
|
36
|
+
raise ArgumentError.new('more than one Add for the same client') unless added_keys.uniq.size == added_keys.size
|
|
37
|
+
staying_keys = []
|
|
38
|
+
@ratchet_tree.each_leaf_node { |leaf_node, leaf_index| staying_keys << leaf_node.signature_key unless removed.include?(leaf_index) }
|
|
39
|
+
raise ArgumentError.new('Add for a client already in the group') unless (added_keys & staying_keys).empty?
|
|
40
|
+
|
|
41
|
+
psk_ids = psks.map { |proposal, _sender| proposal.psk.psk.raw }
|
|
42
|
+
raise ArgumentError.new('more than one PreSharedKey proposal for the same PreSharedKeyID') unless psk_ids.uniq.size == psk_ids.size
|
|
43
|
+
raise ArgumentError.new('more than one GroupContextExtensions proposal') if gces.size > 1
|
|
44
|
+
# ReInit must be alone and ExternalInit must be absent (12.2); both are rejected as unsupported in apply_proposals
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 12.4.2, 7.6, 7.9.2: a received UpdatePath, checked against the tree after the proposals and before the merge
|
|
48
|
+
def validate_update_path!(tree, update_path, committer, joiners, extensions)
|
|
49
|
+
leaf_node = update_path.leaf_node
|
|
50
|
+
validate_leaf_node!(leaf_node, context: :commit, leaf_index: committer, tree: tree, extensions: extensions,
|
|
51
|
+
replacing: tree.leaf_node_at(committer), sending: false)
|
|
52
|
+
# 7.9.2: the parent hash in the new leaf is the one this tree and path imply
|
|
53
|
+
raise ArgumentError.new('UpdatePath leaf node parent hash does not verify') unless tree.verify_parent_hash_of_path(committer, update_path)
|
|
54
|
+
# 12.4.2, 16.7: the path's public keys are new to the tree and distinct from each other
|
|
55
|
+
path_keys = [leaf_node.encryption_key] + update_path.nodes.map(&:encryption_key)
|
|
56
|
+
raise ArgumentError.new('UpdatePath public keys are not distinct') unless path_keys.uniq.size == path_keys.size
|
|
57
|
+
tree_keys = tree.nodes.compact.map(&:public_encryption_key)
|
|
58
|
+
raise ArgumentError.new('a public key in the UpdatePath already appears in the tree') unless (path_keys & tree_keys).empty?
|
|
59
|
+
# 7.6: each node carries one ciphertext per member of its copath resolution, the new leaves excluded
|
|
60
|
+
copath_nodes = tree.copath_nodes_of_filtered_direct_path(committer * 2)
|
|
61
|
+
raise ArgumentError.new('malformed update path') unless copath_nodes.count == update_path.nodes.count
|
|
62
|
+
excluded = joiners.map { _1.leaf_index * 2 }
|
|
63
|
+
update_path.nodes.each_with_index do |node, i|
|
|
64
|
+
expected = (tree.resolution(copath_nodes[i]) - excluded).count
|
|
65
|
+
next if node.encrypted_path_secret.count == expected
|
|
66
|
+
raise ArgumentError.new("UpdatePath node #{i} has #{node.encrypted_path_secret.count} ciphertexts, expected #{expected}")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# 10.1: a KeyPackage being added, in the tree it is (or will be) added to.
|
|
72
|
+
# leaf_index: where it sits in tree, or nil if it is not in tree yet
|
|
73
|
+
def validate_key_package!(key_package, tree:, leaf_index:, extensions:, sending:)
|
|
74
|
+
raise ArgumentError.new("KeyPackage has version #{key_package.version}") unless key_package.version == Melos::Constants::Version::MLS10
|
|
75
|
+
raise ArgumentError.new("KeyPackage is for cipher suite #{key_package.cipher_suite}") unless key_package.cipher_suite == @suite.suite_id
|
|
76
|
+
Melos::Struct::Extension.check_unique_types!(key_package.extensions, 'KeyPackage extensions')
|
|
77
|
+
validate_leaf_node!(key_package.leaf_node, context: :key_package, leaf_index: leaf_index, tree: tree, extensions: extensions, sending: sending)
|
|
78
|
+
raise ArgumentError.new('KeyPackage signature does not verify') unless key_package.verify
|
|
79
|
+
raise ArgumentError.new('KeyPackage init_key equals its leaf encryption_key') if key_package.init_key == key_package.leaf_node.encryption_key
|
|
80
|
+
# 10: extensions of the KeyPackage itself must also be in the leaf's capabilities
|
|
81
|
+
unsupported = key_package.extensions.map(&:extension_type) - DEFAULT_EXTENSION_TYPES - key_package.leaf_node.capabilities.export.extensions
|
|
82
|
+
raise ArgumentError.new("KeyPackage extensions not in its capabilities: #{unsupported.map { '0x%04x' % _1 }.join(', ')}") unless unsupported.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 7.3: a LeafNode in one of its contexts.
|
|
86
|
+
# context: :key_package (in a KeyPackage), :update (an Update proposal), :commit (the leaf of an UpdatePath),
|
|
87
|
+
# or :tree (a leaf already in the tree, e.g. when joining; its source is taken as given)
|
|
88
|
+
# leaf_index: the leaf's index in tree (needed to verify update/commit signatures); nil if not in tree yet
|
|
89
|
+
# tree, extensions: the ratchet tree and GroupContext extensions the leaf must be compatible with
|
|
90
|
+
# replacing: for :update and :commit, the leaf node being replaced
|
|
91
|
+
# sending: the leaf is in a message we are about to send, so its lifetime is checked against the current time
|
|
92
|
+
def validate_leaf_node!(leaf_node, context:, leaf_index:, tree:, extensions:, replacing: nil, sending: false)
|
|
93
|
+
expected_source = { key_package: Melos::Constants::LeafNodeSource::KEY_PACKAGE, update: Melos::Constants::LeafNodeSource::UPDATE,
|
|
94
|
+
commit: Melos::Constants::LeafNodeSource::COMMIT }[context]
|
|
95
|
+
if expected_source && leaf_node.leaf_node_source != expected_source
|
|
96
|
+
raise ArgumentError.new("leaf node source is #{leaf_node.leaf_node_source}, expected #{expected_source} for #{context}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 5.3.1: the credential, by the application
|
|
100
|
+
raise ArgumentError.new('leaf node credential rejected') unless @credential_validator.call(leaf_node.credential, leaf_node)
|
|
101
|
+
|
|
102
|
+
# the signature; update and commit leaves are bound to this group and leaf index
|
|
103
|
+
verified = if leaf_node.leaf_node_source == Melos::Constants::LeafNodeSource::KEY_PACKAGE
|
|
104
|
+
leaf_node.verify(@suite, nil, nil)
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError.new('leaf index needed to verify this leaf node') if leaf_index.nil?
|
|
107
|
+
leaf_node.verify(@suite, @group_id, leaf_index)
|
|
108
|
+
end
|
|
109
|
+
raise ArgumentError.new('leaf node signature does not verify') unless verified
|
|
110
|
+
|
|
111
|
+
# 7.3, 11: compatible with the group's version, cipher suite, extensions and required capabilities
|
|
112
|
+
capabilities = leaf_node.capabilities.export
|
|
113
|
+
raise ArgumentError.new('leaf node does not support MLS 1.0') unless capabilities.versions.include?(Melos::Constants::Version::MLS10)
|
|
114
|
+
raise ArgumentError.new("leaf node does not support cipher suite #{@suite.suite_id}") unless capabilities.cipher_suites.include?(@suite.suite_id)
|
|
115
|
+
others = []
|
|
116
|
+
tree.each_leaf_node { |other, index| others << other unless index == leaf_index }
|
|
117
|
+
validate_capabilities!(leaf_node, capabilities, extensions, others.map { _1.credential.credential_type })
|
|
118
|
+
# 7.3, 13.3: the other members support this leaf's credential type
|
|
119
|
+
others.each do |other|
|
|
120
|
+
next if other.capabilities.export.credentials.include?(leaf_node.credential.credential_type)
|
|
121
|
+
raise ArgumentError.new('a current member does not support this leaf node\'s credential type')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 7.2, 7.3: lifetime, for leaves that came from a KeyPackage
|
|
125
|
+
if leaf_node.leaf_node_source == Melos::Constants::LeafNodeSource::KEY_PACKAGE
|
|
126
|
+
lifetime = leaf_node.lifetime
|
|
127
|
+
if @max_leaf_node_lifetime && lifetime.not_after - lifetime.not_before > @max_leaf_node_lifetime
|
|
128
|
+
raise ArgumentError.new('leaf node lifetime is longer than allowed')
|
|
129
|
+
end
|
|
130
|
+
if sending
|
|
131
|
+
now = Time.now.to_i
|
|
132
|
+
raise ArgumentError.new('leaf node lifetime does not cover the current time') unless lifetime.not_before <= now && now <= lifetime.not_after
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Melos::Struct::Extension.check_unique_types!(leaf_node.extensions, 'leaf node extensions')
|
|
137
|
+
|
|
138
|
+
# 7.3, 12.4.2: a replacement leaf brings a new encryption key
|
|
139
|
+
if replacing && leaf_node.encryption_key == replacing.encryption_key
|
|
140
|
+
raise ArgumentError.new('replacement leaf node reuses the encryption key of the leaf it replaces')
|
|
141
|
+
end
|
|
142
|
+
# 7.3, 16.7: keys are unique among members
|
|
143
|
+
others.each do |other|
|
|
144
|
+
raise ArgumentError.new('leaf node signature key is already used by another member') if other.signature_key == leaf_node.signature_key
|
|
145
|
+
raise ArgumentError.new('leaf node encryption key is already used by another member') if other.encryption_key == leaf_node.encryption_key
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# 7.2, 7.3, 11.1, 13.4: a leaf's capabilities cover its own extensions and credential type, the group's
|
|
150
|
+
# non-default extensions, the required_capabilities extension if any, and the credential types in use
|
|
151
|
+
def validate_capabilities!(leaf_node, capabilities, extensions, credential_types_in_use)
|
|
152
|
+
own_extension_types = leaf_node.extensions.map(&:extension_type) - DEFAULT_EXTENSION_TYPES
|
|
153
|
+
missing = own_extension_types - capabilities.extensions
|
|
154
|
+
raise ArgumentError.new("leaf node extensions not in its capabilities: #{missing.map { '0x%04x' % _1 }.join(', ')}") unless missing.empty?
|
|
155
|
+
raise ArgumentError.new('leaf node credential type not in its capabilities') unless capabilities.credentials.include?(leaf_node.credential.credential_type)
|
|
156
|
+
|
|
157
|
+
group_extension_types = extensions.map(&:extension_type) - DEFAULT_EXTENSION_TYPES
|
|
158
|
+
missing = group_extension_types - capabilities.extensions
|
|
159
|
+
raise ArgumentError.new("leaf node does not support GroupContext extensions #{missing.map { '0x%04x' % _1 }.join(', ')}") unless missing.empty?
|
|
160
|
+
|
|
161
|
+
if (required = required_capabilities(extensions))
|
|
162
|
+
missing = (required.extension_types - DEFAULT_EXTENSION_TYPES) - capabilities.extensions
|
|
163
|
+
raise ArgumentError.new("leaf node lacks required extensions #{missing.map { '0x%04x' % _1 }.join(', ')}") unless missing.empty?
|
|
164
|
+
missing = (required.proposal_types - DEFAULT_PROPOSAL_TYPES) - capabilities.proposals
|
|
165
|
+
raise ArgumentError.new("leaf node lacks required proposal types #{missing.map { '0x%04x' % _1 }.join(', ')}") unless missing.empty?
|
|
166
|
+
missing = required.credential_types - capabilities.credentials
|
|
167
|
+
raise ArgumentError.new("leaf node lacks required credential types #{missing.join(', ')}") unless missing.empty?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
missing = credential_types_in_use.uniq - capabilities.credentials
|
|
171
|
+
raise ArgumentError.new("leaf node does not support credential types in use: #{missing.join(', ')}") unless missing.empty?
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# 12.2, 12.1.7: after a proposal list is applied, every remaining member is compatible with the (possibly
|
|
175
|
+
# new) extensions and with every other member, and keys are unique. Signatures were checked when each leaf
|
|
176
|
+
# entered the tree, so they are not verified again here.
|
|
177
|
+
def validate_members!(tree, extensions)
|
|
178
|
+
leaves = []
|
|
179
|
+
tree.each_leaf_node { |leaf_node, _index| leaves << leaf_node }
|
|
180
|
+
signature_keys = leaves.map(&:signature_key)
|
|
181
|
+
raise ArgumentError.new('two members share a signature key') unless signature_keys.uniq.size == signature_keys.size
|
|
182
|
+
encryption_keys = leaves.map(&:encryption_key)
|
|
183
|
+
raise ArgumentError.new('two members share an encryption key') unless encryption_keys.uniq.size == encryption_keys.size
|
|
184
|
+
credential_types = leaves.map { _1.credential.credential_type }
|
|
185
|
+
leaves.each do |leaf_node|
|
|
186
|
+
validate_capabilities!(leaf_node, leaf_node.capabilities.export, extensions, credential_types)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# 12.4.3.1: what a new member checks about the state it was welcomed into, once it is assembled
|
|
191
|
+
def validate_joined_state!
|
|
192
|
+
validate_tree!(@ratchet_tree, @extensions)
|
|
193
|
+
# the private keys derived from the Welcome's path secret must be the ones for the public keys in the tree
|
|
194
|
+
raise ArgumentError.new('derived private keys do not match the ratchet tree') unless @private_tree.consistent?(@ratchet_tree)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# 12.4.3.1, 7.9.2, 7.3: the integrity of a ratchet tree received from someone else.
|
|
198
|
+
# Also what makes 13.4 hold for us: validate_leaf_node! on our own leaf checks that we support every GroupContext extension.
|
|
199
|
+
def validate_tree!(tree, extensions)
|
|
200
|
+
raise ArgumentError.new('ratchet tree has a parent node that is not parent-hash valid') unless tree.parent_hash_valid?
|
|
201
|
+
tree.each_leaf_node do |leaf_node, leaf_index|
|
|
202
|
+
validate_leaf_node!(leaf_node, context: :tree, leaf_index: leaf_index, tree: tree, extensions: extensions)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
encryption_keys = []
|
|
206
|
+
tree.each_leaf_node { |leaf_node, _index| encryption_keys << leaf_node.encryption_key }
|
|
207
|
+
tree.nodes.each_with_index do |node, node_index|
|
|
208
|
+
next if node.nil? || node.parent_node.nil?
|
|
209
|
+
parent_node = node.parent_node
|
|
210
|
+
encryption_keys << parent_node.encryption_key
|
|
211
|
+
parent_node.unmerged_leaves.each do |leaf_index|
|
|
212
|
+
raise ArgumentError.new("unmerged leaf #{leaf_index} is blank") if tree.leaf_node_at(leaf_index).nil?
|
|
213
|
+
direct_path = Melos::Tree.direct_path(leaf_index * 2, tree.n_leaves)
|
|
214
|
+
raise ArgumentError.new("unmerged leaf #{leaf_index} is not under node #{node_index}") unless direct_path.include?(node_index)
|
|
215
|
+
# every non-blank node between the leaf and this node lists the leaf as unmerged too
|
|
216
|
+
direct_path.take_while { _1 != node_index }.each do |between|
|
|
217
|
+
next if tree[between].nil?
|
|
218
|
+
next if tree[between].parent_node.unmerged_leaves.include?(leaf_index)
|
|
219
|
+
raise ArgumentError.new("unmerged leaf #{leaf_index} of node #{node_index} is merged at node #{between} below it")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
# 12.4.3.1, 16.7: no encryption key appears in two nodes
|
|
224
|
+
raise ArgumentError.new('an encryption key appears in more than one node of the ratchet tree') unless encryption_keys.uniq.size == encryption_keys.size
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# 11.1: the required_capabilities extension of a GroupContext, decoded, or nil.
|
|
228
|
+
# Returns an object with extension_types, proposal_types and credential_types as integer arrays.
|
|
229
|
+
def required_capabilities(extensions)
|
|
230
|
+
extension = extensions.find { _1.extension_type == Melos::Constants::ExtensionType::REQUIRED_CAPABILITIES }
|
|
231
|
+
return nil if extension.nil?
|
|
232
|
+
struct = Melos::Struct::RequiredCapabilities.new(extension.extension_data)
|
|
233
|
+
Struct.new(:extension_types, :proposal_types, :credential_types).new(
|
|
234
|
+
struct.extension_types.unpack('S>*'), struct.proposal_types.unpack('S>*'), struct.credential_types.unpack('S>*')
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
end
|