fix: tighten device identity helper coverage

This commit is contained in:
Peter Steinberger 2026-03-13 23:50:15 +00:00
parent 8240fc519a
commit e8c300c353
2 changed files with 67 additions and 0 deletions

View File

@ -0,0 +1,61 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
deriveDeviceIdFromPublicKey,
loadOrCreateDeviceIdentity,
normalizeDevicePublicKeyBase64Url,
publicKeyRawBase64UrlFromPem,
signDevicePayload,
verifyDeviceSignature,
} from "./device-identity.js";
async function withIdentity(
run: (identity: ReturnType<typeof loadOrCreateDeviceIdentity>) => void,
) {
await withTempDir("openclaw-device-identity-", async (dir) => {
const identity = loadOrCreateDeviceIdentity(path.join(dir, "device.json"));
run(identity);
});
}
describe("device identity crypto helpers", () => {
it("derives the same canonical raw key and device id from pem and encoded public keys", async () => {
await withIdentity((identity) => {
const publicKeyRaw = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
const paddedBase64 = `${publicKeyRaw.replaceAll("-", "+").replaceAll("_", "/")}==`;
expect(normalizeDevicePublicKeyBase64Url(identity.publicKeyPem)).toBe(publicKeyRaw);
expect(normalizeDevicePublicKeyBase64Url(paddedBase64)).toBe(publicKeyRaw);
expect(deriveDeviceIdFromPublicKey(identity.publicKeyPem)).toBe(identity.deviceId);
expect(deriveDeviceIdFromPublicKey(publicKeyRaw)).toBe(identity.deviceId);
});
});
it("signs payloads that verify against pem and raw public key forms", async () => {
await withIdentity((identity) => {
const payload = JSON.stringify({
action: "system.run",
ts: 1234,
});
const signature = signDevicePayload(identity.privateKeyPem, payload);
const publicKeyRaw = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
expect(verifyDeviceSignature(identity.publicKeyPem, payload, signature)).toBe(true);
expect(verifyDeviceSignature(publicKeyRaw, payload, signature)).toBe(true);
expect(verifyDeviceSignature(publicKeyRaw, `${payload}!`, signature)).toBe(false);
});
});
it("fails closed for invalid public keys and signatures", async () => {
await withIdentity((identity) => {
const payload = "hello";
const signature = signDevicePayload(identity.privateKeyPem, payload);
expect(normalizeDevicePublicKeyBase64Url("-----BEGIN PUBLIC KEY-----broken")).toBeNull();
expect(deriveDeviceIdFromPublicKey("%%%")).toBeNull();
expect(verifyDeviceSignature("%%%invalid%%%", payload, signature)).toBe(false);
expect(verifyDeviceSignature(identity.publicKeyPem, payload, "%%%invalid%%%")).toBe(false);
});
});
});

View File

@ -134,6 +134,9 @@ export function normalizeDevicePublicKeyBase64Url(publicKey: string): string | n
return base64UrlEncode(derivePublicKeyRaw(publicKey)); return base64UrlEncode(derivePublicKeyRaw(publicKey));
} }
const raw = base64UrlDecode(publicKey); const raw = base64UrlDecode(publicKey);
if (raw.length === 0) {
return null;
}
return base64UrlEncode(raw); return base64UrlEncode(raw);
} catch { } catch {
return null; return null;
@ -145,6 +148,9 @@ export function deriveDeviceIdFromPublicKey(publicKey: string): string | null {
const raw = publicKey.includes("BEGIN") const raw = publicKey.includes("BEGIN")
? derivePublicKeyRaw(publicKey) ? derivePublicKeyRaw(publicKey)
: base64UrlDecode(publicKey); : base64UrlDecode(publicKey);
if (raw.length === 0) {
return null;
}
return crypto.createHash("sha256").update(raw).digest("hex"); return crypto.createHash("sha256").update(raw).digest("hex");
} catch { } catch {
return null; return null;