Initial Commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# OpenSpec Change: network-plugin-intra-relay-security
|
||||
|
||||
- **Change ID**: network-plugin-intra-relay-security
|
||||
- **Package**: `@suaveplan/network-plugin-intra-relay` (extension)
|
||||
- **Status**: PROPOSED
|
||||
- **Type**: extension
|
||||
- **Date**: 2026-05-05
|
||||
|
||||
## Why
|
||||
|
||||
The core `network-plugin-intra-relay` change shipped the relay primitives (discovery, selection, FSM, envelope, loop prevention, performance monitor) but deferred `RelaySecurityManager` per the parent proposal's `## Deferred to follow-up changes` block. The security manager couples to `network-plugin-crypto-messages` for `PeerIdentityVerifier` and adds per-peer relay rate limiting; both warranted their own design + test surface rather than landing inline with the core.
|
||||
|
||||
This change adds `RelaySecurityManager` to the existing package without breaking the core surface.
|
||||
|
||||
## What changes
|
||||
|
||||
- **ADDED**: `RelaySecurityManager` class — authenticates relay nodes using the `PeerIdentityVerifier` from `@suaveplan/network-plugin-crypto-messages`, validates relay-message signatures, and enforces per-peer relay rate limits to prevent DoS.
|
||||
- **ADDED**: `RelayAuthPolicy` — `{ requireSignature: boolean; trustedPeers: ReadonlySet<string> }` for callers that want to allowlist a specific relay set.
|
||||
- **ADDED**: `RelayRateLimit` — `{ maxMessagesPerSecond: number; burstSize: number }` token-bucket configuration.
|
||||
- **ADDED**: Three error codes appended to `NETWORK_INTRA_RELAY_CODES`: `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, `RELAY_RATE_LIMIT_EXCEEDED`.
|
||||
- **ADDED**: `IntraRelayPlugin.security` getter wires the new subsystem onto the plugin facade.
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/security/relay-security-manager.spec-conformance.test.ts` — pins peer-identity verification, signature validation, rate-limit enforcement (per-peer + global), trusted-peer allowlist, signature-required toggle.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. Existing consumers of `IntraRelayPlugin` see one new optional config field (`security?: RelaySecurityConfig`).
|
||||
@@ -0,0 +1,94 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec: network-intra-relay (security delta)
|
||||
|
||||
**Package**: `@suaveplan/network-plugin-intra-relay`
|
||||
**Status**: ARCHIVED — folded into `openspec/specs/network-intra-relay/spec.md` on 2026-05-10.
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: RelaySecurityManager authorises inbound relay messages
|
||||
|
||||
The package SHALL export a `RelaySecurityManager` class that runs three checks in order against every inbound relay message: trusted-peer allowlist, `ed25519` signature validation, and per-peer + global token-bucket rate limit. The class SHALL throw a `NetworkIntraRelayError` carrying one of `RELAY_PEER_UNTRUSTED`, `RELAY_SIGNATURE_INVALID`, or `RELAY_RATE_LIMIT_EXCEEDED` on failure. The class SHALL accept a `RelaySecurityManagerConfig` carrying `{ policy: RelayAuthPolicy; rateLimit: RelayRateLimit; peerKeyResolver: PeerKeyResolver; clock?: () => number; signatureVerifier?: SignatureVerifier }`.
|
||||
|
||||
**Verified by:** `src/security/relay-security-manager.spec-conformance.test.ts`
|
||||
|
||||
#### Scenario: admits a peer in the trusted-set allowlist
|
||||
|
||||
- **WHEN** `policy.trustedPeers` is `Set(["peer-a"])` and `authorize` is called with `senderPeerId: "peer-a"` and a valid signature
|
||||
- **THEN** the call does NOT throw
|
||||
|
||||
#### Scenario: rejects a peer not in a non-empty allowlist
|
||||
|
||||
- **WHEN** `policy.trustedPeers` is `Set(["peer-a"])` and `authorize` is called with `senderPeerId: "peer-b"`
|
||||
- **THEN** the call throws `NetworkIntraRelayError(RELAY_PEER_UNTRUSTED)`
|
||||
|
||||
#### Scenario: empty allowlist admits any peer
|
||||
|
||||
- **WHEN** `policy.trustedPeers` is the empty set
|
||||
- **THEN** any peer id is admitted (subject to the signature and rate-limit checks)
|
||||
|
||||
#### Scenario: rejects bad signature with RELAY_SIGNATURE_INVALID
|
||||
|
||||
- **WHEN** a message arrives with a signature that fails ed25519 verification against the resolved public key
|
||||
- **THEN** `authorize` throws `NetworkIntraRelayError(RELAY_SIGNATURE_INVALID)`
|
||||
|
||||
#### Scenario: missing signature when policy requires one
|
||||
|
||||
- **WHEN** `policy.requireSignature` is `true` and the input lacks a `signature`
|
||||
- **THEN** `authorize` throws `NetworkIntraRelayError(RELAY_SIGNATURE_INVALID)`
|
||||
|
||||
#### Scenario: missing signature when policy does not require one
|
||||
|
||||
- **WHEN** `policy.requireSignature` is `false` and the input lacks a `signature`
|
||||
- **THEN** `authorize` does NOT throw on the signature check
|
||||
|
||||
#### Scenario: per-peer rate-limit exhaustion
|
||||
|
||||
- **WHEN** a single peer consumes more than `rateLimit.burstSize` tokens before the clock advances enough to refill the bucket
|
||||
- **THEN** the next `authorize` call from that peer throws `NetworkIntraRelayError(RELAY_RATE_LIMIT_EXCEEDED)` with `context.scope === "per-peer"`
|
||||
|
||||
#### Scenario: global rate-limit exhaustion separate from per-peer
|
||||
|
||||
- **WHEN** multiple peers collectively exhaust the global bucket while each peer's individual bucket still has tokens
|
||||
- **THEN** further `authorize` calls throw `NetworkIntraRelayError(RELAY_RATE_LIMIT_EXCEEDED)` with `context.scope === "global"`
|
||||
|
||||
#### Scenario: isRateLimited inspection without consuming
|
||||
|
||||
- **WHEN** `isRateLimited(peerId)` is called
|
||||
- **THEN** the result reflects whether `authorize` would currently fail on the rate-limit check, without consuming a token
|
||||
|
||||
### Requirement: error-code registry extended with three security codes
|
||||
|
||||
The `NETWORK_INTRA_RELAY_CODES` registry SHALL include `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, and `RELAY_RATE_LIMIT_EXCEEDED` as frozen members.
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts`
|
||||
|
||||
#### Scenario: registry exposes the three codes
|
||||
|
||||
- **WHEN** `NETWORK_INTRA_RELAY_CODES` is inspected
|
||||
- **THEN** it contains `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, `RELAY_RATE_LIMIT_EXCEEDED` mapped to `"NET_INTRA_RELAY.<KEY>"` values
|
||||
|
||||
### Requirement: IntraRelayPlugin exposes a security getter
|
||||
|
||||
`IntraRelayPlugin` SHALL accept an optional `security?: RelaySecurityManagerConfig` field in its constructor config and SHALL expose a `security` getter returning `RelaySecurityManager | null`. Omitting the field SHALL leave security disabled (`security` returns `null`); supplying it SHALL construct a `RelaySecurityManager` wired to the supplied policy / rate-limit / resolver.
|
||||
|
||||
**Verified by:** `src/plugin/intra-relay-plugin.spec-conformance.test.ts`
|
||||
|
||||
#### Scenario: security getter returns null when no config supplied
|
||||
|
||||
- **WHEN** `IntraRelayPlugin` is constructed without `security`
|
||||
- **THEN** the `security` getter returns `null`
|
||||
|
||||
#### Scenario: security getter returns a manager when configured
|
||||
|
||||
- **WHEN** `IntraRelayPlugin` is constructed with a valid `security` config
|
||||
- **THEN** the `security` getter returns a `RelaySecurityManager` whose `isRateLimited` and `authorize` methods are wired correctly
|
||||
|
||||
### Requirement: types are exported for consumers
|
||||
|
||||
The package SHALL export `RelayAuthPolicy`, `RelayRateLimit`, and `RelaySecurityCheckInput` types from the root barrel and from a subpath export at `./security/security`.
|
||||
|
||||
**Verified by:** `tests/dep-graph.test.ts`, package `exports` map.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Tasks — network-plugin-intra-relay-security
|
||||
|
||||
> Blocked on: `network-plugin-intra-relay` (core), `network-plugin-crypto-messages`.
|
||||
|
||||
## 1. Implementation
|
||||
|
||||
- [x] 1.1 Append three codes to `NETWORK_INTRA_RELAY_CODES`: `RELAY_SIGNATURE_INVALID`, `RELAY_PEER_UNTRUSTED`, `RELAY_RATE_LIMIT_EXCEEDED`.
|
||||
- [x] 1.2 `src/security/relay-security-manager.ts` — `RelaySecurityManager`.
|
||||
- [x] 1.3 Authenticate relay nodes using `PeerIdentityVerifier` from `@suaveplan/network-plugin-crypto-messages`.
|
||||
- [x] 1.4 Validate relay-message signatures.
|
||||
- [x] 1.5 Token-bucket rate limit per peer + global.
|
||||
- [x] 1.6 `RelayAuthPolicy` + `RelayRateLimit` types in `src/types/security.types.ts`.
|
||||
- [x] 1.7 Wire `IntraRelayPlugin.security` getter.
|
||||
|
||||
## 2. Tests
|
||||
|
||||
- [x] 2.1 `src/security/relay-security-manager.spec-conformance.test.ts` — auth, signature, rate limit, allowlist, signature-required toggle.
|
||||
|
||||
## 3. Verify
|
||||
|
||||
- [x] 3.1 `bunx turbo lint typecheck test --filter=@suaveplan/network-plugin-intra-relay` green.
|
||||
- [x] 3.2 `bunx turbo test:coverage --filter=@suaveplan/network-plugin-intra-relay` 100/100/100/100.
|
||||
- [x] 3.3 `bun scripts/openspec-conformance.ts @suaveplan/network-plugin-intra-relay` PASS.
|
||||
Reference in New Issue
Block a user