fix(mobile): verify signed ownership profile evidence - #7588
Conversation
🔐 Codex Security Review
|
jedwards27
left a comment
There was a problem hiding this comment.
Review: request changes
Reviewed exact range e17cdd9d5c7e2b836b4670ae88bb87a79f94337a...df17ec4e90bffdd28e00d8b131518c62383eddfc as :bot: Jude’s code review agent. The signed-event and NIP-OA verification itself is sound in the reviewed paths, but two multi-profile consumers bypass the new deterministic replacement selector, leaving the user-visible ownership result dependent on relay response order.
Findings
-
[P2] Shared owner lookup can resurrect revoked ownership —
mobile/lib/shared/mentions/agent_identity_provider.dart:80-87agentOwnersProviderverifies and folds every returned profile revision directly intoownersrather than selecting the authoritative replacement first. Given an older genuinely signed positive profile and a newer negative/revocation, the older positive can still populate the map; response order also decides between competing positives. This map feeds known-agent classification and owner-based behavior in channel details/actions, compose, and mentions, so an obsolete Agent/owner state remains visibly trusted despite the PR's revocation-safe projection contract.This was reproduced through the real provider on this exact head: a temporary regression expecting no owner for
[newer negative, older signed positive]failed with the older owner's pubkey. The temporary test was removed and the checkout returned clean. -
[P2] Global mention search still selects equal-time profiles by arrival order —
mobile/lib/features/channels/mentions/mention_candidates_provider.dart:38-52The local reducer replaces only when
createdAtis greater. For two same-second kind-0 replacements, it keeps whichever event arrived first instead of the lowest event ID selected bylatestProfileEvents(mobile/lib/shared/crypto/nip_oa.dart:66-80). Equal-time positive/negative revisions can therefore flip the displayed identity and owner classification by response order while the directory, profile, and cache choose deterministically. The same account can appear human in the people directory and agent-managed in autocomplete, or vice versa.
Required author action
Route both multi-profile readers through latestProfileEvents before deriving owner/agent state—at minimum agentOwnersProvider and mentionUserSearchProvider. Add production-seam regressions with genuinely signed events covering:
- older positive + newer negative in both delivery orders;
- equal-time positive + negative in both orders, asserting the lowest event ID controls both displayed profile and owner/agent classification.
The regressions should fail when either production call site stops using the shared selector; helper-only coverage is not sufficient.
Validation and residual risk
gh api userreportedjedwards27; live PR author isloganj, so this is not self-review.- Full mobile suite on one clean exact-head checkout: 2,125 passed;
flutter analyze: no issues. - Independent full mobile run: 2,124 passed / 1 unrelated failure in
voice_note_recording_test.dart:395teardown (PathNotFoundException); focused changed-path suites then passed (282 tests). - Focused crypto/directory validation passed (38 tests). Source review confirmed canonical key/id/signature checks, event-ID recomputation, Schnorr verification, kind/timestamp bounds, duplicate/self rejection, strict condition evaluation, malformed fail-closed behavior, and genuinely signed positive fixtures (
mobile/lib/shared/crypto/signed_event.dart:7-26;mobile/lib/shared/crypto/nip_oa.dart:22-63,85-108;mobile/test/shared/crypto/nip_oa_test.dart). - No layout or interaction component changed. Native iOS observation was not run; this is a confidence gap, not additional author action.
- Remote CI had no reported failure but still had active jobs at review submission; required gates must complete on this exact head.
Verification owner: author for the code and causal regressions; reviewer/CI for refreshed exact-head validation. Any new head requires delta review.
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — REQUEST CHANGES
Reviewed exact head df17ec4e90bffdd28e00d8b131518c62383eddfc against base e17cdd9d5c7e2b836b4670ae88bb87a79f94337a.
The cryptographic verifier is sound in reviewed paths, but two multi-profile projections bypass the new deterministic replacement selector:
-
[P2] Revoked ownership can remain trusted based on relay response order.
agentOwnersProviderverifies and folds every returned profile directly into its owner map (mobile/lib/shared/mentions/agent_identity_provider.dart:76-88). A genuinely signed older positive processed after a newer negative/revocation restores obsolete ownership; reverse delivery can produce the opposite result. This provider feeds channel details/actions, compose, and mention surfaces. A production-provider repro failed by returning the older owner where no owner was expected. -
[P2] Global mention search resolves equal-time profiles by arrival order.
mentionUserSearchProvideronly replaces on a greater timestamp (mobile/lib/features/channels/mentions/mention_candidates_provider.dart:38-52), whilelatestProfileEventsuses the deterministic lowest-event-id tie break (mobile/lib/shared/crypto/nip_oa.dart:66-80). Equal-second positive/negative revisions can therefore make mention identity/owner status disagree with directory/cache/profile surfaces.
Author action: route both multi-profile readers—at minimum agentOwnersProvider and mentionUserSearchProvider—through latestProfileEvents before deriving owner/agent state. Add genuine signed-event production-seam regressions for older positive + newer negative and equal-time positive + negative in both delivery orders, asserting one displayed profile and owner classification everywhere.
Verification owner: author for code and causal regressions; reviewer/CI for exact-new-head gates and mutation proof at each production caller.
Validation at exact head: full Mobile 2,125/2,125, Flutter analysis, focused crypto/directory suites, and git diff --check passed. Canonical encoding, event-id recomputation, Schnorr verification, duplicate/self rejection, condition evaluation, malformed fail-closed behavior, and genuine signed positive fixtures are sound. Native iOS observation is a confidence gap only.
df17ec4 to
46bbe82
Compare
|
Head-bound response to reviews 5182369256 / 5182370001 at the new head P2 1 — shared owner lookup could resurrect revoked ownership ( P2 2 — equal-time mention search selection by arrival order ( Requested regressions, delivered at this exact head:
Also landing here per the same scope: mention candidates take ownership only from a currently available owner source (no display/search Exact-head local evidence: full Mobile 2,130 passed, exit 0; analyzer no issues; format 557 files, zero changes; 222 locked package roots verified before/after. Limits: the agent-owner reader here remains a one-shot read — live ownership activation is intentionally in #7389 on top of this branch; no native device/simulator journey or live-relay validation is claimed. The green CI on superseded |
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — REQUEST CHANGES
Reviewed exact head 46bbe82307c52db6bff33b7bd216704d545ba2d6 against base 78618804ec86a014524ad7d1fb55928e8f5c3edf.
The previous stale-positive and equal-time arrival-order blockers are fixed and mutation-proved. One user-trust defect remains:
[P2] Invalid latest envelopes can replace a user's visible identity. latestProfileEvents selects kind-0 replacements using the envelope's claimed pubkey, createdAt, and id without first validating the event signature/id (mobile/lib/shared/crypto/nip_oa.dart:68-81). Directory and global mention search then parse/display the selected event (mobile/lib/features/channels/channel_management_provider.dart:307-319; mobile/lib/features/channels/mentions/mention_candidates_provider.dart:44-61), and cache admission accepts it into visible profile state (mobile/lib/shared/profile/user_cache_provider.dart:201-237). Ownership verification fails closed, but identity presentation does not. A forged or malformed event claiming a victim pubkey can therefore win by timestamp/tie ID and replace the victim's name/avatar/handle across directory, autocomplete, and cache-backed surfaces. The new test currently codifies this split by expecting a tampered winner's display name while expecting ownerPubkey == null.
Author action: reject envelopes that fail verifySignedEvent before latest-profile selection/cache admission. Add both-order production-seam regressions showing newer and equal-time tampered events change neither displayed identity nor owner classification, and mutation-prove the shared validation seam.
Integrated evidence: both assigned lanes re-reviewed this exact head. Full Mobile passed 2,130/2,130; just mobile-check and git diff --check passed. Both prior fixes were mutation-proved. No native/live-relay run was performed; that is a reviewer/tooling confidence gap, not an additional author defect. GitHub CI was still active at submission; red Desktop/relay leaf jobs were outside this mobile-only diff while aggregate result jobs were green, so they are not the basis of this verdict.
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — REQUEST CHANGES
Reviewed exact head 46bbe82307c52db6bff33b7bd216704d545ba2d6 against base 78618804ec86a014524ad7d1fb55928e8f5c3edf.
The two prior selector defects are resolved: agentOwnersProvider and global mention search now use latestProfileEvents, and production-call-site mutations fail the new both-order regressions. One remaining trust defect is introduced/ratified at the new shared selection boundary.
Finding
[P2] An invalid profile envelope can still become the authoritative visible identity — mobile/lib/shared/crypto/nip_oa.dart:68-80, mobile/lib/features/channels/channel_management_provider.dart:307-319, mobile/lib/features/channels/mentions/mention_candidates_provider.dart:44-61, mobile/lib/shared/profile/user_cache_provider.dart:201-237
latestProfileEvents chooses a profile by its unverified claimed pubkey, createdAt, and id. Its consumers then parse that chosen event into the displayed name, avatar, and NIP-05 handle; only the owner projection calls verifySignedEvent. Cache admission follows the same pattern. A malformed event claiming another pubkey with a newer timestamp—or a lower claimed ID at the same timestamp—therefore suppresses the latest valid signed profile and controls the identity users see in the directory, autocomplete, and cache-backed surfaces. It cannot acquire owner authority, but it can impersonate or mislabel the account being selected and mentioned.
The new production-seam regression currently codifies this failure: it makes a tampered-ID/timestamp event the winner and expects ProfileData.fromEvent(forged).displayName while only expecting owner null (mobile/test/features/channels/channel_management_provider_test.dart:35-45,68-76). That contradicts the PR’s stated end-to-end verified shared selection: verification happens after an invalid replacement has already displaced the valid one.
Required author action
Exclude envelopes failing verifySignedEvent before they participate in latest-profile selection or cache admission. Add both-order production-seam regressions showing that a tampered newer event and a tampered equal-time/lower-ID event can neither replace the displayed signed profile nor alter its owner classification. Mutation-prove the validation at the shared selector/admission boundaries.
Validation and residual risk
- Both assigned lanes reviewed clean local/live head
46bbe82307c52db6bff33b7bd216704d545ba2d6;gh api userisjedwards27and PR author isloganj, so this is not self-review. just mobile-check: passed (557 files formatted, zero changed; analyzer clean).just mobile-test: 2,130 passed in both independent lanes.git diff --check 78618804...HEAD: passed; no added productionunwrap/expect, and new public declarations have documentation.- Causal mutations restoring the two prior arrival-order bugs failed the intended production-provider assertions, then were restored; final review trees were clean.
- At verdict time, exact-head Mobile and several Desktop jobs were still running. The displayed Desktop E2E shard failures roll up through a successful
Relay and PostgreSQL / Resultsjob and are outside this mobile-only diff; no PR-causal gate failure is asserted here. The authorized security-review execution was skipped, which remains a confidence gap rather than author rework. - No native iOS/simulator or live-relay journey was run. No layout/input component changed; this is a confidence gap only.
Verification owner: author for the filtering change and causal regressions; reviewer for mutation recheck and refreshed exact-head gates. Any new head expires this verdict.
46bbe82 to
80c5315
Compare
|
Head-bound response to reviews 5186893478 / 5186895621 at the new head P2 —
Both-order production-seam regressions at this exact head: tampered newer and tampered equal-time events, asserted in both delivery orders, change neither displayed identity (name/avatar/handle) nor owner classification at every consumer using the shared selector ( Causal mutations bound to the final tree prove the seams you flagged: omitting the shared selector's outer guard empties the owner map instead of retaining the genuine owner; omitting cache-ingress authentication makes the cached name Forged instead of Owned; DM-selector bypass makes the actual label Forged; merge/hydration/sheet selector bypasses each leak a forged Scope and evidence: this head is the complete disclosed envelope correction — 1,112 changed lines = 325 production + 787 tests/fixtures across 24 Not claimed: this is not CI/security/merge clearance — head-bound CI had just started at publication, the exact-head security-review authorization remains outstanding, and as you noted any new head expires prior verdicts. A fresh review at this exact head would be appreciated: https://github.com/block/buzz/pull/7588/files |
…ission Preserve authenticated negative ownership evidence, profile edit and display consumers, and independently fenced opening about snapshots. Bind profile admission to originating cache generations and exercise signed replacement and context-retirement workflows. Signed-off-by: Logan Johnson <loganj@squareup.com>
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — APPROVE
Reviewed exact head 80c531511511e1edb66c3354833ad54d3b46e466 against base f3940ff21e8b6050ff617c645578aefa45db7e9f (merge-base 78618804ec86a014524ad7d1fb55928e8f5c3edf). Both assigned review lanes independently recommend approval. No unresolved author-actionable defect was found.
Resolved findings
The prior forged-visible-identity defect is fixed at both trust boundaries:
latestProfileEventsauthenticates each kind-0 envelope withverifySignedEventbefore using its claimed pubkey, timestamp, or ID for replacement selection (mobile/lib/shared/crypto/nip_oa.dart:66-81). Invalid newer and equal-time/lower-ID events therefore cannot displace a valid signed profile.- Cache ingress independently verifies the envelope before changing display metadata or replacement ordering (
mobile/lib/shared/profile/user_cache_provider.dart:205-223).
The earlier stale-positive and arrival-order defects also remain resolved: owner lookup, directory, global mention search, DM labels, invites, profile hydration/readback, and profile-sheet snapshots route through authenticated deterministic selection or generation-bound cache admission. Authentic newer no-auth evidence still advances ordering and revokes stale ownership; equal-time signed replacements select the lower event ID independent of delivery order.
Validation
- Live PR head matched clean detached review heads at
80c531511511e1edb66c3354833ad54d3b46e466;gh api userisjedwards27, while the PR author isloganj, so this is not self-review. just mobile-check: passed; 558 files formatted with zero changes and Flutter analysis reported no issues.just mobile-test: 2,142 passed in both independent lanes.- Additional focused validation: 161 identity/profile/directory/mention/lifecycle tests passed in one lane; a fresh five-file changed-path run passed 66/66 in the other.
git diff --check: passed.- Causal mutations removing shared-selector authentication or cache-ingress authentication failed the intended production regressions, then were restored; final trees were clean.
- Exact-head security linters, macOS/Windows builds, Mobile Swift, relay/PostgreSQL integration, Semgrep, zizmor, and DCO checks reported success or expected path-based skips at verdict time.
Clients / Mobileand several Desktop jobs remained in progress; these are gate-freshness gaps owned by CI, not author defects.
Residual risk
No native iOS/Android accessibility/device journey or live-relay exercise was run. The change affects identity evidence and state plumbing rather than visual or interactive structure, so this is a confidence gap rather than required author action. Exact-head Codex security execution was skipped by its workflow despite authorization; the reviewed cryptographic boundary received source, regression, and mutation validation, but workflow authorization remains owned by the security/CI maintainers.
Author action: none.
Verification owner: CI/security workflow owners for remaining exact-head checks; mobile/release QA for optional native or live-relay confidence work. Any new head invalidates this approval.
80c5315 to
ef698b4
Compare
|
🤖 |
🤖
Summary
A forged profile could replace someone’s name, avatar or handle in the mobile app by claiming to be newer. This change checks that a profile really was signed by its author before choosing which profile to display or cache. Genuine signed ownership revocations still take effect; forged updates cannot replace them.
This branch now authenticates the profile envelope at both decision seams, not just one:
mobile/lib/shared/crypto/nip_oa.dart) rejects events that fail end-to-end signature verification (mobile/lib/shared/crypto/signed_event.dart: canonical encoding, event-id recomputation, author signature) before any claimed ordering is read — the newest verified profile wins, and equal timestamps resolve by lowest event id, independent of arrival order. Signature validity is not a ranking filter: an authentic signed profile with a missing/invalid owner tag is still replacement evidence and can revoke ownership; it just cannot masquerade as a newer identity.mobile/lib/shared/profile/user_cache_provider.dart) independently re-authenticates every profile update at ingress before it can change either display metadata or owner ordering — so even if some surface bypassed the shared selector, a forged envelope cannot enter the cache. Genuine signed negative evidence still advances the ordering, so a real revocation cannot be buried by a forged positive.Observable effect: valid signed profiles behave exactly as before; an unsigned or mis-signed event can no longer win selection, replace a displayed identity, or be admitted as cached metadata in any delivery order. A genuine revocation or re-authorization remains effective.
Scope boundary: ownership evidence consistency. The agent-owner lookup itself remains a one-shot read per session (it is not re-evaluated on every live profile update), and when ownership data is fetched or refreshed is unchanged here — the live ownership/lifecycle corrections land in #7389, which builds directly on this branch. Native/device and live-relay validation are outside this branch.
Related issue
Groundwork for #7389 (does not close it — the live ownership half lands there). Closest existing PR: #7389; no duplicate found.
Testing
Published head
ef698b4d6048adcb73785c986e55fe6249ecbada; actual basemainat6c35e82bd50f4ad6587554eeb429e7378d474ba7. The commit’s sole parent isf3940ff21e8b6050ff617c645578aefa45db7e9f; actual main has since advanced to the base target above without changing Mobile.The chain inherits main’s MinIO image fix (#7599), with no Mobile source change from that base update. Mobile subtrees and complete immediate patches are byte-identical to the validated source heads, so the source tests, statics, locked-package checks and technical review evidence below are reused through equivalence, not newly executed at these heads. This does not establish CI, security, native-device/live-relay or merge clearance.
80c531511511e1edb66c3354833ad54d3b46e466: full Mobile 2,142 passed; analyzer no issues; format 558 files, zero changed; 222 locked package roots verified before/after.042c01c5) remains source/delta evidence through equivalence; it is not a new GitHub approval or waiver of reviewer head/base conditions.