1import { beforeAll, describe, expect, it } from "vitest";2import {3 buildSignedDeviceManifest,4 canonicalise,5 DeviceManifestError,6 verifyDeviceManifest,7} from "./device-manifest";8import { deriveSyncSigningKeypair } from "./sync-identity";9import {10 DEVICE_MANIFEST_FRESHNESS_MS,11 DEVICE_MANIFEST_VERSION,12 type ManifestDeviceEntry,13} from "./sync-types";1415const MASTER_KEY = new Uint8Array(32).fill(0xab);16const ISSUER_DEVICE_ID = "11111111-1111-4111-8111-111111111111";17const PEER_DEVICE_ID = "22222222-2222-4222-8222-222222222222";1819function entry(): ManifestDeviceEntry {20 return {21 deviceId: PEER_DEVICE_ID,22 deviceName: "June's Pixel 9",23 deviceType: "android",24 longTermPublicKeyHex: "a".repeat(64),25 signingPublicKeyHex: "b".repeat(64),26 pairedAt: 1_700_000_000_000,27 };28}2930describe("canonicalise", () => {31 it("sorts object keys deterministically", () => {32 const a = canonicalise({ b: 2, a: 1 });33 const b = canonicalise({ a: 1, b: 2 });34 expect(a).toBe(b);35 expect(a).toBe('{"a":1,"b":2}');36 });3738 it("serialises nested arrays of objects in order", () => {39 expect(canonicalise([{ y: 2, x: 1 }, { z: 3 }])).toBe('[{"x":1,"y":2},{"z":3}]');40 });41});4243describe("device manifest sign+verify round trip", () => {44 let signingPrivateKey: CryptoKey;45 let signingPubkey: Uint8Array;4647 beforeAll(async () => {48 const kp = await deriveSyncSigningKeypair(MASTER_KEY, ISSUER_DEVICE_ID);49 signingPrivateKey = kp.privateKey;50 signingPubkey = kp.publicKeyRaw;51 });5253 it("derives a stable 32-byte Ed25519 public key", () => {54 expect(signingPubkey.length).toBe(32);55 });5657 it("signs and verifies a fresh manifest", async () => {58 const m = await buildSignedDeviceManifest(59 ISSUER_DEVICE_ID,60 [entry()],61 signingPrivateKey,62 );63 expect(m.body.manifestVersion).toBe(DEVICE_MANIFEST_VERSION);64 expect(m.body.issuerDeviceId).toBe(ISSUER_DEVICE_ID);65 expect(m.signatureHex).toMatch(/^[0-9a-f]{128}$/);66 const body = await verifyDeviceManifest(m, signingPubkey);67 expect(body.devices).toHaveLength(1);68 expect(body.devices[0].deviceId).toBe(PEER_DEVICE_ID);69 });7071 it("rejects a stale manifest", async () => {72 const now = Date.now();73 const m = await buildSignedDeviceManifest(74 ISSUER_DEVICE_ID,75 [entry()],76 signingPrivateKey,77 { now: now - DEVICE_MANIFEST_FRESHNESS_MS - 1_000 },78 );79 await expect(verifyDeviceManifest(m, signingPubkey, { now })).rejects.toMatchObject({80 kind: "freshness",81 } as DeviceManifestError);82 });8384 it("rejects a forged signature", async () => {85 const m = await buildSignedDeviceManifest(86 ISSUER_DEVICE_ID,87 [entry()],88 signingPrivateKey,89 );90 91 const sig = m.signatureHex;92 const flipped =93 sig.slice(0, 2) +94 ((parseInt(sig.slice(2, 4), 16) ^ 0xff).toString(16).padStart(2, "0")) +95 sig.slice(4);96 await expect(97 verifyDeviceManifest({ ...m, signatureHex: flipped }, signingPubkey),98 ).rejects.toMatchObject({ kind: "signature" } as DeviceManifestError);99 });100101 it("rejects a body tampered after signing", async () => {102 const m = await buildSignedDeviceManifest(103 ISSUER_DEVICE_ID,104 [entry()],105 signingPrivateKey,106 );107 const tampered = {108 ...m,109 body: {110 ...m.body,111 devices: [{ ...entry(), deviceName: "different-name" }],112 },113 };114 await expect(verifyDeviceManifest(tampered, signingPubkey)).rejects.toMatchObject({115 kind: "signature",116 } as DeviceManifestError);117 });118119 it("rejects an unknown manifest version", async () => {120 const m = await buildSignedDeviceManifest(121 ISSUER_DEVICE_ID,122 [entry()],123 signingPrivateKey,124 );125 const future = { ...m, body: { ...m.body, manifestVersion: 0x99 } };126 await expect(verifyDeviceManifest(future, signingPubkey)).rejects.toMatchObject({127 kind: "version",128 } as DeviceManifestError);129 });130131 it("derives the same keypair across calls (determinism)", async () => {132 const kp2 = await deriveSyncSigningKeypair(MASTER_KEY, ISSUER_DEVICE_ID);133 expect(Array.from(kp2.publicKeyRaw)).toEqual(Array.from(signingPubkey));134 });135});