fix(mesh): a peer's claim about its own trustworthiness is not evidence (#1253) - #1278
Merged
Conversation
…ce (#1253) `wire_trust_level` mapped a PeerQueryResponse's `trust_state = 0x00` to `TrustLevel::Verified` and wrote it into the peer cache that `trust_filter_allows` consults. That byte is the responder's claim ABOUT ITSELF, carried in an envelope this node does not authenticate before importing — so a station could assert its own trustworthiness and thereby satisfy the operator's `strict` relay policy, which exists precisely to decide whose third-party traffic we retransmit. The mapping is deleted rather than corrected: there is no honest mapping, because this crate has no local trust store to resolve the claim against (no TrustStore reference, no such dependency). A function whose only correct answer is one constant is not a mapping. Every peer-asserted state now imports as Unknown. Consequence: `TrustedOnly` can no longer be satisfied, so `strict` relays nothing. The node warns loudly and still runs rather than refusing to start — untrusted is this project's default for synchronized operation including relay and mesh, and operators restrict upward by accepting trust levels, not by the protocol refusing to run. The operator asked for a restriction this crate cannot implement; the honest response is to run, relay nothing, and say why. Tests are UNIT tests deliberately: the properties are about `peer_cache` and `trust_filter_allows`, both private, and exporting accessors so an integration test could reach them is the "public API for an instrument" shape #1271 is about. The first draft did exactly that and was deleted. Results: 3/3 pass, and all three FAIL against the restored defect, each on its own assertion; restored by sha256sum. Whole crate green. Implements: REQ-SEC-09 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
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 #1253.
The defect
wire_trust_levelmapped aPeerQueryResponse'strust_state = 0x00toTrustLevel::Verifiedandwrote it straight into the peer cache that
trust_filter_allowsconsults:That byte is the responder's claim about itself, carried in an envelope this node does not
authenticate before importing. So a station could assert its own trustworthiness and thereby satisfy
the operator's
strictrelay policy — the policy that exists precisely to decide whose third-partytraffic this station retransmits. The operator-configured policy was being enforced against a
peer-supplied value.
Found by a verifier while refuting a neighbouring audit finding about unknown-code catch-alls. The
catch-all was harmless (
Reducedis the most restrictive level the enum has); this was a differentbyte, in the opposite direction.
The fix is a deletion, not a correction
There is no honest mapping from a self-assertion to a trust level, because this crate has no local
trust store to resolve the claim against — verified: no
TrustStorereference anywhere incrates/openpulse-mesh/src/, and no such dependency in itsCargo.toml. A function whose onlycorrect answer is one constant is not a mapping, so
wire_trust_levelis deleted rather thancorrected, and every peer-asserted state imports as
Unknown.The consequence, and why the node still starts
With every import
Unknown,TrustedOnlycan never be satisfied — sostrictrelays nothing.The node warns loudly and still runs rather than refusing to start. That follows the maintainer's
standing ruling: untrusted is this project's default for synchronized operation including relay and
mesh, and operators restrict upward by accepting trust levels, not by the protocol refusing to
run. The operator asked for a restriction this crate cannot implement; the honest response is to run,
relay nothing, and say why — naming the fix (
balanced, or the full daemon, which resolves trustagainst the signed handshake).
Tests — unit tests, deliberately
The properties are about
peer_cacheandtrust_filter_allows, both private. My first draft was anintegration test that added
peer_trust_levelandrelay_allowed_forto the public API to reachthem — which is the "public API for an instrument" shape this project bans and #1271 is currently
about. That draft was deleted rather than shipped.
Three tests in
mod peer_asserted_trust_tests, driving the real receive path (step→handle_peer_query_response) with a frame injected over a loopback backend:0x00,0x01,0x02,0x03,0xFF— imports asUnknown, with apositive control first that the import path ran at all, so the assertion cannot pass because
nothing was imported;
TrustedOnly;strictrefuses even a peer we genuinely heard, with abalancedcontrol so it cannot pass ina build where relaying is broken outright.
All three fail against the restored defect, each on its own assertion
(
trust_state = 0x00 must import as Unknown; a peer's claim about itself is not evidence); restoredby
sha256sum.Fixture note: the response carries an empty
descriptor_signature, and that is part of thepoint — the import path never verifies it, which is exactly why the
trust_statebeside it cannot betreated as evidence. (A 64-byte one also pushes the envelope past one 255-byte frame.)
Gate
Docs
architecture.md'sTrustFiltertable now says the filter is only as good as where the cached levelcame from;
openpulse-manual.mdwarns thatstrictrelays nothing in this binary and names thealternatives.
Compounds with #1251 — same binary, and since that change
openpulse-meshhas no route to a soundcard at all, which bounds the on-air exposure but not the correctness.
🤖 Generated with Claude Code
https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6