Require a pairing record store for the PIN methods - #167
Merged
Conversation
CanOffer required an IPairingRecordStore for pairing_psk but not for dynamic_pin or static_pin. A client configured with a PIN method and no record store ran the whole PIN exchange, reached HandleServerPairFinalize, logged "record NOT persisted" - and raised PairingCompleted anyway. The server then held a long-term record for a client that stored nothing, so the client failed to authenticate on the next connection while the app had been told pairing succeeded. The check goes in CanRun rather than CanOffer. CanRun already documents the rule - "implemented, enabled, and holding every dependency the method needs... or the server is told an offer exists that every attempt will refuse" - and already carries the IPinLockoutStore requirement for exactly this reason. A record store is a dependency in the same sense, so an unrunnable PIN method now also drops out of client/hello and reports enabled: false from get-pairing-config, instead of being advertised and then refused. pairing_psk stays out of CanRun: it is mandatory, so withholding it would leave a store-less client advertising nothing. HandleServerPairFinalize is restructured so the only path that raises PairingCompleted is the one that actually persisted. The old else branch was reachable in two ways, claimed "no record store configured" for both, and raised the event with an empty server id. The null-store half is now unreachable; the null-ServerId half is a degenerate peer and returns without claiming success. Test harnesses that configured a PIN method without a record store now supply one - that is the behaviour change, and it accounted for 24 of 25 initial failures. New tests verified to discriminate: 4 of 6 fail with the CanRun clauses reverted (the 2 that pass are the positive controls), and the pair-finalize test fails with the restructure reverted. Suite 736 green, clean Release build on both TFMs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #158.
The defect
CanOfferrequired anIPairingRecordStoreforpairing_pskbut not fordynamic_pinorstatic_pin. A client configured with a PIN method and no record store completed the entire PIN exchange, reachedHandleServerPairFinalize, took the branch that logs "Pairing completed but no record store configured; record NOT persisted" — and then fell through and raisedPairingCompletedanyway.The server ends up holding a long-term record for a client that stored nothing. It is self-correcting on the next connection (
pairing_required), but in the meantime the app has been told pairing succeeded.Where the check goes, and why
In
CanRun, notCanOffer.CanRun's own doc comment already states the rule:A record store is a dependency in exactly that sense, and
CanRunalready carries_pinLockoutStore is not nullfor the same reason. Putting it there means an unrunnable PIN method is withheld rather than advertised-then-refused, so the server never renders pairing UX for a method that cannot work.CanOfferpicks it up for free, since both PIN arms delegate toCanRun.pairing_pskdeliberately stays out ofCanRun— it is mandatory, so folding it in would leave a store-less client advertising zero pair methods. The PIN methods are optional, so withholding an unusable one costs nothing.The fall-through the issue asked about
Issue item 2 asked whether
HandleServerPairFinalize'selseshould raisePairingCompletedat all once every method is gated. It should not, and the branch was doing more than it looked:_pairingStore is null— now unreachable, sinceCanOffergates all three methods on a store.ServerId is null— still reachable. It comes from the Noise session, not from configuration.Both were logging "no record store configured" (untrue for the second) and raising
PairingCompletedwithstring.Empty. Restructured so there is one success path and it is the one that actually persisted; every other path returns without raising.ServerIdis non-null by then, so the?? string.Emptyfudge goes too.Behaviour change
A client with PIN methods and no record store stops advertising them and answers
method_not_supportedif asked. This is the visible change #158 flagged for its own review, and it is what accounted for 24 of the 25 initial test failures — harnesses that configured a PIN method without a store.PairingWindowEndToEndTestswas checked and already supplies one, so it is not passing vacuously.Documented in
MIGRATION-10.0.0.md§3 and the NuGet README, both flagging that it fails silently: nothing throws, the method just never appears.Verification
dotnet build -c Release --no-incremental: 0 errors, both library TFMs.CanRunclauses: 4 of 6 new tests fail. The 2 that pass are theWithAPairingRecordStore_IsAdvertisedpositive controls, which exist precisely so that a change refusing PIN methods outright cannot pass this block.HandleServerPairFinalizerestructure:PairFinalize_WithAStoreButNoServerId_PersistsNothingAndDoesNotClaimSuccessfails. This one was added deliberately — theCanRuntests reach the abort and never enter the finalize handler, so without it the restructure would have been unpinned.