/** * Signature Service Tests * * Tests for message signing and verification. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { SignatureService } from "./signature.service"; import { FederationService } from "./federation.service"; import { generateKeyPairSync } from "crypto"; import type Redis from "ioredis"; describe("SignatureService", () => { let service: SignatureService; let mockFederationService: Partial; let mockRedis: Partial; // Test keypair const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048, publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" }, }); beforeEach(async () => { mockFederationService = { getInstanceIdentity: vi.fn().mockResolvedValue({ id: "test-id", instanceId: "instance-123", name: "Test Instance", url: "https://test.example.com", publicKey, privateKey, capabilities: {}, metadata: {}, createdAt: new Date(), updatedAt: new Date(), }), }; mockRedis = { get: vi.fn().mockResolvedValue(null), set: vi.fn().mockResolvedValue("OK"), setex: vi.fn().mockResolvedValue("OK"), }; const module: TestingModule = await Test.createTestingModule({ providers: [ SignatureService, { provide: FederationService, useValue: mockFederationService, }, { provide: "REDIS_CLIENT", useValue: mockRedis, }, ], }).compile(); service = module.get(SignatureService); }); it("should be defined", () => { expect(service).toBeDefined(); }); describe("sign", () => { it("should create a valid signature for a message", () => { const message = { instanceId: "instance-123", timestamp: Date.now(), data: "test data", }; const signature = service.sign(message, privateKey); expect(signature).toBeDefined(); expect(typeof signature).toBe("string"); expect(signature.length).toBeGreaterThan(0); }); it("should create different signatures for different messages", () => { const message1 = { data: "message 1", timestamp: 1 }; const message2 = { data: "message 2", timestamp: 2 }; const signature1 = service.sign(message1, privateKey); const signature2 = service.sign(message2, privateKey); expect(signature1).not.toBe(signature2); }); it("should create consistent signatures for the same message", () => { const message = { data: "test", timestamp: 12345 }; const signature1 = service.sign(message, privateKey); const signature2 = service.sign(message, privateKey); // RSA signatures are deterministic for the same input expect(signature1).toBe(signature2); }); }); describe("verify", () => { it("should verify a valid signature", () => { const message = { instanceId: "instance-123", timestamp: Date.now(), data: "test data", }; const signature = service.sign(message, privateKey); const result = service.verify(message, signature, publicKey); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it("should reject an invalid signature", () => { const message = { instanceId: "instance-123", timestamp: Date.now(), data: "test data", }; const invalidSignature = "invalid-signature-data"; const result = service.verify(message, invalidSignature, publicKey); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); }); it("should reject a tampered message", () => { const originalMessage = { instanceId: "instance-123", timestamp: Date.now(), data: "original data", }; const signature = service.sign(originalMessage, privateKey); const tamperedMessage = { ...originalMessage, data: "tampered data", }; const result = service.verify(tamperedMessage, signature, publicKey); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); }); it("should reject a signature from wrong key", () => { const message = { data: "test" }; // Generate a different keypair const { publicKey: wrongPublicKey, privateKey: wrongPrivateKey } = generateKeyPairSync( "rsa", { modulusLength: 2048, publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" }, } ); const signature = service.sign(message, wrongPrivateKey); const result = service.verify(message, signature, publicKey); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); }); }); describe("validateTimestamp", () => { it("should accept recent timestamps", () => { const recentTimestamp = Date.now(); const result = service.validateTimestamp(recentTimestamp); expect(result).toBe(true); }); it("should accept timestamps within 60 seconds", () => { const fiftySecondsAgo = Date.now() - 50 * 1000; const result = service.validateTimestamp(fiftySecondsAgo); expect(result).toBe(true); }); it("should reject timestamps older than 60 seconds", () => { const twoMinutesAgo = Date.now() - 2 * 60 * 1000; const result = service.validateTimestamp(twoMinutesAgo); expect(result).toBe(false); }); it("should reject future timestamps beyond tolerance", () => { const farFuture = Date.now() + 10 * 60 * 1000; const result = service.validateTimestamp(farFuture); expect(result).toBe(false); }); it("should accept slightly future timestamps (clock skew tolerance)", () => { const slightlyFuture = Date.now() + 30 * 1000; // 30 seconds const result = service.validateTimestamp(slightlyFuture); expect(result).toBe(true); }); }); describe("signMessage", () => { it("should sign a message with instance private key", async () => { const message = { instanceId: "instance-123", timestamp: Date.now(), data: "test", }; const signature = await service.signMessage(message); expect(signature).toBeDefined(); expect(typeof signature).toBe("string"); expect(signature.length).toBeGreaterThan(0); }); it("should create verifiable signatures with instance keys", async () => { const message = { instanceId: "instance-123", timestamp: Date.now(), }; const signature = await service.signMessage(message); const result = service.verify(message, signature, publicKey); expect(result.valid).toBe(true); }); }); describe("verifyConnectionRequest", () => { it("should verify a valid connection request", async () => { const timestamp = Date.now(); const request = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: { supportsQuery: true }, timestamp, }; const signature = service.sign(request, privateKey); const signedRequest = { ...request, signature }; const result = await service.verifyConnectionRequest(signedRequest); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); expect(mockRedis.setex).toHaveBeenCalled(); }); it("should reject request with invalid signature", async () => { const request = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: {}, timestamp: Date.now(), signature: "invalid-signature", }; const result = await service.verifyConnectionRequest(request); expect(result.valid).toBe(false); expect(result.error).toContain("signature"); }); it("should reject request with expired timestamp", async () => { const expiredTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes ago const request = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: {}, timestamp: expiredTimestamp, }; const signature = service.sign(request, privateKey); const signedRequest = { ...request, signature }; const result = await service.verifyConnectionRequest(signedRequest); expect(result.valid).toBe(false); expect(result.error).toContain("timestamp"); }); }); describe("replay attack prevention", () => { it("should reject replayed message with same signature", async () => { const timestamp = Date.now(); const request = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: {}, timestamp, }; const signature = service.sign(request, privateKey); const signedRequest = { ...request, signature }; // First request should succeed const result1 = await service.verifyConnectionRequest(signedRequest); expect(result1.valid).toBe(true); // Mock Redis to indicate nonce was already used mockRedis.get = vi.fn().mockResolvedValue("1"); // Second request with same signature should be rejected const result2 = await service.verifyConnectionRequest(signedRequest); expect(result2.valid).toBe(false); expect(result2.error).toContain("replay"); }); it("should store nonce with 60 second TTL", async () => { const timestamp = Date.now(); const request = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: {}, timestamp, }; const signature = service.sign(request, privateKey); const signedRequest = { ...request, signature }; await service.verifyConnectionRequest(signedRequest); expect(mockRedis.setex).toHaveBeenCalledWith(expect.stringContaining("nonce:"), 60, "1"); }); it("should allow different messages with different signatures", async () => { const timestamp1 = Date.now(); const request1 = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: {}, timestamp: timestamp1, }; const signature1 = service.sign(request1, privateKey); const signedRequest1 = { ...request1, signature: signature1 }; const result1 = await service.verifyConnectionRequest(signedRequest1); expect(result1.valid).toBe(true); // Different timestamp creates different signature const timestamp2 = Date.now() + 1; const request2 = { instanceId: "instance-123", instanceUrl: "https://test.example.com", publicKey, capabilities: {}, timestamp: timestamp2, }; const signature2 = service.sign(request2, privateKey); const signedRequest2 = { ...request2, signature: signature2 }; // Reset mock to simulate nonce not found mockRedis.get = vi.fn().mockResolvedValue(null); const result2 = await service.verifyConnectionRequest(signedRequest2); expect(result2.valid).toBe(true); }); }); });