import { beforeAll, describe, expect, it } from "vitest"; import { buildSignedDeviceManifest, canonicalise, DeviceManifestError, verifyDeviceManifest, } from "./device-manifest"; import { deriveSyncSigningKeypair } from "./sync-identity"; import { DEVICE_MANIFEST_FRESHNESS_MS, DEVICE_MANIFEST_VERSION, type ManifestDeviceEntry, } from "./sync-types"; const MASTER_KEY = new Uint8Array(32).fill(0xab); const ISSUER_DEVICE_ID = "11111111-1111-4111-8111-111111111111"; const PEER_DEVICE_ID = "22222222-2222-4222-8222-222222222222"; function entry(): ManifestDeviceEntry { return { deviceId: PEER_DEVICE_ID, deviceName: "June's Pixel 9", deviceType: "android", longTermPublicKeyHex: "a".repeat(64), signingPublicKeyHex: "b".repeat(64), pairedAt: 1_700_000_000_000, }; } describe("canonicalise", () => { it("sorts object keys deterministically", () => { const a = canonicalise({ b: 2, a: 1 }); const b = canonicalise({ a: 1, b: 2 }); expect(a).toBe(b); expect(a).toBe('{"a":1,"b":2}'); }); it("serialises nested arrays of objects in order", () => { expect(canonicalise([{ y: 2, x: 1 }, { z: 3 }])).toBe('[{"x":1,"y":2},{"z":3}]'); }); }); describe("device manifest sign+verify round trip", () => { let signingPrivateKey: CryptoKey; let signingPubkey: Uint8Array; beforeAll(async () => { const kp = await deriveSyncSigningKeypair(MASTER_KEY, ISSUER_DEVICE_ID); signingPrivateKey = kp.privateKey; signingPubkey = kp.publicKeyRaw; }); it("derives a stable 32-byte Ed25519 public key", () => { expect(signingPubkey.length).toBe(32); }); it("signs and verifies a fresh manifest", async () => { const m = await buildSignedDeviceManifest( ISSUER_DEVICE_ID, [entry()], signingPrivateKey, ); expect(m.body.manifestVersion).toBe(DEVICE_MANIFEST_VERSION); expect(m.body.issuerDeviceId).toBe(ISSUER_DEVICE_ID); expect(m.signatureHex).toMatch(/^[0-9a-f]{128}$/); const body = await verifyDeviceManifest(m, signingPubkey); expect(body.devices).toHaveLength(1); expect(body.devices[0].deviceId).toBe(PEER_DEVICE_ID); }); it("rejects a stale manifest", async () => { const now = Date.now(); const m = await buildSignedDeviceManifest( ISSUER_DEVICE_ID, [entry()], signingPrivateKey, { now: now - DEVICE_MANIFEST_FRESHNESS_MS - 1_000 }, ); await expect(verifyDeviceManifest(m, signingPubkey, { now })).rejects.toMatchObject({ kind: "freshness", } as DeviceManifestError); }); it("rejects a forged signature", async () => { const m = await buildSignedDeviceManifest( ISSUER_DEVICE_ID, [entry()], signingPrivateKey, ); // Flip one byte of the signature — must fail verification. const sig = m.signatureHex; const flipped = sig.slice(0, 2) + ((parseInt(sig.slice(2, 4), 16) ^ 0xff).toString(16).padStart(2, "0")) + sig.slice(4); await expect( verifyDeviceManifest({ ...m, signatureHex: flipped }, signingPubkey), ).rejects.toMatchObject({ kind: "signature" } as DeviceManifestError); }); it("rejects a body tampered after signing", async () => { const m = await buildSignedDeviceManifest( ISSUER_DEVICE_ID, [entry()], signingPrivateKey, ); const tampered = { ...m, body: { ...m.body, devices: [{ ...entry(), deviceName: "different-name" }], }, }; await expect(verifyDeviceManifest(tampered, signingPubkey)).rejects.toMatchObject({ kind: "signature", } as DeviceManifestError); }); it("rejects an unknown manifest version", async () => { const m = await buildSignedDeviceManifest( ISSUER_DEVICE_ID, [entry()], signingPrivateKey, ); const future = { ...m, body: { ...m.body, manifestVersion: 0x99 } }; await expect(verifyDeviceManifest(future, signingPubkey)).rejects.toMatchObject({ kind: "version", } as DeviceManifestError); }); it("derives the same keypair across calls (determinism)", async () => { const kp2 = await deriveSyncSigningKeypair(MASTER_KEY, ISSUER_DEVICE_ID); expect(Array.from(kp2.publicKeyRaw)).toEqual(Array.from(signingPubkey)); }); }); describe("manifest v0x02 canonical form — cross-platform KAT", () => { // Pinned byte-for-byte against peach-android's canonicalDeviceManifestBody. // If either side reorders a key, renames a field, or changes number // formatting, this string moves and the two stop verifying each other's // signatures — the exact silent divergence the manifest version guards. const CANONICAL = '{"devices":[{"deviceId":"peer-1","deviceName":"Peer One","deviceType":"android",' + '"longTermPublicKeyHex":"' + "aa".repeat(32) + '","pairedAt":1700000000001,' + '"signingPublicKeyHex":"' + "bb".repeat(32) + '","wsPort":49142}],' + '"issuedAt":1700000000000,"issuerDeviceId":"issuer-1","manifestVersion":2,' + '"revoked":[{"deviceId":"gone-1","longTermPublicKeyHex":"' + "cc".repeat(32) + '",' + '"revokedAt":1700000000002,"revokedByDeviceId":"issuer-1"}]}'; it("canonicalises a manifest body carrying a revocation to the pinned string", () => { expect( canonicalise({ manifestVersion: 2, issuerDeviceId: "issuer-1", issuedAt: 1_700_000_000_000, devices: [ { deviceId: "peer-1", deviceName: "Peer One", deviceType: "android", longTermPublicKeyHex: "aa".repeat(32), signingPublicKeyHex: "bb".repeat(32), pairedAt: 1_700_000_000_001, wsPort: 49142, }, ], revoked: [ { deviceId: "gone-1", longTermPublicKeyHex: "cc".repeat(32), revokedAt: 1_700_000_000_002, revokedByDeviceId: "issuer-1", }, ], }), ).toBe(CANONICAL); }); });