fix(openclaw-gateway): bind the device signature to the presented credential
The signed device payload bound only `authToken`. The OpenClaw gateway verifies the signature against the credential actually presented for the connection (`auth.token` when a shared token is supplied, otherwise `auth.deviceToken`), so every device-token-only reconnect of a paired device failed with "device signature invalid". The signature token now follows the same precedence as the `auth` object (`authToken ?? deviceToken`). `resolveDeviceSignatureToken` and `buildDeviceAuthPayloadV3` are exported for the focused tests added alongside. Fixes #13218
This commit is contained in:
parent
932c8bec56
commit
38ed481823
|
|
@ -1,5 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildAgentParams, resolveClaimedApiKeyPath, resolveSessionKey } from "./execute.js";
|
||||
import {
|
||||
buildAgentParams,
|
||||
buildDeviceAuthPayloadV3,
|
||||
resolveClaimedApiKeyPath,
|
||||
resolveDeviceSignatureToken,
|
||||
resolveSessionKey,
|
||||
} from "./execute.js";
|
||||
|
||||
describe("resolveSessionKey", () => {
|
||||
it("prefixes run-scoped session keys with the configured agent", () => {
|
||||
|
|
@ -123,3 +129,42 @@ describe("resolveClaimedApiKeyPath", () => {
|
|||
expect(resolveClaimedApiKeyPath({})).toBe(DEFAULT_PATH);
|
||||
});
|
||||
});
|
||||
|
||||
describe("device signature token binding", () => {
|
||||
const basePayload = {
|
||||
deviceId: "device-id",
|
||||
clientId: "cli",
|
||||
clientMode: "cli",
|
||||
role: "operator",
|
||||
scopes: ["operator.read", "operator.write"],
|
||||
signedAtMs: 1_700_000_000_000,
|
||||
nonce: "nonce-1",
|
||||
platform: "linux",
|
||||
deviceFamily: null,
|
||||
};
|
||||
|
||||
it("binds the device token when no shared token is configured (device-token-only reconnect)", () => {
|
||||
const token = resolveDeviceSignatureToken({ authToken: null, deviceToken: "D" });
|
||||
expect(token).toBe("D");
|
||||
const payload = buildDeviceAuthPayloadV3({ ...basePayload, token });
|
||||
expect(payload.split("|")[7]).toBe("D");
|
||||
});
|
||||
|
||||
it("binds the shared token when both are configured (gateway precedence: token, then deviceToken)", () => {
|
||||
const token = resolveDeviceSignatureToken({ authToken: "S", deviceToken: "D" });
|
||||
expect(token).toBe("S");
|
||||
const payload = buildDeviceAuthPayloadV3({ ...basePayload, token });
|
||||
expect(payload.split("|")[7]).toBe("S");
|
||||
});
|
||||
|
||||
it("leaves the token slot empty only when no credential of either kind is configured", () => {
|
||||
expect(resolveDeviceSignatureToken({ authToken: " ", deviceToken: undefined })).toBeNull();
|
||||
const payload = buildDeviceAuthPayloadV3({ ...basePayload, token: null });
|
||||
expect(payload.split("|")[7]).toBe("");
|
||||
});
|
||||
|
||||
it("keeps the v3 payload field order stable", () => {
|
||||
const payload = buildDeviceAuthPayloadV3({ ...basePayload, token: "D" });
|
||||
expect(payload).toBe("v3|device-id|cli|cli|operator|operator.read,operator.write|1700000000000|D|nonce-1|linux|");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -559,7 +559,21 @@ function signDevicePayload(privateKeyPem: string, payload: string): string {
|
|||
return base64UrlEncode(sig);
|
||||
}
|
||||
|
||||
function buildDeviceAuthPayloadV3(params: {
|
||||
/**
|
||||
* The OpenClaw gateway verifies the signed device payload against the credential the client
|
||||
* actually presents for the connection: `auth.token` when a shared token is supplied, otherwise
|
||||
* `auth.deviceToken`. Binding only the shared token breaks every device-token-only reconnect
|
||||
* (the gateway answers "device signature invalid"), so the signature token must follow the
|
||||
* same precedence as the `auth` object built for `connect`.
|
||||
*/
|
||||
export function resolveDeviceSignatureToken(params: {
|
||||
authToken?: string | null;
|
||||
deviceToken?: string | null;
|
||||
}): string | null {
|
||||
return nonEmpty(params.authToken) ?? nonEmpty(params.deviceToken) ?? null;
|
||||
}
|
||||
|
||||
export function buildDeviceAuthPayloadV3(params: {
|
||||
deviceId: string;
|
||||
clientId: string;
|
||||
clientMode: string;
|
||||
|
|
@ -1276,7 +1290,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
role,
|
||||
scopes,
|
||||
signedAtMs,
|
||||
token: authToken,
|
||||
token: resolveDeviceSignatureToken({ authToken, deviceToken }),
|
||||
nonce,
|
||||
platform: process.platform,
|
||||
deviceFamily,
|
||||
|
|
|
|||
Loading…
Reference in New Issue