From 0717b204f129fefdade87cc2b70e4e1442091b9b Mon Sep 17 00:00:00 2001 From: nav Date: Thu, 9 Apr 2026 01:13:44 +0000 Subject: [PATCH] test: expand auth failure coverage --- tests/auth-failures.test.ts | 565 ++++++++++++++++++++++++++++++++---- 1 file changed, 506 insertions(+), 59 deletions(-) diff --git a/tests/auth-failures.test.ts b/tests/auth-failures.test.ts index bfe56fe..85d72f5 100644 --- a/tests/auth-failures.test.ts +++ b/tests/auth-failures.test.ts @@ -4,8 +4,7 @@ import { buildAuthRequest, decodeBuiltin, encodeBuiltin, - createAuthRequestSigningInput, - YONEXUS_PROTOCOL_VERSION + createAuthRequestSigningInput } from "../../Yonexus.Protocol/src/index.js"; import { createYonexusServerRuntime } from "../plugin/core/runtime.js"; import type { ClientRecord } from "../plugin/core/persistence.js"; @@ -71,6 +70,42 @@ function createMockTransport() { return { transport, sent }; } +async function buildSignedAuthRequest(options: { + identifier: string; + secret: string; + privateKey: string; + publicKey: string; + nonce: string; + proofTimestamp: number; + requestId?: string; + signatureOverride?: string; + publicKeyOverride?: string; +}) { + const signature = + options.signatureOverride ?? + (await signMessage( + options.privateKey, + createAuthRequestSigningInput({ + secret: options.secret, + nonce: options.nonce, + proofTimestamp: options.proofTimestamp + }) + )); + + return encodeBuiltin( + buildAuthRequest( + { + identifier: options.identifier, + nonce: options.nonce, + proofTimestamp: options.proofTimestamp, + signature, + publicKey: options.publicKeyOverride ?? options.publicKey + }, + { requestId: options.requestId, timestamp: options.proofTimestamp } + ) + ); +} + describe("YNX-1105c: Auth Failure Paths", () => { let now = 1_710_000_000; @@ -83,6 +118,282 @@ describe("YNX-1105c: Auth Failure Paths", () => { vi.useRealTimers(); }); + it("AF-01: unknown identifier returns auth_failed(unknown_identifier)", async () => { + const keyPair = await generateKeyPair(); + const store = createMockStore([]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("rogue-client"); + await runtime.handleMessage( + connection, + await buildSignedAuthRequest({ + identifier: "rogue-client", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now, + requestId: "req-auth-unknown" + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ + identifier: "rogue-client", + reason: "unknown_identifier" + }); + }); + + it("AF-02: auth before pairing returns auth_failed(not_paired)", async () => { + const keyPair = await generateKeyPair(); + const store = createMockStore([ + { + identifier: "client-a", + pairingStatus: "unpaired", + status: "offline", + recentNonces: [], + recentHandshakeAttempts: [], + createdAt: now - 10, + updatedAt: now - 10 + } + ]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + runtime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: connection.ws, + isAuthenticated: false, + connectedAt: now, + lastActivityAt: now, + publicKey: keyPair.publicKey.trim() + }); + + await runtime.handleMessage( + connection, + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now, + requestId: "req-auth-not-paired" + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "not_paired" }); + }); + + it("AF-03: invalid signature returns auth_failed(invalid_signature)", async () => { + const keyPair = await generateKeyPair(); + const wrongKeyPair = await generateKeyPair(); + const store = createMockStore([ + { + identifier: "client-a", + pairingStatus: "paired", + publicKey: keyPair.publicKey.trim(), + secret: "shared-secret", + status: "offline", + recentNonces: [], + recentHandshakeAttempts: [], + createdAt: now - 10, + updatedAt: now - 10 + } + ]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + runtime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: connection.ws, + isAuthenticated: false, + connectedAt: now, + lastActivityAt: now, + publicKey: keyPair.publicKey.trim() + }); + + await runtime.handleMessage( + connection, + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: wrongKeyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now, + requestId: "req-auth-invalid-signature" + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "invalid_signature" }); + }); + + it("AF-05: stale timestamp returns auth_failed(stale_timestamp)", async () => { + const keyPair = await generateKeyPair(); + const store = createMockStore([ + { + identifier: "client-a", + pairingStatus: "paired", + publicKey: keyPair.publicKey.trim(), + secret: "shared-secret", + status: "offline", + recentNonces: [], + recentHandshakeAttempts: [], + createdAt: now - 20, + updatedAt: now - 20 + } + ]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + runtime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: connection.ws, + isAuthenticated: false, + connectedAt: now, + lastActivityAt: now, + publicKey: keyPair.publicKey.trim() + }); + + await runtime.handleMessage( + connection, + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now - 11, + requestId: "req-auth-stale" + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "stale_timestamp" }); + }); + + it("AF-06: future timestamp returns auth_failed(future_timestamp)", async () => { + const keyPair = await generateKeyPair(); + const store = createMockStore([ + { + identifier: "client-a", + pairingStatus: "paired", + publicKey: keyPair.publicKey.trim(), + secret: "shared-secret", + status: "offline", + recentNonces: [], + recentHandshakeAttempts: [], + createdAt: now - 20, + updatedAt: now - 20 + } + ]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + runtime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: connection.ws, + isAuthenticated: false, + connectedAt: now, + lastActivityAt: now, + publicKey: keyPair.publicKey.trim() + }); + + await runtime.handleMessage( + connection, + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now + 11, + requestId: "req-auth-future" + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "future_timestamp" }); + }); + it("AF-07: nonce collision triggers re_pair_required", async () => { const keyPair = await generateKeyPair(); const store = createMockStore([ @@ -125,52 +436,33 @@ describe("YNX-1105c: Auth Failure Paths", () => { }); const nonce = "NONCE1234567890123456789"; - const signingInput = createAuthRequestSigningInput({ - secret: "shared-secret", - nonce, - proofTimestamp: now - }); - const signature = await signMessage(keyPair.privateKey, signingInput); await runtime.handleMessage( connection, - encodeBuiltin( - buildAuthRequest( - { - identifier: "client-a", - nonce, - proofTimestamp: now, - signature, - publicKey: keyPair.publicKey.trim() - }, - { requestId: "req-auth-1", timestamp: now } - ) - ) + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce, + proofTimestamp: now, + requestId: "req-auth-1" + }) ); - // Second request with same nonce triggers re-pair now += 1; - const signingInput2 = createAuthRequestSigningInput({ - secret: "shared-secret", - nonce, - proofTimestamp: now - }); - const signature2 = await signMessage(keyPair.privateKey, signingInput2); await runtime.handleMessage( connection, - encodeBuiltin( - buildAuthRequest( - { - identifier: "client-a", - nonce, - proofTimestamp: now, - signature: signature2, - publicKey: keyPair.publicKey.trim() - }, - { requestId: "req-auth-2", timestamp: now } - ) - ) + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce, + proofTimestamp: now, + requestId: "req-auth-2" + }) ); const lastMessage = decodeBuiltin(sent.at(-1)!.message); @@ -223,28 +515,17 @@ describe("YNX-1105c: Auth Failure Paths", () => { publicKey: keyPair.publicKey.trim() }); - const nonce = "NONCE987654321098765432"; - const signingInput = createAuthRequestSigningInput({ - secret: "shared-secret", - nonce, - proofTimestamp: now - }); - const signature = await signMessage(keyPair.privateKey, signingInput); - await runtime.handleMessage( connection, - encodeBuiltin( - buildAuthRequest( - { - identifier: "client-a", - nonce, - proofTimestamp: now, - signature, - publicKey: keyPair.publicKey.trim() - }, - { requestId: "req-auth", timestamp: now } - ) - ) + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE9876543210987654321", + proofTimestamp: now, + requestId: "req-auth-rate-limit" + }) ); const lastMessage = decodeBuiltin(sent.at(-1)!.message); @@ -255,4 +536,170 @@ describe("YNX-1105c: Auth Failure Paths", () => { expect(record?.secret).toBeUndefined(); expect(record?.pairingStatus).toBe("revoked"); }); + + it("AF-09: wrong public key returns auth_failed(invalid_signature)", async () => { + const keyPair = await generateKeyPair(); + const rotatedKeyPair = await generateKeyPair(); + const store = createMockStore([ + { + identifier: "client-a", + pairingStatus: "paired", + publicKey: keyPair.publicKey.trim(), + secret: "shared-secret", + status: "offline", + recentNonces: [], + recentHandshakeAttempts: [], + createdAt: now - 10, + updatedAt: now - 10 + } + ]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + runtime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: connection.ws, + isAuthenticated: false, + connectedAt: now, + lastActivityAt: now, + publicKey: keyPair.publicKey.trim() + }); + + await runtime.handleMessage( + connection, + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: rotatedKeyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + publicKeyOverride: rotatedKeyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now, + requestId: "req-auth-wrong-public-key" + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "invalid_signature" }); + }); + + it("AF-10: malformed auth_request payload returns protocol error", async () => { + const store = createMockStore([]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + await runtime.handleMessage( + connection, + encodeBuiltin({ + type: "auth_request", + requestId: "req-auth-malformed", + timestamp: now + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("error"); + expect(lastMessage.payload).toMatchObject({ + code: "MALFORMED_MESSAGE", + message: "auth_request payload is required" + }); + }); + + it("AF-11: tampered signature returns auth_failed(invalid_signature)", async () => { + const keyPair = await generateKeyPair(); + const store = createMockStore([ + { + identifier: "client-a", + pairingStatus: "paired", + publicKey: keyPair.publicKey.trim(), + secret: "shared-secret", + status: "offline", + recentNonces: [], + recentHandshakeAttempts: [], + createdAt: now - 10, + updatedAt: now - 10 + } + ]); + const { transport, sent } = createMockTransport(); + const runtime = createYonexusServerRuntime({ + config: { + followerIdentifiers: ["client-a"], + notifyBotToken: "stub-token", + adminUserId: "admin-user", + listenHost: "127.0.0.1", + listenPort: 8787 + }, + store, + transport, + now: () => now + }); + + await runtime.start(); + + const connection = createConnection("client-a"); + runtime.state.registry.sessions.set("client-a", { + identifier: "client-a", + socket: connection.ws, + isAuthenticated: false, + connectedAt: now, + lastActivityAt: now, + publicKey: keyPair.publicKey.trim() + }); + + const validMessage = decodeBuiltin( + await buildSignedAuthRequest({ + identifier: "client-a", + secret: "shared-secret", + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey.trim(), + nonce: "NONCE1234567890123456789", + proofTimestamp: now, + requestId: "req-auth-tampered" + }) + ); + + await runtime.handleMessage( + connection, + encodeBuiltin({ + ...validMessage, + payload: { + ...validMessage.payload, + signature: `A${String(validMessage.payload?.signature).slice(1)}` + } + }) + ); + + const lastMessage = decodeBuiltin(sent.at(-1)!.message); + expect(lastMessage.type).toBe("auth_failed"); + expect(lastMessage.payload).toMatchObject({ identifier: "client-a", reason: "invalid_signature" }); + }); });