Implements PLAN.md Phase 7: the server half of end-to-end encryption.
Devices upload identity, one-time and fallback keys; other devices query
and claim them, across servers too, and every one-time key is handed out
exactly once. Cross-signing, signature uploads, device list tracking and
server-side key backup complete the set.
internal/keyserver is the component. Keys are stored in canonical JSON
so that re-uploading the same key is a byte comparison, and signatures
uploaded later are kept apart from the key they sign and merged in on
read, so a key's own JSON stays what its owner uploaded. Device keys must
be for the caller's own device; cross-signing keys must be the
uploader's, carry the right usage and, for self- and user-signing keys,
be signed by the master key. Signatures are verified against the key as
stored. Replacing a master key asks for the password; a first upload or
an upload of the keys already stored does not, as the spec allows. A
signature of another user's master key is shown only to the user who
made it and never federated.
Every write that changes what /keys/query answers moves the device list
stream in the same transaction, one position per device, and wakes the
syncs of everyone sharing a room. The stream is the sixth field of a sync
token; older tokens still parse. The syncapi works out device_lists
changed and left from the stream and from membership moves in the
window, skipping invites and knocks, and reports
device_one_time_keys_count and device_unused_fallback_key_types, the
latter as [] when empty. GET /keys/changes uses the same logic.
Over federation: /user/keys/query, /user/keys/claim and
/user/devices/{userId}, and m.device_list_update and m.signing_key_update
both ways. Remote keys are not cached; an update about a remote user
sharing a room moves them in the stream so local clients re-query.
Device display names are not sent to other servers. With federation off,
remote users are reported in failures at once.
Deleting a device deletes its keys, through a new userapi
DeviceObserver. Key backup keeps the better copy of each session by the
spec's rule, never reuses a deleted version's number, and answers writes
to a stale version with M_WRONG_ROOM_KEYS_VERSION.
Storage is migration 0008 for sqlite and postgres, with a PocketBase
stub. The Complement target sets gain the key, device list, fallback,
cross-signing and backup tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
5.0 KiB
Bash
Executable File
97 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run Complement against a freshly built Warp image.
|
|
#
|
|
# server/test/complement/run.sh # the C-S test set, federation off
|
|
# COMPLEMENT_PROFILE=federation server/test/complement/run.sh # the federation test set
|
|
# server/test/complement/run.sh TestLogin # one test
|
|
# COMPLEMENT_TESTS='Test.*' server/test/complement/run.sh
|
|
#
|
|
# Complement is a separate Go module, so it has to be checked out and run on
|
|
# its own. This script keeps that checkout in a cache directory rather than
|
|
# vendoring it: it is a test harness we track, not a dependency we pin.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
|
|
|
: "${COMPLEMENT_REPO:=https://github.com/matrix-org/complement.git}"
|
|
: "${COMPLEMENT_REF:=main}"
|
|
: "${COMPLEMENT_DIR:=${REPO_ROOT}/.complement}"
|
|
: "${COMPLEMENT_IMAGE:=complement-warp}"
|
|
: "${COMPLEMENT_PROFILE:=csapi}"
|
|
|
|
# The target sets for the phases that are done, from PLAN.md section 7. Add
|
|
# to them as phases land rather than switching to the whole suite: a pass
|
|
# count that goes down because a later phase is missing is noise.
|
|
#
|
|
# csapi: the Client-Server tests, with federation off. Phase 4 added the
|
|
# stream tests: /sync in all its forms, /messages, /context, filters,
|
|
# typing, receipts and to-device messaging. What is still out is anything
|
|
# needing a later phase — the notification counts of the push rules
|
|
# (Phase 8). Phase 6 added the media tests: uploads, async uploads,
|
|
# downloads with their file names and thumbnails. The remote-media tests
|
|
# are in the federation set, which TestRemote.* already covers. Phase 7
|
|
# added the keyserver's: key uploads, claims, changes, device lists,
|
|
# fallback keys, cross-signing and key backup.
|
|
CSAPI_TESTS='TestUploadKey.*|TestKeyClaimOrdering|TestKeyChanges.*|TestDeviceListUpdates.*|TestE2EKeyBackup.*|TestFallbackKeys.*|TestCrossSigning.*|TestMedia.*|TestContent.*|TestLocalPngThumbnail|TestAsyncUpload.*|TestUploadLimit|TestRegistration|TestLogin|TestDeviceManagement|TestRoomCreate|TestRoomState|TestRoomAlias|TestRoomForget|TestMembership.*|TestRedaction.*|TestJoinRoom.*|TestLeaveRoom.*|TestKickRoom.*|TestBanRoom.*|TestPublicRoom.*|TestSync.*|TestMessages.*|TestContext.*|TestFilter.*|TestTyping.*|TestReceipt.*|TestToDevice.*|TestRoomCreationReportsEventsToMyself|TestLazyLoading.*|TestWriteMDirectAccountData'
|
|
|
|
# federation: Phase 5. Joins, leaves and invites across servers, keys,
|
|
# transactions, missing events, server ACLs and the queries. Partial-state
|
|
# joins (MSC3706, TestPartialStateJoin*) are not implemented and are left
|
|
# out; so is anything needing an application service (Phase 9). Phase 7
|
|
# added device lists across servers.
|
|
FEDERATION_TESTS='TestDeviceListUpdates.*|TestDeviceListsUpdateOverFederation.*|TestFederation.*|TestJoinViaRoomIDAndServerName|TestJoinFederatedRoom.*|TestBannedUserCannotSendJoin|TestCannotSendNon.*Via.*|TestCannotSendKnockViaSend.*|TestOutboundFederation.*|TestInboundFederation.*|TestInboundCanReturnMissingEvents|TestRemoteAliasRequestsUnderstandUnicode|TestNetworkPartitionOrdering|TestACLs|TestGetMissingEventsGapFilling|TestEventAuth|TestUnbanViaInvite|TestJoinRoomWithFederation.*|TestRemote.*'
|
|
|
|
case "$COMPLEMENT_PROFILE" in
|
|
csapi)
|
|
: "${COMPLEMENT_TESTS:=$CSAPI_TESTS}"
|
|
PACKAGES=(./tests/csapi/...)
|
|
FEDERATION_ENABLED=false
|
|
;;
|
|
federation)
|
|
: "${COMPLEMENT_TESTS:=$FEDERATION_TESTS}"
|
|
PACKAGES=(./tests/... )
|
|
FEDERATION_ENABLED=true
|
|
;;
|
|
*)
|
|
echo "complement: unknown COMPLEMENT_PROFILE '$COMPLEMENT_PROFILE' (want csapi or federation)" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
if [[ $# -gt 0 ]]; then
|
|
COMPLEMENT_TESTS="$(IFS='|'; echo "$*")"
|
|
fi
|
|
|
|
docker="${DOCKER:-docker}"
|
|
if ! command -v "$docker" >/dev/null 2>&1; then
|
|
echo "complement: $docker is not installed; Complement needs a container runtime" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "complement: building $COMPLEMENT_IMAGE"
|
|
"$docker" build -t "$COMPLEMENT_IMAGE" -f "${REPO_ROOT}/server/test/complement/Dockerfile" "$REPO_ROOT"
|
|
|
|
if [[ ! -d "$COMPLEMENT_DIR" ]]; then
|
|
echo "complement: cloning $COMPLEMENT_REPO into $COMPLEMENT_DIR"
|
|
git clone --depth 1 --branch "$COMPLEMENT_REF" "$COMPLEMENT_REPO" "$COMPLEMENT_DIR"
|
|
else
|
|
git -C "$COMPLEMENT_DIR" fetch --depth 1 origin "$COMPLEMENT_REF"
|
|
git -C "$COMPLEMENT_DIR" checkout -q FETCH_HEAD
|
|
fi
|
|
|
|
echo "complement: running '${COMPLEMENT_TESTS}' (profile ${COMPLEMENT_PROFILE}, federation ${FEDERATION_ENABLED})"
|
|
cd "$COMPLEMENT_DIR"
|
|
|
|
# COMPLEMENT_SPAWN_HS_TIMEOUT_SECS is generous because the first container
|
|
# of a run has to apply the migrations on a cold SQLite file.
|
|
#
|
|
# COMPLEMENT_SHARE_ENV_PREFIX hands every host variable starting with
|
|
# PASS_ to the homeserver containers, prefix removed: it is how the profile
|
|
# reaches the entrypoint's federation switch.
|
|
COMPLEMENT_BASE_IMAGE="$COMPLEMENT_IMAGE" \
|
|
COMPLEMENT_SPAWN_HS_TIMEOUT_SECS="${COMPLEMENT_SPAWN_HS_TIMEOUT_SECS:-60}" \
|
|
COMPLEMENT_DEBUG="${COMPLEMENT_DEBUG:-0}" \
|
|
COMPLEMENT_SHARE_ENV_PREFIX=PASS_ \
|
|
PASS_WARP_FEDERATION_ENABLED="$FEDERATION_ENABLED" \
|
|
go test -count=1 -v -timeout 30m -run "^(${COMPLEMENT_TESTS})$" "${PACKAGES[@]}"
|