Files
aios/contracts/personal-weixin/v1/SECURITY.md
T

7.0 KiB

Personal Weixin v1 security contract

Status: normative design contract for R10 B6/B7. It authorizes no runtime or production change.

Trust boundaries and ownership

The browser is untrusted. The Portal authenticates the human user and resolves tenant identity from the server session. The Portal adapter is least-privileged: it may request account-scoped operations but must not store provider credentials, tokens, raw iLink state, or OpenClaw protocol state. OpenClaw owns provider credentials and protocol state. The Gateway transport authenticator, not an event-body boolean or identity field, establishes whether an inbound event is authentic.

Every operation is keyed by server-resolved (tenantId, accountId). Entitlements are allow-only, server-owned records. A default account is display preference, never authorization. The browser-visible accountId is an untrusted reference to a server-preallocated account slot: the server MUST resolve it through the authenticated user's tenant and current entitlement on list, readiness, login start, login wait, status, inbound challenge, canary, and rollback checks. Start and every wait poll re-run that resolution. Missing, arbitrary, stale, cross-tenant, or no-longer-entitled values fail closed before account existence or state is disclosed; there is no default/first/configured-account fallback.

Login state and secret handling

sessionKey is a required, nonempty opaque value returned by OpenClaw. The adapter MUST forward its exact bytes unchanged. It MUST NOT trim, normalize, case-fold, derive, regenerate, alias, or substitute the value. A missing or empty sessionKey fails closed and MUST NOT fall back to accountId or any identifier. Durable state stores only a one-way session-key digest where correlation is required; raw sessionKey is not persisted.

verifyCode is accepted only as an authenticated, CSRF-protected wait POST test/input field, matches 4-8 decimal digits, is forwarded once, and is discarded. challengeCode is accepted only as an authenticated Gateway event test/input field, is compared through a one-way digest, and is discarded. Neither secret may be persisted in raw form or appear in URLs, responses, logs, traces, metrics labels, analytics, audit payloads, exceptions, or background-job arguments. Golden fixtures containing these values are synthetic test vectors only and MUST NOT be copied into production telemetry or responses.

QR data URLs are transient no-store response data with a 1 MiB bound and MUST NOT be logged or persisted. Canonical login outcomes are qr_pending, need_verifycode, invalid_verifycode, expired, failed, already_connected, and confirmed. Any unknown upstream status maps fail-closed to canonical failed with errorCode=unknown_upstream_status; raw unknown status is bounded evidence only. Upstream binded_redirect maps only to already_connected with connected=false, alreadyConnected=true, and terminal=true, never to confirmed.

Canonical readiness

Readiness is one live, authenticated snapshot from the Gateway/OpenClaw boundary with one unified response shape for ready, not_ready, and error. It MUST include Gateway reachability/error, exact plugin id/version/digest, selected account identity, configured/enabled/running/last-error state, current tenant/account entitlement, reasons, observation time, and freshness deadline. ready=true is legal only when every required field is present, the snapshot is still fresh, expected plugin version/digest match, the selected account is entitled/configured/enabled/running with null error, and the Gateway is reachable. Missing fields, an old snapshot, digest mismatch, no entitlement, not-running state, unauthenticated evidence, or any contradiction returns ready=false with code=not_ready or code=error and at least one stable reason. Cached, synthetic, file-only, default-account, or service-up-only evidence cannot produce ready.

Authenticated inbound-event challenge

The event body contains exactly eventId, accountId, senderId, chatId, challengeCode, and observedAt. It does not and cannot assert authentication, tenant id, Portal user id, or session key. Those values are absent by contract. Transport authentication MUST cover HTTP method, path, timestamp header, nonce header, and raw body digest; enforce a bounded clock window; durably reserve nonce/event replay keys for at least that window; compare signatures in constant time; and complete before body values reach logs or mutable business state.

After transport authentication, the server hashes the ephemeral challengeCode and resolves the pending durable challenge. That server-created record already owns tenant id, Portal user id, entitled account id, session-key digest, issued/expiry/consumed times, and state (issued, consumed, or expired). The body accountId must exactly match the record and current entitlement; it never supplies tenant/user/session authority. The first valid authenticated event atomically binds its provider senderId and chatId.

Issuance and consumption MUST run in a database transaction with row locking or an equivalent compare-and-swap primitive. issued -> consumed is single-use and atomic. An exact replay of the same authenticated event is idempotent. A changed event payload, sender, chat, account, durable tenant/user/account/session binding, expired or already-consumed record, duplicate event with different payload, missing entitlement, or unauthenticated transport fails closed with zero mutation. Raw challenge codes and raw session keys are never stored.

Unsupported unbind

End-to-end unbind is unsupported in R10. Every public or internal unbind entry point MUST return the exact schema response with HTTP 501, ok=false, supported=false, code=unbind_not_supported, and mutationCount=0. The 501 decision occurs before token deletion, account update, session or challenge mutation, Gateway/OpenClaw call, business-state audit mutation, or background-job enqueue.

Web, canary, and rollback controls

Only exact B6/B7 allowlisted paths in the R10 amendment may change. Portal endpoints require authenticated Django sessions, CSRF protection for mutations, server-scoped object lookup, IDOR tests, rate limits, request-size limits, Cache-Control: no-store, and structured redaction of session keys, verification codes, challenge codes, QR data, tokens, and provider payloads. Stable public errors never expose upstream bodies or cross-tenant existence.

The canary uses one explicitly entitled tenant/account pair and proves restart, concurrent waits, atomic challenge consumption, exact replay, altered replay denial, cross-tenant denial, all canonical statuses/readiness shapes, and zero-mutation 501 behavior. Promotion requires byte-identical artifacts and digests from the verified canary. Rollback restores the immediately preceding byte-identical artifacts/configuration and re-runs canonical readiness; database rollback is forbidden unless an explicitly approved manifest proves backward incompatibility and supplies a separate recovery plan.