import { describe, expect, it } from 'vitest' import { AUTH_HEADERS, Ed25519Signer, Ed25519Verifier, RequestAuth, type RequestSigner, type SignatureVerifier } from '../../src/utils/request-auth.js' import { AuthError } from '../../src/utils/errors.js' const NOW = 1_700_000_000_000 async function signed(signer: RequestSigner, path = '/kv/readMany', body = '{"keys":["a"]}', at = NOW) { return new Headers(await RequestAuth.headers(signer, path, body, at)) } describe('RequestAuth', () => { it('accepts a correctly signed request and returns the public key', async () => { const signer = await Ed25519Signer.generate() const auth = new RequestAuth([new Ed25519Verifier()], { now: () => NOW }) const headers = await signed(signer) expect(headers.get(AUTH_HEADERS.algorithm)).toBe('ed25519') expect(signer.publicKey).toMatch(/^[0-9a-f]{64}$/) expect(await auth.verify(headers, '/kv/readMany', '{"keys":["a"]}')).toBe(signer.publicKey) }) it('rejects tampered bodies, paths and timestamps', async () => { const signer = await Ed25519Signer.generate() const auth = new RequestAuth([new Ed25519Verifier()], { now: () => NOW }) await expect(auth.verify(await signed(signer), '/kv/readMany', '{"keys":["b"]}')).rejects.toThrow(/Invalid signature/) await expect(auth.verify(await signed(signer), '/kv/writeMany', '{"keys":["a"]}')).rejects.toThrow(/Invalid signature/) const headers = await signed(signer) headers.set(AUTH_HEADERS.timestamp, String(NOW + 1)) await expect(auth.verify(headers, '/kv/readMany', '{"keys":["a"]}')).rejects.toThrow(/Invalid signature/) const other = await Ed25519Signer.generate() const swapped = await signed(signer) swapped.set(AUTH_HEADERS.publicKey, other.publicKey) await expect(auth.verify(swapped, '/kv/readMany', '{"keys":["a"]}')).rejects.toThrow(/Invalid signature/) }) it('rejects old or future timestamps, replays, missing headers and unknown algorithms', async () => { const signer = await Ed25519Signer.generate() const auth = new RequestAuth([new Ed25519Verifier()], { now: () => NOW, maxSkewMs: 1000 }) await expect(auth.verify(await signed(signer, '/p', 'b', NOW - 5000), '/p', 'b')).rejects.toThrow(/window/) await expect(auth.verify(await signed(signer, '/p', 'b', NOW + 5000), '/p', 'b')).rejects.toThrow(/window/) const once = await signed(signer, '/p', 'b') await auth.verify(once, '/p', 'b') await expect(auth.verify(once, '/p', 'b')).rejects.toThrow(/Replayed/) await expect(auth.verify(new Headers(), '/p', 'b')).rejects.toBeInstanceOf(AuthError) const unknown = await signed(signer, '/p', 'c') unknown.set(AUTH_HEADERS.algorithm, 'rot13') await expect(auth.verify(unknown, '/p', 'c')).rejects.toThrow(/Unsupported signature algorithm "rot13"/) }) it('restores an Ed25519 key from its exported private key', async () => { const signer = await Ed25519Signer.generate() const restored = await Ed25519Signer.fromPrivateKey(await signer.exportPrivateKey()) expect(restored.publicKey).toBe(signer.publicKey) const auth = new RequestAuth([new Ed25519Verifier()], { now: () => NOW }) expect(await auth.verify(await signed(restored), '/kv/readMany', '{"keys":["a"]}')).toBe(signer.publicKey) }) it('supports other algorithms through the verifier registry', async () => { // A toy algorithm (signature = sha256(publicKey + payload)) standing in for e.g. secp256k1 via libauth. const digest = async (publicKey: string, payload: Uint8Array) => new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array([...new TextEncoder().encode(publicKey), ...payload]))) const toySigner: RequestSigner = { algorithm: 'toy', publicKey: 'abcd', sign: (payload) => digest('abcd', payload) } const toyVerifier: SignatureVerifier = { algorithm: 'toy', verify: async (publicKey, payload, signature) => (await digest(publicKey, payload)).every((byte, i) => byte === signature[i]), } const auth = new RequestAuth([new Ed25519Verifier(), toyVerifier], { now: () => NOW }) expect(await auth.verify(await signed(toySigner), '/kv/readMany', '{"keys":["a"]}')).toBe('abcd') }) })