Skip to content

fix(pairing): restore qr bootstrap onboarding handoff#58382

Merged
ngutman merged 6 commits into
mainfrom
fix/qr-bootstrap-onboarding-handoff
Mar 31, 2026
Merged

fix(pairing): restore qr bootstrap onboarding handoff#58382
ngutman merged 6 commits into
mainfrom
fix/qr-bootstrap-onboarding-handoff

Conversation

@ngutman

@ngutman ngutman commented Mar 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: /pair qr bootstrap onboarding consumed the one-shot bootstrap token, then left iOS retrying with dead bootstrap auth and no persisted node device-token handoff.
  • Why it matters: fresh iPhone onboarding regressed from auto-approved QR pairing to a stalled flow that needed manual approval and then churned on bootstrap_token_invalid.
  • What changed: the gateway now silently auto-approves only the fresh baseline bootstrap node handshake, preserves empty-scope node token baselines so hello-ok.auth.deviceToken is issued, and iOS now clears bootstrap auth after node connect while preventing the operator loop from reusing bootstrap auth when a stored operator token exists.
  • What did NOT change (scope boundary): bootstrap tokens remain single-use and node-scoped; repairs, role upgrades, scope upgrades, metadata upgrades, and non-baseline bootstrap profiles still require approval.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: bootstrap QR setup codes pair a node with role=node and empty scopes, but the gateway still funneled that fresh bootstrap handshake through the normal pending-pairing flow, consuming the single-use bootstrap token before iOS ever received a reusable device token.
  • Missing detection / guardrail: there was no regression test that forced bootstrap-only auth, asserted bootstrap replay rejection, and then verified device-token reconnect for the QR flow.
  • Prior context (git blame, prior PR, issue, or refactor if known): QR setup codes already switched to one-shot bootstrap tokens; the handshake and iOS reconnect behavior did not fully complete the handoff.
  • Why this regressed now: the one-shot bootstrap path became stricter, but the fresh-node silent-approval and empty-scope node token baseline were not handled consistently across gateway pairing, token issuance, and iOS reconnect auth selection.
  • If unknown, what was ruled out: verified that the regression was not caused by relaxing bootstrap security; the fix keeps the token single-use and narrows auto-approval to the baseline QR bootstrap node case.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/gateway/server.auth.control-ui.suite.ts
    • src/infra/device-pairing.test.ts
    • apps/ios/Tests/NodeAppModelInvokeTests.swift
  • Scenario the test should lock in: fresh bootstrap node pairing auto-approves only for the baseline QR profile, returns a reusable node device token, rejects bootstrap replay, and keeps operator reconnects off bootstrap auth when only stored operator device auth should be used.
  • Why this is the smallest reliable guardrail: the regression crosses gateway auth, pairing persistence, and iOS reconnect behavior, so a gateway seam test plus the smallest persistence/helper tests catches it without requiring full device E2E for every change.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Fresh /pair qr onboarding can complete in one node handshake again without a manual /pair approve step.
  • iOS stops persisting and reusing the consumed bootstrap token after the first successful node connect.
  • Stored operator device auth no longer gets masked by bootstrap-only reconnect auth during QR onboarding.

Diagram (if applicable)

Before:
/pair qr -> iPhone scans -> bootstrap token consumed -> pairing pending/manual approve -> iPhone retries dead bootstrap auth

After:
/pair qr -> iPhone scans -> gateway silently approves fresh baseline node -> hello-ok returns node device token -> iPhone clears bootstrap auth -> reconnects use device token

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) Yes
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation: token handling now clears consumed bootstrap auth sooner and preserves empty-scope node device-token baselines. Auto-approval is still limited to bootstrap-authenticated fresh node connects with role=node, empty scopes, and no existing paired device.

Repro + Verification

Environment

  • OS: macOS 26.3.1
  • Runtime/container: Node 24 / pnpm / Xcode 17E192
  • Model/provider: N/A
  • Integration/channel (if any): Gateway WS + iOS QR onboarding
  • Relevant config (redacted): dev gateway in local mode with LAN bind + TLS enabled

Steps

  1. Generate a QR setup code and scan it from iOS.
  2. Connect with bootstrap-only auth as a fresh node.
  3. Reconnect using the issued device token.

Expected

  • Fresh baseline bootstrap node onboarding auto-approves.
  • Bootstrap replay is rejected.
  • Device-token reconnect succeeds.

Actual

  • Fixed by this PR for the covered gateway + iOS reconnect paths.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • pnpm test -- src/gateway/server.auth.control-ui.test.ts -t bootstrap
    • pnpm test -- src/infra/device-pairing.test.ts
    • pnpm build
    • pnpm check
    • xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 17' -configuration Debug build
  • Edge cases checked:
    • bootstrap replay rejection
    • non-baseline bootstrap operator pairing still requires approval
    • bootstrap-auth role upgrade on an already-paired device still requires approval
    • empty-scope node device-token verify/ensure now works
  • What you did not verify:
    • full real-device iPhone onboarding after this commit (doing that next)
    • full src/gateway/server.auth.control-ui.test.ts file: still failing in this checkout on unrelated pre-existing trusted-proxy/control-ui cases
    • targeted Swift test execution: currently blocked by unrelated pre-existing compile failures in apps/ios/Tests/GatewayConnectionSecurityTests.swift and apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkSystemSpeechSynthesizerTests.swift

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: the silent bootstrap auto-approval path could grow beyond the intended QR baseline handshake.
    • Mitigation: the gate is restricted to bootstrap-authenticated fresh devices with role=node, empty scopes, and no existing paired device, and negative tests lock in non-baseline cases.
  • Risk: clearing bootstrap auth too early could strand reconnects.
    • Mitigation: the gateway now preserves empty-scope node token baselines and the bootstrap-path test verifies replay rejection plus successful device-token reconnect.

@ngutman ngutman requested a review from a team as a code owner March 31, 2026 12:52
@aisle-research-bot

aisle-research-bot Bot commented Mar 31, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Remote silent device pairing allowed via bootstrap token (auto-approval without locality/interactive checks)
1. 🟠 Remote silent device pairing allowed via bootstrap token (auto-approval without locality/interactive checks)
Property Value
Severity High
CWE CWE-285
Location src/gateway/server/ws-connection/message-handler.ts:793-833

Description

The websocket handshake allows silent (auto-approved) device pairing when authenticating with a bootstrap token for a baseline node profile.

  • silent: true triggers an immediate approveDevicePairing(...) call in the handshake (if (pairing.request.silent === true) { approved = await approveDevicePairing(...) }).
  • The new allowSilentBootstrapPairing condition enables this silent/auto-approval path for authMethod === "bootstrap-token" without requiring isLocalClient, Origin checks, or other proximity constraints.
  • A successful pairing yields a long-lived deviceToken (see tests asserting hello-ok auth.deviceToken and reconnect with that token), meaning anyone who can obtain a bootstrap token during its validity window can potentially gain persistent device access by pairing a new device identity.

While bootstrap tokens are intended as onboarding secrets, this change expands their impact from “start pairing flow” to “complete pairing without operator approval” even for remote clients, increasing the blast radius of token leakage/guessing.

Recommendation

Require an additional constraint before enabling silent/auto-approved pairing for bootstrap tokens.

Options (choose one consistent with threat model):

  1. Constrain silent bootstrap pairing to local onboarding only (LAN/loopback), similar to shouldAllowSilentLocalPairing:
const allowSilentBootstrapPairing =
  authMethod === "bootstrap-token" &&
  isLocalClient &&
  reason === "not-paired" &&
  role === "node" &&
  scopes.length === 0 &&
  !existingPairedDevice;
  1. Require interactive approval for bootstrap-token pairing (i.e., remove allowSilentBootstrapPairing), and instead fix the iOS retry behavior by:
  • delaying bootstrap token consumption until pairing is approved, or
  • minting a one-time pairing session identifier that survives approval.
  1. Add proof-of-possession / second factor for silent approval, e.g. a short-lived challenge signed by the operator UI, or a local-only setup channel.

Also consider adding explicit audit logging and rate limiting specifically on bootstrap redemption/pairing attempts, and ensure the resulting deviceToken is scoped/minimized for baseline node access.


Analyzed PR: #58382 at commit 7b74147

Last updated on: 2026-03-31T18:13:53Z

@openclaw-barnacle openclaw-barnacle Bot added app: ios App: ios gateway Gateway runtime size: M maintainer Maintainer-authored PR labels Mar 31, 2026
@greptile-apps

greptile-apps Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the QR bootstrap onboarding regression where the one-shot bootstrap token was consumed before iOS received a reusable device token. The fix spans three layers:

  • Gateway (message-handler.ts): adds a narrow allowSilentBootstrapPairing gate (bootstrap-auth + role=node + empty scopes + unpaired device) and explicitly revokes the bootstrap token after silent auto-approval, so iOS never gets stuck retrying a spent token.
  • Infra (device-bootstrap.ts, device-pairing.ts): verifyDeviceBootstrapToken now binds the token to the first device identity that presents it (cross-device replay rejected, same-device re-verify allowed until explicit revoke); mergeScopes preserves an explicit empty-scope list so node device-token baselines are actually issued.
  • iOS (NodeAppModel.swift, GatewaySettingsStore.swift, GatewayTLSPinning.swift): the operator gateway loop is suppressed when bootstrap-only auth is in use (preventing it from reusing a consumed token); the bootstrap token is cleared from both memory and Keychain after the first successful node connect; onboarding reset now also wipes preferred/last-discovered gateway stable IDs and TLS fingerprints.

Test coverage is solid: the gateway seam test covers auto-approval, replay rejection, and device-token reconnect; unit tests cover shouldStartOperatorGatewayLoop, clearingBootstrapToken, and the mergeScopes empty-baseline fix.

Minor findings:

  • The "already-bound" and "first bind" branches in verifyDeviceBootstrapToken share identical tails — see inline comment for a de-duplication suggestion.
  • Every call to verifyDeviceBootstrapToken for an already-bound token writes the full state file back to disk (only to update lastUsedAtMs). Not a correctness concern for the QR flow, but worth noting for high-retry scenarios.

Confidence Score: 5/5

Safe to merge — no P0 or P1 issues found; the auto-approval gate is tightly scoped and the bootstrap token is properly revoked after use.

All findings are P2 style/cleanup suggestions. The security-relevant invariants (single-use binding, role/scope restrictions on auto-approval, explicit revocation, TTL expiry) are correctly preserved across the gateway, infra, and iOS layers. The test suite directly covers the regression scenarios described in the PR.

No files require special attention.

Important Files Changed

Filename Overview
src/infra/device-bootstrap.ts Bootstrap token verification now binds the token to the first presenting device identity; both binding branches share identical tails (minor duplication) and each write to disk unnecessarily on re-verify.
src/gateway/server/ws-connection/message-handler.ts Adds tightly-gated silent auto-approval for fresh baseline node bootstrap connects and explicit token revocation after approval.
src/infra/device-pairing.ts mergeScopes now preserves an explicit empty-scope list, enabling node device-token baseline issuance.
apps/ios/Sources/Model/NodeAppModel.swift Operator loop suppressed during bootstrap-only auth; bootstrap token cleared from memory and Keychain after first successful node connect.
apps/ios/Sources/Gateway/GatewaySettingsStore.swift Adds clear helpers for preferred/last-discovered gateway stable IDs and bootstrap token.
apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift Adds clearFingerprint and clearAllFingerprints using the separate 'ai.openclaw.tls-pinning' keychain service, safely scoped away from gateway credentials.
apps/ios/Sources/Settings/SettingsTab.swift Onboarding reset now also clears preferred/last-discovered gateway stable IDs and TLS fingerprints.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/device-bootstrap.ts
Line: 216-228

Comment:
**Duplicate state-update branches**

Both the "already-bound to same device" branch and the "first bind" fallthrough block execute exactly the same `state[tokenKey] = { …, lastUsedAtMs }` + `persistState` + `return { ok: true }`. The only behavioural distinction is the mismatch rejection in the early `return`. The shared tail can be de-duplicated:

```suggestion
    if (boundDeviceId || boundPublicKey) {
      if (boundDeviceId !== deviceId || boundPublicKey !== publicKey) {
        return { ok: false, reason: "bootstrap_token_invalid" };
      }
    }
    state[tokenKey] = {
      ...record,
      profile: allowedProfile,
      deviceId,
      publicKey,
      lastUsedAtMs: Date.now(),
    };
    await persistState(state, params.baseDir);
    return { ok: true };
```

Not a bug, but the duplication obscures that the two paths are intentionally identical once the identity check passes.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/infra/device-bootstrap.ts
Line: 220-228

Comment:
**Unnecessary disk write on every repeated same-device verification**

When a token is already bound (`boundDeviceId && boundPublicKey` match), the code still writes the full state back to disk solely to update `lastUsedAtMs`. On the happy-path QR flow this fires at most once, but if iOS retries during a slow network it will trigger redundant I/O under the same lock that serialises all bootstrap operations. Consider skipping the persist when the only change is `lastUsedAtMs` (e.g., a tolerance window or a flag to mark it dirty only when the binding fields change).

How can I resolve this? If you propose a fix, please make it concise.

Reviews (4): Last reviewed commit: "fix(pairing): clear ios tls trust on onb..." | Re-trigger Greptile

Comment thread apps/ios/Sources/Model/NodeAppModel.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8c49d0ea8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/ios/Sources/Model/NodeAppModel.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39e1dd78e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/server/ws-connection/message-handler.ts
@ngutman

ngutman commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

Addressed the follow-up reconnect/auth issues in 161846c:

  • gateway-scoped device auth now preserves A -> B -> A token storage
  • legacy unscoped iOS device tokens can migrate forward only for the last-used gateway, so existing installs keep same-gateway reconnect without reopening cross-gateway reuse
  • bootstrap auto-approval now revokes the bootstrap token after approval

I also resolved the now-fixed review threads for the stale in-memory bootstrap token and the stale cross-gateway operator-token path.

@codex review
@greptile-apps please re-review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 161846c8fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/ios/Sources/Model/NodeAppModel.swift Outdated
@ngutman

ngutman commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

Rescoped the PR to stay tightly focused on the original /pair qr regression.

What changed in c6172a7:

  • removed the gateway-scoped iOS stored-device-auth migration work from this PR
  • kept the QR bootstrap handoff fix itself:
    • narrow silent auto-approve for fresh baseline bootstrap node onboarding
    • empty-scope node pairing/device-token baseline fix
    • iOS bootstrap-token cleanup + operator-loop suppression during bootstrap handoff
  • tightened bootstrap verification so the setup code binds to the first device identity that redeems it before silent approval, and still gets revoked after approval

This should leave the PR focused on restoring the original onboarding flow without the broader cross-gateway token-migration scope.

@codex review
@greptile-apps please re-review
@aisle-research-bot please re-run the security review

@aisle-research-bot

aisle-research-bot Bot commented Mar 31, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Auth signature token precedence differs from auth decision, enabling handshake failures (DoS) when multiple tokens provided
1. 🔵 Auth signature token precedence differs from auth decision, enabling handshake failures (DoS) when multiple tokens provided
Property Value
Severity Low
CWE CWE-287
Location src/gateway/server/ws-connection/handshake-auth-helpers.ts:62-69

Description

resolveConnectAuthDecision() will accept a bootstrapToken before trying a deviceToken, but device signature verification uses a different precedence (tokendeviceTokenbootstrapToken).

This mismatch means a client can present valid credentials yet be rejected at the signature-verification step if it signs the challenge with the token that the auth-decision code won't prioritize (or vice versa).

  • Auth decision precedence: shared secret auth → bootstrap token → device token (see resolveConnectAuthDecision)
  • Signature verification precedence: shared token → device token → bootstrap token (see resolveSignatureToken)

Impact:

  • An attacker (or buggy client) can cause repeated handshake failures by including multiple tokens (e.g., both bootstrapToken and deviceToken) and signing with the non-selected token.
  • This is primarily a denial-of-service / reliability issue during onboarding/pairing flows; it can lock a device into retry loops.

Vulnerable code (signature token precedence):

function resolveSignatureToken(connectParams: ConnectParams): string | null {
  return (
    connectParams.auth?.token ??
    connectParams.auth?.deviceToken ??
    connectParams.auth?.bootstrapToken ??
    null
  );
}

Recommendation

Align signature-token selection with the server-side auth method/precedence, and ideally bind the signature to the exact credential actually used for authorization.

Option A (simplest): match auth decision precedence for signature verification (shared token → bootstrap token → device token):

function resolveSignatureToken(connectParams: ConnectParams): string | null {
  return (
    connectParams.auth?.token ??
    connectParams.auth?.bootstrapToken ??
    connectParams.auth?.deviceToken ??
    null
  );
}

Option B (stronger): after resolveConnectAuthDecision determines authMethod, select the signature token based on that method only (e.g., use bootstrapToken iff authMethod === "bootstrap-token"), so there is no ambiguity when multiple credentials are present.

Also add tests covering combinations where multiple credentials are provided to ensure signature verification and auth decision remain consistent.


Analyzed PR: #58382 at commit c6172a7

Last updated on: 2026-03-31T15:59:35Z

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6172a7530

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

bootstrapToken?: string;
deviceToken?: string;
}) {
return opts?.token ?? opts?.bootstrapToken ?? opts?.deviceToken;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align signature token precedence with gateway connect logic

The helper now signs device payloads with bootstrapToken before deviceToken, but the gateway verifies signatures in resolveSignatureToken using token -> deviceToken -> bootstrapToken and the production client also signs with device-token precedence when both are present. In tests that pass both fields, this helper will generate signatures the server rejects, creating false failures and behavior drift from real clients; the precedence here should match gateway/client logic.

Useful? React with 👍 / 👎.

@ngutman

ngutman commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

Follow-up pushed in 22f24a0 (fix(pairing): clear ios tls trust on onboarding reset).

This narrows the remaining iOS reset issue by making Reset Onboarding also clear:

  • trusted gateway TLS fingerprints
  • remembered preferred gateway stable ID
  • remembered last discovered gateway stable ID

Validation:

  • xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.4' -configuration Debug build
  • rebuilt and reinstalled the iOS test app on the real device
  • reran fresh gateway reset + QR onboarding flow and verified the gateway now sees the new node auto-approve path again

@codex review
@greptile-apps review
@aisle-research-bot review

@aisle-research-bot

aisle-research-bot Bot commented Mar 31, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Race condition in bootstrap token binding (in-memory lock only) allows multiple devices to bind same token
1. 🟠 Race condition in bootstrap token binding (in-memory lock only) allows multiple devices to bind same token
Property Value
Severity High
CWE CWE-362
Location src/infra/device-bootstrap.ts:180-239

Description

verifyDeviceBootstrapToken is intended to bind a bootstrap token to the first (deviceId, publicKey) that redeems it, but the read-modify-write sequence is protected only by an in-memory async lock (createAsyncLock).

This means:

  • The critical section (loadState → check record.deviceId/publicKey → write updated state[tokenKey]persistState) is only serialized within the same Node.js process.
  • If the gateway/infra code is executed in multiple processes (cluster/PM2, multiple worker processes, multiple container replicas pointing at the same baseDir volume), two concurrent requests can both:
    • read an unbound record
    • pass the allowlist checks
    • each write a different binding

Impact:

  • A bootstrap token can be redeemed by more than one device (violating the “bind to first device identity” property), potentially allowing an attacker who can race the legitimate device during onboarding to gain access.

Vulnerable code (read-modify-write without cross-process lock):

return await withLock(async () => {
  const state = await loadState(params.baseDir);
  ...
  const [tokenKey, record] = found;
  ...
  const boundDeviceId = record.deviceId?.trim();
  const boundPublicKey = record.publicKey?.trim();
  if (boundDeviceId || boundPublicKey) {
    ...
    state[tokenKey] = { ...record, deviceId, publicKey, lastUsedAtMs: Date.now() };
    await persistState(state, params.baseDir);
    return { ok: true };
  }

  state[tokenKey] = { ...record, deviceId, publicKey, lastUsedAtMs: Date.now() };
  await persistState(state, params.baseDir);
  return { ok: true };
});

Recommendation

Ensure the bootstrap token binding operation is atomic across processes.

Options (pick one appropriate for your deployment model):

  1. Use an OS-level file lock around the bootstrap state file during verification/binding.

    • e.g., use flock via a library (proper-lockfile, fs-ext, etc.)
  2. Move token state to a real datastore that supports atomic conditional updates (recommended for multi-instance deployments).

    • Example approach: store each token as a record and bind with a compare-and-set (CAS) update:
// Pseudocode: atomic bind-if-unbound
const updated = await db.bootstrapTokens.updateWhere(
  { tokenHash, deviceId: null, publicKey: null },
  { deviceId, publicKey, lastUsedAtMs: Date.now() },
);
if (!updated) return { ok: false, reason: "bootstrap_token_invalid" };
  1. If you guarantee single-process operation, document/enforce it (and ideally add a startup guard) so the security property holds.

Analyzed PR: #58382 at commit 22f24a0

Last updated on: 2026-03-31T17:11:11Z

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22f24a0f56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

bootstrapToken?: string;
deviceToken?: string;
}) {
return opts?.token ?? opts?.bootstrapToken ?? opts?.deviceToken;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Align signature token precedence with handshake auth

Update this helper to prefer deviceToken over bootstrapToken when token is absent; the gateway’s handshake verifier uses token ?? deviceToken ?? bootstrapToken in resolveSignatureToken, so connectReq currently signs with a different token whenever both bootstrap and device tokens are provided. In that input shape, the test client will fail with a device-signature error before auth fallback logic runs, which can make integration tests fail for the wrong reason.

Useful? React with 👍 / 👎.

@ngutman ngutman self-assigned this Mar 31, 2026
@ngutman ngutman force-pushed the fix/qr-bootstrap-onboarding-handoff branch from 22f24a0 to 7b74147 Compare March 31, 2026 18:11
@ngutman ngutman merged commit 69fe999 into main Mar 31, 2026
7 checks passed
@ngutman ngutman deleted the fix/qr-bootstrap-onboarding-handoff branch March 31, 2026 18:11
@ngutman

ngutman commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

Landed via temp rebase onto main.

  • Gate: pnpm lint && pnpm build && pnpm test (repo-wide pnpm test hit unrelated existing failures in packages/memory-host-sdk/src/host/embeddings.test.ts plus Vitest worker/OOM instability on this checkout)
  • Scoped validation: pnpm test -- src/gateway/server.auth.control-ui.test.ts -t bootstrap, pnpm test -- src/infra/device-bootstrap.test.ts, pnpm test -- src/infra/device-pairing.test.ts, pnpm test -- src/gateway/test-helpers.server.test.ts
  • Land commit: 7b74147
  • Merge commit: 69fe999

Thanks @ngutman!

steipete pushed a commit to Mlightsnow/openclaw that referenced this pull request Apr 1, 2026
… (thanks @ngutman)

* fix(pairing): restore qr bootstrap onboarding handoff

* fix(pairing): tighten bootstrap handoff follow-ups

* fix(pairing): migrate legacy gateway device auth

* fix(pairing): narrow qr bootstrap handoff scope

* fix(pairing): clear ios tls trust on onboarding reset

* fix(pairing): restore qr bootstrap onboarding handoff (openclaw#58382) (thanks @ngutman)
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
… (thanks @ngutman)

* fix(pairing): restore qr bootstrap onboarding handoff

* fix(pairing): tighten bootstrap handoff follow-ups

* fix(pairing): migrate legacy gateway device auth

* fix(pairing): narrow qr bootstrap handoff scope

* fix(pairing): clear ios tls trust on onboarding reset

* fix(pairing): restore qr bootstrap onboarding handoff (openclaw#58382) (thanks @ngutman)
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
… (thanks @ngutman)

* fix(pairing): restore qr bootstrap onboarding handoff

* fix(pairing): tighten bootstrap handoff follow-ups

* fix(pairing): migrate legacy gateway device auth

* fix(pairing): narrow qr bootstrap handoff scope

* fix(pairing): clear ios tls trust on onboarding reset

* fix(pairing): restore qr bootstrap onboarding handoff (openclaw#58382) (thanks @ngutman)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
… (thanks @ngutman)

* fix(pairing): restore qr bootstrap onboarding handoff

* fix(pairing): tighten bootstrap handoff follow-ups

* fix(pairing): migrate legacy gateway device auth

* fix(pairing): narrow qr bootstrap handoff scope

* fix(pairing): clear ios tls trust on onboarding reset

* fix(pairing): restore qr bootstrap onboarding handoff (openclaw#58382) (thanks @ngutman)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
… (thanks @ngutman)

* fix(pairing): restore qr bootstrap onboarding handoff

* fix(pairing): tighten bootstrap handoff follow-ups

* fix(pairing): migrate legacy gateway device auth

* fix(pairing): narrow qr bootstrap handoff scope

* fix(pairing): clear ios tls trust on onboarding reset

* fix(pairing): restore qr bootstrap onboarding handoff (openclaw#58382) (thanks @ngutman)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: ios App: ios gateway Gateway runtime maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant