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
Implements PLAN.md Phase 5: the Server-Server API. Warp joins rooms on other servers and lets their users join its rooms, invites across servers with the invitee's server countersigning, rejects remote invites (locally if the inviter is unreachable), and exchanges events, typing, receipts and to-device messages with other homeservers — all behind the runtime switch, which now starts and stops real outbound traffic. The protocol lives in internal/federation with no knowledge of rooms: X-Matrix signing (fuzzed parser), the spec's discovery algorithm keeping the dial address and the TLS name apart, a signed client that refuses every request while the switch is off or the policy denies the destination, and a key ring that caches other servers' keys for at most a week, with optional notaries. The roomserver takes events from elsewhere through the spec's intake: signatures and hashes (a hash mismatch keeps the redacted form), the rules against the event's own auth events, against the state before it — with /get_missing_events for small gaps and /state_ids for wide ones — and against the current state, where a failure is a soft failure. Events held without their state are outliers: a column on roomserver_events, because the current state of a room joined over federation is almost all outliers, and excluded from every timeline query. Forks are resolved at the room's head. Membership across servers is make_join/send_join, make_leave/send_leave and countersigned invites, in both directions; m.room.server_acl is enforced on everything another server sends. History before a remote join is backfilled when /messages pages past it, stored at negative stream positions. internal/federationapi is the running component: the inbound endpoints, and an outbound queue that persists what it owes each destination, sends it in transactions of 50 PDUs and 100 EDUs, backs off from ten seconds to six hours, survives restarts, and drops events persisted while federation is off rather than delivering a backlog in one burst. Its Module is what the switch starts and stops. Also: a TLS listener for port 8448, trusted key servers, key validity and an outbound CA bundle in the config; per-destination health and backoff reset in the admin API; remote profiles, aliases and directories in the Client-Server API; a Complement federation profile. See docs/federation.md, including its known gaps (no partial-state joins, no key rotation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
464 lines
14 KiB
Go
464 lines
14 KiB
Go
package federation
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
)
|
|
|
|
// Server keys: publishing ours, and fetching, caching and using everybody
|
|
// else's.
|
|
//
|
|
// Every signature another server makes — on an event, on a request — is
|
|
// checked against a key that server publishes at /_matrix/key/v2/server.
|
|
// The key ring fetches that document, checks that it is signed by the keys
|
|
// it lists (a key response vouches for itself; what makes it trustworthy
|
|
// is that it came from the server over TLS, or from a notary we chose to
|
|
// trust), and caches each key for as long as the server said it would stay
|
|
// valid — but never more than a week, which is the spec's cap on how long
|
|
// one fetch may be believed.
|
|
|
|
// ErrUnknownKey means a server's key could not be found: not cached, and
|
|
// neither the server nor any notary produced it.
|
|
var ErrUnknownKey = errors.New("signing key not found")
|
|
|
|
// maxKeyValidity caps how far ahead a fetched key's valid_until_ts is
|
|
// believed.
|
|
const maxKeyValidity = 7 * 24 * time.Hour
|
|
|
|
// fetchFailureBackoff stops a server whose key server is down from being
|
|
// asked again on every request that needs one of its keys.
|
|
const fetchFailureBackoff = time.Minute
|
|
|
|
// verifyKey and oldVerifyKey are the key objects of a key response.
|
|
type verifyKey struct {
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
type oldVerifyKey struct {
|
|
Key string `json:"key"`
|
|
ExpiredTS int64 `json:"expired_ts"`
|
|
}
|
|
|
|
// serverKeyResponse is the document at /_matrix/key/v2/server.
|
|
type serverKeyResponse struct {
|
|
ServerName string `json:"server_name"`
|
|
ValidUntilTS int64 `json:"valid_until_ts"`
|
|
VerifyKeys map[string]verifyKey `json:"verify_keys"`
|
|
OldVerifyKeys map[string]oldVerifyKey `json:"old_verify_keys"`
|
|
}
|
|
|
|
// KeyRingOptions configure a KeyRing.
|
|
type KeyRingOptions struct {
|
|
ServerName string
|
|
Key *signing.KeyPair
|
|
|
|
// Store caches fetched keys across restarts.
|
|
Store storage.FederationStore
|
|
|
|
// Client fetches keys. It refuses everything while federation is off,
|
|
// which is exactly when no remote key can be needed.
|
|
Client *Client
|
|
|
|
// Notaries are the servers asked for a key the server itself cannot
|
|
// be reached for.
|
|
Notaries []string
|
|
|
|
// Validity is how long our own key response says our key is valid.
|
|
Validity time.Duration
|
|
|
|
Logger *slog.Logger
|
|
Now func() time.Time
|
|
}
|
|
|
|
// KeyRing holds this server's key and everybody else's.
|
|
type KeyRing struct {
|
|
serverName string
|
|
key *signing.KeyPair
|
|
store storage.FederationStore
|
|
client *Client
|
|
notaries []string
|
|
validity time.Duration
|
|
log *slog.Logger
|
|
now func() time.Time
|
|
|
|
mu sync.Mutex
|
|
cache map[string]map[string]storage.ServerKey // server -> key ID -> key
|
|
failedAt map[string]time.Time
|
|
inflight map[string]*fetchCall
|
|
}
|
|
|
|
type fetchCall struct {
|
|
done chan struct{}
|
|
err error
|
|
}
|
|
|
|
// NewKeyRing builds a key ring.
|
|
func NewKeyRing(opts KeyRingOptions) *KeyRing {
|
|
if opts.Logger == nil {
|
|
opts.Logger = slog.Default()
|
|
}
|
|
if opts.Now == nil {
|
|
opts.Now = time.Now
|
|
}
|
|
if opts.Validity <= 0 {
|
|
opts.Validity = maxKeyValidity
|
|
}
|
|
return &KeyRing{
|
|
serverName: opts.ServerName,
|
|
key: opts.Key,
|
|
store: opts.Store,
|
|
client: opts.Client,
|
|
notaries: opts.Notaries,
|
|
validity: opts.Validity,
|
|
log: opts.Logger,
|
|
now: opts.Now,
|
|
cache: map[string]map[string]storage.ServerKey{},
|
|
failedAt: map[string]time.Time{},
|
|
inflight: map[string]*fetchCall{},
|
|
}
|
|
}
|
|
|
|
// OwnKeyResponse is this server's signed key document, as served at
|
|
// /_matrix/key/v2/server and handed out by the notary endpoints.
|
|
func (k *KeyRing) OwnKeyResponse() (json.RawMessage, error) {
|
|
doc := serverKeyResponse{
|
|
ServerName: k.serverName,
|
|
ValidUntilTS: k.now().Add(k.validity).UnixMilli(),
|
|
VerifyKeys: map[string]verifyKey{k.key.KeyID(): {Key: k.key.PublicBase64()}},
|
|
OldVerifyKeys: map[string]oldVerifyKey{},
|
|
}
|
|
raw, err := json.Marshal(doc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signing.SignJSON(k.serverName, k.key, raw)
|
|
}
|
|
|
|
// PublicKey returns a server's key, fetching it when it is not cached or
|
|
// is not known to be valid at atTS.
|
|
//
|
|
// atTS is the time the key has to have been valid at, in milliseconds:
|
|
// the event's origin_server_ts when the room version enforces key
|
|
// validity, now for a request. Zero accepts any key the server has ever
|
|
// published, which is the rule for events in room versions before 5.
|
|
func (k *KeyRing) PublicKey(ctx context.Context, serverName, keyID string, atTS int64) (ed25519.PublicKey, error) {
|
|
if serverName == k.serverName {
|
|
if keyID == k.key.KeyID() {
|
|
return k.key.Public, nil
|
|
}
|
|
return nil, fmt.Errorf("%w: %s is not a key of this server", ErrUnknownKey, keyID)
|
|
}
|
|
|
|
if key, ok, err := k.cached(ctx, serverName, keyID); err != nil {
|
|
return nil, err
|
|
} else if ok && usableAt(key, atTS) {
|
|
return decodeKey(key)
|
|
}
|
|
|
|
if err := k.fetch(ctx, serverName); err != nil {
|
|
// A key we already have may still be usable for an event whose
|
|
// timestamp the old validity covers; only the refresh failed.
|
|
if key, ok, _ := k.cached(ctx, serverName, keyID); ok && usableAt(key, atTS) {
|
|
return decodeKey(key)
|
|
}
|
|
return nil, err
|
|
}
|
|
key, ok, err := k.cached(ctx, serverName, keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s has no key %s", ErrUnknownKey, serverName, keyID)
|
|
}
|
|
if !usableAt(key, atTS) {
|
|
return nil, fmt.Errorf("%w: key %s of %s was not valid at %d", ErrUnknownKey, keyID, serverName, atTS)
|
|
}
|
|
return decodeKey(key)
|
|
}
|
|
|
|
// usableAt reports whether a key may verify something signed at atTS.
|
|
func usableAt(key storage.ServerKey, atTS int64) bool {
|
|
if atTS == 0 {
|
|
return true
|
|
}
|
|
if key.ExpiredTS != 0 {
|
|
return atTS <= key.ExpiredTS
|
|
}
|
|
return atTS <= key.ValidUntilTS
|
|
}
|
|
|
|
func decodeKey(key storage.ServerKey) (ed25519.PublicKey, error) {
|
|
raw, err := signing.DecodeBase64(key.PublicKey)
|
|
if err != nil || len(raw) != ed25519.PublicKeySize {
|
|
return nil, fmt.Errorf("%w: key %s of %s is malformed", ErrUnknownKey, key.KeyID, key.ServerName)
|
|
}
|
|
return ed25519.PublicKey(raw), nil
|
|
}
|
|
|
|
// cached reads a key from memory, then from the store.
|
|
func (k *KeyRing) cached(ctx context.Context, serverName, keyID string) (storage.ServerKey, bool, error) {
|
|
k.mu.Lock()
|
|
key, ok := k.cache[serverName][keyID]
|
|
k.mu.Unlock()
|
|
if ok {
|
|
return key, true, nil
|
|
}
|
|
|
|
keys, err := k.store.ServerKeys(ctx, serverName, []string{keyID})
|
|
if err != nil {
|
|
return storage.ServerKey{}, false, err
|
|
}
|
|
if len(keys) == 0 {
|
|
return storage.ServerKey{}, false, nil
|
|
}
|
|
k.remember(keys)
|
|
return keys[0], true, nil
|
|
}
|
|
|
|
func (k *KeyRing) remember(keys []storage.ServerKey) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
for _, key := range keys {
|
|
if k.cache[key.ServerName] == nil {
|
|
k.cache[key.ServerName] = map[string]storage.ServerKey{}
|
|
}
|
|
k.cache[key.ServerName][key.KeyID] = key
|
|
}
|
|
}
|
|
|
|
// fetch refreshes a server's keys, once at a time per server.
|
|
func (k *KeyRing) fetch(ctx context.Context, serverName string) error {
|
|
k.mu.Lock()
|
|
if at, failed := k.failedAt[serverName]; failed && k.now().Sub(at) < fetchFailureBackoff {
|
|
k.mu.Unlock()
|
|
return fmt.Errorf("%w: fetching the keys of %s failed recently", ErrUnknownKey, serverName)
|
|
}
|
|
if call, ok := k.inflight[serverName]; ok {
|
|
k.mu.Unlock()
|
|
select {
|
|
case <-call.done:
|
|
return call.err
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
call := &fetchCall{done: make(chan struct{})}
|
|
k.inflight[serverName] = call
|
|
k.mu.Unlock()
|
|
|
|
call.err = k.fetchKeys(ctx, serverName)
|
|
|
|
k.mu.Lock()
|
|
delete(k.inflight, serverName)
|
|
if call.err != nil {
|
|
k.failedAt[serverName] = k.now()
|
|
} else {
|
|
delete(k.failedAt, serverName)
|
|
}
|
|
k.mu.Unlock()
|
|
close(call.done)
|
|
return call.err
|
|
}
|
|
|
|
// fetchKeys asks the server, then each notary in turn.
|
|
func (k *KeyRing) fetchKeys(ctx context.Context, serverName string) error {
|
|
raw, err := k.client.ServerKeys(ctx, serverName)
|
|
if err == nil {
|
|
if err = k.acceptResponse(ctx, serverName, raw, ""); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
k.log.Debug("could not fetch keys directly", "server_name", serverName, "error", err)
|
|
|
|
for _, notary := range k.notaries {
|
|
if notary == serverName {
|
|
continue
|
|
}
|
|
responses, nerr := k.client.QueryKeys(ctx, notary, map[string]map[string]KeyCriteria{serverName: {}})
|
|
if nerr != nil {
|
|
k.log.Debug("notary could not supply keys", "notary", notary, "server_name", serverName, "error", nerr)
|
|
continue
|
|
}
|
|
for _, resp := range responses {
|
|
if nerr = k.acceptResponse(ctx, serverName, resp, notary); nerr == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fmt.Errorf("%w: no keys for %s: %v", ErrUnknownKey, serverName, err)
|
|
}
|
|
|
|
// acceptResponse checks a key response and stores its keys.
|
|
//
|
|
// The response has to be about the server asked for, and every signature
|
|
// the server made on it has to come from one of the keys it lists and has
|
|
// to verify — a response that cites a key it does not publish is either
|
|
// broken or forged. A response relayed by a notary must also carry the
|
|
// notary's own signature, which is what the notary is trusted for.
|
|
func (k *KeyRing) acceptResponse(ctx context.Context, serverName string, raw []byte, notary string) error {
|
|
var doc serverKeyResponse
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
return fmt.Errorf("decode key response: %w", err)
|
|
}
|
|
if doc.ServerName != serverName {
|
|
return fmt.Errorf("key response is for %q, not %q", doc.ServerName, serverName)
|
|
}
|
|
sigs, err := signing.ExtractSignatures(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(sigs[serverName]) == 0 {
|
|
return fmt.Errorf("key response from %s is not signed by it", serverName)
|
|
}
|
|
for keyID := range sigs[serverName] {
|
|
vk, ok := doc.VerifyKeys[keyID]
|
|
if !ok {
|
|
return fmt.Errorf("key response from %s is signed with %s, which it does not list", serverName, keyID)
|
|
}
|
|
pub, err := signing.DecodeBase64(vk.Key)
|
|
if err != nil || len(pub) != ed25519.PublicKeySize {
|
|
return fmt.Errorf("key %s of %s is malformed", keyID, serverName)
|
|
}
|
|
if err := signing.VerifyJSON(serverName, keyID, pub, raw); err != nil {
|
|
return fmt.Errorf("key response from %s: %w", serverName, err)
|
|
}
|
|
}
|
|
if notary != "" {
|
|
if err := k.verifyJSONAt(ctx, notary, 0, raw); err != nil {
|
|
return fmt.Errorf("key response for %s relayed by %s: %w", serverName, notary, err)
|
|
}
|
|
}
|
|
|
|
now := k.now()
|
|
validUntil := min(doc.ValidUntilTS, now.Add(maxKeyValidity).UnixMilli())
|
|
var keys []storage.ServerKey
|
|
for keyID, vk := range doc.VerifyKeys {
|
|
keys = append(keys, storage.ServerKey{
|
|
ServerName: serverName, KeyID: keyID, PublicKey: vk.Key,
|
|
ValidUntilTS: validUntil, FetchedAtMS: now.UnixMilli(),
|
|
})
|
|
}
|
|
for keyID, ok := range doc.OldVerifyKeys {
|
|
keys = append(keys, storage.ServerKey{
|
|
ServerName: serverName, KeyID: keyID, PublicKey: ok.Key,
|
|
ValidUntilTS: ok.ExpiredTS, ExpiredTS: ok.ExpiredTS, FetchedAtMS: now.UnixMilli(),
|
|
})
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool { return keys[i].KeyID < keys[j].KeyID })
|
|
if err := k.store.StoreServerKeys(ctx, keys); err != nil {
|
|
return err
|
|
}
|
|
k.remember(keys)
|
|
return nil
|
|
}
|
|
|
|
// VerifyJSON checks that a signed object carries a valid signature from
|
|
// serverName, made with a key valid now.
|
|
func (k *KeyRing) VerifyJSON(ctx context.Context, serverName string, msg []byte) error {
|
|
return k.verifyJSONAt(ctx, serverName, k.now().UnixMilli(), msg)
|
|
}
|
|
|
|
// verifyJSONAt is VerifyJSON with the time the key must be valid at.
|
|
//
|
|
// One good signature is enough. A server rotating its key signs with the
|
|
// new one, and an object it signed with both is still its object if we
|
|
// can only fetch one of them.
|
|
func (k *KeyRing) verifyJSONAt(ctx context.Context, serverName string, atTS int64, msg []byte) error {
|
|
sigs, err := signing.ExtractSignatures(msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keyIDs := make([]string, 0, len(sigs[serverName]))
|
|
for keyID := range sigs[serverName] {
|
|
if algorithm, _, ok := signing.SplitKeyID(keyID); ok && algorithm == signing.KeyIDPrefix {
|
|
keyIDs = append(keyIDs, keyID)
|
|
}
|
|
}
|
|
if len(keyIDs) == 0 {
|
|
return fmt.Errorf("%w: from %s", signing.ErrNoSignature, serverName)
|
|
}
|
|
sort.Strings(keyIDs)
|
|
|
|
var lastErr error
|
|
for _, keyID := range keyIDs {
|
|
pub, err := k.PublicKey(ctx, serverName, keyID, atTS)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
if err := signing.VerifyJSON(serverName, keyID, pub, msg); err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
return nil
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
// VerifyEventSignature implements auth.Verifier, and is what every event
|
|
// from another server is checked with.
|
|
func (k *KeyRing) VerifyEventSignature(ctx context.Context, serverName string, e *event.Event) error {
|
|
keyIDs := make([]string, 0, len(e.Signatures()[serverName]))
|
|
for keyID := range e.Signatures()[serverName] {
|
|
if algorithm, _, ok := signing.SplitKeyID(keyID); ok && algorithm == signing.KeyIDPrefix {
|
|
keyIDs = append(keyIDs, keyID)
|
|
}
|
|
}
|
|
if len(keyIDs) == 0 {
|
|
return fmt.Errorf("%w: event %s is not signed by %s", signing.ErrNoSignature, e.EventID(), serverName)
|
|
}
|
|
sort.Strings(keyIDs)
|
|
|
|
var atTS int64
|
|
if e.Rules().EnforceSigningKeyValidity {
|
|
atTS = e.OriginServerTS()
|
|
}
|
|
var lastErr error
|
|
for _, keyID := range keyIDs {
|
|
pub, err := k.PublicKey(ctx, serverName, keyID, atTS)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
if err := e.VerifySignature(serverName, keyID, pub); err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
return nil
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
// NotaryResponse answers a /key/v2/query for this server's own keys. The
|
|
// key ring does not relay other servers' responses — it keeps their keys,
|
|
// not the documents they came in — so a query about anybody else is
|
|
// answered with nothing, which a requester treats as "ask somebody else".
|
|
func (k *KeyRing) NotaryResponse(serverName string) ([]json.RawMessage, error) {
|
|
if serverName != k.serverName {
|
|
return nil, nil
|
|
}
|
|
own, err := k.OwnKeyResponse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The response is already signed by this server; as the notary it is
|
|
// the same signature, and canonical JSON keeps it byte-identical.
|
|
canonical, err := canonicaljson.Encode(own)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []json.RawMessage{canonical}, nil
|
|
}
|