171 lines
5.3 KiB
TypeScript
171 lines
5.3 KiB
TypeScript
/**
|
|
* DTP-19 — CdnUploadToken issuance and verification.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import type { CdnUploadToken } from "../src/index.js";
|
|
import {
|
|
canonicalSerializeUploadToken,
|
|
type DriveTemplateError,
|
|
DTP_CODES,
|
|
signUploadToken,
|
|
UPLOAD_TOKEN_TTL_DEFAULT_MS,
|
|
UPLOAD_TOKEN_TTL_MAX_MS,
|
|
verifyUploadToken,
|
|
} from "../src/index.js";
|
|
import { makeKeys } from "./_helpers.js";
|
|
|
|
const NOW = 2_000_000_000;
|
|
|
|
function baseToken(): Omit<CdnUploadToken, "serverSignature" | "expiresAt"> {
|
|
return {
|
|
fileId: "f".repeat(64),
|
|
chunkId: 12,
|
|
chunkHash: "blake3:abcdef",
|
|
uploaderPubkey: "pubkey-hex",
|
|
};
|
|
}
|
|
|
|
describe("DTP-19 — CdnUploadToken", () => {
|
|
it("default TTL pinned at 60s; max at 5m", { timeout: 5000 }, () => {
|
|
expect(UPLOAD_TOKEN_TTL_DEFAULT_MS).toBe(60_000);
|
|
expect(UPLOAD_TOKEN_TTL_MAX_MS).toBe(300_000);
|
|
});
|
|
|
|
it("valid token accepted", { timeout: 5000 }, () => {
|
|
const keys = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: NOW + 30_000 },
|
|
keys.secret,
|
|
{ clock: () => NOW }
|
|
);
|
|
expect(verifyUploadToken(tok, keys.publicHex, () => NOW + 1_000)).toBe(
|
|
true
|
|
);
|
|
});
|
|
|
|
it("expired token rejected", { timeout: 5000 }, () => {
|
|
const keys = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: NOW + 30_000 },
|
|
keys.secret,
|
|
{ clock: () => NOW }
|
|
);
|
|
try {
|
|
verifyUploadToken(tok, keys.publicHex, () => NOW + 100_000);
|
|
throw new Error("expected expired");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.CDN_BAD_SIGNATURE
|
|
);
|
|
}
|
|
});
|
|
|
|
it("tampered token rejected", { timeout: 5000 }, () => {
|
|
const keys = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: NOW + 30_000 },
|
|
keys.secret,
|
|
{ clock: () => NOW }
|
|
);
|
|
const tampered = { ...tok, chunkId: tok.chunkId + 1 };
|
|
try {
|
|
verifyUploadToken(tampered, keys.publicHex, () => NOW);
|
|
throw new Error("expected sig failure");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.CDN_BAD_SIGNATURE
|
|
);
|
|
}
|
|
});
|
|
|
|
it("token signed by wrong key rejected", { timeout: 5000 }, () => {
|
|
const k1 = makeKeys();
|
|
const k2 = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: NOW + 30_000 },
|
|
k1.secret,
|
|
{ clock: () => NOW }
|
|
);
|
|
try {
|
|
verifyUploadToken(tok, k2.publicHex, () => NOW);
|
|
throw new Error("expected sig failure");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.CDN_BAD_SIGNATURE
|
|
);
|
|
}
|
|
});
|
|
|
|
it("invalid server pubkey hex rejected", { timeout: 5000 }, () => {
|
|
const keys = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: NOW + 30_000 },
|
|
keys.secret,
|
|
{ clock: () => NOW }
|
|
);
|
|
try {
|
|
verifyUploadToken(tok, "not-hex", () => NOW);
|
|
throw new Error("expected pubkey rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.CDN_BAD_SIGNATURE
|
|
);
|
|
}
|
|
});
|
|
|
|
it("ttlMs out of range rejected at sign", { timeout: 5000 }, () => {
|
|
const keys = makeKeys();
|
|
expect(() =>
|
|
signUploadToken({ ...baseToken(), expiresAt: 0 }, keys.secret, {
|
|
clock: () => NOW,
|
|
ttlMs: 0,
|
|
})
|
|
).toThrow();
|
|
expect(() =>
|
|
signUploadToken({ ...baseToken(), expiresAt: 0 }, keys.secret, {
|
|
clock: () => NOW,
|
|
ttlMs: UPLOAD_TOKEN_TTL_MAX_MS + 1,
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it("canonicalSerializeUploadToken is deterministic", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const tok = { ...baseToken(), expiresAt: NOW + 30_000 };
|
|
const a = canonicalSerializeUploadToken(tok);
|
|
const b = canonicalSerializeUploadToken(tok);
|
|
expect(a).toEqual(b);
|
|
});
|
|
|
|
it("default options use Date.now() as clock", { timeout: 5000 }, () => {
|
|
const keys = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: 0 },
|
|
keys.secret
|
|
);
|
|
expect(tok.expiresAt).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("malformed signature buffer triggers verify catch arm", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const keys = makeKeys();
|
|
const tok = signUploadToken(
|
|
{ ...baseToken(), expiresAt: NOW + 30_000 },
|
|
keys.secret,
|
|
{ clock: () => NOW }
|
|
);
|
|
const broken = { ...tok, serverSignature: new Uint8Array(8) };
|
|
try {
|
|
verifyUploadToken(broken, keys.publicHex, () => NOW);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.CDN_BAD_SIGNATURE
|
|
);
|
|
}
|
|
});
|
|
});
|