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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f79041ce8bf4db3f70e527f732c1581533617e3d80f16bf9098bb7629dcdeb4a
|
|
4
|
+
data.tar.gz: 641494ab2a7829bbdfbe61cd90d401a8d3848d2a4beb67c35e12fb49ac00902e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c230d9ef5487be5be256ac46378921f716b3c5bcaf2b01752c5db74a63f450943497884c39a3688226daf8a2b5c1d323ea24c5326bce928b27f29fe61bd8ea54
|
|
7
|
+
data.tar.gz: b4afcee3755000a00319251b7d4755b85e61378c6ea43296230a43e5ec6c4fd552188efe03e4cda5ac42d0fe5729ff647542a82c45e8b692342f67f68167f290
|
data/README.md
CHANGED
|
@@ -7,18 +7,70 @@ a [Messaging Layer Security Protocol](https://www.rfc-editor.org/rfc/rfc9420.htm
|
|
|
7
7
|
|
|
8
8
|
(yes, [an mls gem](https://rubygems.org/gems/mls) happened to exist since 2014, so...)
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Implementation status
|
|
11
11
|
|
|
12
|
-
As of version 0.0
|
|
12
|
+
As of version 0.1.0, this implements:
|
|
13
13
|
|
|
14
|
-
- Serialization
|
|
15
|
-
- Key
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
14
|
+
- Serialization and deserialization of the RFC 9420 structures
|
|
15
|
+
- Key schedule, secret tree, pre-shared keys, transcript hashes
|
|
16
|
+
- The ratchet tree: TreeKEM (creating and processing UpdatePaths), tree hashes, parent hashes
|
|
17
|
+
- Groups: creating a group, Add / Remove / PreSharedKey / GroupContextExtensions proposals,
|
|
18
|
+
commits with an UpdatePath and the matching Welcome, joining via Welcome,
|
|
19
|
+
application messages as PrivateMessage, handshake messages as PublicMessage or PrivateMessage
|
|
20
|
+
- Processing Update proposals and commits from other members
|
|
21
|
+
- Most MUST-level validations of the RFC; [docs/validation.md](docs/validation.md) maps each one to the code
|
|
22
|
+
- All test vectors in [mls-implementations/test-vectors.md](https://github.com/mlswg/mls-implementations/blob/main/test-vectors.md),
|
|
23
|
+
plus round-trip tests in which several members create, join, message and leave a group
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
It does not yet implement:
|
|
21
26
|
|
|
22
|
-
- Creating
|
|
23
|
-
-
|
|
24
|
-
-
|
|
27
|
+
- Creating Update proposals
|
|
28
|
+
- ReInit, external commits (ExternalInit), external senders, resumption PSKs, subgroup branching
|
|
29
|
+
- Decrypting application messages from earlier epochs, and keeping unused keys for out-of-order delivery
|
|
30
|
+
- The deletion schedule of Section 9.2 beyond dropping the previous epoch
|
|
31
|
+
- Credential validation beyond basic credentials (an application hook, `credential_validator:`, is provided)
|
|
32
|
+
- Persistence of client and group state
|
|
33
|
+
- Interop testing against other implementations
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Messages cross between members as `Melos::Struct::MLSMessage` objects; `raw` gives the bytes to
|
|
38
|
+
send and `Melos::Struct::MLSMessage.new(bytes)` parses them.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require 'melos'
|
|
42
|
+
|
|
43
|
+
# cipher suite 1 is MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519
|
|
44
|
+
alice = Melos::Client.new(1, identity: 'alice')
|
|
45
|
+
bob = Melos::Client.new(1, identity: 'bob')
|
|
46
|
+
|
|
47
|
+
# Bob publishes a KeyPackage
|
|
48
|
+
bob_key_package = bob.create_key_package
|
|
49
|
+
|
|
50
|
+
# Alice creates a group and adds Bob
|
|
51
|
+
group = alice.create_group("some group id")
|
|
52
|
+
group.create_add_proposal(bob_key_package.message)
|
|
53
|
+
result = group.create_commit
|
|
54
|
+
# result.commit is the Commit to send to the group, result.welcome the Welcome for Bob;
|
|
55
|
+
# merge once the Delivery Service has accepted the commit
|
|
56
|
+
group.merge_commit(result.staged)
|
|
57
|
+
|
|
58
|
+
# Bob joins
|
|
59
|
+
bob_group = bob.join_group(result.welcome)
|
|
60
|
+
|
|
61
|
+
# and talks
|
|
62
|
+
message = bob_group.create_application_message("hello")
|
|
63
|
+
group.process_message(message).content.application_data # => "hello"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Melos::Group` holds one member's view of one group. `Melos::Client` holds the long-lived identity,
|
|
67
|
+
the external PSKs and the KeyPackages it has published.
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
bundle exec rake test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
runs a subset of the test vectors; `TEST_ALL=1 bundle exec rake test` runs all of them, for every cipher suite.
|
|
76
|
+
The vectors are the ones from [mls-implementations](https://github.com/mlswg/mls-implementations), in `test_vectors/`.
|
data/docs/validation.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# RFC 9420 validation checklist
|
|
2
|
+
|
|
3
|
+
MUST-level requirements of RFC 9420 that apply to a client processing or producing messages,
|
|
4
|
+
mapped to the code; updated as validation lands (last: tier 5). Status:
|
|
5
|
+
|
|
6
|
+
- **done**: enforced today, with where.
|
|
7
|
+
- **partial**: enforced in some paths or weaker than the RFC asks.
|
|
8
|
+
- **missing**: not enforced.
|
|
9
|
+
- **n/a**: for features melos does not implement yet (external senders, external commits,
|
|
10
|
+
ReInit, branching); the code rejects these inputs with a clear error instead.
|
|
11
|
+
|
|
12
|
+
Items marked *(application)* are MUSTs the RFC places on the application or the Authentication
|
|
13
|
+
Service; melos should offer a hook or a documented default, not decide the policy.
|
|
14
|
+
|
|
15
|
+
## 1. Message framing and unprotect (Sections 6, 6.1, 6.2, 6.3)
|
|
16
|
+
|
|
17
|
+
| # | Requirement | RFC | Status | Code |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| 1.1 | Verify the signature with the key selected by sender_type | 6.1 | partial | `Group#unprotect`, `#sender_signature_key`: member only; external, new_member_proposal, new_member_commit raise "not supported" |
|
|
20
|
+
| 1.2 | external sender: content_type MUST be proposal, proposal_type MUST be one allowed for external senders | 6.1, 12.1.8 | n/a | rejected with 1.1 |
|
|
21
|
+
| 1.3 | new_member_commit: content_type MUST be commit; new_member_proposal: proposal of type add | 6.1 | n/a | rejected with 1.1 |
|
|
22
|
+
| 1.4 | PublicMessage: check membership_tag for member senders | 6.2 | done | `PublicMessage#unprotect` returns nil, `Group#unprotect` raises |
|
|
23
|
+
| 1.5 | FramedContentAuthData valid: signature and, for commits, confirmation_tag | 6.1, 6.2, 6.3.1 | done | signature in `Group#unprotect`; tag in `Group#stage` |
|
|
24
|
+
| 1.6 | PrivateMessage padding MUST be all zero bytes; reject otherwise | 6.3.1 | done | `PrivateMessage#unprotect` via `PrivateMessageContent#padding_zero?` |
|
|
25
|
+
| 1.7 | SenderData.leaf_index MUST identify a non-blank leaf | 6.3.2 | partial | caught later by `#sender_signature_key` ("sender leaf is blank") after decryption; the RFC wants it checked when the sender data is parsed |
|
|
26
|
+
| 1.8 | Application data MUST be sent as PrivateMessage | 6 | done | `AuthenticatedContent#sign` raises on send; `#verify` returns false on receive |
|
|
27
|
+
| 1.9 | Proposal and Commit epoch MUST equal the current epoch | 12.1, 12.4.2 | done | `Group#unprotect` |
|
|
28
|
+
| 1.10 | Application messages from earlier epochs may be decrypted (15.3); not a MUST, but today `Group#unprotect` rejects any epoch other than the current one, which is stricter than the RFC | 15.3 | behavior gap | needs per-epoch secret trees kept for a while (also 9.2) |
|
|
29
|
+
| 1.11 | MLSMessage.version MUST be mls10 | 6 | done | `Group.check_message_version!` in `#unprotect`, `.from_welcome`, `#create_add_proposal` |
|
|
30
|
+
| 1.12 | AAD plus ciphertext MUST fit the AEAD limits | 6.3.1, 15.2 | missing *(application)* | no size accounting anywhere |
|
|
31
|
+
|
|
32
|
+
## 2. Proposals as received (Section 12.1)
|
|
33
|
+
|
|
34
|
+
| # | Requirement | RFC | Status | Code |
|
|
35
|
+
|---|---|---|---|---|
|
|
36
|
+
| 2.1 | Verify signature and epoch before caching | 12.1 | done | `Group#unprotect`, `#process_proposal` |
|
|
37
|
+
| 2.2 | Add: KeyPackage valid per 10.1 (see section 4) | 12.1.1 | done | `#apply_proposals` -> `#validate_key_package!` |
|
|
38
|
+
| 2.3 | Update: LeafNode valid for an Update per 7.3, source `update`, encryption_key differs from the replaced leaf | 12.1.2, 7.3 | done | `#apply_proposals` -> `#validate_leaf_node!(context: :update)`; creating Update proposals is still unimplemented |
|
|
39
|
+
| 2.4 | Remove: `removed` MUST be a non-blank leaf | 12.1.3 | done | `#apply_proposals` (both sides), `#create_remove_proposal` |
|
|
40
|
+
| 2.5 | PreSharedKey: psk_nonce length MUST be KDF.Nh | 12.1.4 | done | `#psk_secret_for` |
|
|
41
|
+
| 2.6 | PreSharedKey: resumption with usage reinit or branch is invalid outside those operations | 12.1.4, 11.2, 11.3 | **missing** | `#psk_secret_for` looks up any resumption PSK by epoch regardless of usage |
|
|
42
|
+
| 2.7 | ReInit: version MUST NOT be lower than the current one | 12.1.5 | n/a | ReInit rejected as unsupported |
|
|
43
|
+
| 2.8 | GroupContextExtensions: invalid if required_capabilities not supported by all members after the commit | 12.1.7, 11.1 | done | `#validate_members!` after the list is applied |
|
|
44
|
+
| 2.9 | External proposals MUST be PublicMessage; only add, remove, psk, reinit, gce from external senders; external_senders extension must be present | 12.1.8 | n/a | rejected with 1.1 |
|
|
45
|
+
| 2.10 | Unknown (non-default) proposal types MUST be supported by every member that processes the commit | 12.2, 13.2 | done | `#apply_proposals` rejects any type outside `KNOWN_PROPOSAL_TYPES` |
|
|
46
|
+
|
|
47
|
+
## 3. Proposal list validation (Section 12.2), on both creating and processing a Commit
|
|
48
|
+
|
|
49
|
+
`Group#validate_proposal_list!` runs in both `#create_commit` and `#stage`.
|
|
50
|
+
|
|
51
|
+
| # | Requirement | RFC | Status |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| 3.1 | Every individual proposal valid (sections 2 and 4) | 12.2 | done (sections 2, 4, 5) |
|
|
54
|
+
| 3.2 | No Update proposal by the committer | 12.2 | done |
|
|
55
|
+
| 3.3 | No Remove of the committer | 12.2 | done (`#stage`, `#create_commit`) |
|
|
56
|
+
| 3.4 | No two Update/Remove proposals for the same leaf | 12.2 | done |
|
|
57
|
+
| 3.5 | No two Adds for the same client (e.g. same signature key) | 12.2 | done (same signature key) |
|
|
58
|
+
| 3.6 | No Add of a client already in the group unless a Remove in the same list removes it | 12.2 | done (same signature key) |
|
|
59
|
+
| 3.7 | No two PreSharedKey proposals with the same PreSharedKeyID | 12.2 | done |
|
|
60
|
+
| 3.8 | At most one GroupContextExtensions proposal | 12.2 | done |
|
|
61
|
+
| 3.9 | ReInit MUST be alone in the list | 12.2 | n/a (ReInit rejected) |
|
|
62
|
+
| 3.10 | No ExternalInit in a regular commit | 12.2 | done by accident (`#apply_proposals` raises "unsupported") |
|
|
63
|
+
| 3.11 | After applying, every leaf in the tree is valid per 7.3 | 12.2 | done: capabilities and key uniqueness of every remaining leaf (`#validate_members!`); signatures were verified when each leaf entered |
|
|
64
|
+
| 3.12 | Reject the Commit if the list is invalid | 12.2 | follows from the above |
|
|
65
|
+
| 3.13 | External commit list rules (exactly one ExternalInit, at most one Remove, PSKs only) | 12.2 | n/a |
|
|
66
|
+
|
|
67
|
+
## 4. KeyPackage validation (Sections 10, 10.1), when adding and when processing an Add
|
|
68
|
+
|
|
69
|
+
| # | Requirement | RFC | Status | Code |
|
|
70
|
+
|---|---|---|---|---|
|
|
71
|
+
| 4.1 | cipher_suite and version match the GroupContext | 10.1 | done | `#apply_proposals` (Add) |
|
|
72
|
+
| 4.2 | leaf_node valid for a KeyPackage per 7.3 (section 5 below) | 10.1 | done | `#validate_key_package!` |
|
|
73
|
+
| 4.3 | Signature verifies with the leaf's signature key | 10, 10.1 | done | `#validate_key_package!` |
|
|
74
|
+
| 4.4 | leaf_node.encryption_key MUST differ from init_key | 10.1 | done | `#validate_key_package!` |
|
|
75
|
+
| 4.5 | leaf_node_source MUST be key_package | 10 | done | `#validate_leaf_node!(context: :key_package)` |
|
|
76
|
+
| 4.6 | Extensions in KeyPackage.extensions and leaf_node.extensions MUST be listed in leaf_node.capabilities | 10 | done | `#validate_key_package!`, `#validate_capabilities!` |
|
|
77
|
+
| 4.7 | KeyPackage.version MUST equal the enclosing MLSMessage.version | 10 | done | `Group#create_add_proposal` |
|
|
78
|
+
| 4.8 | Unknown capabilities values and unknown extensions MUST be ignored | 13.4 | done by construction | nothing inspects them yet; keep it that way when 4.6 is added |
|
|
79
|
+
| 4.9 | Sender: init_key MUST be unique among the client's KeyPackages | 10 | done | fresh random key per `KeyPackageBundle.generate` |
|
|
80
|
+
| 4.10 | Sender: capabilities MUST list every supported value and MUST NOT list default proposal/extension types | 7.2 | done | `Melos::Capabilities#set_default` lists no proposal or extension types |
|
|
81
|
+
|
|
82
|
+
## 5. LeafNode validation (Section 7.3), for Add, Update, the Commit path leaf, and every leaf when joining
|
|
83
|
+
|
|
84
|
+
| # | Requirement | RFC | Status | Code |
|
|
85
|
+
|---|---|---|---|---|
|
|
86
|
+
| 5.1 | Credential valid per 5.3.1 (Authentication Service) | 7.3, 5.3.1 | done (hook) | `credential_validator:` option; default accepts basic credentials only |
|
|
87
|
+
| 5.2 | Signature valid with signature_key (with group_id and leaf_index for update/commit sources) | 7.3 | done | `#validate_leaf_node!` |
|
|
88
|
+
| 5.3 | If GroupContext has required_capabilities, its extensions, proposals and credential types MUST be in the leaf's capabilities | 7.3, 11.1, 13.4 | done | `#validate_capabilities!` |
|
|
89
|
+
| 5.4 | Credential type supported by all members; leaf supports every credential type in use | 7.3, 13.3 | done | `#validate_leaf_node!`, `#validate_members!` |
|
|
90
|
+
| 5.5 | Lifetime: sender MUST check current time is within lifetime when sending a leaf (Add of a KeyPackage); receiver RECOMMENDED | 7.3 | done | `#validate_leaf_node!(sending: true)` from `#create_add_proposal` and `#create_commit` |
|
|
91
|
+
| 5.6 | Application MUST define a maximum total lifetime and reject longer ones | 7.2 | done (option) | `max_leaf_node_lifetime:`; no default limit because the interop vectors use the full uint64 range |
|
|
92
|
+
| 5.7 | Every non-default extension in the leaf MUST be listed in capabilities.extensions; credential type MUST be in capabilities.credentials | 7.2, 7.3 | done | `#validate_capabilities!` |
|
|
93
|
+
| 5.8 | leaf_node_source matches the context: key_package / update (and new encryption_key) / commit | 7.3 | done | `#validate_leaf_node!` |
|
|
94
|
+
| 5.9 | signature_key and encryption_key unique among members | 7.3, 16.7 | done | `#validate_leaf_node!`, `#validate_members!`, `#validate_tree!` |
|
|
95
|
+
| 5.10 | Each GroupContext extension (non-default) MUST be supported by a new member's capabilities | 13.4 | done | `#validate_capabilities!` |
|
|
96
|
+
|
|
97
|
+
## 6. Commit processing (Sections 12.4, 12.4.2, 7.5, 7.6, 7.9.2)
|
|
98
|
+
|
|
99
|
+
| # | Requirement | RFC | Status | Code |
|
|
100
|
+
|---|---|---|---|---|
|
|
101
|
+
| 6.1 | Epoch, membership tag or decryption, signature | 12.4.2 | done | `Group#unprotect` |
|
|
102
|
+
| 6.2 | Proposal list valid (section 3) | 12.4.2 | done | `#validate_proposal_list!` in `#stage` and `#create_commit` |
|
|
103
|
+
| 6.3 | All PreSharedKey proposals available | 12.4.2 | done | `#psk_secret_for` raises |
|
|
104
|
+
| 6.4 | Path MUST be present if the list is empty or has a path-required type | 12.4 | done | `Group#stage` |
|
|
105
|
+
| 6.5 | Path leaf valid per 7.3 with leaf_node_source commit | 12.4.2 | done | `#validate_update_path!` -> `#validate_leaf_node!(context: :commit)` |
|
|
106
|
+
| 6.6 | Path leaf encryption_key MUST differ from the committer's current leaf | 12.4.2 | done | `#validate_update_path!` |
|
|
107
|
+
| 6.7 | None of the UpdatePath public keys appear in any node of the new tree | 12.4.2 | done | `#validate_update_path!` |
|
|
108
|
+
| 6.8 | Recompute the committer's leaf parent_hash and verify it matches the supplied leaf | 7.9.2 | done | `#validate_update_path!` via `RatchetTree#verify_parent_hash_of_path` |
|
|
109
|
+
| 6.9 | Each UpdatePathNode's encrypted_path_secret length MUST equal the copath resolution excluding new leaves | 7.6 | done | `#validate_update_path!` checks every node |
|
|
110
|
+
| 6.10 | Confirmation tag verifies against the new epoch | 12.4.2 | done | `Group#stage` |
|
|
111
|
+
| 6.11 | A Commit that removes us: no further sending, delete state (SHOULD) | 12.4.2 | done | `#stage` returns `removes_us`, `#merge_commit` makes the group inactive; last epoch's secrets are kept for late messages |
|
|
112
|
+
| 6.12 | After a ReInit commit the group MUST NOT be used; wait for the Welcome | 12.4.2 | n/a | ReInit rejected |
|
|
113
|
+
| 6.13 | A member that observed proposals MUST commit before sending application data | 12.4 | done | `#create_application_message` |
|
|
114
|
+
| 6.14 | Generating a Commit MUST NOT modify state; Welcome MUST NOT be delivered until the Commit is accepted | 14 | done / *(application)* | `#create_commit` returns a `StagedCommit`; delivery is the caller's |
|
|
115
|
+
| 6.15 | After processing a path, outdated key material MUST be deleted | 7.5, 9.2 | partial | old epochs are dropped on merge; the `PrivateTree` keeps stale keys for replaced nodes, and `EpochSecrets` memoizes derived secrets for the epoch's lifetime |
|
|
116
|
+
|
|
117
|
+
## 7. Joining via Welcome (Sections 12.4.3, 12.4.3.1, 12.4.3.3, 13.4)
|
|
118
|
+
|
|
119
|
+
| # | Requirement | RFC | Status | Code |
|
|
120
|
+
|---|---|---|---|---|
|
|
121
|
+
| 7.1 | Find the entry for one of our KeyPackages; error if none or if cipher suites differ | 12.4.3.1 | done | `Client#join_group` finds the entry; `Group.from_welcome` compares the KeyPackage's cipher_suite with the Welcome's |
|
|
122
|
+
| 7.2 | Error if a PSK in GroupSecrets is unknown | 12.4.3.1 | done | `Group.from_welcome` |
|
|
123
|
+
| 7.3 | A resumption PSK with usage reinit or branch MUST be the only such PSK; GroupInfo epoch MUST be 1; reinit/branch consistency checks | 12.4.3.1, 11.2, 11.3 | n/a | resumption PSKs in a Welcome rejected as unsupported |
|
|
124
|
+
| 7.4 | GroupInfo signature with the signer leaf's key; error if blank or invalid | 12.4.3 | done | `Group.from_welcome` |
|
|
125
|
+
| 7.5 | group_id MUST be unique among the groups the client is in | 12.4.3 | done | `Client#ensure_group_id_free!` on create and join |
|
|
126
|
+
| 7.6 | GroupInfo cipher_suite matches the KeyPackage's | 12.4.3.1 | done | see 7.1 |
|
|
127
|
+
| 7.7 | Tree hash matches GroupContext.tree_hash | 12.4.3.1, 12.4.3.3 | done | `Group.from_welcome` |
|
|
128
|
+
| 7.8 | Every non-blank parent node is parent-hash valid | 12.4.3.1, 7.9.2 | done | `#validate_tree!` via `RatchetTree#parent_hash_valid?` |
|
|
129
|
+
| 7.9 | Every non-blank leaf valid per 7.3 | 12.4.3.1 | done | `#validate_tree!` -> `#validate_leaf_node!(context: :tree)` |
|
|
130
|
+
| 7.10 | unmerged_leaves: each entry is a non-blank descendant leaf; every non-blank node between also lists it; parent encryption keys unique in the tree | 12.4.3.1 | done | `#validate_tree!` |
|
|
131
|
+
| 7.11 | Our leaf is the one identical to the KeyPackage's LeafNode | 12.4.3.1 | done | `Group.from_welcome` matches the whole LeafNode |
|
|
132
|
+
| 7.12 | Derived private keys MUST correspond to the public keys in the nodes | 12.4.3.1 | done | `#validate_joined_state!` via `PrivateTree#consistent?` |
|
|
133
|
+
| 7.13 | Confirmation tag verifies with the derived confirmation key | 12.4.3.1 | done | `Group.from_welcome` |
|
|
134
|
+
| 7.14 | Joiner MUST support every GroupContext extension, else the GroupInfo is invalid | 13.4 | done | our own leaf through `#validate_tree!`; a non-default extension we do not list is rejected |
|
|
135
|
+
| 7.15 | Unknown GroupInfo.extensions MUST be ignored | 12.4.3, 13.4 | done | only ratchet_tree is looked up |
|
|
136
|
+
| 7.16 | ratchet_tree extension: last node non-blank, pad to 2^(d+1)-1 | 12.4.3.3 | done | `RatchetTree.parse`, `#initialize` |
|
|
137
|
+
| 7.17 | Download the tree before processing if not in the Welcome | 12.4.3.1 | done | `ratchet_tree:` argument; error if neither |
|
|
138
|
+
|
|
139
|
+
## 8. Sending side
|
|
140
|
+
|
|
141
|
+
| # | Requirement | RFC | Status | Code |
|
|
142
|
+
|---|---|---|---|---|
|
|
143
|
+
| 8.1 | Fresh random 4-byte reuse guard, included in SenderData | 6.3.1 | done | `PrivateMessage.protect` |
|
|
144
|
+
| 8.2 | psk_nonce fresh random of length KDF.Nh | 8.4, 12.1.4 | done | `Group#create_psk_proposal` |
|
|
145
|
+
| 8.3 | Proposal list valid when creating a commit | 12.2, 12.4.1 | done | `#validate_proposal_list!` |
|
|
146
|
+
| 8.4 | Welcome MUST cover every new member; psks in proposal order | 12.4.1 | done | `#create_welcome` |
|
|
147
|
+
| 8.5 | Creator MUST pick the best version/cipher suite supported by all added members | 11 | partial | one suite per group; 4.1 covers the check that an added KeyPackage matches it |
|
|
148
|
+
| 8.6 | Adding a member: its capabilities MUST cover each GroupContext extension and required_capabilities | 13.4, 11.1 | done | `#validate_key_package!` on `#create_add_proposal` |
|
|
149
|
+
| 8.7 | Lifetime of a KeyPackage leaf checked against the current time when sending an Add | 7.3 | done | see 5.5 |
|
|
150
|
+
| 8.8 | Capabilities content (see 4.10) | 7.2 | done | `Melos::Capabilities` |
|
|
151
|
+
|
|
152
|
+
## 9. Wire format
|
|
153
|
+
|
|
154
|
+
| # | Requirement | RFC | Status | Code |
|
|
155
|
+
|---|---|---|---|---|
|
|
156
|
+
| 9.1 | Variable-length vector headers MUST use the minimum length; prefix 3 is invalid | 2.1.2 | done | `Melos::Vec.parse_stream`; truncated input raises `ArgumentError` too |
|
|
157
|
+
| 9.2 | Any list of extensions MUST NOT contain two of the same type | 13.4 | done | `Extension.check_unique_types!` on join, creation, GroupContextExtensions proposals and Adds; the commit path leaf is covered by tier 3 |
|
|
158
|
+
| 9.3 | version fields (MLSMessage, KeyPackage, GroupContext) are mls10 | 6, 8.1, 10 | done | `Group` entry points, see 1.11, 4.1, 4.7 |
|
|
159
|
+
| 9.4 | Node hash input optional present iff node non-blank | 7.8 | done by construction | `RatchetTree#tree_hash` |
|
|
160
|
+
|
|
161
|
+
## Suggested order
|
|
162
|
+
|
|
163
|
+
1. ~~**Cheap and self-contained**: 1.6 padding, 1.11/9.3 version fields, 9.2 duplicate extensions, 2.4 blank Remove, 2.5 nonce length, 2.10 unknown proposal types, 4.10 Capabilities bug, 6.13 commit-before-send, 6.11 detect our own removal.~~ Done in aee3c38, plus 3.3, 4.1, 4.7 and the 9.1 vector checks.
|
|
164
|
+
2. ~~**Commit integrity**: 6.5 to 6.9 (path leaf validation, key uniqueness, parent hash check, per-node ciphertext counts), then the proposal list rules 3.2 to 3.8 and 3.11, shared by `create_commit` and `stage`.~~ Done.
|
|
165
|
+
3. ~~**KeyPackage and LeafNode validation**: sections 4 and 5 as one `LeafNode` validator parameterized by context (key_package / update / commit / tree), used by Add processing, commit path processing and join; includes signature, source, extension and capability checks, key uniqueness. Credential validation and maximum lifetime become hooks with defaults.~~ Done.
|
|
166
|
+
4. ~~**Join integrity**: 7.8 to 7.12 and 7.14 on top of the validator from step 3.~~ Done.
|
|
167
|
+
5. ~~**Client-level**: 7.5 group_id uniqueness, 7.1 KeyPackage suite check.~~ Done.
|
|
168
|
+
6. **Later, with the features they belong to**: external senders (1.2, 2.9), resumption PSK rules (2.6, 7.3), ReInit, external commits, old-epoch application messages (1.10), deletion schedule (6.15).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Melos::Capabilities
|
|
2
|
+
attr_accessor :versions, :cipher_suites, :extensions, :proposals, :credentials
|
|
3
|
+
|
|
4
|
+
def initialize
|
|
5
|
+
set_default
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def set_default
|
|
9
|
+
@versions = [1]
|
|
10
|
+
@cipher_suites = [1, 2, 3, 4, 5, 6, 7]
|
|
11
|
+
@extensions = [] # 7.2: likewise the default extension types
|
|
12
|
+
@proposals = [] # 7.2: the default proposal types (add .. group_context_extensions) MUST NOT be listed
|
|
13
|
+
@credentials = [1]
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/melos/client.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require_relative 'crypto'
|
|
2
|
+
require_relative 'key_package_bundle'
|
|
3
|
+
require_relative 'group'
|
|
4
|
+
require_relative 'struct/structs'
|
|
5
|
+
|
|
6
|
+
# A client (RFC 9420 Section 5.3): a long-lived identity, i.e. a signature key pair and a
|
|
7
|
+
# credential, plus what a client keeps across groups: the external PSKs it knows (8.4) and the
|
|
8
|
+
# KeyPackages it has published and may still be welcomed into a group with (10).
|
|
9
|
+
class Melos::Client
|
|
10
|
+
attr_reader :cipher_suite, :credential, :external_psks, :key_packages, :groups
|
|
11
|
+
|
|
12
|
+
def initialize(cipher_suite_id, identity: '')
|
|
13
|
+
@cipher_suite = Melos::Crypto::CipherSuite.new(cipher_suite_id)
|
|
14
|
+
@signature_key_pair = Melos::Crypto.generate_signature_key_pair(@cipher_suite)
|
|
15
|
+
@credential = Melos::Struct::Credential.create_basic_credential(identity)
|
|
16
|
+
@external_psks = {} # psk_id -> psk; shared by reference with the groups this client is in
|
|
17
|
+
@key_packages = {} # KeyPackageRef -> Melos::KeyPackageBundle, until used to join a group
|
|
18
|
+
@groups = {} # group_id -> Melos::Group this client created or joined
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def signature_private_key
|
|
22
|
+
Melos::Crypto.serialize_private_key(@cipher_suite, @signature_key_pair)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def signature_public_key
|
|
26
|
+
Melos::Crypto.serialize_public_key(@cipher_suite, @signature_key_pair)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 8.4: an external PSK that commits in this client's groups may reference
|
|
30
|
+
def add_external_psk(psk_id, psk)
|
|
31
|
+
@external_psks[psk_id] = psk
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 10: publish a KeyPackage. The bundle is kept so that a Welcome addressed to it can be processed.
|
|
35
|
+
def create_key_package(capabilities: nil, lifetime: nil, extensions: [])
|
|
36
|
+
bundle = Melos::KeyPackageBundle.generate(
|
|
37
|
+
@cipher_suite, signature_private_key: signature_private_key, signature_public_key: signature_public_key,
|
|
38
|
+
credential: @credential, capabilities: capabilities, lifetime: lifetime, extensions: extensions
|
|
39
|
+
)
|
|
40
|
+
@key_packages[bundle.ref] = bundle
|
|
41
|
+
bundle
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 11: create a group with this client as its only member.
|
|
45
|
+
# options are passed to Melos::Group (credential_validator:, max_leaf_node_lifetime:)
|
|
46
|
+
def create_group(group_id, extensions: [], **options)
|
|
47
|
+
bundle = Melos::KeyPackageBundle.generate(
|
|
48
|
+
@cipher_suite, signature_private_key: signature_private_key, signature_public_key: signature_public_key, credential: @credential
|
|
49
|
+
)
|
|
50
|
+
ensure_group_id_free!(group_id)
|
|
51
|
+
@groups[group_id] = Melos::Group.create(
|
|
52
|
+
@cipher_suite, group_id, bundle.key_package.leaf_node,
|
|
53
|
+
encryption_private_key: bundle.encryption_private_key, signature_private_key: signature_private_key,
|
|
54
|
+
extensions: extensions, external_psks: @external_psks, **options
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 12.4.3.1: join the group a Welcome adds us to, using whichever of our KeyPackages it is addressed to.
|
|
59
|
+
# That KeyPackage is then forgotten: its init key is single-use.
|
|
60
|
+
def join_group(welcome, ratchet_tree: nil, **options)
|
|
61
|
+
welcome_struct = welcome.is_a?(Melos::Struct::MLSMessage) ? welcome.welcome : welcome
|
|
62
|
+
raise ArgumentError.new('not a Welcome message') if welcome_struct.nil?
|
|
63
|
+
bundle = welcome_struct.secrets.map(&:new_member).filter_map { @key_packages[_1] }.first
|
|
64
|
+
raise ArgumentError.new('Welcome is not addressed to any KeyPackage of this client') if bundle.nil?
|
|
65
|
+
group = Melos::Group.from_welcome(
|
|
66
|
+
@cipher_suite, welcome,
|
|
67
|
+
key_package: bundle.key_package, init_private_key: bundle.init_private_key,
|
|
68
|
+
encryption_private_key: bundle.encryption_private_key, signature_private_key: signature_private_key,
|
|
69
|
+
external_psks: @external_psks, ratchet_tree: ratchet_tree, **options
|
|
70
|
+
)
|
|
71
|
+
ensure_group_id_free!(group.group_id)
|
|
72
|
+
@key_packages.delete(bundle.ref)
|
|
73
|
+
@groups[group.group_id] = group
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# 12.4.3: a group_id MUST be unique among the groups this client is participating in;
|
|
79
|
+
# a group this client has been removed from no longer counts
|
|
80
|
+
def ensure_group_id_free!(group_id)
|
|
81
|
+
existing = @groups[group_id]
|
|
82
|
+
return if existing.nil? || !existing.active?
|
|
83
|
+
raise ArgumentError.new('this client is already in a group with this group_id')
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/melos/crypto.rb
CHANGED
|
@@ -5,6 +5,14 @@ require_relative 'vec'
|
|
|
5
5
|
class Melos::Crypto
|
|
6
6
|
class CipherSuite
|
|
7
7
|
module X25519
|
|
8
|
+
def self.serialize_public_key(key)
|
|
9
|
+
key.raw_public_key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.serialize_private_key(key)
|
|
13
|
+
key.raw_private_key
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
def self.deserialize_public_encapsulation_key(raw)
|
|
9
17
|
OpenSSL::PKey.new_raw_public_key('X25519', raw)
|
|
10
18
|
end
|
|
@@ -21,12 +29,28 @@ class Melos::Crypto
|
|
|
21
29
|
OpenSSL::PKey.new_raw_private_key('ED25519', raw)
|
|
22
30
|
end
|
|
23
31
|
|
|
32
|
+
def self.encapsulation_algorithm_name
|
|
33
|
+
'X25519'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.signature_algorithm_name
|
|
37
|
+
'ED25519'
|
|
38
|
+
end
|
|
39
|
+
|
|
24
40
|
def self.hash_algorithm
|
|
25
41
|
nil
|
|
26
42
|
end
|
|
27
43
|
end
|
|
28
44
|
|
|
29
45
|
module X448
|
|
46
|
+
def self.serialize_public_key(key)
|
|
47
|
+
key.raw_public_key
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.serialize_private_key(key)
|
|
51
|
+
key.raw_private_key
|
|
52
|
+
end
|
|
53
|
+
|
|
30
54
|
def self.deserialize_public_encapsulation_key(raw)
|
|
31
55
|
OpenSSL::PKey.new_raw_public_key('X448', raw)
|
|
32
56
|
end
|
|
@@ -43,6 +67,14 @@ class Melos::Crypto
|
|
|
43
67
|
OpenSSL::PKey.new_raw_private_key('ED448', raw)
|
|
44
68
|
end
|
|
45
69
|
|
|
70
|
+
def self.encapsulation_algorithm_name
|
|
71
|
+
'X448'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.signature_algorithm_name
|
|
75
|
+
'ED448'
|
|
76
|
+
end
|
|
77
|
+
|
|
46
78
|
def self.hash_algorithm
|
|
47
79
|
nil
|
|
48
80
|
end
|
|
@@ -50,6 +82,14 @@ class Melos::Crypto
|
|
|
50
82
|
|
|
51
83
|
class EC
|
|
52
84
|
# also would like to depend on HPKE gem...
|
|
85
|
+
def self.serialize_public_key(key)
|
|
86
|
+
key.public_key.to_bn.to_s(2)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.serialize_private_key(key)
|
|
90
|
+
key.private_key.to_s(2)
|
|
91
|
+
end
|
|
92
|
+
|
|
53
93
|
def self.deserialize_private_key(secret)
|
|
54
94
|
asn1_seq = OpenSSL::ASN1.Sequence([
|
|
55
95
|
OpenSSL::ASN1.Integer(1),
|
|
@@ -116,49 +156,50 @@ class Melos::Crypto
|
|
|
116
156
|
end
|
|
117
157
|
end
|
|
118
158
|
|
|
119
|
-
attr_accessor :level, :digest, :hpke, :kdf, :pkey
|
|
159
|
+
attr_accessor :level, :digest, :hpke, :kdf, :pkey, :suite_id
|
|
120
160
|
def initialize(suite_id)
|
|
161
|
+
@suite_id = suite_id
|
|
121
162
|
case suite_id
|
|
122
163
|
when 1 # MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519
|
|
123
164
|
@level = 128
|
|
124
165
|
@digest = OpenSSL::Digest.new('sha256')
|
|
125
|
-
@hpke = HPKE.new(
|
|
166
|
+
@hpke = HPKE.new(HPKE::DHKEM_X25519_HKDF_SHA256, HPKE::HKDF_SHA256, HPKE::AES_128_GCM)
|
|
126
167
|
@kdf = @hpke.hkdf
|
|
127
168
|
@pkey = Melos::Crypto::CipherSuite::X25519
|
|
128
169
|
when 2 # MLS_128_DHKEMP256_AES128GCM_SHA256_P256
|
|
129
170
|
@level = 128
|
|
130
171
|
@digest = OpenSSL::Digest.new('sha256')
|
|
131
|
-
@hpke = HPKE.new(
|
|
172
|
+
@hpke = HPKE.new(HPKE::DHKEM_P256_HKDF_SHA256, HPKE::HKDF_SHA256, HPKE::AES_128_GCM)
|
|
132
173
|
@kdf = @hpke.hkdf
|
|
133
174
|
@pkey = Melos::Crypto::CipherSuite::P256
|
|
134
175
|
when 3 # MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519
|
|
135
176
|
@level = 128
|
|
136
177
|
@digest = OpenSSL::Digest.new('sha256')
|
|
137
|
-
@hpke = HPKE.new(
|
|
178
|
+
@hpke = HPKE.new(HPKE::DHKEM_X25519_HKDF_SHA256, HPKE::HKDF_SHA256, HPKE::CHACHA20_POLY1305)
|
|
138
179
|
@kdf = @hpke.hkdf
|
|
139
180
|
@pkey = Melos::Crypto::CipherSuite::X25519
|
|
140
181
|
when 4 # MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448
|
|
141
182
|
@level = 256
|
|
142
183
|
@digest = OpenSSL::Digest.new('sha512')
|
|
143
|
-
@hpke = HPKE.new(
|
|
184
|
+
@hpke = HPKE.new(HPKE::DHKEM_X448_HKDF_SHA512, HPKE::HKDF_SHA512, HPKE::AES_256_GCM)
|
|
144
185
|
@kdf = @hpke.hkdf
|
|
145
186
|
@pkey = Melos::Crypto::CipherSuite::X448
|
|
146
187
|
when 5 # MLS_256_DHKEMP521_AES256GCM_SHA512_P521
|
|
147
188
|
@level = 256
|
|
148
189
|
@digest = OpenSSL::Digest.new('sha512')
|
|
149
|
-
@hpke = HPKE.new(
|
|
190
|
+
@hpke = HPKE.new(HPKE::DHKEM_P521_HKDF_SHA512, HPKE::HKDF_SHA512, HPKE::AES_256_GCM)
|
|
150
191
|
@kdf = @hpke.hkdf
|
|
151
192
|
@pkey = Melos::Crypto::CipherSuite::P521
|
|
152
193
|
when 6 # MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448
|
|
153
194
|
@level = 256
|
|
154
195
|
@digest = OpenSSL::Digest.new('sha512')
|
|
155
|
-
@hpke = HPKE.new(
|
|
196
|
+
@hpke = HPKE.new(HPKE::DHKEM_X448_HKDF_SHA512, HPKE::HKDF_SHA512, HPKE::CHACHA20_POLY1305)
|
|
156
197
|
@kdf = @hpke.hkdf
|
|
157
198
|
@pkey = Melos::Crypto::CipherSuite::X448
|
|
158
199
|
when 7 # MLS_256_DHKEMP384_AES256GCM_SHA384_P384
|
|
159
200
|
@level = 256
|
|
160
201
|
@digest = OpenSSL::Digest.new('sha384')
|
|
161
|
-
@hpke = HPKE.new(
|
|
202
|
+
@hpke = HPKE.new(HPKE::DHKEM_P384_HKDF_SHA384, HPKE::HKDF_SHA384, HPKE::AES_256_GCM)
|
|
162
203
|
@kdf = @hpke.hkdf
|
|
163
204
|
@pkey = Melos::Crypto::CipherSuite::P384
|
|
164
205
|
end
|
|
@@ -172,7 +213,7 @@ class Melos::Crypto
|
|
|
172
213
|
end
|
|
173
214
|
|
|
174
215
|
def self.ref_hash(suite, label, value)
|
|
175
|
-
ref_hash_input = Melos::Vec.
|
|
216
|
+
ref_hash_input = Melos::Vec.string_to_vec(label) + Melos::Vec.string_to_vec(value)
|
|
176
217
|
suite.digest.digest(ref_hash_input)
|
|
177
218
|
end
|
|
178
219
|
|
|
@@ -189,7 +230,7 @@ class Melos::Crypto
|
|
|
189
230
|
end
|
|
190
231
|
|
|
191
232
|
def self.expand_with_label(suite, secret, label, context, length)
|
|
192
|
-
kdf_label = [length].pack('S>') + Melos::Vec.
|
|
233
|
+
kdf_label = [length].pack('S>') + Melos::Vec.string_to_vec("MLS 1.0 " + label) + Melos::Vec.string_to_vec(context)
|
|
193
234
|
suite.kdf.expand(secret, kdf_label, length)
|
|
194
235
|
end
|
|
195
236
|
|
|
@@ -227,26 +268,26 @@ class Melos::Crypto
|
|
|
227
268
|
end
|
|
228
269
|
|
|
229
270
|
def self.encrypt_with_label(suite, public_key, label, context, plaintext)
|
|
230
|
-
encrypt_context = Melos::Vec.
|
|
271
|
+
encrypt_context = Melos::Vec.string_to_vec("MLS 1.0 " + label) + Melos::Vec.string_to_vec(context)
|
|
231
272
|
pkey = suite.pkey.deserialize_public_encapsulation_key(public_key)
|
|
232
273
|
seal_base(suite, pkey, encrypt_context, "", plaintext)
|
|
233
274
|
end
|
|
234
275
|
|
|
235
276
|
def self.decrypt_with_label(suite, private_key, label, context, kem_output, ciphertext)
|
|
236
|
-
encrypt_context = Melos::Vec.
|
|
277
|
+
encrypt_context = Melos::Vec.string_to_vec("MLS 1.0 " + label) + Melos::Vec.string_to_vec(context)
|
|
237
278
|
pkey = suite.pkey.deserialize_private_encapsulation_key(private_key)
|
|
238
279
|
open_base(suite, kem_output, pkey, encrypt_context, "", ciphertext)
|
|
239
280
|
end
|
|
240
281
|
|
|
241
282
|
def self.sign_with_label(suite, signature_key, label, content)
|
|
242
283
|
skey = suite.pkey.deserialize_private_signature_key(signature_key)
|
|
243
|
-
sign_content = Melos::Vec.
|
|
284
|
+
sign_content = Melos::Vec.string_to_vec("MLS 1.0 " + label) + Melos::Vec.string_to_vec(content)
|
|
244
285
|
skey.sign(suite.pkey.hash_algorithm, sign_content)
|
|
245
286
|
end
|
|
246
287
|
|
|
247
288
|
def self.verify_with_label(suite, verification_key, label, content, signature_value)
|
|
248
289
|
vkey = suite.pkey.deserialize_public_signature_key(verification_key)
|
|
249
|
-
sign_content = Melos::Vec.
|
|
290
|
+
sign_content = Melos::Vec.string_to_vec("MLS 1.0 " + label) + Melos::Vec.string_to_vec(content)
|
|
250
291
|
vkey.verify(suite.pkey.hash_algorithm, signature_value, sign_content)
|
|
251
292
|
end
|
|
252
293
|
|
|
@@ -301,7 +342,35 @@ class Melos::Crypto
|
|
|
301
342
|
end
|
|
302
343
|
|
|
303
344
|
def self.parent_hash(suite, encryption_key, ph_of_parent, sibling_hash)
|
|
304
|
-
parent_hash_input = Melos::Vec.
|
|
345
|
+
parent_hash_input = Melos::Vec.string_to_vec(encryption_key) + Melos::Vec.string_to_vec(ph_of_parent) + Melos::Vec.string_to_vec(sibling_hash)
|
|
305
346
|
Melos::Crypto.hash(suite, parent_hash_input)
|
|
306
347
|
end
|
|
348
|
+
|
|
349
|
+
def self.generate_encapsulation_key_pair(suite)
|
|
350
|
+
if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448)
|
|
351
|
+
# is an Edwards curve
|
|
352
|
+
OpenSSL::PKey.generate_key(suite.pkey.encapsulation_algorithm_name)
|
|
353
|
+
else
|
|
354
|
+
# is an EC
|
|
355
|
+
OpenSSL::PKey::EC.generate(suite.pkey.curve_name)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def self.generate_signature_key_pair(suite)
|
|
360
|
+
if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448)
|
|
361
|
+
# is an Edwards curve
|
|
362
|
+
OpenSSL::PKey.generate_key(suite.pkey.signature_algorithm_name)
|
|
363
|
+
else
|
|
364
|
+
# is an EC
|
|
365
|
+
OpenSSL::PKey::EC.generate(suite.pkey.curve_name)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def self.serialize_public_key(suite, pkey)
|
|
370
|
+
suite.pkey.serialize_public_key(pkey)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def self.serialize_private_key(suite, pkey)
|
|
374
|
+
suite.pkey.serialize_private_key(pkey)
|
|
375
|
+
end
|
|
307
376
|
end
|