The SDK reports a device verified after discarding both results that could say otherwise #130
Labels
No labels
blocked
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
waiting-on-julian
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/matrix-mcp#130
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A bot logs that it verified its own device, the person reading its messages sees "Encrypted by a device not verified by its owner", and nothing anywhere disagrees.
The measurement
Synapse's signature table, with a control strong enough to trust the negatives: 164 signature rows across all users, and one human account's own 11 devices splitting 8 signed to 3 not, so the query does report both outcomes.
@mantis_ai_botis the one whose logs say it succeeded. At 16:25:12 on the current pod the SDK logged, in order: cross-signing keys fetched with master, self-signing and user-signing all true; "Successfully imported the self-signing key, attempting to sign our own device"; then "Successfully signed our own device, the device is now verified". It is unsigned in the table.Why the log and the table do not conflict
Verified in the vendored sources rather than inferred.
/keys/signatures/uploadreturns afailuresmap and the response is thrown away.matrix-sdk-0.17.0/src/encryption/identities/devices.rs:291:The discarded type is
ruma-client-api-0.23.1/src/keys/upload_signatures.rs:46-49, whose only field ispub failures: BTreeMap<OwnedUserId, BTreeMap<String, Failure>>— a per-key rejection map returned inside a 200. Greppingmatrix-sdk-crypto-0.17.0forfailuresfinds it in ten files and in no signature-upload path, and the ten hits are the control that the string is present and greppable.The follow-up query that exists to check the result also discards it.
matrix-sdk-0.17.0/src/encryption/secret_storage/secret_store.rs:443-452:The comment says the query exists to ensure the signatures are attached. Nothing reads whether they are. So the log line means a 2xx came back and a second request was sent, and every value that could contradict it is dropped before it reaches a branch.
What to build
A guard of our own, because the library will not give us one. After
recover, re-query and assert that the device carries a self-signature before reporting anything. That is the half that closes the observable.Reading the
failuresmap is the other half, and it needs a path that does not go throughDevice::verify, since that function has already thrown the response away. Either build the signature-upload request directly, or treat the post-query as authoritative and drop the claim entirely.Whichever is chosen, the log line has to stop asserting what it cannot observe. "Uploaded a self-signature" is true and cheap; "the device is now verified" is a statement about the homeserver's state made without reading it.
What this is not
Not #88. That is device keys never landing server-side, where the SDK logs that our own device might have been deleted and a client reports the device does not support encryption. This is a device whose keys published fine and whose self-signature did not attach. Different layer, and merging #88 would neither fix nor obscure it.
Not something
bootstrap_cross_signingshould be pointed at. It would sign one account and leave the reporting defect in place, which is how this stays invisible for another three months.@lucybothaving no cross-signing keys at all is #128, a third state with the same root as its empty allowlist: nothing runs a new bot's setup.The general shape
A call that succeeds because the request was accepted, read as a statement about what the far side did with it. Same as a JMAP
notCreatedunder an HTTP 200, and the same as adockerstatus tick that is a skip.Two issues, two owners, and this one should not be picked up as a one-liner.
Device::verifysends and discards inside matrix-sdk:So by the time this crate sees anything, the map is gone. Logging it means building and sending
/keys/signatures/uploaddirectly and reading the body, rather than adding a line.An estimate in an issue body is a claim like any other, and a wrong one gets the issue picked up by the wrong person. Someone with twenty minutes takes a one-liner, and the twenty minutes is where they find out. That does not show up as a mistake anywhere afterwards.
The third thing, which is neither of the above and is free. The log line has to stop asserting what it cannot observe. "Uploaded a self-signature" is true and costs nothing. "The device is now verified" is a claim about the far side made without reading it, and while that line exists any guard built here sits beside a message contradicting it.
Evidence this is live rather than theoretical, measured on
v0.10.6on 2026-08-26:@mantis_ai_boton deviceMDFPODBQQJlogged the success line at 16:25:12, and fifteen minutes later Synapse held no self-signature for that device. Four of eight bots unsigned. The query carries its own validation, 164 self-signature rows across all users and Julian's own eleven devices splitting 8 signed to 3 not, without which the table is a claim rather than a measurement.This is downstream of #143, and the upload had nothing to land in
Measured by Clark against Synapse and MAS, 2026-08-29:
Synapse has never held a device key for that device. Not a stale key, not a superseded one: none. Seven OAuth sessions contending over
/keys/uploadhave left the record empty, where the one-session control device carries both a key and a signature.So the reporting defect described in this issue is real and is not the cause.
Device::verifydiscarding thefailuresmap, andsecret_store.rsdiscarding the follow-up/keys/querythat exists to confirm the signature attached, are why nobody could see it. What they were hiding is an upload into a device record that does not exist.@mantis_ai_botlogged "Successfully signed our own device, the device is now verified" and is unsigned in Clark's table, which is exactly that.Fixing the reporting is still worth doing on its own terms — a call that succeeds because the request was accepted, read as a statement about what the far side did with it, is the shape this repository keeps paying for. But it will not make a device signed, and anyone picking this up should read #143 first.
Treat #143, #130 and #139 as one cause until the code contradicts it. Nothing in this repository signs or uploads anything that would behave differently under seven sessions than under one.
The one-cause reading is under a question: the control may have dissolved
Do not build on the single-mechanism conclusion in the comment above until this resolves.
That conclusion rests on a contrast, not on a single number:
The control device has since been reported as
8 live. If it holds eight live sessions and a key and a signature, then contention does not prevent keys from sticking, and the argument loses the only thing supporting it.It may be two predicates rather than two answers, since settled and live need not count the same thing, and which query produced each is being established rather than guessed at.
What stands regardless:
has_keys = 0, sigs = 0onDLX8SKHKis a direct measurement of the device this issue is about. What is in doubt is the inference from it — that seven contending sessions are the reason — which is the part the one-cause statement, and its echo on the sibling issues, were built on.Recording the doubt rather than retracting, because a retraction should carry the resolving measurement rather than the uncertainty. This line exists so that nobody reads the paragraph above it as settled in the meantime.
One thing that is settled and does not depend on any of it: the bearer-to-session mapping exists in MAS's database as
oauth2_access_tokens.oauth2_session_id, so telling the seven apart needs no change to this server and nojtifield. The runtimejtiwould be a convenience and nothing currently turns on it.Retracted: contention by session count is not the mechanism
The resolving measurement, both rows from the same query with the same predicate:
The device with more sessions is the signed one. The earlier "one settled session" was a mislabel: Synapse's
sigs = 1was read as a session count and written as one. The counts never moved; the noun was wrong.So every inference from that contrast is withdrawn, including the one-cause statement placed on #143, #130 and #139, and the claim that seven contending sessions are why the record is empty. This replaces it rather than sitting beside it.
What stands, and always did:
has_keys = 0, sigs = 0onMATRIXMCP-DLX8SKHKis a direct measurement of that device. The why has gone, not the what.Nothing here should be folded into a replacement hypothesis yet. One is on the table, that
KZ9M8ZY8has been quiescent for eight weeks whileDLX8SKHKis still accumulating sessions, but that is a single variable chosen after seeing the outcome on two devices, which is the shape that fits perfectly and explains the wrong thing. The cheaper discriminator is whetherDLX8SKHKwas ever quiescent and still keyless, which would kill it without spending anybody's time.How the retracted claim got here is on #143 under Provenance of the one-cause statement, so a later reader can see it arrived as an instruction on an unmeasured contrast rather than as a measurement.
And this issue's prediction goes with it
I wrote that fixing the reporting "will not make a device signed". That rested entirely on the dead contrast and should not survive as an orphan: it is withdrawn.
What this issue claims on its own evidence is unchanged and is still true.
Device::verifydiscards thefailuresmap returned inside a 200, andsecret_store.rsdiscards the follow-up/keys/querywhose comment says it exists to confirm the signature attached. So the SDK logs "Successfully signed our own device, the device is now verified" on evidence that cannot support it, and@mantis_ai_botproduced that line while unsigned. That is measured here and does not depend on why the record is empty.Whether repairing the reporting also repairs the signing is now open rather than answered in the negative.
Churn-recency is dead too, and the state to leave here is a measurement with no mechanism
A lone session for nine days did not leave a key. So neither session count nor recency explains it: eight-session
KZ9M8ZY8is signed, andDLX8SKHKis keyless through two quiescent windows. Withdrawn, like contention-by-count before it.The current-state reading is load-bearing rather than a gap: device keys live in
e2e_device_keys_jsonkeyed on(user, device), persist across sessions, and go when the device is deleted. The same device id spans all eight sessions including one that finished on 08-01 and is still indevices, so a key uploaded during the lone-session window would still be there. Onedevice_lists_streamrow and zerodevice_keys_jsonrows corroborate: one announcement, no key lifecycle.has_keys = 0, sigs = 0has now survived every explanation offered for it. That is the state to leave on this issue: a measurement with no mechanism, rather than a mechanism.So do not build a contention fix, and the PVC constraint recorded above is a constraint on a design nobody should now be starting.
Where the upload is actually gated, and a candidate that is already filed
matrix-sdk-crypto-0.17.0/src/olm/account.rs:666:Device keys are offered for upload only while
sharedis false. Once that flag latches true,keys_for_uploadreturnsNonefor device keys forever, and every later/keys/uploadcarries one-time keys only.That is the exact mechanism #88 describes, open since 2026-05-21 and mergeable: the per-account
sharedflag latches on the first apparently-successful upload and is never re-verified against the homeserver, so a client operates against a device that does not exist server-side. Its repro was a device with 50 one-time keys and zero rows ine2e_device_keys_json.The discriminator is one query and it is cheap: does
@julian's account hold one-time keys forDLX8SKHKwhile holding no device keys? If yes, the state matches #88's signature exactly and #88 becomes a candidate fix rather than an adjacent issue. If no, it is something else again.Offered as a candidate with a test, not as the answer. Two hypotheses have already died here and this one has the same shape as both: it fits, and fitting is what the last two did.
And one bound on my own contribution. #88's canonical SDK line, "Our own device might have been deleted", appears zero times in this pod's entire log. That is not evidence. There is no matrix-sdk client for
@julianon this pod at all: sixteen introspects, zero tool spans, no client-lifecycle line, measured earlier in this thread. A line that only a running client can emit, absent where no client runs, says nothing. The control is that the same grep returns 39 for a string that is there.Earlier in this thread I said #88 was not the fix. That was about the cross-signing warning and it remains correct for it: publication and signing are different layers. This is a publication measurement, which is #88's own subject.