Files
Warp/server/internal/keyserver/query.go
T
tajniak81andClaude Opus 5.5 74f7bc8805
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
keyserver: cache remote users' device and cross-signing keys
POST /keys/query for a remote user who shares a room with a local user is
now answered from a cache, fetched whole with GET /user/devices/{userId}
on a miss. Incoming m.device_list_update EDUs are applied when their
prev_id matches the cached stream_id; anything else marks the copy stale
and it is fetched again in the background, and a stale copy is never
served. m.signing_key_update replaces the cached cross-signing keys. A
user's copy is dropped once no local user shares a room with them, by a
keyserver listener on the roomserver's emit stage.

Migration 0009 adds keyserver_remote_users, keyserver_remote_device_keys
and keyserver_remote_cross_signing_keys for sqlite and postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:36:08 +02:00

516 lines
15 KiB
Go

package keyserver
import (
"context"
"encoding/json"
"errors"
"slices"
"sync"
"time"
"github.com/dariuszjaniak/warp/server/internal/federation"
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// Reading keys: POST /keys/query and /keys/claim, and their federation
// counterparts.
// QueryRequest is the body of POST /keys/query.
type QueryRequest struct {
// DeviceKeys is user ID to the device IDs wanted; an empty list asks
// for every device.
DeviceKeys map[string][]string
// Timeout is how long to wait for other servers.
Timeout time.Duration
}
// QueryResponse is the answer to POST /keys/query.
type QueryResponse struct {
DeviceKeys map[string]map[string]json.RawMessage `json:"device_keys"`
Failures map[string]any `json:"failures"`
MasterKeys map[string]json.RawMessage `json:"master_keys"`
SelfSigningKeys map[string]json.RawMessage `json:"self_signing_keys"`
UserSigningKeys map[string]json.RawMessage `json:"user_signing_keys"`
}
// ClaimRequest is the body of POST /keys/claim.
type ClaimRequest struct {
// OneTimeKeys is user ID, then device ID, then the algorithm wanted.
OneTimeKeys map[string]map[string]string
Timeout time.Duration
}
// ClaimResponse is the answer to POST /keys/claim.
type ClaimResponse struct {
OneTimeKeys map[string]map[string]map[string]json.RawMessage `json:"one_time_keys"`
Failures map[string]any `json:"failures"`
}
// remoteFailure is what a server that could not be asked is reported as,
// in the shape other homeservers use.
func remoteFailure(err error) map[string]any {
status := 503
var httpErr *federation.HTTPError
if errors.As(err, &httpErr) {
status = httpErr.Status
}
return map[string]any{"status": status, "message": err.Error()}
}
// --- reading keys ---------------------------------------------------------------
// publishedDeviceKeys reads the keys of a user's devices with every stored
// signature merged in. For a local user only devices that still exist are
// returned, and names maps them to their display names when the caller
// wants those.
func (k *API) publishedDeviceKeys(ctx context.Context, userID string, deviceIDs []string) (map[string]json.RawMessage, error) {
return k.deviceKeys(ctx, userID, deviceIDs, false)
}
func (k *API) deviceKeys(ctx context.Context, userID string, deviceIDs []string, withNames bool) (map[string]json.RawMessage, error) {
stored, err := k.store.DeviceKeys(ctx, userID, deviceIDs)
if err != nil {
return nil, err
}
out := map[string]json.RawMessage{}
if len(stored) == 0 {
return out, nil
}
names := map[string]string{}
if localpart, ok := k.localpartOf(userID); ok {
devices, err := k.devices.Devices(ctx, localpart)
if err != nil {
return nil, err
}
for _, d := range devices {
names[d.ID] = d.DisplayName
}
}
sigs, err := k.store.Signatures(ctx, userID)
if err != nil {
return nil, err
}
for _, d := range stored {
name, exists := names[d.DeviceID]
if k.isLocal(userID) && !exists {
continue
}
var mine []storage.KeySignature
for _, sig := range sigs {
if sig.TargetKeyID == d.DeviceID {
mine = append(mine, sig)
}
}
if !withNames {
name = ""
}
merged, err := withSignatures(d.JSON, mine, name)
if err != nil {
return nil, err
}
out[d.DeviceID] = merged
}
return out, nil
}
// crossSigningKeys reads a user's cross-signing keys as requester is
// allowed to see them.
//
// The master key carries the signatures of the user's own devices, which
// everybody may see, and the signature of requester's user-signing key if
// they made one: who a user has verified is between them and their own
// devices. The user-signing key is only ever shown to its owner.
func (k *API) crossSigningKeys(ctx context.Context, userID, requester string) (master, selfSigning, userSigning json.RawMessage, err error) {
keys, err := k.store.CrossSigningKeys(ctx, userID)
if err != nil || len(keys) == 0 {
return nil, nil, nil, err
}
sigs, err := k.store.Signatures(ctx, userID)
if err != nil {
return nil, nil, nil, err
}
read := func(purpose string) (json.RawMessage, error) {
raw, ok := keys[purpose]
if !ok {
return nil, nil
}
var key crossSigningKeyObject
if err := json.Unmarshal(raw, &key); err != nil {
return nil, err
}
pub, _ := key.ed25519Key()
var visible []storage.KeySignature
for _, sig := range sigs {
if sig.TargetKeyID != pub {
continue
}
if sig.SignerUserID == userID || (requester != "" && sig.SignerUserID == requester) {
visible = append(visible, sig)
}
}
return withSignatures(raw, visible, "")
}
if master, err = read(storage.CrossSigningMaster); err != nil {
return nil, nil, nil, err
}
if selfSigning, err = read(storage.CrossSigningSelfSigning); err != nil {
return nil, nil, nil, err
}
if requester == userID {
if userSigning, err = read(storage.CrossSigningUserSigning); err != nil {
return nil, nil, nil, err
}
}
return master, selfSigning, userSigning, nil
}
// --- /keys/query ----------------------------------------------------------------
// QueryKeys answers POST /keys/query for requester: local users from the
// store, remote ones from the cache or by asking their servers, all at
// once.
func (k *API) QueryKeys(ctx context.Context, requester string, req QueryRequest) (QueryResponse, error) {
resp := QueryResponse{
DeviceKeys: map[string]map[string]json.RawMessage{},
Failures: map[string]any{},
MasterKeys: map[string]json.RawMessage{},
SelfSigningKeys: map[string]json.RawMessage{},
UserSigningKeys: map[string]json.RawMessage{},
}
remote := map[string]map[string][]string{}
for userID, deviceIDs := range req.DeviceKeys {
server, err := id.ServerNameOf(userID)
if err != nil {
continue
}
if server != k.serverName {
if remote[server] == nil {
remote[server] = map[string][]string{}
}
remote[server][userID] = deviceIDs
continue
}
devices, err := k.deviceKeys(ctx, userID, deviceIDs, true)
if err != nil {
return QueryResponse{}, err
}
resp.DeviceKeys[userID] = devices
master, selfSigning, userSigning, err := k.crossSigningKeys(ctx, userID, requester)
if err != nil {
return QueryResponse{}, err
}
setIfPresent(resp.MasterKeys, userID, master)
setIfPresent(resp.SelfSigningKeys, userID, selfSigning)
setIfPresent(resp.UserSigningKeys, userID, userSigning)
}
k.queryRemote(ctx, requester, remote, remoteTimeout(req.Timeout), &resp)
return resp, nil
}
// queryRemote answers for other servers' users: from the cache for those
// who share a room with somebody here, by asking their server for the
// rest. A copy that is missing or stale is fetched whole and cached, so
// the next query for the same user does not leave this server.
//
// The servers are asked in parallel. One that does not answer in time is a
// failure, not an error: the keys of everybody else are still worth
// returning.
func (k *API) queryRemote(ctx context.Context, requester string, byServer map[string]map[string][]string, timeout time.Duration, resp *QueryResponse) {
if len(byServer) == 0 {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var (
mu sync.Mutex
wg sync.WaitGroup
)
fail := func(server string, err error) {
mu.Lock()
defer mu.Unlock()
resp.Failures[server] = remoteFailure(err)
}
for server, users := range byServer {
passOn := map[string][]string{}
var fetch []string
for _, userID := range sortedKeys(users) {
if !k.cacheable(ctx, userID) {
// A user nobody here shares a room with any more may still
// have a copy from when somebody did.
k.forget(ctx, userID)
passOn[userID] = users[userID]
continue
}
if keys, ok := k.freshCopy(ctx, userID); ok {
mu.Lock()
k.serveRemote(ctx, requester, keys, users[userID], resp)
mu.Unlock()
continue
}
fetch = append(fetch, userID)
}
if len(passOn) == 0 && len(fetch) == 0 {
continue
}
if k.remote == nil {
fail(server, errors.New("federation is not available on this server"))
continue
}
for _, userID := range fetch {
wg.Go(func() {
keys, err := k.fetchRemoteKeys(ctx, server, userID)
if err != nil {
fail(server, err)
return
}
mu.Lock()
defer mu.Unlock()
k.serveRemote(ctx, requester, keys, users[userID], resp)
})
}
if len(passOn) == 0 {
continue
}
wg.Go(func() {
answer, err := k.remote.QueryUserKeys(ctx, server, federation.UserKeysQueryRequest{DeviceKeys: passOn})
if err != nil {
fail(server, err)
return
}
mu.Lock()
defer mu.Unlock()
for userID, devices := range answer.DeviceKeys {
// A server answers for its own users and nobody else's.
if _, asked := passOn[userID]; asked && devices != nil {
resp.DeviceKeys[userID] = devices
}
}
for userID, master := range answer.MasterKeys {
if _, asked := passOn[userID]; asked {
resp.MasterKeys[userID] = k.withRequesterSignature(ctx, userID, requester, master)
}
}
for userID, selfSigning := range answer.SelfSigningKeys {
if _, asked := passOn[userID]; asked {
resp.SelfSigningKeys[userID] = selfSigning
}
}
})
}
wg.Wait()
}
// withRequesterSignature adds requester's own signature of a remote user's
// master key, which is stored here and nowhere else.
func (k *API) withRequesterSignature(ctx context.Context, userID, requester string, master json.RawMessage) json.RawMessage {
if requester == "" {
return master
}
sigs, err := k.store.Signatures(ctx, userID)
if err != nil || len(sigs) == 0 {
return master
}
var key crossSigningKeyObject
if json.Unmarshal(master, &key) != nil {
return master
}
pub, _ := key.ed25519Key()
var mine []storage.KeySignature
for _, sig := range sigs {
if sig.TargetKeyID == pub && sig.SignerUserID == requester {
mine = append(mine, sig)
}
}
merged, err := withSignatures(master, mine, "")
if err != nil {
return master
}
return merged
}
func setIfPresent(m map[string]json.RawMessage, key string, value json.RawMessage) {
if len(value) > 0 {
m[key] = value
}
}
// QueryLocalKeys answers another server's POST /user/keys/query: the keys
// of this server's users, without device display names, which Warp keeps
// to itself.
func (k *API) QueryLocalKeys(ctx context.Context, req federation.UserKeysQueryRequest) (federation.UserKeysQueryResponse, error) {
resp := federation.UserKeysQueryResponse{
DeviceKeys: map[string]map[string]json.RawMessage{},
MasterKeys: map[string]json.RawMessage{},
SelfSigningKeys: map[string]json.RawMessage{},
}
for userID, deviceIDs := range req.DeviceKeys {
if !k.isLocal(userID) {
continue
}
devices, err := k.publishedDeviceKeys(ctx, userID, deviceIDs)
if err != nil {
return federation.UserKeysQueryResponse{}, err
}
resp.DeviceKeys[userID] = devices
master, selfSigning, _, err := k.crossSigningKeys(ctx, userID, "")
if err != nil {
return federation.UserKeysQueryResponse{}, err
}
setIfPresent(resp.MasterKeys, userID, master)
setIfPresent(resp.SelfSigningKeys, userID, selfSigning)
}
return resp, nil
}
// UserDevices answers another server's GET /user/devices/{userId}: a local
// user's whole device list, and its position in the device list stream,
// which is the stream ID the server's later updates count from.
func (k *API) UserDevices(ctx context.Context, userID string) (federation.UserDevicesResponse, error) {
resp := federation.UserDevicesResponse{UserID: userID, Devices: []federation.UserDevice{}}
if !k.isLocal(userID) {
return resp, invalidParamf("%s is not a user of this server", userID)
}
pos, err := k.store.DeviceListPosition(ctx, userID)
if err != nil {
return resp, err
}
resp.StreamID = pos
devices, err := k.publishedDeviceKeys(ctx, userID, nil)
if err != nil {
return resp, err
}
for _, deviceID := range sortedKeys(devices) {
resp.Devices = append(resp.Devices, federation.UserDevice{DeviceID: deviceID, Keys: devices[deviceID]})
}
resp.MasterKey, resp.SelfSigningKey, _, err = k.crossSigningKeys(ctx, userID, "")
return resp, err
}
// --- /keys/claim ----------------------------------------------------------------
// ClaimKeys answers POST /keys/claim.
func (k *API) ClaimKeys(ctx context.Context, req ClaimRequest) (ClaimResponse, error) {
resp := ClaimResponse{
OneTimeKeys: map[string]map[string]map[string]json.RawMessage{},
Failures: map[string]any{},
}
local := map[string]map[string]string{}
remote := map[string]map[string]map[string]string{}
for userID, devices := range req.OneTimeKeys {
server, err := id.ServerNameOf(userID)
switch {
case err != nil:
continue
case server == k.serverName:
local[userID] = devices
default:
if remote[server] == nil {
remote[server] = map[string]map[string]string{}
}
remote[server][userID] = devices
}
}
claimed, err := k.ClaimLocalKeys(ctx, local)
if err != nil {
return ClaimResponse{}, err
}
for userID, devices := range claimed {
resp.OneTimeKeys[userID] = devices
}
k.claimRemote(ctx, remote, remoteTimeout(req.Timeout), &resp)
return resp, nil
}
// ClaimLocalKeys claims keys of this server's users' devices, for a local
// client or another server. A device with no key of the algorithm asked
// for is left out, which is how the spec says "none left".
func (k *API) ClaimLocalKeys(ctx context.Context, want map[string]map[string]string) (map[string]map[string]map[string]json.RawMessage, error) {
out := map[string]map[string]map[string]json.RawMessage{}
for userID, devices := range want {
if !k.isLocal(userID) {
continue
}
for deviceID, algorithm := range devices {
key, ok, err := k.store.ClaimKey(ctx, userID, deviceID, algorithm)
if err != nil {
return nil, err
}
if !ok {
continue
}
if out[userID] == nil {
out[userID] = map[string]map[string]json.RawMessage{}
}
out[userID][deviceID] = map[string]json.RawMessage{key.KeyID: key.JSON}
}
}
return out, nil
}
// claimRemote claims keys from other servers in parallel.
func (k *API) claimRemote(ctx context.Context, byServer map[string]map[string]map[string]string, timeout time.Duration, resp *ClaimResponse) {
if len(byServer) == 0 {
return
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var (
mu sync.Mutex
wg sync.WaitGroup
)
for server, users := range byServer {
if k.remote == nil {
resp.Failures[server] = remoteFailure(errors.New("federation is not available on this server"))
continue
}
wg.Go(func() {
answer, err := k.remote.ClaimUserKeys(ctx, server, federation.UserKeysClaimRequest{OneTimeKeys: users})
mu.Lock()
defer mu.Unlock()
if err != nil {
resp.Failures[server] = remoteFailure(err)
return
}
for userID, devices := range answer.OneTimeKeys {
if _, asked := users[userID]; asked {
resp.OneTimeKeys[userID] = devices
}
}
})
}
wg.Wait()
}
// sortedKeys is a map's keys in order.
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for key := range m {
out = append(out, key)
}
slices.Sort(out)
return out
}