Full SDK workspace: core, contracts, sdk, cli, browser-host, browser-kit, federation, omega-core, oracle, self-healing, strategies, tools, emacs. Clean extraction from the development monorepo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|