test(relay): cross-package e2e adversarial security harness
New e2e/ package wires the REAL P5(auth)+P4(crypto)+P2(agent) exports through in-memory seams and an untrusted-relay attacker vantage (RelaySpy). Dynamically re-validates F1–F4/F6 plus MITM/reflection/replay/reorder/INV2/cross-tenant/ single-use — every assertion runs the production code path. 21 tests green, tsc clean. node_modules via symlinks (gitignored); no npm install needed.
This commit is contained in:
99
e2e/tests/flow.test.ts
Normal file
99
e2e/tests/flow.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Happy-path full flow, end-to-end across P5 (auth) + P4 (E2E crypto) + P2 (replay), asserting each
|
||||
* stage. Everything runs through the REAL package exports; only registries/buckets/sockets are faked.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { buildRelayWorld, DEFAULT_NOW, type RelayWorld } from '../harness/world.js'
|
||||
|
||||
const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s)
|
||||
const fromUtf8 = (b: Uint8Array): string => new TextDecoder().decode(b)
|
||||
const NOW = DEFAULT_NOW
|
||||
|
||||
describe('relay flow (happy path)', () => {
|
||||
let world: RelayWorld
|
||||
beforeEach(async () => {
|
||||
world = await buildRelayWorld(NOW)
|
||||
})
|
||||
|
||||
it('issueCap → upgrade allows (origin ok, DPoP bound, deny-by-default satisfied)', async () => {
|
||||
const { hostA } = world
|
||||
const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud })
|
||||
const out = await world.upgrade({
|
||||
raw: cap.raw,
|
||||
dpop: cap.dpop,
|
||||
host: hostA.hostId,
|
||||
aud: hostA.aud,
|
||||
origin: hostA.origin,
|
||||
now: NOW,
|
||||
})
|
||||
expect(out.ok).toBe(true)
|
||||
expect(world.audit.events.at(-1)).toMatchObject({ outcome: 'allow', action: 'attach' })
|
||||
})
|
||||
|
||||
it('handshake establishes MATCHING session keys on both sides (client↔host round-trip)', async () => {
|
||||
const { client, host } = await world.establishSession()
|
||||
// Matching keys ⇒ bidirectional plaintext round-trips through the direction split.
|
||||
const c2h = host.open(client.seal(utf8('ls -la')))
|
||||
expect(fromUtf8(c2h)).toBe('ls -la')
|
||||
const h2c = client.open(host.seal(utf8('total 0')))
|
||||
expect(fromUtf8(h2c)).toBe('total 0')
|
||||
})
|
||||
|
||||
it('seals client→host and host→client through the RelaySpy; the RelaySpy never sees plaintext (INV2)', async () => {
|
||||
const { client, host, spy } = await world.establishSession()
|
||||
const marker = `PLAINTEXT_${crypto.randomUUID()}`
|
||||
|
||||
const up = spy.forward(client.seal(utf8(marker)))
|
||||
expect(fromUtf8(host.open(up))).toBe(marker)
|
||||
const down = spy.forward(host.seal(utf8(`reply_${marker}`)))
|
||||
expect(fromUtf8(client.open(down))).toBe(`reply_${marker}`)
|
||||
|
||||
expect(spy.contains(marker)).toBe(false)
|
||||
expect(spy.captured.length).toBe(2)
|
||||
})
|
||||
|
||||
it('reattach to an own-account session is allowed', async () => {
|
||||
const { hostA } = world
|
||||
const sessionId = crypto.randomUUID()
|
||||
world.addSession({ sessionId, hostId: hostA.hostId, accountId: hostA.accountId })
|
||||
const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud })
|
||||
const out = await world.reattach({
|
||||
raw: cap.raw,
|
||||
dpop: cap.dpop,
|
||||
host: hostA.hostId,
|
||||
aud: hostA.aud,
|
||||
origin: hostA.origin,
|
||||
sessionId,
|
||||
now: NOW,
|
||||
})
|
||||
expect(out.ok).toBe(true)
|
||||
expect(world.audit.events.at(-1)).toMatchObject({ outcome: 'allow', action: 'reattach' })
|
||||
})
|
||||
|
||||
it('revokeToken then re-presenting the same jti (fresh DPoP) is denied', async () => {
|
||||
const { hostA } = world
|
||||
const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud })
|
||||
const first = await world.upgrade({
|
||||
raw: cap.raw,
|
||||
dpop: cap.dpop,
|
||||
host: hostA.hostId,
|
||||
aud: hostA.aud,
|
||||
origin: hostA.origin,
|
||||
now: NOW,
|
||||
})
|
||||
expect(first.ok).toBe(true)
|
||||
if (!first.ok) return
|
||||
|
||||
await world.revokeToken(first.jti)
|
||||
|
||||
const second = await world.upgrade({
|
||||
raw: cap.raw,
|
||||
dpop: await cap.newDpop(NOW), // fresh DPoP jti so we reach the revocation gate, not the DPoP cache
|
||||
host: hostA.hostId,
|
||||
aud: hostA.aud,
|
||||
origin: hostA.origin,
|
||||
now: NOW,
|
||||
})
|
||||
expect(second).toMatchObject({ ok: false, status: 403, reason: 'token_revoked' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user