/** * Security Guardrails Integration Tests * * Tests the EdgeGateway's security features: * - Anti-relay protection * - Replay detection * - Rate limiting */ import { describe, it } from 'node:test'; import { strict as assert } from 'node:assert'; import { FederationPeer, ExactInstanceResolver, WIRE_PROFILES, InMemoryFilterBroker, getLegacyIngressTopic, getMxIngressTopic, ReplayCache, TokenBucketRateLimiter, type SecurityBlockReason, createMxEnvV1, serializeMxEnvV1, } from '@open-matrix/federation'; describe('Security Guardrails', () => { it('rejects open-relay attempt (mxenv1)', async () => { const REALM = 'target-realm'; const backbone = new InMemoryFilterBroker(); const localBus = new InMemoryFilterBroker(); const securityBlocks: { reason: SecurityBlockReason }[] = []; const gateway = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus, enforceAntiRelay: true, onSecurityBlock: (reason) => { securityBlocks.push({ reason }); }, }); gateway.start(); await new Promise(r => setTimeout(r, 50)); // Send open-relay attempt: from attacker-realm, but replyTo victim-realm const maliciousEnvelope = createMxEnvV1({ msgType: 'Cmd', toRealm: REALM, toPath: 'victim.accepts.cmd', fromRealm: 'attacker-realm', fromPath: 'attacker', replyToRealm: 'victim-realm', // Different from fromRealm! replyToPath: '_inbox', correlationId: 'attack-1', body: { attack: true }, }); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(maliciousEnvelope)); await new Promise(r => setTimeout(r, 200)); assert.ok(securityBlocks.length > 0, 'Should detect open-relay attempt'); assert.strictEqual(securityBlocks[0].reason, 'anti_relay', 'Block reason should be anti_relay'); const metrics = gateway.getMetrics(); assert.ok(metrics.securityBlocks.antiRelay > 0, 'Anti-relay metric should increase'); gateway.stop(); }); it('rejects replay attempt (mxenv1)', async () => { const REALM = 'test-realm'; const backbone = new InMemoryFilterBroker(); const localBus = new InMemoryFilterBroker(); const replayCache = new ReplayCache(100); const securityBlocks: { reason: SecurityBlockReason }[] = []; const gateway = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus, replayCache, onSecurityBlock: (reason) => { securityBlocks.push({ reason }); }, }); gateway.start(); // Set up a handler so the first request can complete localBus.subscribe(`test.accepts.cmd`, (topic, payload) => { const req = JSON.parse(payload) as { replyTo: string; correlationId: string }; localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId })); }); await new Promise(r => setTimeout(r, 50)); // Create envelope with a fixed msgId const envelope = createMxEnvV1({ msgType: 'Cmd', msgId: 'unique-msg-12345', toRealm: REALM, toPath: 'test.accepts.cmd', fromRealm: 'sender-realm', fromPath: 'sender', replyToRealm: 'sender-realm', replyToPath: '_inbox', correlationId: 'req-1', body: {}, }); // Send same message twice backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope)); await new Promise(r => setTimeout(r, 100)); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope)); await new Promise(r => setTimeout(r, 200)); assert.ok(securityBlocks.length >= 1, 'Should detect replay attempt'); assert.strictEqual(securityBlocks[0].reason, 'replay', 'Block reason should be replay'); const metrics = gateway.getMetrics(); assert.ok(metrics.securityBlocks.replay > 0, 'Replay metric should increase'); gateway.stop(); }); it('enforces rate limits (mxenv1)', async () => { const REALM = 'rate-test'; const backbone = new InMemoryFilterBroker(); const localBus = new InMemoryFilterBroker(); // Very restrictive rate limiter for testing: 3 capacity, 0 refill (never refills) const rateLimiter = new TokenBucketRateLimiter(3, 0); const securityBlocks: { reason: SecurityBlockReason }[] = []; const gateway = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus, rateLimiter, onSecurityBlock: (reason) => { securityBlocks.push({ reason }); }, }); gateway.start(); // Set up a handler so requests can complete localBus.subscribe(`test.accepts.cmd`, (topic, payload) => { const req = JSON.parse(payload) as { replyTo: string; correlationId: string }; localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId })); }); await new Promise(r => setTimeout(r, 50)); // Send 6 requests rapidly (limit is 3) for (let i = 0; i < 6; i++) { const envelope = createMxEnvV1({ msgType: 'Cmd', toRealm: REALM, toPath: 'test.accepts.cmd', fromRealm: 'sender', fromPath: 'sender', replyToRealm: 'sender', replyToPath: '_inbox', correlationId: `req-${i}`, body: {}, }); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope)); } await new Promise(r => setTimeout(r, 300)); // Should have blocked at least 3 requests (6 - 3 = 3) const rateLimitBlocks = securityBlocks.filter(b => b.reason === 'rate_limit').length; assert.ok(rateLimitBlocks >= 3, `Should rate limit excess requests (blocked ${rateLimitBlocks})`); const metrics = gateway.getMetrics(); assert.ok(metrics.securityBlocks.rateLimit >= 3, 'Rate limit metric should be >= 3'); gateway.stop(); }); it('allows legitimate requests with anti-relay', async () => { const REALM = 'legit-realm'; const backbone = new InMemoryFilterBroker(); const localBus = new InMemoryFilterBroker(); let receivedRequest = false; const gateway = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus, enforceAntiRelay: true, }); gateway.start(); // Handler that marks success localBus.subscribe(`test.accepts.ping`, (topic, payload) => { receivedRequest = true; const req = JSON.parse(payload) as { replyTo: string; correlationId: string }; localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId })); }); await new Promise(r => setTimeout(r, 50)); // Legitimate request: from and replyTo have same realm const envelope = createMxEnvV1({ msgType: 'Cmd', toRealm: REALM, toPath: 'test.accepts.ping', fromRealm: 'client-realm', fromPath: 'client', replyToRealm: 'client-realm', // Same as fromRealm - legitimate replyToPath: '_inbox', correlationId: 'legit-1', body: { message: 'hello' }, }); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(envelope)); await new Promise(r => setTimeout(r, 200)); assert.ok(receivedRequest, 'Legitimate request should reach handler'); const metrics = gateway.getMetrics(); assert.strictEqual(metrics.securityBlocks.antiRelay, 0, 'No anti-relay blocks for legitimate request'); gateway.stop(); }); it('tracks security metrics correctly', async () => { const REALM = 'metrics-test'; const backbone = new InMemoryFilterBroker(); const localBus = new InMemoryFilterBroker(); const replayCache = new ReplayCache(100); const rateLimiter = new TokenBucketRateLimiter(2, 0); const gateway = new FederationPeer({ localRealm: REALM, profile: WIRE_PROFILES.mxenv1, backbone, localBus, enforceAntiRelay: true, replayCache, rateLimiter, }); gateway.start(); // Handler localBus.subscribe(`test.accepts.cmd`, (topic, payload) => { const req = JSON.parse(payload) as { replyTo: string; correlationId: string }; localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId })); }); await new Promise(r => setTimeout(r, 50)); const initialMetrics = gateway.getMetrics(); // 1. Anti-relay violation const antiRelayEnv = createMxEnvV1({ msgType: 'Cmd', toRealm: REALM, toPath: 'test.accepts.cmd', fromRealm: 'attacker', fromPath: 'a', replyToRealm: 'victim', replyToPath: '_inbox', correlationId: 'ar-1', body: {}, }); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(antiRelayEnv)); // 2. Replay (send same twice) const replayEnv = createMxEnvV1({ msgType: 'Cmd', msgId: 'replay-test-id', toRealm: REALM, toPath: 'test.accepts.cmd', fromRealm: 'sender', fromPath: 's', replyToRealm: 'sender', replyToPath: '_inbox', correlationId: 'rp-1', body: {}, }); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(replayEnv)); await new Promise(r => setTimeout(r, 50)); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(replayEnv)); // 3. Rate limit (send 3 more, only 1 capacity left after replay) for (let i = 0; i < 3; i++) { const rlEnv = createMxEnvV1({ msgType: 'Cmd', toRealm: REALM, toPath: 'test.accepts.cmd', fromRealm: 'sender', fromPath: 's', replyToRealm: 'sender', replyToPath: '_inbox', correlationId: `rl-${i}`, body: {}, }); backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(rlEnv)); } await new Promise(r => setTimeout(r, 300)); const finalMetrics = gateway.getMetrics(); assert.ok( finalMetrics.securityBlocks.antiRelay > initialMetrics.securityBlocks.antiRelay, 'Anti-relay metric should increase' ); assert.ok( finalMetrics.securityBlocks.replay > initialMetrics.securityBlocks.replay, 'Replay metric should increase' ); assert.ok( finalMetrics.securityBlocks.rateLimit > initialMetrics.securityBlocks.rateLimit, 'Rate limit metric should increase' ); gateway.stop(); }); });