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
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>
650 lines
23 KiB
Go
650 lines
23 KiB
Go
package keyserver_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"slices"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/federation"
|
|
"github.com/dariuszjaniak/warp/server/internal/keyserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
|
|
"github.com/dariuszjaniak/warp/server/internal/storage"
|
|
_ "github.com/dariuszjaniak/warp/server/internal/storage/drivers"
|
|
)
|
|
|
|
const (
|
|
alice = "@alice:warp.example"
|
|
bob = "@bob:warp.example"
|
|
carol = "@carol:remote.example"
|
|
)
|
|
|
|
// --- fakes --------------------------------------------------------------------
|
|
|
|
type fakeRooms struct {
|
|
mu sync.Mutex
|
|
sharing map[string][]string
|
|
}
|
|
|
|
func (f *fakeRooms) UsersSharingRooms(_ context.Context, userID string) ([]string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return slices.Clone(f.sharing[userID]), nil
|
|
}
|
|
|
|
// set replaces who shares a room with userID.
|
|
func (f *fakeRooms) set(userID string, users ...string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.sharing[userID] = users
|
|
}
|
|
|
|
type fakeNotifier struct {
|
|
mu sync.Mutex
|
|
woken []string
|
|
}
|
|
|
|
func (n *fakeNotifier) NotifyUsers(userIDs []string) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
n.woken = append(n.woken, userIDs...)
|
|
}
|
|
|
|
func (n *fakeNotifier) wasWoken(userID string) bool {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
return slices.Contains(n.woken, userID)
|
|
}
|
|
|
|
type fakeDevices struct{ byLocalpart map[string][]storage.Device }
|
|
|
|
func (f fakeDevices) Devices(_ context.Context, localpart string) ([]storage.Device, error) {
|
|
return f.byLocalpart[localpart], nil
|
|
}
|
|
|
|
type sentEDU struct {
|
|
destinations []string
|
|
edu federation.EDU
|
|
}
|
|
|
|
type fakeOutbound struct {
|
|
mu sync.Mutex
|
|
sent []sentEDU
|
|
}
|
|
|
|
func (o *fakeOutbound) Running() bool { return true }
|
|
|
|
func (o *fakeOutbound) QueueEDU(_ context.Context, destinations []string, edu federation.EDU) {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
o.sent = append(o.sent, sentEDU{destinations: destinations, edu: edu})
|
|
}
|
|
|
|
func (o *fakeOutbound) ofType(t string) []sentEDU {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
var out []sentEDU
|
|
for _, s := range o.sent {
|
|
if s.edu.Type == t {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type harness struct {
|
|
keys *keyserver.API
|
|
store storage.KeyStore
|
|
rooms *fakeRooms
|
|
notifier *fakeNotifier
|
|
outbound *fakeOutbound
|
|
}
|
|
|
|
func newHarness(t *testing.T, remote keyserver.Remote) harness {
|
|
t.Helper()
|
|
path := filepath.ToSlash(filepath.Join(t.TempDir(), "warp.db"))
|
|
db, err := storage.Open(t.Context(), storage.Config{
|
|
Driver: storage.DriverSQLite,
|
|
ConnectionString: "file:" + path + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)",
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("storage.Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
if err := db.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
h := harness{store: db.Keys(), notifier: &fakeNotifier{}, outbound: &fakeOutbound{}}
|
|
h.rooms = &fakeRooms{sharing: map[string][]string{
|
|
alice: {alice, bob, carol},
|
|
bob: {alice, bob},
|
|
carol: {alice, carol},
|
|
}}
|
|
h.keys, err = keyserver.New(keyserver.Options{
|
|
ServerName: "warp.example",
|
|
Store: db.Keys(),
|
|
Rooms: h.rooms,
|
|
Notifier: h.notifier,
|
|
Devices: fakeDevices{byLocalpart: map[string][]storage.Device{
|
|
"alice": {{Localpart: "alice", ID: "D1", DisplayName: "Alice's phone"}, {Localpart: "alice", ID: "D2"}},
|
|
"bob": {{Localpart: "bob", ID: "B1"}},
|
|
}},
|
|
Remote: remote,
|
|
Outbound: h.outbound,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("keyserver.New: %v", err)
|
|
}
|
|
t.Cleanup(h.keys.Close)
|
|
return h
|
|
}
|
|
|
|
// --- keys ---------------------------------------------------------------------
|
|
|
|
// newKey makes an ed25519 key named by its own public key, which is how a
|
|
// cross-signing key is named; a device key is renamed to its device ID.
|
|
func newKey(t *testing.T) *signing.KeyPair {
|
|
t.Helper()
|
|
kp, err := signing.Generate("")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kp.ID = kp.PublicBase64()
|
|
return kp
|
|
}
|
|
|
|
func signed(t *testing.T, userID string, kp *signing.KeyPair, v any) json.RawMessage {
|
|
t.Helper()
|
|
raw, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := signing.SignJSON(userID, kp, raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// device is a device's identity key pair and its signed device_keys.
|
|
func device(t *testing.T, userID, deviceID string) (*signing.KeyPair, json.RawMessage) {
|
|
t.Helper()
|
|
kp := newKey(t)
|
|
pub := kp.PublicBase64()
|
|
kp.ID = deviceID
|
|
return kp, signed(t, userID, kp, map[string]any{
|
|
"user_id": userID,
|
|
"device_id": deviceID,
|
|
"algorithms": []string{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
|
|
"keys": map[string]string{
|
|
"ed25519:" + deviceID: pub,
|
|
"curve25519:" + deviceID: "curve-" + deviceID,
|
|
},
|
|
})
|
|
}
|
|
|
|
func crossSigningKey(userID, usage string, kp *signing.KeyPair) map[string]any {
|
|
return map[string]any{
|
|
"user_id": userID,
|
|
"usage": []string{usage},
|
|
"keys": map[string]string{"ed25519:" + kp.PublicBase64(): kp.PublicBase64()},
|
|
}
|
|
}
|
|
|
|
// crossSigning is a user's three cross-signing keys, uploaded.
|
|
type crossSigning struct {
|
|
master, selfSigning, userSigning *signing.KeyPair
|
|
req keyserver.CrossSigningRequest
|
|
}
|
|
|
|
func newCrossSigning(t *testing.T, userID string) crossSigning {
|
|
t.Helper()
|
|
cs := crossSigning{master: newKey(t), selfSigning: newKey(t), userSigning: newKey(t)}
|
|
cs.req = keyserver.CrossSigningRequest{
|
|
MasterKey: signed(t, userID, newKey(t), crossSigningKey(userID, "master", cs.master)),
|
|
SelfSigningKey: signed(t, userID, cs.master, crossSigningKey(userID, "self_signing", cs.selfSigning)),
|
|
UserSigningKey: signed(t, userID, cs.master, crossSigningKey(userID, "user_signing", cs.userSigning)),
|
|
}
|
|
// The master key's own signature above is by a throwaway key, which
|
|
// the server does not check; what it checks is that the master key
|
|
// signed the other two.
|
|
raw, _ := json.Marshal(crossSigningKey(userID, "master", cs.master))
|
|
cs.req.MasterKey = raw
|
|
return cs
|
|
}
|
|
|
|
func decodeObject(t *testing.T, raw json.RawMessage) map[string]any {
|
|
t.Helper()
|
|
var out map[string]any
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
t.Fatalf("decode %s: %v", raw, err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func signaturesBy(t *testing.T, raw json.RawMessage, userID string) map[string]any {
|
|
t.Helper()
|
|
sigs, _ := decodeObject(t, raw)["signatures"].(map[string]any)
|
|
out, _ := sigs[userID].(map[string]any)
|
|
return out
|
|
}
|
|
|
|
// --- tests --------------------------------------------------------------------
|
|
|
|
func TestUploadAndQueryDeviceKeys(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
_, d1 := device(t, alice, "D1")
|
|
|
|
counts, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
|
|
DeviceKeys: d1,
|
|
OneTimeKeys: map[string]json.RawMessage{
|
|
"signed_curve25519:AAA": json.RawMessage(`{"key":"a"}`),
|
|
"signed_curve25519:BBB": json.RawMessage(`{"key":"b"}`),
|
|
},
|
|
FallbackKeys: map[string]json.RawMessage{
|
|
"signed_curve25519:FFF": json.RawMessage(`{"key":"f","fallback":true}`),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UploadKeys: %v", err)
|
|
}
|
|
if counts["signed_curve25519"] != 2 {
|
|
t.Errorf("one_time_key_counts = %v, want 2 signed_curve25519", counts)
|
|
}
|
|
|
|
// Everybody sharing a room hears of it: the local ones are woken, the
|
|
// remote server gets an m.device_list_update with the keys in it.
|
|
if !h.notifier.wasWoken(bob) {
|
|
t.Error("a user sharing a room was not woken")
|
|
}
|
|
updates := h.outbound.ofType(federation.EDUDeviceListUp)
|
|
if len(updates) != 1 || !slices.Equal(updates[0].destinations, []string{"remote.example"}) {
|
|
t.Fatalf("device list EDUs = %+v, want one to remote.example", updates)
|
|
}
|
|
var content federation.DeviceListUpdateContent
|
|
_ = json.Unmarshal(updates[0].edu.Content, &content)
|
|
if content.UserID != alice || content.DeviceID != "D1" || content.Deleted || len(content.Keys) == 0 || content.StreamID == 0 {
|
|
t.Errorf("EDU content = %+v", content)
|
|
}
|
|
|
|
resp, err := h.keys.QueryKeys(ctx, bob, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
|
|
if err != nil {
|
|
t.Fatalf("QueryKeys: %v", err)
|
|
}
|
|
got, ok := resp.DeviceKeys[alice]["D1"]
|
|
if !ok {
|
|
t.Fatalf("query did not return D1: %+v", resp.DeviceKeys)
|
|
}
|
|
unsigned, _ := decodeObject(t, got)["unsigned"].(map[string]any)
|
|
if unsigned["device_display_name"] != "Alice's phone" {
|
|
t.Errorf("unsigned = %v, want the device's display name", unsigned)
|
|
}
|
|
if _, err := signing.SigningBytes(got); err != nil {
|
|
t.Errorf("queried keys are not a signed object: %v", err)
|
|
}
|
|
|
|
// Somebody else's device, or keys naming another device, are refused.
|
|
_, other := device(t, alice, "D2")
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{DeviceKeys: other}); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("keys for another device = %v, want ErrInvalidParam", err)
|
|
}
|
|
_, bobs := device(t, bob, "D1")
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{DeviceKeys: bobs}); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("keys for another user = %v, want ErrInvalidParam", err)
|
|
}
|
|
|
|
// The same one-time key again is fine, encoded differently or not; a
|
|
// different key under its ID is not.
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
|
|
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{ "key" : "a" }`)},
|
|
}); err != nil {
|
|
t.Errorf("re-uploading the same key: %v", err)
|
|
}
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
|
|
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{"key":"z"}`)},
|
|
}); !errors.Is(err, keyserver.ErrKeyConflict) {
|
|
t.Errorf("a conflicting key = %v, want ErrKeyConflict", err)
|
|
}
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
|
|
OneTimeKeys: map[string]json.RawMessage{"no-algorithm": json.RawMessage(`"x"`)},
|
|
}); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("a key ID with no algorithm = %v, want ErrInvalidParam", err)
|
|
}
|
|
}
|
|
|
|
func TestClaimKeys(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
_, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
|
|
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{"key":"a"}`)},
|
|
FallbackKeys: map[string]json.RawMessage{"signed_curve25519:FFF": json.RawMessage(`{"key":"f"}`)},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
claim := func() keyserver.ClaimResponse {
|
|
t.Helper()
|
|
resp, err := h.keys.ClaimKeys(ctx, keyserver.ClaimRequest{OneTimeKeys: map[string]map[string]string{
|
|
alice: {"D1": "signed_curve25519", "D2": "signed_curve25519"},
|
|
carol: {"C1": "signed_curve25519"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("ClaimKeys: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
first := claim()
|
|
if _, ok := first.OneTimeKeys[alice]["D1"]["signed_curve25519:AAA"]; !ok {
|
|
t.Errorf("first claim = %v, want the one-time key", first.OneTimeKeys)
|
|
}
|
|
if _, ok := first.OneTimeKeys[alice]["D2"]; ok {
|
|
t.Error("a device with no keys was given one")
|
|
}
|
|
if _, ok := first.Failures["remote.example"]; !ok {
|
|
t.Errorf("a server that cannot be asked is not a failure: %v", first.Failures)
|
|
}
|
|
|
|
second := claim()
|
|
if _, ok := second.OneTimeKeys[alice]["D1"]["signed_curve25519:FFF"]; !ok {
|
|
t.Errorf("second claim = %v, want the fallback key", second.OneTimeKeys)
|
|
}
|
|
if types, _ := h.keys.UnusedFallbackKeyTypes(ctx, alice, "D1"); len(types) != 0 {
|
|
t.Errorf("a claimed fallback key is still unused: %v", types)
|
|
}
|
|
}
|
|
|
|
// fakeRemote answers for remote.example.
|
|
type fakeRemote struct{}
|
|
|
|
func (fakeRemote) QueryUserKeys(_ context.Context, _ string, req federation.UserKeysQueryRequest) (federation.UserKeysQueryResponse, error) {
|
|
return federation.UserKeysQueryResponse{
|
|
DeviceKeys: map[string]map[string]json.RawMessage{
|
|
dave: {"V1": json.RawMessage(`{"device_id":"V1"}`)},
|
|
"@mallory:other.test": {"M1": json.RawMessage(`{}`)},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (fakeRemote) ClaimUserKeys(context.Context, string, federation.UserKeysClaimRequest) (federation.UserKeysClaimResponse, error) {
|
|
return federation.UserKeysClaimResponse{}, errors.New("unreachable")
|
|
}
|
|
|
|
func (fakeRemote) UserDevices(context.Context, string, string) (federation.UserDevicesResponse, error) {
|
|
return federation.UserDevicesResponse{}, errors.New("unreachable")
|
|
}
|
|
|
|
// dave is a remote user nobody here shares a room with.
|
|
const dave = "@dave:remote.example"
|
|
|
|
func TestQueryAsksOtherServers(t *testing.T) {
|
|
h := newHarness(t, fakeRemote{})
|
|
resp, err := h.keys.QueryKeys(t.Context(), alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{dave: nil}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := resp.DeviceKeys[dave]["V1"]; !ok {
|
|
t.Errorf("remote keys = %v", resp.DeviceKeys)
|
|
}
|
|
if _, ok := resp.DeviceKeys["@mallory:other.test"]; ok {
|
|
t.Error("a server's answer about a user it was not asked about was passed on")
|
|
}
|
|
|
|
claimed, _ := h.keys.ClaimKeys(t.Context(), keyserver.ClaimRequest{OneTimeKeys: map[string]map[string]string{carol: {"C1": "signed_curve25519"}}})
|
|
if failure, ok := claimed.Failures["remote.example"].(map[string]any); !ok || failure["status"] != 503 {
|
|
t.Errorf("failures = %v, want a 503 for remote.example", claimed.Failures)
|
|
}
|
|
}
|
|
|
|
func TestCrossSigning(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
cs := newCrossSigning(t, alice)
|
|
|
|
if needs, err := h.keys.CrossSigningNeedsAuth(ctx, alice, cs.req); err != nil || needs {
|
|
t.Fatalf("a first upload needs auth = %v, %v; want false", needs, err)
|
|
}
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, alice, cs.req); err != nil {
|
|
t.Fatalf("UploadCrossSigningKeys: %v", err)
|
|
}
|
|
if updates := h.outbound.ofType(federation.EDUSigningKeys); len(updates) != 1 {
|
|
t.Errorf("signing key EDUs = %d, want 1", len(updates))
|
|
}
|
|
|
|
// The same keys again need no password; a new master key does.
|
|
if needs, _ := h.keys.CrossSigningNeedsAuth(ctx, alice, cs.req); needs {
|
|
t.Error("re-uploading the same keys needs auth")
|
|
}
|
|
replacement := newCrossSigning(t, alice)
|
|
if needs, _ := h.keys.CrossSigningNeedsAuth(ctx, alice, replacement.req); !needs {
|
|
t.Error("replacing the master key needs no auth")
|
|
}
|
|
|
|
// The user-signing key is only shown to its owner.
|
|
mine, _ := h.keys.QueryKeys(ctx, alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
|
|
theirs, _ := h.keys.QueryKeys(ctx, bob, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
|
|
if _, ok := mine.UserSigningKeys[alice]; !ok {
|
|
t.Error("the owner does not see their user-signing key")
|
|
}
|
|
if _, ok := theirs.UserSigningKeys[alice]; ok {
|
|
t.Error("another user sees the user-signing key")
|
|
}
|
|
if _, ok := theirs.MasterKeys[alice]; !ok {
|
|
t.Error("another user does not see the master key")
|
|
}
|
|
if _, ok := theirs.SelfSigningKeys[alice]; !ok {
|
|
t.Error("another user does not see the self-signing key")
|
|
}
|
|
|
|
// A self-signing key the master key did not sign is refused.
|
|
forged := keyserver.CrossSigningRequest{
|
|
SelfSigningKey: signed(t, alice, newKey(t), crossSigningKey(alice, "self_signing", newKey(t))),
|
|
}
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, alice, forged); !errors.Is(err, keyserver.ErrInvalidSignature) {
|
|
t.Errorf("a self-signing key not signed by the master = %v, want ErrInvalidSignature", err)
|
|
}
|
|
wrongUsage := keyserver.CrossSigningRequest{
|
|
SelfSigningKey: signed(t, alice, cs.master, crossSigningKey(alice, "user_signing", newKey(t))),
|
|
}
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, alice, wrongUsage); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("a key with the wrong usage = %v, want ErrInvalidParam", err)
|
|
}
|
|
|
|
// A new master key on its own drops the keys the old one signed.
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, alice, keyserver.CrossSigningRequest{MasterKey: replacement.req.MasterKey}); err != nil {
|
|
t.Fatalf("replacing the master key: %v", err)
|
|
}
|
|
after, _ := h.keys.QueryKeys(ctx, alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
|
|
if _, ok := after.SelfSigningKeys[alice]; ok {
|
|
t.Error("the old self-signing key survived a new master key")
|
|
}
|
|
|
|
// And nothing but a master key needs one first.
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, bob, keyserver.CrossSigningRequest{
|
|
SelfSigningKey: signed(t, bob, newKey(t), crossSigningKey(bob, "self_signing", newKey(t))),
|
|
}); !errors.Is(err, keyserver.ErrMissingParam) {
|
|
t.Errorf("a self-signing key with no master = %v, want ErrMissingParam", err)
|
|
}
|
|
}
|
|
|
|
func TestSignatureUploads(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
|
|
deviceKey, d1 := device(t, alice, "D1")
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{DeviceKeys: d1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cs := newCrossSigning(t, alice)
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, alice, cs.req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bobs := newCrossSigning(t, bob)
|
|
if err := h.keys.UploadCrossSigningKeys(ctx, bob, bobs.req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var stripped map[string]any
|
|
_ = json.Unmarshal(d1, &stripped)
|
|
delete(stripped, "signatures")
|
|
|
|
masterPub := cs.master.PublicBase64()
|
|
failures, err := h.keys.UploadSignatures(ctx, alice, map[string]map[string]json.RawMessage{
|
|
alice: {
|
|
// The device, by the self-signing key.
|
|
"D1": signed(t, alice, cs.selfSigning, stripped),
|
|
// The master key, by the device.
|
|
masterPub: signed(t, alice, deviceKey, crossSigningKey(alice, "master", cs.master)),
|
|
},
|
|
// Bob's master key, by Alice's user-signing key.
|
|
bob: {bobs.master.PublicBase64(): signed(t, alice, cs.userSigning, crossSigningKey(bob, "master", bobs.master))},
|
|
})
|
|
if err != nil || len(failures) != 0 {
|
|
t.Fatalf("UploadSignatures = %v, %v", failures, err)
|
|
}
|
|
|
|
resp, _ := h.keys.QueryKeys(ctx, alice, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil, bob: nil}})
|
|
if _, ok := signaturesBy(t, resp.DeviceKeys[alice]["D1"], alice)["ed25519:"+cs.selfSigning.PublicBase64()]; !ok {
|
|
t.Error("the device is not shown signed by the self-signing key")
|
|
}
|
|
if _, ok := signaturesBy(t, resp.MasterKeys[alice], alice)["ed25519:D1"]; !ok {
|
|
t.Error("the master key is not shown signed by the device")
|
|
}
|
|
if _, ok := signaturesBy(t, resp.MasterKeys[bob], alice)["ed25519:"+cs.userSigning.PublicBase64()]; !ok {
|
|
t.Error("Alice does not see her signature of Bob's master key")
|
|
}
|
|
|
|
// Who Alice has verified is between her and her devices.
|
|
asCarol, _ := h.keys.QueryKeys(ctx, carol, keyserver.QueryRequest{DeviceKeys: map[string][]string{bob: nil}})
|
|
if sigs := signaturesBy(t, asCarol.MasterKeys[bob], alice); len(sigs) != 0 {
|
|
t.Errorf("another user sees Alice's signature of Bob's key: %v", sigs)
|
|
}
|
|
|
|
// A signature that does not verify against the stored key is refused,
|
|
// and so is one of a key that does not exist.
|
|
stripped["algorithms"] = []string{"tampered"}
|
|
failures, err = h.keys.UploadSignatures(ctx, alice, map[string]map[string]json.RawMessage{
|
|
alice: {
|
|
"D1": signed(t, alice, cs.selfSigning, stripped),
|
|
"NOPE": signed(t, alice, cs.selfSigning, stripped),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failures[alice]["D1"].ErrCode != "M_INVALID_SIGNATURE" {
|
|
t.Errorf("a signature over a different object = %+v", failures[alice]["D1"])
|
|
}
|
|
if failures[alice]["NOPE"].ErrCode != "M_NOT_FOUND" {
|
|
t.Errorf("a signature of an unknown device = %+v", failures[alice]["NOPE"])
|
|
}
|
|
}
|
|
|
|
func TestDeletedDevicesLoseTheirKeys(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
_, d1 := device(t, alice, "D1")
|
|
if _, err := h.keys.UploadKeys(ctx, alice, "D1", keyserver.UploadRequest{
|
|
DeviceKeys: d1,
|
|
OneTimeKeys: map[string]json.RawMessage{"signed_curve25519:AAA": json.RawMessage(`{}`)},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
h.keys.DevicesDeleted(ctx, "alice", []string{"D1"})
|
|
|
|
resp, _ := h.keys.QueryKeys(ctx, bob, keyserver.QueryRequest{DeviceKeys: map[string][]string{alice: nil}})
|
|
if len(resp.DeviceKeys[alice]) != 0 {
|
|
t.Errorf("keys of a deleted device are still served: %v", resp.DeviceKeys[alice])
|
|
}
|
|
if counts, _ := h.keys.OneTimeKeyCounts(ctx, alice, "D1"); counts["signed_curve25519"] != 0 {
|
|
t.Errorf("one-time keys of a deleted device survived: %v", counts)
|
|
}
|
|
updates := h.outbound.ofType(federation.EDUDeviceListUp)
|
|
var last federation.DeviceListUpdateContent
|
|
_ = json.Unmarshal(updates[len(updates)-1].edu.Content, &last)
|
|
if !last.Deleted || last.DeviceID != "D1" || len(last.PrevID) != 1 {
|
|
t.Errorf("the last device list EDU = %+v, want D1 deleted after its upload", last)
|
|
}
|
|
}
|
|
|
|
func TestRemoteKeyChangesOnlyConcernSharedUsers(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
|
|
stranger := federation.DeviceListUpdateContent{UserID: dave, DeviceID: "V1", StreamID: 1}
|
|
if err := h.keys.RemoteDeviceListUpdate(ctx, stranger); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pos, _ := h.store.DeviceListPosition(ctx, dave); pos != 0 {
|
|
t.Error("an update about a user nobody here shares a room with was recorded")
|
|
}
|
|
if _, ok, _ := h.store.RemoteKeys(ctx, dave); ok {
|
|
t.Error("an update about a stranger left something in the cache")
|
|
}
|
|
|
|
if err := h.keys.RemoteDeviceListUpdate(ctx, federation.DeviceListUpdateContent{UserID: carol, DeviceID: "C1", StreamID: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pos, _ := h.store.DeviceListPosition(ctx, carol); pos == 0 {
|
|
t.Error("an update about a user sharing a room was not recorded")
|
|
}
|
|
if !h.notifier.wasWoken(alice) {
|
|
t.Error("the local user sharing a room was not woken")
|
|
}
|
|
if len(h.outbound.sent) != 0 {
|
|
t.Errorf("a remote update was echoed over federation: %+v", h.outbound.sent)
|
|
}
|
|
}
|
|
|
|
func TestKeyBackupVersions(t *testing.T) {
|
|
ctx := t.Context()
|
|
h := newHarness(t, nil)
|
|
|
|
v1, err := h.keys.CreateBackup(ctx, alice, "m.megolm_backup.v1.curve25519-aes-sha2", json.RawMessage(`{"public_key":"a"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.keys.CreateBackup(ctx, alice, "", json.RawMessage(`{}`)); !errors.Is(err, keyserver.ErrMissingParam) {
|
|
t.Errorf("a backup with no algorithm = %v", err)
|
|
}
|
|
if _, err := h.keys.CreateBackup(ctx, alice, "alg", json.RawMessage(`[1]`)); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("auth_data that is not an object = %v", err)
|
|
}
|
|
|
|
rooms := map[string]keyserver.RoomKeyBackup{"!r:x": {Sessions: map[string]keyserver.KeyBackupData{
|
|
"s1": {FirstMessageIndex: 1, SessionData: json.RawMessage(`{"ciphertext":"c"}`)},
|
|
}}}
|
|
result, err := h.keys.PutBackupKeys(ctx, alice, v1, rooms)
|
|
if err != nil || result.Count != 1 {
|
|
t.Fatalf("PutBackupKeys = %+v, %v", result, err)
|
|
}
|
|
|
|
v2, _ := h.keys.CreateBackup(ctx, alice, "m.megolm_backup.v1.curve25519-aes-sha2", json.RawMessage(`{"public_key":"b"}`))
|
|
var wrong *keyserver.WrongBackupVersionError
|
|
if _, err := h.keys.PutBackupKeys(ctx, alice, v1, rooms); !errors.As(err, &wrong) || wrong.Current != v2 {
|
|
t.Errorf("writing to an old version = %v, want WrongBackupVersionError{%s}", err, v2)
|
|
}
|
|
if _, err := h.keys.PutBackupKeys(ctx, alice, "99", rooms); !errors.Is(err, keyserver.ErrNoSuchBackup) {
|
|
t.Errorf("writing to a version that does not exist = %v", err)
|
|
}
|
|
|
|
if err := h.keys.UpdateBackup(ctx, alice, v2, "another.algorithm", "", json.RawMessage(`{}`)); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("changing a version's algorithm = %v", err)
|
|
}
|
|
if err := h.keys.UpdateBackup(ctx, alice, v2, "m.megolm_backup.v1.curve25519-aes-sha2", v1, json.RawMessage(`{}`)); !errors.Is(err, keyserver.ErrInvalidParam) {
|
|
t.Errorf("a body version that is not the path's = %v", err)
|
|
}
|
|
|
|
old, err := h.keys.BackupKeys(ctx, alice, v1, "", "")
|
|
if err != nil || len(old["!r:x"].Sessions) != 1 {
|
|
t.Errorf("an old version's keys = %v, %v; still readable", old, err)
|
|
}
|
|
}
|