test: expand auth failure coverage
This commit is contained in:
@@ -4,8 +4,7 @@ import {
|
|||||||
buildAuthRequest,
|
buildAuthRequest,
|
||||||
decodeBuiltin,
|
decodeBuiltin,
|
||||||
encodeBuiltin,
|
encodeBuiltin,
|
||||||
createAuthRequestSigningInput,
|
createAuthRequestSigningInput
|
||||||
YONEXUS_PROTOCOL_VERSION
|
|
||||||
} from "../../Yonexus.Protocol/src/index.js";
|
} from "../../Yonexus.Protocol/src/index.js";
|
||||||
import { createYonexusServerRuntime } from "../plugin/core/runtime.js";
|
import { createYonexusServerRuntime } from "../plugin/core/runtime.js";
|
||||||
import type { ClientRecord } from "../plugin/core/persistence.js";
|
import type { ClientRecord } from "../plugin/core/persistence.js";
|
||||||
@@ -71,6 +70,42 @@ function createMockTransport() {
|
|||||||
return { transport, sent };
|
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", () => {
|
describe("YNX-1105c: Auth Failure Paths", () => {
|
||||||
let now = 1_710_000_000;
|
let now = 1_710_000_000;
|
||||||
|
|
||||||
@@ -83,6 +118,282 @@ describe("YNX-1105c: Auth Failure Paths", () => {
|
|||||||
vi.useRealTimers();
|
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 () => {
|
it("AF-07: nonce collision triggers re_pair_required", async () => {
|
||||||
const keyPair = await generateKeyPair();
|
const keyPair = await generateKeyPair();
|
||||||
const store = createMockStore([
|
const store = createMockStore([
|
||||||
@@ -125,52 +436,33 @@ describe("YNX-1105c: Auth Failure Paths", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const nonce = "NONCE1234567890123456789";
|
const nonce = "NONCE1234567890123456789";
|
||||||
const signingInput = createAuthRequestSigningInput({
|
|
||||||
secret: "shared-secret",
|
|
||||||
nonce,
|
|
||||||
proofTimestamp: now
|
|
||||||
});
|
|
||||||
const signature = await signMessage(keyPair.privateKey, signingInput);
|
|
||||||
|
|
||||||
await runtime.handleMessage(
|
await runtime.handleMessage(
|
||||||
connection,
|
connection,
|
||||||
encodeBuiltin(
|
await buildSignedAuthRequest({
|
||||||
buildAuthRequest(
|
identifier: "client-a",
|
||||||
{
|
secret: "shared-secret",
|
||||||
identifier: "client-a",
|
privateKey: keyPair.privateKey,
|
||||||
nonce,
|
publicKey: keyPair.publicKey.trim(),
|
||||||
proofTimestamp: now,
|
nonce,
|
||||||
signature,
|
proofTimestamp: now,
|
||||||
publicKey: keyPair.publicKey.trim()
|
requestId: "req-auth-1"
|
||||||
},
|
})
|
||||||
{ requestId: "req-auth-1", timestamp: now }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second request with same nonce triggers re-pair
|
|
||||||
now += 1;
|
now += 1;
|
||||||
const signingInput2 = createAuthRequestSigningInput({
|
|
||||||
secret: "shared-secret",
|
|
||||||
nonce,
|
|
||||||
proofTimestamp: now
|
|
||||||
});
|
|
||||||
const signature2 = await signMessage(keyPair.privateKey, signingInput2);
|
|
||||||
|
|
||||||
await runtime.handleMessage(
|
await runtime.handleMessage(
|
||||||
connection,
|
connection,
|
||||||
encodeBuiltin(
|
await buildSignedAuthRequest({
|
||||||
buildAuthRequest(
|
identifier: "client-a",
|
||||||
{
|
secret: "shared-secret",
|
||||||
identifier: "client-a",
|
privateKey: keyPair.privateKey,
|
||||||
nonce,
|
publicKey: keyPair.publicKey.trim(),
|
||||||
proofTimestamp: now,
|
nonce,
|
||||||
signature: signature2,
|
proofTimestamp: now,
|
||||||
publicKey: keyPair.publicKey.trim()
|
requestId: "req-auth-2"
|
||||||
},
|
})
|
||||||
{ requestId: "req-auth-2", timestamp: now }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
||||||
@@ -223,28 +515,17 @@ describe("YNX-1105c: Auth Failure Paths", () => {
|
|||||||
publicKey: keyPair.publicKey.trim()
|
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(
|
await runtime.handleMessage(
|
||||||
connection,
|
connection,
|
||||||
encodeBuiltin(
|
await buildSignedAuthRequest({
|
||||||
buildAuthRequest(
|
identifier: "client-a",
|
||||||
{
|
secret: "shared-secret",
|
||||||
identifier: "client-a",
|
privateKey: keyPair.privateKey,
|
||||||
nonce,
|
publicKey: keyPair.publicKey.trim(),
|
||||||
proofTimestamp: now,
|
nonce: "NONCE9876543210987654321",
|
||||||
signature,
|
proofTimestamp: now,
|
||||||
publicKey: keyPair.publicKey.trim()
|
requestId: "req-auth-rate-limit"
|
||||||
},
|
})
|
||||||
{ requestId: "req-auth", timestamp: now }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
const lastMessage = decodeBuiltin(sent.at(-1)!.message);
|
||||||
@@ -255,4 +536,170 @@ describe("YNX-1105c: Auth Failure Paths", () => {
|
|||||||
expect(record?.secret).toBeUndefined();
|
expect(record?.secret).toBeUndefined();
|
||||||
expect(record?.pairingStatus).toBe("revoked");
|
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" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user