hivecast-sdk/packages/federation/tests/integration/security-guardrails.spec.ts

345 lines
10 KiB
TypeScript
Raw Normal View History

/**
* 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();
});
});