Files
ELY-Browser/cloudflare/tests/device_revocation_proof.test.ts

195 lines
6.7 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
compareDeviceIds,
deviceRevocationProofBytes,
deviceRevocationRequest,
pendingDeviceRevocationProofBytes,
} from "../src/device_revocation_schema.js";
import { DeviceSchemaError } from "../src/device_schema.js";
import { handleRequest } from "../src/index.js";
import {
ACCESS_TOKEN,
PUBLIC_KEY,
signDeviceMessage,
testD1Database,
testEnv,
} from "./devices_test_support.js";
const USER_ID = "user-01", APPROVER_ID = "device-01", TARGET_ID = "device-02";
const OLD_KEY = "a".repeat(64), NEW_KEY = "b".repeat(64);
const IDEMPOTENCY_KEY = "device-revocation-0001";
const SUITE = "HPKE-BASE-X25519-HKDF-SHA256-CHACHA20POLY1305";
describe("device revocation proof schema", () => {
it("rejects malformed envelope recipients and rotation metadata", async () => {
for (const body of [
approvedBody({ envelopes: [envelope(TARGET_ID)] }),
approvedBody({ envelopes: [envelope(APPROVER_ID), envelope(APPROVER_ID)] }),
approvedBody({ envelopes: [envelope(APPROVER_ID, { encapped_key: `${"A".repeat(42)}B` })] }),
approvedBody({ new_generation: 3 }),
approvedBody({ new_key_id: OLD_KEY }),
{ ...approvedBody(), mode: undefined },
{ ...pendingBody(), new_key_id: NEW_KEY },
]) {
await assert.rejects(
deviceRevocationRequest(request({ ...body, rotation_proof: "0".repeat(128) })),
DeviceSchemaError,
);
}
});
it("uses one ASCII code-unit order for proof and exact recipient checks", async () => {
const ids = ["a_1", "a:1", "a.1", "a-1", "A_1", "A-1"];
const expected = ["A-1", "A_1", "a-1", "a.1", "a:1", "a_1"];
assert.deepEqual([...ids].sort(compareDeviceIds), expected);
const parsed = await deviceRevocationRequest(request(
await signedBody(approvedBody({ envelopes: ids.map((id) => envelope(id)) })),
));
assert.equal(parsed.mode, "approved_rotate");
if (parsed.mode !== "approved_rotate") throw new Error("approved rotation expected");
assert.deepEqual(parsed.envelopes.map((item) => item.recipientDeviceId), expected);
});
it("uses the frozen v2 approved rotation proof wire", async () => {
const parsed = await deviceRevocationRequest(request(
await signedBody(approvedBody({ envelopes: [envelope(APPROVER_ID)] })),
));
if (parsed.mode !== "approved_rotate") throw new Error("approved rotation expected");
const { rotationProof: _, ...unsigned } = parsed;
assert.equal(new TextDecoder().decode(
deviceRevocationProofBytes(USER_ID, APPROVER_ID, unsigned),
), [
"28:elydora-device-revocation-v2",
"7:user-01",
"9:device-01",
"9:device-02",
`64:${OLD_KEY}`,
"1:1",
`64:${NEW_KEY}`,
"1:2",
"22:device-revocation-0001",
"1:1",
"9:device-01",
"1:1",
`45:${SUITE}`,
`43:${"A".repeat(43)}`,
`64:${"B".repeat(64)}`,
].join(""));
});
it("uses the frozen v2 pending revocation proof wire", async () => {
const parsed = await deviceRevocationRequest(request(await signedBody(pendingBody())));
if (parsed.mode !== "pending_revoke") throw new Error("pending revocation expected");
const { pendingRevocationProof: _, ...unsigned } = parsed;
assert.equal(new TextDecoder().decode(
pendingDeviceRevocationProofBytes(USER_ID, APPROVER_ID, unsigned),
), [
"36:elydora-pending-device-revocation-v2",
"7:user-01",
"9:device-01",
"9:device-02",
"22:device-revocation-0001",
].join(""));
});
it("rejects invalid and tampered proofs before revocation state reads", async () => {
const cases = [
approvedBody({ rotation_proof: "0".repeat(128) }),
{ ...await signedBody(approvedBody()), new_key_id: "c".repeat(64) },
pendingBody({ pending_revocation_proof: "0".repeat(128) }),
];
for (const body of cases) {
const d1 = testD1Database({ firstRows: [approverRow()] });
const response = await handleRequest(request(body, true), testEnv({ d1 }));
assert.equal(response.status, 403);
assert.equal(d1.queries.length, 1);
assert.deepEqual(d1.batches, []);
}
});
});
function approvedBody(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
version: 2,
mode: "approved_rotate",
device_id: TARGET_ID,
previous_key_id: OLD_KEY,
previous_generation: 1,
new_key_id: NEW_KEY,
new_generation: 2,
envelopes: [envelope("device-03"), envelope(APPROVER_ID)],
idempotency_key: IDEMPOTENCY_KEY,
...overrides,
};
}
function pendingBody(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
version: 2,
mode: "pending_revoke",
device_id: TARGET_ID,
idempotency_key: IDEMPOTENCY_KEY,
...overrides,
};
}
function envelope(
recipient: string,
overrides: Record<string, unknown> = {},
): Record<string, unknown> {
const other = recipient === "device-03";
return {
recipient_device_id: recipient,
envelope: {
version: 1,
suite: SUITE,
encapped_key: other ? `${"C".repeat(42)}E` : "A".repeat(43),
ciphertext: other ? "D".repeat(64) : "B".repeat(64),
...overrides,
},
};
}
async function signedBody(body: Record<string, unknown>): Promise<Record<string, unknown>> {
if (body.mode === "pending_revoke") {
if (body.pending_revocation_proof !== undefined) return body;
const draft = { ...body, pending_revocation_proof: "0".repeat(128) };
const parsed = await deviceRevocationRequest(request(draft));
if (parsed.mode !== "pending_revoke") throw new Error("pending revocation expected");
const { pendingRevocationProof: _, ...unsigned } = parsed;
return {
...body,
pending_revocation_proof: await signDeviceMessage(
pendingDeviceRevocationProofBytes(USER_ID, APPROVER_ID, unsigned),
),
};
}
if (body.rotation_proof !== undefined) return body;
const draft = { ...body, rotation_proof: "0".repeat(128) };
const parsed = await deviceRevocationRequest(request(draft));
if (parsed.mode !== "approved_rotate") throw new Error("approved rotation expected");
const { rotationProof: _, ...unsigned } = parsed;
return {
...body,
rotation_proof: await signDeviceMessage(
deviceRevocationProofBytes(USER_ID, APPROVER_ID, unsigned),
),
};
}
function request(body: Record<string, unknown>, authenticated = false): Request {
return new Request("https://elydora.test/api/devices/revoke", {
method: "POST",
headers: {
...(authenticated ? { authorization: `Bearer ${ACCESS_TOKEN}` } : {}),
"content-type": "application/json",
},
body: JSON.stringify(body),
});
}
function approverRow(): Record<string, unknown> {
return { device_id: APPROVER_ID, signing_public_key: PUBLIC_KEY };
}