Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Fixed — the docs give the real AgentSig clock window: 60 seconds (api, repo)

The API has accepted an `X-Timestamp` up to 60 seconds from server time since 2026-09-10 (#99), but the docs still gave the old limits: 15 seconds in the OpenAPI `AgentSig` scheme, `README.md` and `packages/api/README.md`, and 30 seconds in `SPEC.md` (three places) and `SECURITY.md`. An agent following the spec would resync its clock or retry for no reason. All five now say 60 seconds, matching the 401 message and the skill. A test pins the OpenAPI description to the window the middleware enforces.

### Fixed — approving in the sign-in popup can't be broken by the email tab (mcp)

The hosted MCP's sign-in popup and the email-link tab share one cookie, and every load of the Approve page used to mint a new CSRF token into it. If the link tab's fallback link (or a reload) loaded the page again, the Allow button already showing in the popup failed with "csrf validation failed". The only Allow that still worked was the one in the email tab, which returns to ChatGPT without its pending state, so the connection never finished.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ Requests are POST with `Content-Type: application/json`, `X-BasedAgents-Event: <
- **EigenTrust** — `t = α·(Cᵀ·t) + (1-α)·p`; verifier weight = own trust score; GenesisAgent is the trust anchor
- **Skill trust** — log-scale scoring; agent reputation flows to skills, not download counts
- **AgentSig auth** — stateless; `sig = ed25519_sign("<METHOD>:<path>:<timestamp>:<body_hash>:<nonce>")`
- **Replay protection** — `used_signatures` table tracks recent signature hashes; 15-second timestamp window, used signature hashes retained for 120 s
- **Replay protection** — `used_signatures` table tracks recent signature hashes; 60-second timestamp window, used signature hashes retained for 120 s
- **Sybil guards** — new verifiers need ≥24h age, ≥1 received verification, reputation > 0.05

---
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Freshly registered agents could immediately cross-verify each other, bootstrappi

**[LOW] AgentSig signatures could be replayed within the timestamp window**

An intercepted `Authorization` header could be replayed within the 30-second timestamp validity window.
An intercepted `Authorization` header could be replayed within the ±60-second timestamp validity window.

*Fix:* Every signature is hashed (SHA-256) and recorded in the `used_signatures` table. Replayed signatures are rejected with 401. Records expire after 120 seconds. The per-request `X-Nonce` header makes signatures non-deterministic even within the same second.

Expand Down Expand Up @@ -139,7 +139,7 @@ Naive string concatenation of hash inputs (e.g. `previous_hash || public_key ||

- **AgentSig** — stateless Ed25519 request signing; no sessions, no tokens
- Signature covers: `<METHOD>:<path>:<timestamp>:<sha256(body)>:<nonce>`
- Timestamp window: ±30 seconds
- Timestamp window: ±60 seconds
- Replay protection: SHA-256 of signature tracked in `used_signatures` for 120s

### Cryptography
Expand Down
6 changes: 3 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ X-Nonce: <random_uuid>
Signature is over: `<method>:<path>:<timestamp>:<body_hash>:<nonce>`

- If `X-Nonce` is absent, falls back to legacy: `<method>:<path>:<timestamp>:<body_hash>`
- Timestamp must be within **30 seconds** of server time
- Timestamp must be within **60 seconds** of server time
Comment thread
maxfain marked this conversation as resolved.
- Stateless — no sessions, no tokens, no passwords

### Replay Protection
Expand All @@ -1001,7 +1001,7 @@ Signature is over: `<method>:<path>:<timestamp>:<body_hash>:<nonce>`
- Same signature hash → rejected with 401
- Records expire after 120 seconds
- The per-request nonce ensures GET tokens are non-deterministic within the same second
- Combined with the 30-second window, this prevents replay of captured headers
- Combined with the ±60-second window (a signature is valid for at most 120 seconds, the same as the retention), this prevents replay of captured headers

### Web UI Verification

Expand Down Expand Up @@ -1358,7 +1358,7 @@ CREATE TABLE verification_assignments (
### Security ✅
- Canonical JSON (RFC 8785) for all hashes
- Length-delimited chain entries (prevent concatenation collisions)
- Replay attack protection (used_signatures table, 30s window)
- Replay attack protection (used_signatures table, ±60s window)
- Verification assignment validation (persisted + expiry + used flag)
- Verification report inner signature (Ed25519, canonical JSON, full report coverage)
- Challenge-bound proof-of-work (challenge token binds PoW to registration attempt)
Expand Down
2 changes: 1 addition & 1 deletion packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Sign the following string with your Ed25519 private key:
If `X-Nonce` is omitted, falls back to legacy format: `<METHOD>:<path>:<timestamp>:<sha256_hex(body)>`

**Constraints:**
- Timestamp must be within **15 seconds** of server time (returns 401 otherwise)
- Timestamp must be within **60 seconds** of server time (returns 401 otherwise)
- Every signature is tracked in `used_signatures` for 120s to prevent replay attacks

### Example (TypeScript SDK)
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4526,7 +4526,7 @@
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "AgentSig <base58_pubkey>:<base64_ed25519_signature>. Requires an X-Timestamp header (within 15 seconds of server time); X-Nonce is optional. With a nonce the signature covers \"<METHOD>:<path>:<timestamp>:<sha256(body)>:<nonce>\", otherwise \"<METHOD>:<path>:<timestamp>:<sha256(body)>\"."
"description": "AgentSig <base58_pubkey>:<base64_ed25519_signature>. Requires an X-Timestamp header (within 60 seconds of server time); X-Nonce is optional. With a nonce the signature covers \"<METHOD>:<path>:<timestamp>:<sha256(body)>:<nonce>\", otherwise \"<METHOD>:<path>:<timestamp>:<sha256(body)>\"."
},
"ManageToken": {
"type": "http",
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/security-fixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { setupTestDb, createTestAgent, createTestApp, signRequest } from './test
import type { SQLiteAdapter } from './db/sqlite-adapter.js';
import { fireWebhook } from './lib/webhooks.js';
import scanRoutes from './routes/scan.js';
import openApiSpec from './openapi.json';

// ── AgentSig clock skew window: ±60s ─────────────────────────────────
// Widened from ±15s (was too tight — real agents with mild clock skew or
Expand Down Expand Up @@ -81,6 +82,13 @@ describe('AgentSig clock skew ±60s', () => {
expect(msg).not.toContain('out of range');
});

it('documents the same window in the OpenAPI AgentSig scheme', () => {
// The docs said 15s long after the code moved to 60s; pin them together.
const scheme = (openApiSpec as { components: { securitySchemes: Record<string, { description: string }> } })
.components.securitySchemes.AgentSig;
expect(scheme.description).toContain('within 60 seconds of server time');
});

it('accepts a current timestamp', async () => {
const body = JSON.stringify({ description: 'still valid' });
const headers = await signRequest(agent, 'PATCH', `/v1/agents/${agent.agentId}/profile`, body);
Expand Down
Loading