From 471a5386225d8dba8447a4eb096c37081ef87089 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:32:47 +0000 Subject: [PATCH] remove cute language --- plans/2026-06-25-name-resolution.md | 124 ++++++++++++------------- src/Simplex/Chat/Badges.hs | 17 ++-- src/Simplex/Chat/Library/Commands.hs | 23 ++--- src/Simplex/Chat/Library/Subscriber.hs | 2 +- src/Simplex/Chat/Store/Direct.hs | 4 +- src/Simplex/Chat/Types.hs | 2 +- 6 files changed, 87 insertions(+), 85 deletions(-) diff --git a/plans/2026-06-25-name-resolution.md b/plans/2026-06-25-name-resolution.md index dac58da101..c6fe4476d9 100644 --- a/plans/2026-06-25-name-resolution.md +++ b/plans/2026-06-25-name-resolution.md @@ -12,7 +12,7 @@ Reduce the feature to the minimum coherent, secure shape: typed `Maybe SimplexNameInfo`. No second "locally-known" copy on the entity row. - A local **verification status** as a 3-state `Maybe Bool` (not a timestamp): not-attempted / failed / verified. A failed check never blocks connecting. -- Connect-by-name gated by **consent** and **born verified**. +- Connect-by-name requires the entity to claim the name, and the result is created as verified. - The **proof** (address-key signature, context-bound) is **in scope for link contexts** — connect-by-name, 1-time invitations, contact addresses, channel join links — so those names are verifiable in this release. Only a name with @@ -27,17 +27,17 @@ timestamps. ## 2. Why (trust model) A name resolves to a link via the agent (`resolveSimplexName`); a `NameRecord` -carries **lists** of links (`nrSimplexContact`, `nrSimplexChannel`), so +has **lists** of links (`nrSimplexContact`, `nrSimplexChannel`), so verification matches against **any** of them. "Match any" is safe **only because the namespace is an on-chain, ENS-style registry**: `nrOwner`/`nrResolver` are Ethereum addresses (`Names/Record.hs:22-23,36`), so each name has a **single owner** who sets all its links. An attacker can register *their* name → your -address (the offensive-name case, handled by the consent gate below) but **cannot -add a link to your name** — on-chain ownership blocks impersonation. This premise -is load-bearing; if names were not single-owner, "match any" would be exploitable. +address (the offensive-name case, handled by the claim check below) but **cannot +add a link to your name** — on-chain ownership blocks impersonation. Everything +here depends on that; if names were not single-owner, "match any" would be exploitable. The registrant of a name controls what it points to, with no proof that the -target address consents — anyone can publish `@offensive → your address` or +target address agreed to it — anyone can publish `@offensive → your address` or `#offensive → your channel`. Therefore: - One-directional verification ("does `resolve(name)` equal a stored link") is @@ -46,11 +46,11 @@ target address consents — anyone can publish `@offensive → your address` or copy a link into a profile. **Control of a link is proven only by an actual connection/join through it.** - Sound verification is the intersection: `resolve(name) → link`, **and** the - entity advertises that name in its profile (consent), **and** that link is one + entity advertises that name in its profile (it claims the name), **and** that link is one we connected/joined through (control-proven). - Verifying a name without having connected through its link needs a **signature by the address key over the name, bound to the presentation context** (§4.8). - This is **in scope** for link contexts: a 1-time invitation carries such a proof + This is **in scope** for link contexts: a 1-time invitation includes such a proof bound to the invite, so resolving the name → address → address key verifies it. Consequence: names are verifiable in this release for **channels** (join link), @@ -130,7 +130,7 @@ soft-decode** (`decodeSimplexName` dropped for the name columns). `Profile.simplexName`.) - `Profile.contactDomainProof :: Maybe ClaimProof` — NEW; a flat sibling of `contactDomain`/`contactLink`, a **wire profile field like `Profile.badge`**. The - **stored** own profile (`contact_profiles`) carries the name but **no proof**; the + **stored** own profile (`contact_profiles`) has the name but **no proof**; the proof is generated and **added to the outgoing profile at send/save** (§4.8), exactly as the badge is. `PHSimplexLink` (verifiable) when the profile is saved to a contact address / 1-time invite; `PHTest` over an established connection (the gap). @@ -142,21 +142,21 @@ soft-decode** (`decodeSimplexName` dropped for the name columns). - `PublicGroupAccess.groupDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)` — RETYPE from `Maybe Text` (`Types.hs:857`); JSON string, wire-identical to the released text (§4.1). Owner-set, broadcast in `XGrpInfo`. **Only public, relay-backed - groups** (`groups.use_relays`) can carry a name — they have a channel join link to + groups** (`groups.use_relays`) can have a name — they have a channel join link to resolve to and an owner key (`groups.member_priv_key`, `chat_schema.sql:193`) to sign with; p2p groups have neither. (`contactDomain` on a member is a contact attribute, unaffected.) - `GroupInfo.groupDomainVerification :: Maybe Bool` — local status, read from the `groups` table (there is no `LocalGroupProfile`, and `GroupProfile` is the wire - type, so the status cannot ride the profile). + type, so the status cannot be sent with the profile). - DROP `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`, and both `*VerifiedAt` timestamps. Asymmetry, intentional: contact name + status both live on `contact_profiles` -(surfaced via `LocalProfile`); the group name lives on `group_profiles` -(surfaced via `GroupProfile`/`PublicGroupAccess`) but its status lives on -`groups` (surfaced via `GroupInfo`). This is forced — `group_profiles` is the +(exposed via `LocalProfile`); the group name lives on `group_profiles` +(exposed via `GroupProfile`/`PublicGroupAccess`) but its status lives on +`groups` (exposed via `GroupInfo`). This is forced — `group_profiles` is the shared wire profile with no local columns, while `contact_profiles` already -carries local state (`local_alias`). +holds local state (`local_alias`). ### 4.3 Schema — rewrite `M20260603_simplex_name` (branch unreleased) @@ -196,14 +196,14 @@ Verification column decode: `Maybe BoolInt → Maybe Bool` (`NULL` = not attempt argument and the `connections.simplex_name` carrier param on `createConnection_`. - `updateContactProfile` / `updateGroupProfile`: write `contact_domain` / `group_domain` from the received profile. Reset the verification to `NULL` - (not-attempted) **only when the name changes**; an XInfo/XGrpInfo carrying the + (not-attempted) **only when the name changes**; an XInfo/XGrpInfo with the **same** name keeps the existing status (a verified name stays verified), exactly as a badge does. No conflict clearing (no UNIQUE index). - `getContactBySimplexName` / `getGroupIdBySimplexName`: look up by the **verified** profile name (the name column joined with verification = `Just True`); on a miss or unverified, fall through to resolve-and-connect. Consistent with the existing by-address lookup `getContactViaShortLinkToConnect` (`Direct.hs:963`), which - matches the link with no verified gate — a link is the identity, a name is a + matches the link with no verification check — a link is the identity, a name is a claim that only becomes a usable pointer once verified. ### 4.5 Redaction (`Library/Internal.hs:1246`) @@ -217,14 +217,14 @@ redactedMemberProfile g m Profile {…, contactLink, contactDomain} = , shortDescr = removeSimplexLink =<< shortDescr -- via allowSimplexLinks , contactLink = if allowSimplexLinks then contactLink else Nothing , contactDomain = if allowDirect then contactDomain else Nothing - , contactDomainProof = Nothing } -- member profiles are contextless; never carry a proof + , contactDomainProof = Nothing } -- member profiles are contextless; never include a proof ``` - `allowDirect` is the single primitive (the `DirectMessages` permission); it - gates the name and is reused inside `allowSimplexLinks`. No `allowName` flag, + controls the name and is reused inside `allowSimplexLinks`. No `allowName` flag, no second lookup. - **Behavior changes vs current:** `contactLink` flips from unconditionally - dropped to gated on `allowSimplexLinks` (a member's contact address becomes + dropped to controlled by `allowSimplexLinks` (a member's contact address becomes visible whenever links+DMs are allowed — the meaning of "links allowed"); the name follows the looser `allowDirect`. Rationale: a link is one-tap-to-connect (low friction), a name only resolves if the recipient deliberately looks it up @@ -237,23 +237,23 @@ redactedMemberProfile g m Profile {…, contactLink, contactDomain} = (both reduce to `groupFeatureMemberAllowed' f (memberRole (membership g)) (fullGroupPreferences g)`, `Types.hs:646–652`). - Collapses `groupUserAllowSimplexLinks` (`Types.hs:656`) and the pre-computed - `allowSimplexLinks` plumbing at the call sites (`Internal.hs:1244`, + `allowSimplexLinks` wiring at the call sites (`Internal.hs:1244`, `Subscriber.hs:842,2817,3239`, `Commands.hs:3748,4057`). `Commands.hs:3748` has `Maybe GroupInfo` → "no group ⇒ no redaction" at the call site. ### 4.6 Resolution + verification (`Library/Commands.hs`) -Two regimes that differ in whether verification can *fail*: +Two cases that differ in whether verification can *fail*: **Connect-by-name** (`connectPlanName` / `dispatchResolvedRecord`) — verification is a **precondition** of connecting. After decoding the resolved short link's embedded -profile, **add the consent gate**: require that profile's `contactDomain` / +profile, **add the claim check**: require that profile's `contactDomain` / `groupDomain` to equal the resolved name, else fail with `CESimplexNameNotFound` ("name unknown") and **do not connect**. (The current branch decodes the profile but never compares the name — this is the missing check.) On success the prepared contact/group is created with the name on the profile, `connLinkToConnect` = the -resolved link, verification = `Just True` (**born verified**). There is no "failed" -outcome here — failing to resolve/consent just means no connection. +resolved link, verification = `Just True` (**created as verified**). There is no "failed" +outcome here — failing to resolve or to claim the name just means no connection. **Connected NOT by name** (via an address link or a 1-time link) — the peer's profile may *claim* a name; it starts **unverified** (`Nothing`). Verification is **post-hoc @@ -264,10 +264,10 @@ and non-blocking**: keep `apiVerifySimplexName` (`/_verify simplex name`). the usual contact-address case), check the `ClaimProof` signature over `name <> presHeader`, and check the proof's `presHeader` link == `preparedContact.connLinkToConnect` (**not** `profile.contactLink` — the branch bug). - Contact addresses and 1-time invites both carry the proof. + Contact addresses and 1-time invites both include the proof. - **Channels** verify by **presence / link-match**: `resolve(#name)` includes `preparedGroup.connLinkToConnect` (the join link), whose owner-signed data already - carries `groupDomain` (no `ClaimProof` — §4.8). + has `groupDomain` (no `ClaimProof` — §4.8). Result is `Just True` (holds) or `Just False` (fails) — and **`Just False` must NOT prevent the connection**, exactly as a failed *badge* verification doesn't. **This is why the status must be 3-state** (`Nothing` not attempted / `Just False` failed / @@ -284,7 +284,7 @@ Keep the pure helpers `firstNameLink` (per-type link pick, cross-type rejection) ### 4.7 Display (`View.hs`) A name is shown **when there is a proof to verify**, together with its status — -verified, failed, or not-yet-verified. All three states are surfaced (a failed or +verified, failed, or not-yet-verified. All three states are shown (a failed or pending check still shows the name, flagged), so the user sees the name and its trust level rather than a silent omission. Rendering those three states is **out of scope for this PR** (UI work), but the core stores the data — the 3-state status and @@ -295,10 +295,10 @@ name on an `XInfo` profile over an established connection — is **not shown**. A name resolves to a **contact address** (the persistent identity link). The proof asserts "the owner of that address asserts this name", so it is **signed by the -address's key** (the key the resolved address carries) — **not** by the -per-connection or 1-time-link key. Every proof is **bound to a presentation -context** (anti-replay), so a proof minted for one context cannot be replayed in -another; these are distinct proofs. Carry the context in a presentation header: +address's key** (the resolved address's own key) — **not** by the +per-connection or 1-time-link key. Every proof is **tied to the link it's shown +through**, so a proof made for one link can't be reused on another; these are +distinct proofs. Store the link in a presentation header: **rename `BadgePresHeader` (`Badges.hs:212`) → `ProofPresHeader`** (now shared by badge and name proofs) and add a **`PHSimplexLink AConnShortLink`** constructor. `AConnShortLink` (`Agent.Protocol.hs:1536`, @@ -320,11 +320,11 @@ variant. } The signature is by the key of the signer's **owner identity** in the link's owner -chain (`OwnerAuth`, `Agent/Protocol.hs:1829`); `ClaimProof` carries +chain (`OwnerAuth`, `Agent/Protocol.hs:1829`); `ClaimProof` has **`linkOwnerId :: Maybe OwnerId`** (`OwnerId`, `:1827`) to name it, so the verifier checks exactly that key (no iterating the owner list): -- **Channels** (which can have delegated owners) sign with the user's **owner key**, +- **Channels** (which can have owners other than the address) sign with the user's **owner key**, `linkOwnerId = Just oid` — **not** the root key — `groups.member_priv_key` (`chat_schema.sql:193`). - A **contact address** has a single owner = its creator, so it signs with the **root @@ -349,46 +349,46 @@ Per presentation context: proof-for-invites. The address can also serve as the **badge** proof's context. - **1-time invitation**: include a proof **signed by the address key, bound to the 1-time link** as context. Feasible **now** — the 1-time link is a unique, - single-use anti-replay context; no general mechanism required. This is what makes + single-use context; no general mechanism required. This is what makes a 1-time-invite contact verifiable. -- **`XInfo` over an established connection**: no link context → the augmentation stamps - `PHTest` (unbound) → unverifiable → not shown. The deferred gap, same as the badge's +- **`XInfo` over an established connection**: no link context → the step that adds the proof sets + `PHTest` (unbound) → unverifiable → not shown. Left for later, same as the badge's `PHTest`. (For **group members** the proof is dropped entirely by redaction, §4.5.) Home: **inside the profile** — `Profile.contactDomainProof :: Maybe ClaimProof`, a flat sibling of `contactDomain`/`contactLink`, exactly like `Profile.badge`. It is **not** stored on the own profile; like the badge, it is **generated fresh and added to the outgoing profile at send/save** — when the profile is sent to a peer (`XInfo`) or saved -to a link — by signing `name <> presHeader` with the address root key and stamping the +to a link — by signing `name <> presHeader` with the address root key and setting the **destination as the `presHeader` context**. Because `presentUserBadge` -(`Internal.hs:2037`) already augments the outgoing profile with the badge proof at *both* -peer-sends and link-data writes, the name-proof augmentation belongs in the **same +(`Internal.hs:2037`) already adds the badge proof to the outgoing profile at *both* +peer-sends and link-data writes, the name-proof step belongs in the **same function**. The context decides whether a proof is useful: saving to a contact address / -1-time invite stamps `PHSimplexLink(that link)` (verifiable — the in-scope cases); an -established-connection peer-send stamps `PHTest` (unbound — the deferred gap, same as the +1-time invite sets `PHSimplexLink(that link)` (verifiable — the in-scope cases); an +established-connection peer-send sets `PHTest` (unbound — left for later, same as the badge). The receiver verifies and stores the 3-state status on -`LocalProfile.contactDomainVerification`, as `localBadge` carries the badge status. -Connect-by-name is **born verified**. +`LocalProfile.contactDomainVerification`, as `localBadge` holds the badge status. +Connect-by-name is **created as verified**. -**Scope:** the **useful** (`PHSimplexLink`) proofs — stamped when the profile is saved to +**Scope:** the **useful** (`PHSimplexLink`) proofs — set when the profile is saved to a contact address or 1-time invite — are in this change; the verifier resolves the name → address → address key → checks the signature and the `presHeader` link. Channels use presence (below). The contextless established-connection case (a `PHTest` name proof) and -group members are the deferred gap. +group members are left for later. Badges stay **in the profile** — person-scoped, presented per connection (`presentUserBadge`, `Internal.hs:2037`), including over established connections via `XInfo`. They share the presentation-header context type with name proofs but sign with the **badge credential key**, not the address key. Two scopes, two homes. -The shared augmentation stamps **both** proof kinds the same way: `PHSimplexLink(link)` +The shared step sets **both** proof kinds the same way: `PHSimplexLink(link)` when the destination is a link (contact address, 1-time invite — verifiable), `PHTest` over an established connection (unbound — the gap). So name proofs and badges both use `PHTest` only for the contextless established-connection case. No group proof field: a channel is always joined via its **join link** (its own -address), whose owner-signed data already carries `groupDomain`, and there is no -"carrying link ≠ resolved address" case for channels — so channels verify by +address), whose owner-signed data already has `groupDomain`, and there is no +"advertised link ≠ resolved address" case for channels — so channels verify by presence/link-match and need no `ClaimProof`. (If full symmetry is wanted later, add `GroupProfile.groupDomainProof` the same way.) @@ -410,7 +410,7 @@ add/change/remove their own name from the UI; the branch has no command for the address). 5. Set `LocalProfile.contactDomain` and **re-publish the contact-address link data**. The `ClaimProof` is added when the profile is saved to that link (the send/save - augmentation, §4.8) — **not** produced by the API itself. (Steps 1–4 are the verify; + proof-adding step, §4.8) — **not** produced by the API itself. (Steps 1–4 are the verify; this step is set + re-publish.) Needs a `ChatCommand` constructor + parser + handler. - **Channel name** (public, relay-backed groups only) — a **separate** @@ -423,8 +423,8 @@ add/change/remove their own name from the UI; the branch has no command for the verified path; factor out the shared `GroupProfile`-update + `XGrpInfo` broadcast so both commands reuse it. Verify is **TLD-dependent**: resolve+compare for `TLDSimplex`, a different/no check for `TLDWeb` (web domains don't resolve through the namespace). -- The own name carries **no stored verification status** — the verify step checks it at - add time; the 3-state status is only for peers' names. No RNAME plumbing (out of scope). +- The own name has **no stored verification status** — the verify step checks it at + add time; the 3-state status is only for peers' names. No RNAME wiring (out of scope). ## 5. Removal checklist (from the current branch) @@ -456,9 +456,9 @@ b. **Lookup (§4.4):** re-point `getContactBySimplexName` (its one caller is c. **Proof (§4.8):** a flat **`Profile.contactDomainProof :: Maybe ClaimProof`**, a wire profile field like `Profile.badge`. Not stored on the own profile; **generated fresh and added to the outgoing profile at send/save** (peer `XInfo` or save-to-link) by the - **same augmentation function** as the badge (`presentUserBadge`, which already runs at + **same function** as the badge (`presentUserBadge`, which already runs at peer-sends *and* link-data writes). Signed by the signer's **owner-identity key** — a - channel's delegated **owner key** (`groups.member_priv_key`, `linkOwnerId = Just oid`) + channel's **owner key** (`groups.member_priv_key`, `linkOwnerId = Just oid`) or a contact address's **root key** (sole owner, `linkOwnerId = Nothing`) — over `name <> presHeader`; `linkOwnerId` selects the verification key (`Nothing` = root, allowed at validation). `PHSimplexLink` for address/invite saves, `PHTest` over @@ -472,17 +472,17 @@ d. **`redactedMemberProfile` contactLink exposure (§4.5):** intended — a memb e. **Verify command + status (§4.6):** keep `apiVerifySimplexName` — required to verify a claimed name for entities connected **not** by name (address / 1-time link). Status is **3-state** because a failed name (or badge) verification must - **not** block the connection; connect-by-name is the only born-verified path - (and there a resolve/consent failure means no connection, not a failed state). + **not** block the connection; connect-by-name is the only created-as-verified path + (and there a failure to resolve or to claim the name means no connection, not a failed state). **Auto-verify on connect** (vs. on-demand only) is left open; it doesn't change the 3-state requirement. ## 7. Rollout & scope -The **consent gate** (§4.6) — a name resolves only if the **resolved link's own +The **claim check** (§4.6) — a name resolves only if the **resolved link's own data claims it** — is the anti-stray-names protection and **must ship regardless**: a name someone registers against a real address must not "work" without that -address owner's consent. It needs no signature (the address-case presence suffices), +address owner's agreement. It needs no signature (the address-case presence suffices), so it lands with the core change. The signed proof (§4.8) can ship in the **same** release (add + verify together) or @@ -497,10 +497,10 @@ context ripples through the badge code — accepted, and bounded: (`:200, :212, :216, :226`); add the `PHSimplexLink AConnShortLink` tag/constructor/`StrEncoding`/accepted-case; `badgeProof` (`:314`) takes the renamed type. -- `presentUserBadge` (`Internal.hs:2037`) + its ~15 call sites — the **shared profile- - augmentation** point (already runs at peer-sends *and* link-data writes): generalize it - to stamp **both** the badge proof and the name `ClaimProof` onto the outgoing profile, +- `presentUserBadge` (`Internal.hs:2037`) + its ~15 call sites — the **shared point that adds + proofs to the outgoing profile** (already runs at peer-sends *and* link-data writes): generalize it + to set **both** the badge proof and the name `ClaimProof` onto the outgoing profile, using `PHSimplexLink link` where the destination is a link, `PHTest` otherwise. The pure rename can land as a **standalone prep commit** ahead of the proof; the -`PHSimplexLink` plumbing + name proof land with the proof work. +`PHSimplexLink` wiring + name proof land with the proof work. diff --git a/src/Simplex/Chat/Badges.hs b/src/Simplex/Chat/Badges.hs index d08ae42454..68b8923472 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -246,9 +246,9 @@ proofPresHeaderAccepted = \case PHSimplexLink _ -> True PHUnknown _ _ -> True --- A name claim proof: signed by the address owner's key (linkOwnerId = Just oid for a --- channel's delegated owner, Nothing = the address root key) over --- strEncode name <> strEncode presHeader, bound to the presentation context (the link). +-- A name claim proof: signed by the address owner's key (linkOwnerId = Just oid when a channel +-- owner other than the address signs, Nothing when the address's own root key signs) over +-- strEncode name <> strEncode presHeader, tied to the link it is shown through. data NameClaimProof = NameClaimProof { linkOwnerId :: Maybe (StrJSON "OwnerId" OwnerId), presHeader :: ProofPresHeader, @@ -256,12 +256,12 @@ data NameClaimProof = NameClaimProof } deriving (Eq, Show) --- the bytes a name proof signs over: the claimed name bound to its presentation context +-- the bytes a name proof signs over: the claimed name together with the link it is shown through nameProofPayload :: SimplexNameInfo -> ProofPresHeader -> ByteString nameProofPayload name presHeader = strEncode name <> strEncode presHeader --- mint a name proof: sign (name, presentation context) with the address owner key. --- linkOwnerId names the signing owner in the link's owner chain (Nothing = root key, the contact-address case). +-- make a name proof: sign (the name and the link it is shown through) with the address owner key. +-- linkOwnerId names the signing owner in the link's owner chain (Nothing = root key, used for a contact address). signNameProof :: C.PrivateKeyEd25519 -> Maybe OwnerId -> SimplexNameInfo -> ProofPresHeader -> NameClaimProof signNameProof key linkOwnerId name presHeader = NameClaimProof @@ -271,12 +271,13 @@ signNameProof key linkOwnerId name presHeader = } -- verify a name proof's signature against the resolved address owner key. The caller must --- SEPARATELY check the proof's presHeader link is the link it is presented through (anti-replay). +-- SEPARATELY check the proof's presHeader link is the one it was shown through, so a proof made +-- for one link can't be reused on another. verifyNameProofSig :: C.PublicKeyEd25519 -> SimplexNameInfo -> NameClaimProof -> Bool verifyNameProofSig ownerKey name NameClaimProof {presHeader, signature} = C.verify' ownerKey signature (nameProofPayload name presHeader) --- the link a proof is bound to (its anti-replay context), if any +-- the link a proof is tied to (the one it can be checked against), if any proofPresHeaderLink :: ProofPresHeader -> Maybe AConnShortLink proofPresHeaderLink = \case PHSimplexLink lnk -> Just lnk diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index be17a7ea41..99a50f26cd 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4775,7 +4775,7 @@ processChatCommand cxt nm = \case liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12 -- | Dispatch a resolved NameRecord by eagerly preparing a contact/group row --- (the resolved link's embedded profile carries contact_domain / group_domain), +-- (the resolved link's embedded profile has contact_domain / group_domain), -- then returning the same plan shape ('CAPKnown' / 'GLPKnown') the -- local-store-hit branch of 'connectPlanName' returns, so the resolver hit -- reuses the same DB write path as a local-prepare hit. @@ -4801,13 +4801,13 @@ dispatchResolvedRecord cxt nm user ni@SimplexNameInfo {nameType} NameRecord {nrS (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' ContactShortLinkData {profile} <- liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode contact profile from RSLV link") pure - -- consent gate: the resolved profile must claim the resolved name, else "name unknown" + -- the resolved profile must actually claim this name, otherwise "name unknown" let Profile {contactDomain} = profile unless (contactDomain == Just (StrJSON ni)) $ throwChatError $ CESimplexNameNotFound ni let ccLink = CCLink cReq (Just l') accLink = ACCL SCMContact ccLink ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink Nothing - -- born verified: connecting via the resolved name verifies it + -- created as verified — connecting by the resolved name verifies it let Contact {contactId = cId} = ct withStore' $ \db -> setContactDomainVerified db user cId True pure (accLink, CPContactAddress (CAPKnown ct)) @@ -4817,7 +4817,7 @@ dispatchResolvedRecord cxt nm user ni@SimplexNameInfo {nameType} NameRecord {nrS (FixedLinkData {linkConnReq = cReq}, cData@(ContactLinkData _ UserContactData {direct})) <- getShortLinkConnReq' nm user l' GroupShortLinkData {groupProfile, publicGroupData = publicGroupData_} <- liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode group profile from RSLV link") pure - -- consent gate: the resolved group profile must claim the resolved name + -- the resolved group profile must actually claim this name let GroupProfile {publicGroup} = groupProfile unless ((publicGroup >>= publicGroupAccess >>= groupDomain) == Just (StrJSON ni)) $ throwChatError $ CESimplexNameNotFound ni let publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ @@ -4826,12 +4826,12 @@ dispatchResolvedRecord cxt nm user ni@SimplexNameInfo {nameType} NameRecord {nrS gVar <- asks random let ccLink = CCLink cReq (Just l') (g, _hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile False ccLink Nothing useRelays subRole publicMemberCount_ - -- born verified: connecting via the resolved name verifies it + -- created as verified — connecting by the resolved name verifies it let GroupInfo {groupId} = g withStore' $ \db -> setGroupDomainVerified db user groupId True pure (ACCL SCMContact ccLink, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) -- Mirror the inline 'serverShortLink' helper defined in 'processChatCommand' - -- where this dispatch is invoked: RSLV-supplied short links may carry the + -- where this dispatch is invoked: RSLV-supplied short links may use the -- agent's simplex:/ scheme, but the prepared row stores hostname-scheme, -- and the connect-plan / known-link lookups assume the hostname form. serverShortLink' :: ConnShortLink m -> ConnShortLink m @@ -4854,8 +4854,9 @@ firstNameLink nameType simplexChannel simplexContact ni = NTPublicGroup -> simplexChannel NTContact -> simplexContact --- sign the channel's name claim with the owner's member key, bound to the channel link --- (linkOwnerId = Just memberId — channels have delegated owners). No-op without a name or keys. +-- sign the channel's name claim with the owner's member key, tied to the channel link +-- (linkOwnerId = Just memberId — a channel is signed by its owner, not by the address itself). +-- Does nothing without a name or keys. signChannelNameProof :: GroupInfo -> PublicGroupProfile -> PublicGroupAccess -> PublicGroupAccess signChannelNameProof GroupInfo {groupKeys, membership = GroupMember {memberId = MemberId mid}} PublicGroupProfile {groupLink} access@PublicGroupAccess {groupDomain} = case (groupDomain, groupKeys) of @@ -4870,7 +4871,7 @@ signChannelNameProof GroupInfo {groupKeys, membership = GroupMember {memberId = data NameVerifyOutcome = NVOVerified | NVOFailed Text | NVOInconclusive Text -- §4.6: a name verifies when the proof's presHeader is the link the peer was actually connected through --- (anti-replay) AND the proof is signed by the key the name resolves to (the address root key, or the +-- (so the proof can't be reused on a different link) AND the proof is signed by the key the name resolves to (the address root key, or the -- channel owner's key selected by linkOwnerId). The key comes from *resolving the name* (the address), -- not the connected link — for a 1-time invite they differ. A network/agent error propagates as -- ChatErrorAgent so a UI-triggered verify can retry, rather than recording a false verdict. @@ -4898,7 +4899,7 @@ verifyName user nm claim connLink_ proof_ = case (proof_, connLink_) of then NVOVerified else NVOFailed "the name resolves to a different address — its owner did not sign this name proof" --- the proof must be bound (anti-replay) to the link the peer was connected through +-- the proof must be tied to the link the peer connected through, so it can't be reused on a different link proofBoundTo :: NameClaimProof -> AConnShortLink -> Bool proofBoundTo NameClaimProof {presHeader} connLink = maybe False (`sameConnShortLink` connLink) (proofPresHeaderLink presHeader) @@ -4931,7 +4932,7 @@ nameVerifyReason = \case NVOInconclusive r -> Just r -- | Verify a contact's claimed name (§4.6): persist the 3-state status and return the updated contact --- with a Nothing/Just-reason result. Network/resolver failures surface as ChatErrorAgent (retryable). +-- with a Nothing/Just-reason result. Network/resolver failures are reported as ChatErrorAgent (retryable). apiVerifyContactName :: User -> NetworkRequestMode -> ContactId -> CM ChatResponse apiVerifyContactName user nm contactId = do cxt <- chatStoreCxt diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 14d157cf10..429848c14b 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -382,7 +382,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. -- SEDBBusyError will be re-thrown as CRITICAL (via `critical`) as it indicates a transient lock/IO - -- condition that usually resolves after app restart. Other SEDBException flavours surface as + -- condition that usually resolves after app restart. Other SEDBException kinds show up as -- non-CRITICAL store errors. entity <- critical agentConnId $ withStore (\db -> getConnectionEntity db cxt user $ AgentConnId agentConnId) >>= updateConnStatus case agentMessage of diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 494ad0b100..1eecefa069 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -563,7 +563,7 @@ deleteUnusedProfile_ db userId profileId = -- | Updates the contact profile, writing the peer's contact_domain claim onto -- the profile. Resets contact_domain_verification to NULL only when the claimed -- name changes (the prior verification was tied to the prior claim); an XInfo --- carrying the same name keeps the existing status, exactly as a badge does. No +-- with the same name keeps the existing status, exactly as a badge does. No -- conflict clearing (no UNIQUE index). updateContactProfile :: DB.Connection -> StoreCxt -> User -> Contact -> Profile -> ExceptT StoreError IO Contact updateContactProfile db cxt user@User {userId} c p' = do @@ -577,7 +577,7 @@ updateContactProfile db cxt user@User {userId} c p' = do Profile {displayName = newName, contactDomain, preferences} = p' mergedPreferences = contactUserPreferences user userPreferences preferences $ contactConnIncognito c claimChanged = prevDomain /= (unStrJSON <$> contactDomain) - -- a name proof never rides an XInfo peer-send (we never mint or accept a PHTest name proof): + -- a name proof is never sent in a profile update (XInfo) from the peer (we never make or accept a PHTest name proof): -- keep the stored proof when the name is unchanged, drop it when the name changes (then it's unverified) p'' = (p' :: Profile) {contactDomainProof = if claimChanged then Nothing else prevProof} clearVerificationIfClaimChanged = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 198e393629..635c2fabce 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -796,7 +796,7 @@ toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, cont fromLocalProfile :: LocalProfile -> Profile fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, contactDomain} = - -- contactDomainProof is generated fresh at send (presentUserBadge) / dropped by redaction, never carried from the stored profile + -- contactDomainProof is generated fresh at send (presentUserBadge) / dropped by redaction, never copied from the stored profile Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge, contactDomain = StrJSON <$> contactDomain, contactDomainProof = Nothing} where -- any stored peer proof rides the wire (receivers verify independently); the own credential is presented fresh, and a display-only badge never sends