async-matrix 3.0.0-aarch64-linux → 3.0.1-aarch64-linux

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.
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require_relative "canonical_json"
7
+ require_relative "error"
8
+
9
+ module Protocol
10
+ module Matrix
11
+ # Signing JSON, per https://spec.matrix.org/latest/appendices/#signing-json
12
+ #
13
+ # The procedure, in full:
14
+ #
15
+ # 1. remove `signatures` and `unsigned`
16
+ # 2. encode what is left as canonical JSON
17
+ # 3. sign those bytes with ed25519
18
+ # 4. encode the signature as UNPADDED base64
19
+ # 5. store it under signatures[entity]["<algorithm>:<key_id>"]
20
+ #
21
+ # THE KEY MATERIAL IS INJECTED. `signer` is anything answering
22
+ # `sign(String) -> base64`, and `verifier` anything answering
23
+ # `verify_signature(key, message, signature) -> bool` — which is the shape
24
+ # the native E2EE module already exposes. So this file implements the
25
+ # procedure and holds no keys, which is what lets it sit in the protocol
26
+ # layer and be tested without cryptography.
27
+ module Signing
28
+ ED25519 = "ed25519"
29
+
30
+ class Error < Protocol::Matrix::Error; end
31
+
32
+ # No signature from the entity and key we were told to check.
33
+ class MissingSignatureError < Error; end
34
+
35
+ # "{algorithm}:{key_id}" — for a device key the key_id is the device id.
36
+ def self.key_id(name, algorithm: ED25519)
37
+ "#{algorithm}:#{name}"
38
+ end
39
+
40
+ # Sign +object+ as +user_id+ with +key_id+.
41
+ #
42
+ # @returns [Hash] a copy of +object+ with the signature added. EXISTING
43
+ # SIGNATURES ARE PRESERVED: a device-keys object legitimately carries
44
+ # several (our device key, our self-signing key, another user's
45
+ # attestation), and replacing the map rather than merging into it would
46
+ # silently discard them.
47
+ def self.sign(object, signer:, user_id:, key_id:)
48
+ signature = sign_bytes(CanonicalJson.signable_bytes(object), signer: signer)
49
+ existing = object["signatures"] || object[:signatures] || {}
50
+ mine = existing[user_id] || {}
51
+
52
+ object.merge(
53
+ "signatures" => existing.merge(user_id => mine.merge(key_id => signature)),
54
+ )
55
+ end
56
+
57
+ # Sign the given bytes, returning unpadded base64.
58
+ #
59
+ # PADDING IS STRIPPED HERE rather than assumed absent. The spec requires
60
+ # unpadded base64, and whether a given primitive emits padding is its own
61
+ # business; a stray "=" would make every signature we produce fail
62
+ # verification everywhere, which is an expensive thing to discover
63
+ # remotely.
64
+ def self.sign_bytes(message, signer:)
65
+ signer.sign(message).delete_suffix("==").delete_suffix("=")
66
+ end
67
+
68
+ # Is +object+ correctly signed by +user_id+ with +key_id+?
69
+ #
70
+ # @parameter key [String] the public ed25519 key to verify against.
71
+ # @parameter verifier [Object] answers
72
+ # `verify_signature(key, message, signature) -> bool`.
73
+ # @raises [MissingSignatureError] when there is no such signature to check
74
+ # — distinct from a signature that is present and wrong, because the two
75
+ # mean different things about the sender.
76
+ def self.verify(object, key:, verifier:, user_id:, key_id:)
77
+ signature = signature_for(object, user_id: user_id, key_id: key_id)
78
+
79
+ if signature.nil?
80
+ raise MissingSignatureError, "no #{key_id} signature from #{user_id}"
81
+ end
82
+
83
+ verifier.verify_signature(key, CanonicalJson.signable_bytes(object), signature)
84
+ end
85
+
86
+ # The signature +user_id+ made with +key_id+, or nil.
87
+ def self.signature_for(object, user_id:, key_id:)
88
+ signatures = object["signatures"] || object[:signatures] || {}
89
+ (signatures[user_id] || {})[key_id]
90
+ end
91
+
92
+ def self.signed_by?(object, user_id:, key_id:)
93
+ !signature_for(object, user_id: user_id, key_id: key_id).nil?
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ __END__
100
+ describe "Protocol::Matrix::Signing" do
101
+ # A signer that records what it was asked to sign, so a spec can assert on
102
+ # the exact bytes the signature covers.
103
+ def recording_signer(signature = "SIGNATURE")
104
+ signer = Object.new
105
+ signed = []
106
+ signer.define_singleton_method(:signed) { signed }
107
+ signer.define_singleton_method(:sign) do |message|
108
+ signed << message
109
+ signature
110
+ end
111
+ signer
112
+ end
113
+
114
+ def verifier(result = true)
115
+ verifier = Object.new
116
+ calls = []
117
+ verifier.define_singleton_method(:calls) { calls }
118
+ verifier.define_singleton_method(:verify_signature) do |key, message, signature|
119
+ calls << [key, message, signature]
120
+ result
121
+ end
122
+ verifier
123
+ end
124
+
125
+ it "builds a key identifier" do
126
+ Protocol::Matrix::Signing.key_id("JLAFKJWSCS").should == "ed25519:JLAFKJWSCS"
127
+ Protocol::Matrix::Signing.key_id("1", algorithm: "ed25519").should == "ed25519:1"
128
+ end
129
+
130
+ it "stores the signature under entity and key identifier" do
131
+ signed = Protocol::Matrix::Signing.sign(
132
+ {"a" => 1},
133
+ signer: recording_signer,
134
+ user_id: "@alice:example.com",
135
+ key_id: "ed25519:DEV",
136
+ )
137
+
138
+ signed["signatures"].should == {"@alice:example.com" => {"ed25519:DEV" => "SIGNATURE"}}
139
+ end
140
+
141
+ # The signature covers canonical JSON of the object MINUS signatures and
142
+ # unsigned.
143
+ it "signs the canonical form, excluding signatures and unsigned" do
144
+ signer = recording_signer
145
+ Protocol::Matrix::Signing.sign(
146
+ {"b" => 2, "a" => 1, "unsigned" => {"age" => 1}},
147
+ signer: signer,
148
+ user_id: "@alice:example.com",
149
+ key_id: "ed25519:DEV",
150
+ )
151
+
152
+ signer.signed.should == ['{"a":1,"b":2}']
153
+ end
154
+
155
+ # A device-keys object legitimately carries several signatures; replacing the
156
+ # map rather than merging into it would discard them.
157
+ it "preserves signatures already present" do
158
+ object = {
159
+ "a" => 1,
160
+ "signatures" => {
161
+ "@bob:example.com" => {"ed25519:OTHER" => "theirs"},
162
+ "@alice:example.com" => {"ed25519:SSK" => "self-signing"},
163
+ },
164
+ }
165
+ signed = Protocol::Matrix::Signing.sign(
166
+ object,
167
+ signer: recording_signer,
168
+ user_id: "@alice:example.com",
169
+ key_id: "ed25519:DEV",
170
+ )
171
+
172
+ signed["signatures"]["@bob:example.com"].should == {"ed25519:OTHER" => "theirs"}
173
+ signed["signatures"]["@alice:example.com"].should == {
174
+ "ed25519:SSK" => "self-signing", "ed25519:DEV" => "SIGNATURE",
175
+ }
176
+ end
177
+
178
+ it "returns a copy rather than mutating its argument" do
179
+ object = {"a" => 1}
180
+ Protocol::Matrix::Signing.sign(
181
+ object,
182
+ signer: recording_signer,
183
+ user_id: "@alice:example.com",
184
+ key_id: "ed25519:DEV",
185
+ )
186
+
187
+ object.key?("signatures").should == false
188
+ end
189
+
190
+ # The spec requires UNPADDED base64. Whether a primitive emits padding is its
191
+ # own business; a stray "=" would fail verification everywhere.
192
+ it "strips base64 padding from whatever the signer returns" do
193
+ Protocol::Matrix::Signing.sign_bytes("msg", signer: recording_signer("abc=")).should == "abc"
194
+ Protocol::Matrix::Signing.sign_bytes("msg", signer: recording_signer("abc==")).should == "abc"
195
+ Protocol::Matrix::Signing.sign_bytes("msg", signer: recording_signer("abc")).should == "abc"
196
+ end
197
+
198
+ # ── Verifying ─────────────────────────────────────────────────────────────
199
+
200
+ it "verifies against the same bytes it would have signed" do
201
+ check = verifier
202
+ object = {
203
+ "b" => 2,
204
+ "a" => 1,
205
+ "signatures" => {"@alice:example.com" => {"ed25519:DEV" => "SIG"}},
206
+ }
207
+
208
+ Protocol::Matrix::Signing.verify(
209
+ object,
210
+ key: "PUBKEY",
211
+ verifier: check,
212
+ user_id: "@alice:example.com",
213
+ key_id: "ed25519:DEV",
214
+ ).should == true
215
+
216
+ check.calls.should == [["PUBKEY", '{"a":1,"b":2}', "SIG"]]
217
+ end
218
+
219
+ it "reports a signature that does not verify" do
220
+ Protocol::Matrix::Signing.verify(
221
+ {"a" => 1, "signatures" => {"@alice:example.com" => {"ed25519:DEV" => "SIG"}}},
222
+ key: "PUBKEY",
223
+ verifier: verifier(false),
224
+ user_id: "@alice:example.com",
225
+ key_id: "ed25519:DEV",
226
+ ).should == false
227
+ end
228
+
229
+ # "A signature that is absent" and "a signature that is wrong" say different
230
+ # things about the sender, so they are not the same answer.
231
+ it "distinguishes a missing signature from a bad one" do
232
+ lambda {
233
+ Protocol::Matrix::Signing.verify(
234
+ {"a" => 1},
235
+ key: "PUBKEY",
236
+ verifier: verifier,
237
+ user_id: "@alice:example.com",
238
+ key_id: "ed25519:DEV",
239
+ )
240
+ }.should.raise(Protocol::Matrix::Signing::MissingSignatureError)
241
+ end
242
+
243
+ it "treats a signature from a different key as missing" do
244
+ lambda {
245
+ Protocol::Matrix::Signing.verify(
246
+ {"signatures" => {"@alice:example.com" => {"ed25519:OTHER" => "SIG"}}},
247
+ key: "PUBKEY",
248
+ verifier: verifier,
249
+ user_id: "@alice:example.com",
250
+ key_id: "ed25519:DEV",
251
+ )
252
+ }.should.raise(Protocol::Matrix::Signing::MissingSignatureError)
253
+ end
254
+
255
+ it "finds a signature, or reports its absence" do
256
+ object = {"signatures" => {"@alice:example.com" => {"ed25519:DEV" => "SIG"}}}
257
+
258
+ Protocol::Matrix::Signing.signature_for(object, user_id: "@alice:example.com", key_id: "ed25519:DEV")
259
+ .should == "SIG"
260
+ Protocol::Matrix::Signing.signed_by?(object, user_id: "@alice:example.com", key_id: "ed25519:DEV")
261
+ .should == true
262
+ Protocol::Matrix::Signing.signed_by?(object, user_id: "@bob:example.com", key_id: "ed25519:DEV")
263
+ .should == false
264
+ end
265
+
266
+ # A signature survives a round trip through canonical JSON regardless of the
267
+ # order the object was built in -- which is the entire purpose of the
268
+ # canonical encoding.
269
+ it "signs two differently ordered objects identically" do
270
+ first = recording_signer
271
+ second = recording_signer
272
+
273
+ Protocol::Matrix::Signing.sign({"a" => 1, "b" => 2}, signer: first, user_id: "@a:b", key_id: "ed25519:D")
274
+ Protocol::Matrix::Signing.sign({"b" => 2, "a" => 1}, signer: second, user_id: "@a:b", key_id: "ed25519:D")
275
+
276
+ first.signed.should == second.signed
277
+ end
278
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ # The Matrix protocol layer: message formats, event schemas, and the rules for
7
+ # reading them. No HTTP, no sockets, no storage.
8
+ #
9
+ # The split follows the Socketry convention that `protocol-http` and
10
+ # `protocol-grpc` set: a protocol library owns the FORMAT and the state machine
11
+ # over it, and the `async-*` library binds that to IO. So an Event, a Content, an
12
+ # EncryptedMessage and the schemas that validate them live here, and the Client
13
+ # that fetches them lives in Async::Matrix.
14
+ #
15
+ # Practically, that means everything under this namespace can be unit tested
16
+ # with a Hash and no network, no homeserver, and — for EncryptedMessage — no
17
+ # crypto either.
18
+ module Protocol
19
+ module Matrix
20
+ end
21
+ end
22
+
23
+ Dir.glob("#{__dir__}/matrix/**/*.rb").sort.each do |path|
24
+ require path
25
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-matrix
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Nathan Kidd
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-08 00:00:00.000000000 Z
11
+ date: 2026-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -437,31 +437,46 @@ files:
437
437
  - lib/async/matrix/api/chain.rb
438
438
  - lib/async/matrix/api/concat.rb
439
439
  - lib/async/matrix/api/path_tree.rb
440
+ - lib/async/matrix/app_service_client.rb
440
441
  - lib/async/matrix/auth_error.rb
441
442
  - lib/async/matrix/bad_json_error.rb
442
443
  - lib/async/matrix/client.rb
444
+ - lib/async/matrix/client/encryption.rb
445
+ - lib/async/matrix/client/rooms.rb
446
+ - lib/async/matrix/client/sync.rb
443
447
  - lib/async/matrix/config.rb
444
448
  - lib/async/matrix/config/vivify.rb
445
449
  - lib/async/matrix/connection.rb
446
- - lib/async/matrix/content.rb
450
+ - lib/async/matrix/device_store.rb
447
451
  - lib/async/matrix/double_puppet_client.rb
448
452
  - lib/async/matrix/e2ee.rb
453
+ - lib/async/matrix/e2ee/pickle_key.rb
449
454
  - lib/async/matrix/endpoint.rb
450
455
  - lib/async/matrix/error.rb
451
456
  - lib/async/matrix/error_response.rb
452
- - lib/async/matrix/event.rb
453
457
  - lib/async/matrix/homeserver_error.rb
454
458
  - lib/async/matrix/invalid_endpoint_error.rb
455
459
  - lib/async/matrix/media_client.rb
456
460
  - lib/async/matrix/not_found_error.rb
457
461
  - lib/async/matrix/notifier.rb
458
462
  - lib/async/matrix/response_too_large_error.rb
459
- - lib/async/matrix/schema.rb
460
- - lib/async/matrix/schema/registry.rb
461
- - lib/async/matrix/schema/validation_error.rb
462
463
  - lib/async/matrix/server.rb
463
464
  - lib/async/matrix/stream.rb
464
465
  - lib/async/matrix/version.rb
466
+ - lib/protocol/matrix.rb
467
+ - lib/protocol/matrix/canonical_json.rb
468
+ - lib/protocol/matrix/content.rb
469
+ - lib/protocol/matrix/encrypted_message.rb
470
+ - lib/protocol/matrix/error.rb
471
+ - lib/protocol/matrix/event.rb
472
+ - lib/protocol/matrix/key_backup.rb
473
+ - lib/protocol/matrix/keys.rb
474
+ - lib/protocol/matrix/message_batch.rb
475
+ - lib/protocol/matrix/schema.rb
476
+ - lib/protocol/matrix/schema/registry.rb
477
+ - lib/protocol/matrix/schema/validation_error.rb
478
+ - lib/protocol/matrix/secret_storage.rb
479
+ - lib/protocol/matrix/signing.rb
465
480
  homepage: https://github.com/general-intelligence-systems/async-matrix
466
481
  licenses:
467
482
  - Apache-2.0