Skip to content

docs: scope client branding (product name + logo to id.ai), session-bound contract - #103

Open
aterga wants to merge 3 commits into
mainfrom
claude/client-branding-proposal
Open

aterga wants to merge 3 commits into
mainfrom
claude/client-branding-proposal

Conversation

@aterga

@aterga aterga commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A scoping doc, docs/scoping-client-branding.md, for surfacing the MCP client's product identity to Internet Identity. The id.ai consent screen can then show which vetted product will receive the access a connect grants: a name and a logo. This PR is docs-only. The doc is the boundary contract for both sides; the server side is implemented in #200.

Revised (2026-09-24). The first draft had the server put the product slug in the connect-link fragment (&connector=<slug>) and serve an unbound GET {issuer}/branding/{slug}. Whoever drives the browser writes the fragment, so that let a consent screen vouch for a product the server never tied to the connect. The contract now binds branding to the connect itself:

authorize:  MCP server --302 to id.ai/mcp#callback=…&state=…&ttl=…&registration_key=…-->  id.ai   (link unchanged)
lookup:     id.ai --GET {issuer}/branding?state=<state>-->  MCP server
            MCP server --200 {name, logo, verified} | 404, no-store-->  id.ai
logo:       id.ai --<img src="{issuer}/branding/<slug>/logo">-->  MCP server (static SVG)
consent:    id.ai shows the name + logo only if the callback origin is on II's own trusted list

Key points

  • Session-bound. The server answers from the connect's validated redirect_uri, recorded at authorize. It never trusts anything the link or the client asserts.
    • Loopback (native-app) connects get 404, as do unlisted hosts, operator-added allow-list entries, and unknown or expired states. All these 404s are identical.
  • What branding vouches for. The recorded redirect names where the authorization code is delivered. It does not name who started the connect, or which account at that vendor ends up holding the grant. The doc works through the residual risks that follow: a leaked code, a hostile local app reading browser history, and a connect started from an attacker's own vendor account.
  • II requirements (6.2):
    1. derive {issuer} from the #4091-validated callback with a strict parsed check;
    2. accept only a well-formed 200 and treat everything else as "no branding";
    3. use one parsed callback and one state for the #4091 check, the lookup, and the return navigation;
    4. gate the whole treatment (name, logo, and badge) on an exact-origin trust list kept by II, since #4091 proves only that an origin declared the callback;
    5. render safely: plain-text name, <img>-only logo from the callback's origin, CSP, wording about where the access goes;
    6. keep the connector distinct from the server's own origin and app name.
  • Vetting. Only compiled-in callbacks (DEFAULT_ALLOWED_REDIRECTS) are branded. Nothing keys on client_id (DCR ids churn; CIMD ids are the client's own claim). v1 covers redirect-identified web connectors; native apps stay anonymous until a signed software_statement path exists.
  • Accuracy. Code is cited by symbol, not line number. The doc was adversarially checked against Surface vetted-connector branding to Internet Identity, bound to the authorization session #200's code in two passes before this push.

Related

Testing

Docs-only; there is no build or test impact. The server-side behavior it describes is covered by #200's tests, listed in section 9 of the doc.

🤖 Generated with Claude Code

A scoping doc for surfacing the requesting MCP client's product identity to
Internet Identity, so the id.ai consent screen can show which vetted product
is connecting (a name and a logo) instead of an anonymous bridge.

The design:
- The MCP server puts a curated product-name slug in the II connect-link
  fragment, and serves the matching name and logo from two GET endpoints on
  the same origin II already validates for the #4091 callback check.
- Branding is shown for vetted products only; the name and logo are curated
  server-side and never taken from client-supplied DCR metadata (open DCR is
  unauthenticated, so rendering client strings would be a consent-phishing
  vector).
- Nothing keys on the OAuth client_id: those are per-registration UUIDs that
  churn on reinstall, expiry, and LRU eviction. Product identity is derived
  from the vetted redirect vendor instead (the hosted-redirect allow-list),
  which is stable and self-authenticating.
- v1 covers self-authenticating web connectors only. Native/desktop apps
  authenticate over loopback (no verifiable identity), so they stay anonymous
  until a signed software_statement path lands (deferred).

Docs-only; requires a coordinated counterpart change on the id.ai side, which
the doc specifies as the boundary contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
@aterga
aterga requested a balanced review from Copilot July 29, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a scoping/design document defining a proposed “client branding” extension for MCP→Internet Identity authorization, allowing id.ai to show a vetted connector’s product name and logo on the consent screen by using a curated server-side product slug and same-origin endpoints.

Changes:

  • Add docs/scoping-client-branding.md describing the proposed contract/flow, vetting model, endpoints, security considerations, and phased rollout plan.
Comments suppressed due to low confidence (4)

docs/scoping-client-branding.md:55

  • The POST /oauth/register citation is off: the handler is register at src/auth.rs:1897 (the current 1888 line is inside granted_grant_types). Please update the reference to match the actual endpoint implementation.
Dynamic client registration is **unauthenticated and open** (`POST /oauth/register` takes all
callers, `auth.rs:1888`). So any client-supplied `client_name` / `logo_uri` is attacker-
controlled. Rendering it on a consent screen would be a phishing gift: a hostile client

docs/scoping-client-branding.md:70

  • The include_str! reference for the inlined DFINITY logo points to auth.rs:1161, but the logo include is currently CONNECT_LOGO_SVG at src/auth.rs:1170 (1161 is in the preceding doc/comment block). Updating the citation will keep the “verified” claim accurate.
  client `logo_uri`. Logos are compiled-in assets (like the DFINITY logo already inlined into the
  callback page via `include_str!`, `auth.rs:1161`).

docs/scoping-client-branding.md:77

  • The client_id minting citation appears to be pointing at the invalid-redirect error branch (src/auth.rs:1913), not where the client-{uuid_v4} value is created (src/auth.rs:1922). Updating the line reference will prevent confusion when someone goes to verify the claim.
- Minted fresh per registration: `client-{uuid_v4}` (`auth.rs:1913`), a new random value each
  time anyone registers.

docs/scoping-client-branding.md:185

  • This section says the /branding responses are “cacheable”, but earlier the metadata endpoint is specified as no-store (and later you reiterate no-store for the JSON). Consider clarifying that only the logo bytes are intended to be cacheable, while the JSON metadata should be no-store (or adjust the endpoint contract if you intend caching for both).
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
  is a `404`. Responses are small, cacheable, and served from compiled-in assets.
- **Logo rendering on II (the one II-side must).** SVG is acceptable as the logo format, but only
  if II renders it through a fixed-size `<img src=...>`. It must never inline server-returned SVG
  into the consent DOM: inlined SVG can execute script, an `<img>`-loaded SVG cannot. (A raster
  logo would sidestep the concern entirely, but `<img>`-rendered SVG is safe and keeps the crisp
  vector mark.)
- **CORS / caching** mirror the `#4091` well-known: `Access-Control-Allow-Origin` for the JSON
  metadata (II uses `fetch`), and `no-store` so an intermediary cannot serve a mapping stale after
  a curation change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/scoping-client-branding.md Resolved
Copilot noted the "verified, file:line" citations had drifted from
src/auth.rs. The doc's numbers were taken from the pre-merge #98 branch;
after #98 (the MCP05 redirect fix) merged, everything below
redirect_uri_permitted shifted by about nine lines. Re-synced every
citation against current main and verified each against the source:

  authorize            865        -> 874
  validate_client      890 (call) -> 759 (definition)
  #4091 well-known     1082-1109  -> 1091-1118
  error copy           923        -> 932
  ii_mcp_url           1062       -> 1071
  connect_callback_url 1088       -> 1097
  auth_callbacks       1097       -> 1106
  logo include_str!    1161       -> 1170
  register             1888       -> 1897
  client-{uuid}        1913       -> 1922

Unchanged (above the shift): make_room_for_client (340),
DEFAULT_ALLOWED_REDIRECTS (399), the path-pin comment (383-391), and the
loopback-exempt note (393).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
@aterga
aterga marked this pull request as ready for review July 30, 2026 11:54
@aterga
aterga requested review from a team and Copilot July 30, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

docs/scoping-client-branding.md:20

  • This section says “Two decisions frame everything below,” but it lists three bullets. Update the count to match to avoid confusion in the scoping doc.
Two decisions frame everything below:

docs/scoping-client-branding.md:177

  • This bullet says responses are “cacheable,” but a few lines later the doc specifies no-store for the JSON metadata. Clarify which endpoint(s) are intended to be cacheable vs explicitly non-cacheable so implementers don’t pick conflicting headers.
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
  is a `404`. Responses are small, cacheable, and served from compiled-in assets.

@aterga aterga left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from the II-side design point of view

Answering §10, since it's addressed to the II team. Grounded in the current II frontend/backend (the id.ai/mcp connect flow, the declared-callback allow-list, and the production CSP).

The identity/trust model is right — no changes needed there

  • Curated, server-side product identity, never client-supplied. Correct. II isn't a party to the client↔server OAuth, so anything the client can set (DCR client_name, logo_uri) is unauthenticated and phishable. Deriving identity from the vetted redirect vendor is a sound proxy: II's declared-callback allow-list (fetch of /.well-known/ii-auth-callbacks, fail-closed, exact match) already means a connect can only reach us over an origin that published that manifest, so the vendor is effectively attested by the same mechanism.
  • Product slug, not client_id. Correct. The connect link is attacker-craftable and client-{uuid_v4} churns / is LRU-evicted, so a client_id in the fragment would be a spoofable, unstable display key. A closed slug space resolved server-side is the right identifier.
  • Additive / backward-compatible contract. Confirmed on our end — the link parser ignores unknown fragment params today, so an added connector is safe for old II, and old servers simply omit it. The only II code cost is one field added to the link schema.

The one real gap: logo transport will hit II's CSP (§10 Q2)

The doc's model — II renders '&lt;img src="{issuer}/branding/{slug}/logo"&gt;' pointing at the callback origin — is blocked by II's Content-Security-Policy. This is the main thing to revise.

II's production CSP is:

img-src 'self' data: https://*.googleusercontent.com;
connect-src 'self' https:;

So:

  • A cross-origin '&lt;img src="https://&lt;callback-origin&gt;/…/logo"&gt;' won't load — img-src allows only 'self', data:, and one fixed Google-avatar host. We don't want to widen img-src to https:, since that reopens exactly the cross-origin image surface we keep closed.
  • fetch() to any HTTPS origin is allowed (connect-src 'self' https:).

Recommended contract instead: II fetch()es the branding from the trust-confirmed callback origin and renders the logo as a data: URI (already permitted by img-src data:, no CSP change). Simplest shape: serve one metadata document with the logo inlined as a bounded base64 data: image, so II makes a single request to a single origin and never needs a second cross-origin image hop. That also answers §10 Q4 — one doc with an inline logo beats separate name/logo requests for us.

Note this flips one assumption in the doc: "No CORS needed for <img>" doesn't apply, because we reach the logo via fetch() (which does need the CORS header you already specify) and then render data:.

SVG vs raster (§10 Q2 cont.)

The doc's point that an <img>-loaded SVG can't execute script is technically true. But once II is building a data: URI from fetched bytes, I'd steer away from SVG: it keeps a latent "never inline this" invariant that future contributors won't know, whereas raster (PNG) is strictly safer and removes the question entirely. Recommend the curated logos be PNG, with II applying a raster-only content-type allow-list, a byte-size cap, a decode step, and a fail-closed fallback to the anonymous icon on any validation failure — i.e. apply the same hardening the doc reserves for a hypothetical Phase-3 open path, in v1, because II is the side putting bytes on screen.

Trust ordering — please state it explicitly

connector={slug} lives in the fragment, which is attacker-craftable (a malicious link can claim &connector=claude). Safety comes entirely from where II fetches branding: only from the callback origin, and only after that origin is trust-confirmed — i.e. after the certified trusted_url gate confirms the server origin, and after the callback is matched against the origin's declared allow-list. The slug is display-only routing; the authority for "is this really ChatGPT" is that the branding came from an origin II independently trust-confirmed, never the fragment's claimed value. Worth spelling out so the II implementation doesn't fetch branding on the honest-untrusted / unverified path.

§10, point by point

  1. Fragment param + endpoint paths — connector={slug} is fine (additive; parser ignores unknowns). Prefer a single metadata endpoint with an inline logo over a separate /logo route.
  2. Render logo via fixed-size '&lt;img src=…&gt;'? Not cross-origin (CSP blocks it). Yes via fetch() + data:, or an inline base64 data: logo in the JSON — and please make it raster, not SVG.
  3. "Verified connector" treatment + name delivery — deliver the name in the JSON, not mirrored in the fragment (fragment is untrusted; JSON comes from the trust-confirmed origin). The exact verified-vs-anonymous consent UI is ours to specify and I'll take it to the II design owners; the security contract we need from the server is: branding is shown ⇔ trust-confirmed origin + declared callback + valid slug + valid logo; otherwise fall back to the anonymous screen.
  4. Single doc vs. separate requests — single metadata document with the logo inlined as base64 data:. One request, one origin, no CSP friction.

Minor

  • Security section is solid: no client-supplied content, no SSRF in v1 (logos bundled at build time), closed slug space with 404 otherwise. If the Phase-3 open self-service path is ever built, the SSRF-pinned client + https-only + size/dimension caps + raster-only + decode-then-re-encode is exactly right — but the rendering hardening (size cap / raster / fail-closed) should be II-side and present from v1, not deferred.
  • Keeping native/desktop (loopback-redirect) connectors anonymous until a signed software_statement is the correct v1 cut — those are exempt from the callback allow-list, so there's no vetted-vendor anchor to derive identity from.

Bottom line: the trust model needs no changes. The one substantive revision is the logo transport — replace "II renders a cross-origin <img src>" with "II fetches from the trust-confirmed origin (or reads an inline base64 logo from the metadata JSON) and renders a data: raster image, failing closed to the anonymous screen." That's the only part that breaks on contact with II's CSP.


Generated by Claude Code

aterga added a commit that referenced this pull request Aug 18, 2026
Address the review pass on the scoping doc:

* Fix stale code citations to the current snapshot: get_info
  (tools.rs:1883), oql_needs_origin_error (1495), err (2287), the
  resource-not-found calls (2067/2078), and the skill:// resource
  generation loop (2033-2045).
* Replace the reference to a non-existent `scoping-client-branding.md`
  with the client-branding scoping proposal (PR #103).
* Correct the CIMD SSRF note: `markdown_url_for_base` is NOT a reusable
  SSRF guard (host-string compare only, ignores ports, no private-IP
  rejection; its 169.254 rejection is incidental to the fixed skills
  origin). A dedicated SSRF-safe fetcher is needed; the closest building
  block is discover.rs's ip_is_global.
* Drop `oql-schema://` from the public-cacheable resources: it is
  caller-gated (requires derivation_origin/account, runs under the user's
  delegated agent), so a public cache keyed only by canister ID could leak
  one identity's schema to another. Keep it a tool or an identity-keyed
  private resource.
* Fix the x-mcp-header section: there is no top-level `network` field or
  destructive-op discriminator to mirror into Mcp-Param-*; annotating
  absent fields would fail the mandated header/body validation. Use
  Mcp-Name for per-operation policy; scope any new param separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aterga added a commit that referenced this pull request Sep 23, 2026
Implements the imcp2 side of the client-branding extension
(docs/scoping-client-branding.md, PR #103): let II show WHICH vetted
product is asking for a connect, instead of an anonymous "some bridge".

- New `branding` module: a server-curated table of vetted connectors
  (ChatGPT, Claude, Cursor, Grok, Perplexity, Google Antigravity), keyed on
  the request's already-validated `redirect_uri` vendor — never on the
  client-supplied `client_name`/`logo_uri`, which open DCR makes
  attacker-controlled (rendering them would be a consent-phishing gift). A
  connect that doesn't resolve to a vetted web vendor — including every
  loopback (native-app) redirect — keeps the status-quo anonymous screen.
- `authorize` resolves the connector for the validated redirect and passes
  its slug on the II connect link as `&connector=<slug>` (additive fragment
  param; an II that doesn't know it ignores it).
- Two issuer-rooted GET endpoints II fetches same-origin with the
  #4091-validated callback: `/branding/{slug}` (name + absolute logo URL +
  `verified`, `no-store`) and `/branding/{slug}/logo` (SVG, `nosniff`).
  Both 404 outside the curated set so the path can't probe.

The bundled logos are ORIGINAL neutral placeholders (a generic "verified
connector" mark), deliberately not reproductions of vendor logos — each
vendor's own licensed mark is dropped into its `Connector::logo` later.
Actually rendering this needs II's counterpart change (parse the slug,
fetch these endpoints, render); until then it is inert and harmless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first draft put the product slug in the connect-link fragment
(`&connector=<slug>`) and served an unbound `GET {issuer}/branding/{slug}`.
The fragment is written by whoever drives the browser, so that let a
consent screen vouch for a product the server never tied to the connect.

The contract now matches what #200 implements: II asks
`GET {issuer}/branding?state=` and the server answers from the connect's
validated redirect, with nothing about branding in the link. The revision
also:

- states precisely what branding vouches for (where the authorization code
  is delivered, not who started the connect or which vendor account ends up
  holding the grant) and the residual risks that follow;
- spells out the II side as numbered requirements: issuer derivation from
  the validated callback, response acceptance, one callback and one state
  for everything, an exact-origin trust list gating the whole treatment,
  and safe rendering (CSP, <img>, wording);
- notes that branding covers only compiled-in callbacks, never operator
  allow-list entries, and documents the logo hardening;
- replaces line-number citations with symbol references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 24, 2026 15:04
@aterga aterga changed the title docs: scope client-branding extension (product name + logo to id.ai) docs: scope client branding (product name + logo to id.ai), session-bound contract Sep 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The security-sensitive contract spans imcp2, a separate implementation PR, and future Internet Identity changes requiring human validation.

Review effort: Balanced
Findings: None

aterga added a commit that referenced this pull request Oct 2, 2026
CIMD shipped in #191 (opt-in) and #203 (on by default, with
OAUTH_CIMD_ENABLED as a kill switch), so the scoping plan now records
what was built instead of proposing it:

- the implemented gate, flow, fetcher, validation, caching, single-flight
  and in-flight bounds, configuration and rollback, by symbol rather than
  by line number;
- answers to the plan's open questions, and where the build departed from
  the plan: a domain-and-subdomain gate instead of exact origins, no cache
  floor or ETag revalidation, the same-origin redirect rule, discovery's
  SSRF guard tightened rather than left unchanged, and the rate cap
  dropped on purpose;
- branding moved to #103/#200 and keyed on the validated redirect. A
  client_id-domain key would have let any local program pose as a
  loopback CIMD client of a vetted vendor;
- the DoS residuals as built, the tests that pin the behaviour, and an
  index of where it lives.

Section numbers 3.4 and 3.5, which the code cites, keep their meaning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aterga added a commit that referenced this pull request Oct 5, 2026
… and #203) (#143)

* docs: scope CIMD (Client ID Metadata Documents) to replace open DCR

Expands the top-ranked alignment improvement into an implementable plan.
Recommends a trust-policy-gated, additive design: fetch a Client ID
Metadata Document only when the client_id URL's host is already on the
curated vendor allow-list, keep open DCR for everything else, and never
trust the document's display fields. This collapses the new outbound-fetch
surface to a finite set of vetted hosts instead of an arbitrary-URL SSRF
primitive on the unauthenticated /authorize path, while still delivering
spec alignment and a DNS/TLS-authenticated domain key for branding.

Folds in the alignment PR's review correction: skills.rs's
markdown_url_for_base is NOT a usable SSRF guard (host-string compare
only); the real building block is discover.rs's address-pinned fetcher
(resolve_public_url + site_client + ssrf_redirect_policy), which the plan
extracts into a shared module as its Phase 0. Covers the fetch/validate/
cache flow, allow-list re-keying, branding subsumption, a security
analysis, phasing, and open questions — all cited against current code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CIMD doc: origin-exact gating, strict reader, string match, mandatory rate cap

Address the review on the CIMD scoping doc:

* Trust policy matches exact HTTPS ORIGINS, not bare hosts. resolve_public_url
  uses the caller-supplied port (discover.rs:1113), so a host-only gate would
  let https://claude.ai:8443/... reach an unvetted port; require the default
  443 (and reject userinfo/other non-canonical authority) before any DNS/fetch.
* CIMD must use a STRICT capped reader, not discovery's best-effort one
  (discover.rs:1207-1228 returns lossy/partial text without signaling — a
  truncated body whose prefix is valid JSON would be accepted as metadata at
  the auth boundary). Fail closed on over-limit/stream-error/invalid-UTF-8.
* client_id validation is a plain STRING match against the requested URL — no
  normalization (hosted redirects use exact string membership, auth.rs:648, and
  normalizing would mint aliases that disagree with the raw-URL cache key).
* Make the outbound-DoS control mandatory: the cache does NOT bound misses,
  because an attacker can vary the URL PATH on a vetted host to mint unlimited
  distinct keys and concurrent 15s fetches. A per-host + global concurrency/rate
  cap (plus negative-caching) is now a Phase 1 acceptance criterion, not an
  optional nicety.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CIMD doc: disable cross-origin redirects (discovery policy leaks the gate)

Review fix: the doc said to keep discovery's redirect policy, but
ssrf_redirect_policy / redirect_hop_ok (discover.rs:1145-1153) follow any
global-IP literal and any same-host hop WITHOUT a port check — so a vetted
client_id could redirect the fetch to an unvetted public IP or to
vetted-host:8443, escaping the exact-origin gate of §3.1. A CIMD is served
directly at its URL, so §3.3 now says to disable redirects entirely (or, if
truly needed, require an exact same-origin hop), never the discovery policy.
§5's SSRF bullet notes the control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* CIMD doc: never negative-cache per-request redirect failures (poisoning)

Review fix: §3.4 folded request-specific redirect checks into "validation,"
so negative-caching every validation failure by URL would let an attacker
poison a valid client — request a real CIMD URL with a non-member
redirect_uri, the URL gets cached as invalid, and legitimate redirects then
hit the negative entry. Split validation into document-intrinsic (cacheable:
fetch/JSON/client_id==URL) and per-request (never cached: redirect membership
+ redirect_uri_permitted, re-run every request against the positively-cached
document). §3.5 and the §5 DoS bullet now negative-cache only
document-intrinsic failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: turn the CIMD scoping into a design record of what shipped

CIMD shipped in #191 (opt-in) and #203 (on by default, with
OAUTH_CIMD_ENABLED as a kill switch), so the scoping plan now records
what was built instead of proposing it:

- the implemented gate, flow, fetcher, validation, caching, single-flight
  and in-flight bounds, configuration and rollback, by symbol rather than
  by line number;
- answers to the plan's open questions, and where the build departed from
  the plan: a domain-and-subdomain gate instead of exact origins, no cache
  floor or ETag revalidation, the same-origin redirect rule, discovery's
  SSRF guard tightened rather than left unchanged, and the rate cap
  dropped on purpose;
- branding moved to #103/#200 and keyed on the validated redirect. A
  client_id-domain key would have let any local program pose as a
  loopback CIMD client of a vetted vendor;
- the DoS residuals as built, the tests that pin the behaviour, and an
  index of where it lives.

Section numbers 3.4 and 3.5, which the code cites, keep their meaning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CIMD record: untrusted URLs are refused, eviction is by expiry, withdrawals wait for expiry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CIMD record: log sampling protects the default (info) level, not debug

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aterga added a commit that referenced this pull request Nov 10, 2026
…authorization session (#200)

* Surface vetted-connector branding to Internet Identity (name + logo)

Implements the imcp2 side of the client-branding extension
(docs/scoping-client-branding.md, PR #103): let II show WHICH vetted
product is asking for a connect, instead of an anonymous "some bridge".

- New `branding` module: a server-curated table of vetted connectors
  (ChatGPT, Claude, Cursor, Grok, Perplexity, Google Antigravity), keyed on
  the request's already-validated `redirect_uri` vendor — never on the
  client-supplied `client_name`/`logo_uri`, which open DCR makes
  attacker-controlled (rendering them would be a consent-phishing gift). A
  connect that doesn't resolve to a vetted web vendor — including every
  loopback (native-app) redirect — keeps the status-quo anonymous screen.
- `authorize` resolves the connector for the validated redirect and passes
  its slug on the II connect link as `&connector=<slug>` (additive fragment
  param; an II that doesn't know it ignores it).
- Two issuer-rooted GET endpoints II fetches same-origin with the
  #4091-validated callback: `/branding/{slug}` (name + absolute logo URL +
  `verified`, `no-store`) and `/branding/{slug}/logo` (SVG, `nosniff`).
  Both 404 outside the curated set so the path can't probe.

The bundled logos are ORIGINAL neutral placeholders (a generic "verified
connector" mark), deliberately not reproductions of vendor logos — each
vendor's own licensed mark is dropped into its `Connector::logo` later.
Actually rendering this needs II's counterpart change (parse the slug,
fetch these endpoints, render); until then it is inert and harmless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Bind connector branding to the authorization session

The first cut let the connect-link fragment carry `&connector=<slug>` and
served `GET {issuer}/branding/{slug}` unconditionally. The fragment is
craftable by any native client that drives the navigation, so a loopback
session could have presented itself to Internet Identity as "verified
Claude".

Branding is now answered per session: II asks `GET {issuer}/branding?state=`
with the `state` it already holds, and the server derives the connector from
that pending, unexpired connect's validated `redirect_uri`. Nothing in the
fragment asserts branding (the connector param is gone from `ii_mcp_url`),
and a loopback or unlisted session gets the same 404 as an unknown one. The
lookup is read-only, so it cannot disturb the handshake.

Branding and redirect validation now share one host rule (`host_key` +
`host_is_or_under`), so a trailing-dot `claude.ai.` redirect, which
validation accepts, brands as Claude instead of reading as anonymous; the
allow-list parser and the CIMD trust match use the same helpers. A redirect
validation would refuse never resolves to a connector, and a test holds the
curated domains equal to the compiled-in allow-list vendors.

The DCR router test now drives the 200 path over HTTP (authorize -> the
`state` in II's link -> `/branding?state=`), including `no-store` and both
claude.ai spellings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Branding docs: spell out the validation invariant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document the branding routes on mcp_router

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Brand only compiled-in callbacks; harden the branding responses

connector_for_redirect accepted anything the effective allow-list admits,
so an OAUTH_ALLOWED_REDIRECT_PREFIXES entry on a vendor's domain (another
path on claude.ai, say) was shown as that vendor, contrary to the docs.
Branding now requires a redirect admitted by a compiled-in
DEFAULT_ALLOWED_REDIRECTS entry, under validation's own rules
(redirect_uri_on_default_allow_list, sharing hosted_redirect_admitted with
redirect_uri_permitted), so a vendor's name only ever rests on the reviewed
callback paths in this repo.

The branding 404 is now no-store like the 200 it stands in for, and the logo
carries a sandboxing CSP: opened top-level it is an SVG document on the
issuer origin, where <img> isolation does not apply. A test keeps every
bundled logo free of script, event handlers, embedded HTML, and external
references, for when licensed vendor marks replace the placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tighten the bundled-logo guard to internal references only

The logo test banned a handful of literal substrings, so a spaced
`href = "https://…"` or a quoted `url('https://…')` got through. It now
strips whitespace first and requires every href, src, and url() target to
be an internal `#…` reference, and also rejects XHTML, animation, and DTDs.
It checks itself against a set of unsafe SVGs, spaced and quoted spellings
included, so the guard can't quietly weaken when licensed vendor marks
replace the placeholder.

The module doc now says what branding vouches for: where the authorization
code is delivered, not who started the connect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pin the over-cap derivation-origin case on multidex.ai, not oisy.com

oisy.com now serves a real /.well-known/ii-derivation-origin
("https://oisy.com"), so it resolves as Declared: the app's own
declaration wins over the built-in registry, as intended. That broke
resolve_app_identity_skips_probe_for_known_apps, which assumed oisy.com
still served its app shell there, and with it CI for every PR.

multidex.ai is a known app that still answers that path with its app
shell (far over MAX_DERIVATION_ORIGIN_BYTES), so the test keeps pinning
both things it was for: a known app is answered by the registry without
an IC probe, and an over-cap body reads as "declares nothing" rather
than a failed check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tighten branding tests and docs after the merge with main

- branding_is_bound_to_the_session now checks both sides of the expiry
  boundary on the paused clock: branding answers one second before
  CONNECT_TTL and stops at it. The read-only check compares `created`
  after time has moved, so a lookup that re-stamped the connect would fail.
- Restore the hosted_redirect_allow_list doc comment, which the new
  operator-entries test had been inserted beneath and taken over.
- Cover ChatGPT's Exact-pinned callback (the redirect it actually
  registers) in resolution and vetting, and a descendant that the pin
  refuses.
- Pin that a malformed /branding query (a repeated `state`) gets the same
  no-store 404 as the other misses, not axum's 400.
- The CONNECTORS doc now says why loopback stays anonymous even with a
  CIMD client_id on a vetted origin; correct a stale description of what
  the DCR round-trip test registers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants