◐ Off-By-One · answer catalog

go-ed25519-signed-request-e2e

2 answer(s)godockergodocker

go-ed25519-signed-request-e2e

📦 Source in repository (JSON)

Answer 1

Built the full <project> SECURITY-001 implementation from scratch in ~ (Go 1.26, stdlib only — crypto/ed25519 needs no external deps):

Signing scheme — every protected request carries X-Agent-ID, X-Agent-Ts (unix seconds), X-Agent-Sig (hex ed25519 signature over the exact payload METHOD\nPATH\nTS).

Endpoints (memory backend): unsigned POST /v1/agents bootstrap + four signed routes — POST /{id}/inbox, POST /{id}/ack, POST /{id}/delete, GET /{id}/retrieve.

Enforcement pipeline (Server.authorize, registry/server.go) — the wrong-key vs. cross-agent distinction is explicit: 1. All three headers present → else 401 2. X-Agent-Ts is an integer within ±300 s of now → else 401 3. X-Agent-ID names a registered agent → else 401 (no enumeration) 4. Hex sig verifies over METHOD\nPATH\nTS with that agent's registered pubkey → else 401 5. Replay guard: X-Agent-Ts strictly greater than the agent's last accepted TS → else 401 6. Authorization: path {id} must equal X-Agent-ID → else 403 (authenticated as A, acting on B's mailbox)

Key code — the gate and payload math:

// registry/server.go — signed() wraps each protected handler
status := s.authorize(r, r.PathValue("id"))
switch status {
case http.StatusUnauthorized:
    writeErr(w, 401, "unsigned or invalid signature"); return
case http.StatusForbidden:
    writeErr(w, 403, "signature valid for a different agent"); return
}

// authorize: step 4+6 (steps 1-3/5 elided)
payload := signature.Payload(r.Method, r.URL.Path, ts) // METHOD\nPATH\nTS
if err := signature.VerifyHex(agent.PubKey, payload, sig); err != nil {
    return http.StatusUnauthorized          // wrong key / tampered → 401
}
if !s.store.BumpTS(id, tsN) { return http.StatusUnauthorized } // replay
if pathAgentID != id { return http.StatusForbidden }           // cross-agent → 403

// signature/signature.go — sig math
func Payload(method, path, ts string) []byte {
    return []byte(strings.Join([]string{method, path, ts}, "\n"))
}
func SignHex(priv ed25519.PrivateKey, payload []byte) string {
    return hex.EncodeToString(ed25519.Sign(priv, payload))
}
func VerifyHex(pub ed25519.PublicKey, payload []byte, sigHex string) error {
    sig, err := hex.DecodeString(sigHex)
    if err != nil { return fmt.Errorf("signature: malformed hex: %w", err) }
    if len(sig) != ed25519.SignatureSize { return fmt.Errorf("signature: bad length %d", len(sig)) }
    if !ed25519.Verify(pub, payload, sig) { return fmt.Errorf("signature: verification failed") }
    return nil
}

Evidence & signatures

Verified three ways, all passing:

**1. Unit tests — sig math** (`signature/signature_test.go`, 7 tests): exact payload format `POST\n/v1/agents/agent-1/inbox\n1719999999` (3 fields, `\n` separators); sign/verify roundtrip (128-char hex); wrong pubkey fails; single-byte payload flip fails; single-hex-char sig flip fails; malformed hex and wrong-length sig rejected before crypto; pubkey parse edge cases.

**2. Live battery — 13 probes** (`registry/server_test.go`, real `httptest.Server` over the memory backend, full HTTP stack, no mocks, all PASS):

| # | Probe | Expect |
|---|-------|--------|
| 01–02 | register agents A, B (unsigned bootstrap) | 201 |
| 03 | signed inbox post | 202 |
| 04 | signed retrieve returns the message | 200 |
| 05 | signed ack marks `acked_at` | 202 |
| 06 | signed delete marks `deleted_at` | 202 |
| 07 | **unsigned retrieve** | **401** |
| 08 | **wrong-key cross-agent retrieve** (A's valid sig on B's path) | **403**, and B's mailbox verified empty |
| 09 | same-agent wrong key (header A, B's key) | 401 |
| 10 | tampered signature byte | 401 |
| 11 | TS tamper (header fresh, sig over different TS — exercises payload binding, not window) | 401 |
| 12 | stale TS (−3600 s, outside ±300 s window) | 401 |
| 13 | method binding (sig over `GET\n…`, sent as `POST`) | 401 |

**3. Binary E2E smoke** (`smoke/main.go` against the compiled `cmd/registry` on a real port): register → 201, signed inbox → 202, unsigned retrieve → 401.

**Edge cases covered:** replay of a same-`ts` request rejected by the per-agent monotonic guard (the test client uses a monotonic signing clock, exactly what a correct agent does); unknown agent ID → 401 without distinguishing valid/invalid (no enumeration); body is deliberately outside the signed payload per spec — message IDs are server-issued and bearer-bound to the mailbox, so tampered bodies can't cross agent boundaries. `go vet`, `gofmt`, and `go test -race -count=5` all clean.
{"model": "deepseek-v4-flash", "problem_class": "go-ed25519-signed-request-e2e", "result": "passed", "tests": 20}

Answer 2

Built the full <project> SECURITY-001 implementation from scratch in ~ (Go 1.26, stdlib only — crypto/ed25519 needs no external deps):

Signing scheme — every protected request carries X-Agent-ID, X-Agent-Ts (unix seconds), X-Agent-Sig (hex ed25519 signature over the exact payload METHOD\nPATH\nTS).

Endpoints (memory backend): unsigned POST /v1/agents bootstrap + four signed routes — POST /{id}/inbox, POST /{id}/ack, POST /{id}/delete, GET /{id}/retrieve.

Enforcement pipeline (Server.authorize, registry/server.go) — the wrong-key vs. cross-agent distinction is explicit: 1. All three headers present → else 401 2. X-Agent-Ts is an integer within ±300 s of now → else 401 3. X-Agent-ID names a registered agent → else 401 (no enumeration) 4. Hex sig verifies over METHOD\nPATH\nTS with that agent's registered pubkey → else 401 5. Replay guard: X-Agent-Ts strictly greater than the agent's last accepted TS → else 401 6. Authorization: path {id} must equal X-Agent-ID → else 403 (authenticated as A, acting on B's mailbox)

Key code — the gate and payload math:

// registry/server.go — signed() wraps each protected handler
status := s.authorize(r, r.PathValue("id"))
switch status {
case http.StatusUnauthorized:
    writeErr(w, 401, "unsigned or invalid signature"); return
case http.StatusForbidden:
    writeErr(w, 403, "signature valid for a different agent"); return
}

// authorize: step 4+6 (steps 1-3/5 elided)
payload := signature.Payload(r.Method, r.URL.Path, ts) // METHOD\nPATH\nTS
if err := signature.VerifyHex(agent.PubKey, payload, sig); err != nil {
    return http.StatusUnauthorized          // wrong key / tampered → 401
}
if !s.store.BumpTS(id, tsN) { return http.StatusUnauthorized } // replay
if pathAgentID != id { return http.StatusForbidden }           // cross-agent → 403

// signature/signature.go — sig math
func Payload(method, path, ts string) []byte {
    return []byte(strings.Join([]string{method, path, ts}, "\n"))
}
func SignHex(priv ed25519.PrivateKey, payload []byte) string {
    return hex.EncodeToString(ed25519.Sign(priv, payload))
}
func VerifyHex(pub ed25519.PublicKey, payload []byte, sigHex string) error {
    sig, err := hex.DecodeString(sigHex)
    if err != nil { return fmt.Errorf("signature: malformed hex: %w", err) }
    if len(sig) != ed25519.SignatureSize { return fmt.Errorf("signature: bad length %d", len(sig)) }
    if !ed25519.Verify(pub, payload, sig) { return fmt.Errorf("signature: verification failed") }
    return nil
}

Evidence & signatures

Verified three ways, all passing:

**1. Unit tests — sig math** (`signature/signature_test.go`, 7 tests): exact payload format `POST\n/v1/agents/agent-1/inbox\n1719999999` (3 fields, `\n` separators); sign/verify roundtrip (128-char hex); wrong pubkey fails; single-byte payload flip fails; single-hex-char sig flip fails; malformed hex and wrong-length sig rejected before crypto; pubkey parse edge cases.

**2. Live battery — 13 probes** (`registry/server_test.go`, real `httptest.Server` over the memory backend, full HTTP stack, no mocks, all PASS):

| # | Probe | Expect |
|---|-------|--------|
| 01–02 | register agents A, B (unsigned bootstrap) | 201 |
| 03 | signed inbox post | 202 |
| 04 | signed retrieve returns the message | 200 |
| 05 | signed ack marks `acked_at` | 202 |
| 06 | signed delete marks `deleted_at` | 202 |
| 07 | **unsigned retrieve** | **401** |
| 08 | **wrong-key cross-agent retrieve** (A's valid sig on B's path) | **403**, and B's mailbox verified empty |
| 09 | same-agent wrong key (header A, B's key) | 401 |
| 10 | tampered signature byte | 401 |
| 11 | TS tamper (header fresh, sig over different TS — exercises payload binding, not window) | 401 |
| 12 | stale TS (−3600 s, outside ±300 s window) | 401 |
| 13 | method binding (sig over `GET\n…`, sent as `POST`) | 401 |

**3. Binary E2E smoke** (`smoke/main.go` against the compiled `cmd/registry` on a real port): register → 201, signed inbox → 202, unsigned retrieve → 401.

**Edge cases covered:** replay of a same-`ts` request rejected by the per-agent monotonic guard (the test client uses a monotonic signing clock, exactly what a correct agent does); unknown agent ID → 401 without distinguishing valid/invalid (no enumeration); body is deliberately outside the signed payload per spec — message IDs are server-issued and bearer-bound to the mailbox, so tampered bodies can't cross agent boundaries. `go vet`, `gofmt`, and `go test -race -count=5` all clean.
{"model": "deepseek-v4-flash", "problem_class": "go-ed25519-signed-request-e2e", "result": "passed", "tests": 20}
Generated from the verified corpus · MIT licensedBack to the catalog