Files
Warp/server/internal/matrix/event/sign.go
T
tajniak81andClaude Opus 5.5 2fbd5f53ce
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 2: the event core
Implements PLAN.md Phase 2: the library every later phase is built on,
as pure code under server/internal/matrix with no HTTP and no storage.

canonicaljson: the canonical JSON encoding, hand-written because
encoding/json escapes <, > and & and accepts floats. Integers only, in
[-(2^53)+1, 2^53-1]; lone surrogate escapes rejected rather than turned
into U+FFFD.

signing: the Signing JSON algorithm alongside the existing key handling,
and one lenient base64 decoder shared by the packages above it.

id: identifier grammars as they appear inside events, deliberately laxer
than userapi's account rules because rooms carry historical user IDs.

roomversion: the registry. Versions 3 to 12 are supported, each written
out in full as a Rules value with the flags the other packages read.
Versions 1 and 2 are known but refused: their event format and state
resolution v1 wait for federation. createRoom will default to 11, as the
spec asks for a while after 1.16, with 12 available.

event: PDU parsing and validation, content and reference hashes, event
IDs, the redaction algorithm for every supported version, signing and
verification, and a Builder.

auth: the authorisation rules in spec order, power levels with their
defaults, v12 creators with infinite power, the auth events selection
algorithm, rule 3's auth_events checks, and the separate test for
whether a redaction applies. Comments use the room version 12 rule
numbering, with the version 11 numbers given where they differ.

stateres: state resolution v2, and version 12's amendment (an empty
starting state and the conflicted state subgraph).

Tested against the spec's own vectors byte for byte: the canonical JSON
examples, both JSON signing vectors and both event signing vectors.
State resolution ports scenarios from Synapse's suite. canonicaljson
and event.Parse have fuzz targets.

The capabilities endpoint now advertises the registry instead of a
hard-coded "11". docs/event-core.md describes the packages and the
known gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:23:35 +02:00

114 lines
3.6 KiB
Go

package event
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson"
"github.com/dariuszjaniak/warp/server/internal/matrix/roomversion"
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
)
// hashAndSign adds a content hash and a signature to a bare event.
//
// The order matters and is not the obvious one. The content hash is added
// first, so that it covers every property of the unredacted event. The
// event is then *redacted* and that copy is signed, so the signature covers
// only the properties a redaction keeps — plus the hashes, which cover the
// rest. The signature is then copied back onto the full event.
//
// The result is an event whose signature still verifies after someone
// redacts it, which is what lets a server hand out a redacted event and
// have the recipient believe it.
func hashAndSign(raw []byte, rules roomversion.Rules, serverName string, key *signing.KeyPair) ([]byte, error) {
obj, err := decodeObject(raw)
if err != nil {
return nil, err
}
sum, err := contentHash(raw)
if err != nil {
return nil, err
}
obj["hashes"], err = json.Marshal(hashes{SHA256: base64.RawStdEncoding.EncodeToString(sum[:])})
if err != nil {
return nil, fmt.Errorf("%w: encode hashes: %w", ErrInvalidEvent, err)
}
hashed, err := canonicaljson.Marshal(obj)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidEvent, err)
}
redacted, err := redactJSON(hashed, rules)
if err != nil {
return nil, err
}
signed, err := signing.SignJSON(serverName, key, redacted)
if err != nil {
return nil, err
}
sigs, err := signing.ExtractSignatures(signed)
if err != nil {
return nil, err
}
if obj["signatures"], err = json.Marshal(sigs); err != nil {
return nil, fmt.Errorf("%w: encode signatures: %w", ErrInvalidEvent, err)
}
return canonicaljson.Marshal(obj)
}
// VerifySignature checks the event for a valid signature from serverName
// made with keyID.
//
// The event is redacted first, because that is the form that was signed.
// This holds whether we were given the full event or a redacted copy, which
// is why a redacted event can still be shown to be genuine.
func (e *Event) VerifySignature(serverName, keyID string, pub ed25519.PublicKey) error {
redacted, err := redactJSON(e.raw, e.rules)
if err != nil {
return err
}
return signing.VerifyJSON(serverName, keyID, pub, redacted)
}
// Sign returns the event with an additional signature from serverName.
//
// A normal event is signed once, by the sender's own server, when it is
// built. A second signature is needed in the few places where another
// server has to vouch for an event it did not send: the invited server
// countersigning an invite, and a third-party invite exchanged through a
// server other than the sender's.
func (e *Event) Sign(serverName string, key *signing.KeyPair) (*Event, error) {
redacted, err := redactJSON(e.raw, e.rules)
if err != nil {
return nil, err
}
signed, err := signing.SignJSON(serverName, key, redacted)
if err != nil {
return nil, err
}
sigs, err := signing.ExtractSignatures(signed)
if err != nil {
return nil, err
}
obj, err := decodeObject(e.raw)
if err != nil {
return nil, err
}
if obj["signatures"], err = json.Marshal(sigs); err != nil {
return nil, fmt.Errorf("%w: encode signatures: %w", ErrInvalidEvent, err)
}
out, err := canonicaljson.Marshal(obj)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidEvent, err)
}
// Re-parse rather than patching the Event in place: an Event is
// immutable, and the signature block is one of the things Parse
// validates.
return Parse(out, e.rules)
}