EEP Security Model

This document explains the "why" and "how" behind every security decision in EEP.

1. Threat Model

ThreatClassificationMitigation
Spoofed eventsIntegrityDID-based source verification + payload HMAC
Replay attacksFreshnessSingle-use nonce + 60-second timestamp validation window
SSRF via webhooksNetworkStrict IP allowlist validation before dispatch
DDoS amplificationAvailabilityWebSub intent verification before any delivery
Subscription harvestingPrivacySubscriptions are private; never exposed in public APIs
Timing attacks on HMACCryptographytimingSafeEqual mandatory
Stale SSE dataFreshnessMandatory Last-Event-ID replay mechanism
Double-spend / payment replayIntegrityPayment hash consumed-ledger; each tx hash accepted once only
Key compromise / session hijackIntegritysession.revoked event triggers immediate termination of all matching WebSocket sessions
Harvest-now decrypt-later (quantum)CryptographyPQC-ready: hybrid EdDSA + ML-DSA signatures; ML-KEM transport declared via pqc_ready manifest flag

2. Webhook Security: HMAC-SHA256

EEP adopts the Standard Webhooks specification for payload signing.

Why HMAC-SHA256?

  • Symmetric — Same secret to sign and verify. No PKI needed.
  • Fast — Milliseconds overhead per dispatch.
  • Universal — Every major language has built-in HMAC support.

Signing Algorithm

HMAC Signing
import { createHmac } from 'crypto';
const content = `${webhookId}.${timestamp}.${rawBody}`;
const signature = createHmac('sha256', secret)
.update(content)
.digest('base64');
// Header: webhook-signature: v1,<signature>

Timing-Safe Comparison (Mandatory)

Verification
// ✅ CORRECT — timing-safe
import { timingSafeEqual } from 'crypto';
const valid = timingSafeEqual(
Buffer.from(expected, 'base64'),
Buffer.from(received, 'base64')
);
// ❌ WRONG — timing leak
const valid = expected === received;

3. SSRF Prevention

If a subscriber registers an internal URL, the publisher could be tricked into making requests to private services.

Required Protections

  1. DNS resolution before connection — Resolve hostname to IP before connecting
  2. IP blocklist validation — Reject loopback, private networks, link-local, and invalid ranges
  3. HTTPS only — Only https:// URLs in production
  4. No redirect following — Could bypass IP blocklist

SSRF Validation

SSRF Check
import { validateSSRF } from '@eep-dev/validator';
try {
await validateSSRF(subscriberUrl);
// Safe to connect
} catch (err) {
// SSRFError: blocked IP, private network, etc.
console.error('SSRF blocked:', err.message);
}

Blocked IP Ranges

RangeReason
127.0.0.0/8Loopback (localhost)
::1/128IPv6 loopback
10.0.0.0/8Private network (RFC 1918)
172.16.0.0/12Private network (RFC 1918)
192.168.0.0/16Private network (RFC 1918)
169.254.0.0/16Link-local (AWS metadata)
fc00::/7 and fd00::/8IPv6 Unique Local Address (private)
fe80::/10IPv6 link-local

4. WebSub Intent Verification

Prevents DDoS amplification by verifying the subscriber controls the delivery URL.

Intent Verification
// Publisher sends challenge
const challenge = crypto.randomBytes(16).toString('hex');
const verifyUrl = new URL(deliveryUrl);
verifyUrl.searchParams.set('hub.mode', 'subscribe');
verifyUrl.searchParams.set('hub.topic', sourceDid);
verifyUrl.searchParams.set('hub.challenge', challenge);
const res = await fetch(verifyUrl.toString());
const body = await res.text();
if (res.status === 200 && body === challenge) {
// Subscription activated
}

5. SSE Stream Security

  • SSE streams MUST require authentication (Authorization: Bearer or query parameter)
  • Events flagged as private are only accessible to the entity owner
  • Server-side filtering mandatory — never rely on client-side filtering
  • Maximum concurrent SSE connections per API key (recommended: 5 free, 20 paid)

6. WebSocket Security

  • Authentication MUST be verified at the HTTP Upgrade request, before handshake
  • JWT re-authentication for long-lived connections (see Specification §6.4)
  • All received messages MUST be validated against JSON schema before processing

7. Gate and Commerce Security

ThreatMitigation
Tier escalation@eep-dev/gates matches proofs to tiers and returns only the highest qualifying tier
Proof replayStructural validation checks expires_at and rejects future-dated issued_at
Config manipulationparseGateConfig() validates all constraints before use
Commerce state skippingNegotiation state machine rejects invalid transitions
Double receiptUse atomic payment-hash consumption (consumeIfFresh) for distributed safety
Missing semantic verifierresolveAccess is strict fail-closed by default; unmet verifier blocks grant
Fake reviewsVerify reviewer completed a transaction for the service

Two-Phase Validation

Gate Validation
import { resolveAccess, build402Response, ProofVerifierRegistry } from '@eep-dev/gates';
const registry = new ProofVerifierRegistry();
registry.register({
supportedTypes: ['payment'],
verify: async (proof) => {
return await stripe.paymentIntents.retrieve(proof.token)
.then(pi => pi.status === 'succeeded');
},
});
const result = await resolveAccess(proofs, config, resource, registry);
if (!result.granted) {
return res.json(await build402Response(config, resource, proofs), 402);
}

8. Delegation Proof Chains

When a delegated agent (sub-agent acting on behalf of an owner) presents a gate proof, it must attach a Delegation Proofalongside its own DID signature. A Delegation Proof is a W3C Verifiable Credential issued by the owner's DID to the agent's DID, specifying permitted actions, permitted endpoints, and an expiry time.

The publisher must verify:

  1. The Delegation Proof is signed by the owner DID (not the agent DID)
  2. The requested action falls within the explicitly declared delegation scope
  3. The credential has not expired (expirationDate is in the future)
  4. The chain terminates at a DID that directly owns the resource
// Delegation Proof — Verifiable Credential structure
{
  "@context": ["https://www.w3.org/ns/credentials/v2"],
  "type": ["VerifiableCredential", "EEPDelegationCredential"],
  "issuer": "did:web:owner.example.com",          // owner DID
  "credentialSubject": {
    "id": "did:web:agent.example.com",             // agent DID
    "permittedActions": ["subscribe", "read:events"],
    "permittedEndpoints": ["https://api.example.com/eep/*"],
    "expirationDate": "2026-03-07T00:00:00Z"
  }
}
Security rule: Delegation credentials without explicit permittedActionsscope restrictions MUST be rejected. Agents without explicit scope cannot claim unlimited authority.

9. Side-Channel and Constant-Time Security

All gate requirement verifications — credential checks, signature verifications, nonce consumptions — MUST be performed in constant time. Variable-time comparisons leak information about partial matches via timing side-channels.

  • Use timingSafeEqual (Node.js) / hmac.compare_digest (Python) for all cryptographic equality checks
  • Error responses for failed verification MUST NOT vary in detail or timing based on why verification failed — a failed signature and a failed nonce check must return the same HTTP status, same response latency, and generic error body
  • Specific failure reasons are logged internally but never surfaced to the requesting agent
// ✅ Correct — constant-time
import { timingSafeEqual } from 'crypto';
const expected = Buffer.from(computeExpectedSig(secret, content), 'base64');
const actual   = Buffer.from(receivedSig, 'base64');
const valid = expected.length === actual.length && timingSafeEqual(expected, actual);

// ❌ Wrong — variable-time (leaks timing info)
const valid = computeExpectedSig(secret, content) === receivedSig;