package membership import ( "encoding/json" "net/http" "strings" "testing" "time" cs "fn-registry/functions/cybersecurity" "github.com/enmanuel/unibus/pkg/frame" ) // TestAudit_OwnerSpoof ports the auditor's H6 finding: handleCreateRoom did not // bind the body's declared owner to the request signer, so a registered peer // could create rooms in another identity's name. Now the owner endpoint AND the // owner signing key must both be the authenticated signer's. func TestAudit_OwnerSpoof(t *testing.T) { h := newAuthHarness(t, AuthEnforce) bob, _ := cs.GenerateIdentity() register(t, h, bob, "bob") bobEp := frame.EndpointID(bob.SignPub) victim, _ := cs.GenerateIdentity() post := func(id cs.Identity, owner endpointJSON, nonce string) int { body, _ := json.Marshal(createRoomReq{Subject: "some.room", Owner: owner}) code, _ := do(t, signedReq(t, h.ts.URL, "POST", "/rooms", body, id, time.Now().Unix(), nonce)) return code } // Error path: bob signs, body claims victim as owner -> 403. if code := post(bob, endpointJSON{Endpoint: frame.EndpointID(victim.SignPub), SignPub: victim.SignPub, KexPub: victim.KexPub}, "spoof-1"); code != http.StatusForbidden { t.Fatalf("owner-spoofed create should be 403, got %d", code) } // Edge: bob declares his own endpoint but a foreign signing key -> 403 (the // key, not just the endpoint string, is bound to the signer). if code := post(bob, endpointJSON{Endpoint: bobEp, SignPub: victim.SignPub, KexPub: victim.KexPub}, "spoof-2"); code != http.StatusForbidden { t.Fatalf("create with a foreign owner key should be 403, got %d", code) } // Golden: alice creates a room owned by herself -> 201. aliceEp := frame.EndpointID(h.alice.SignPub) if code := post(h.alice, endpointJSON{Endpoint: aliceEp, SignPub: h.alice.SignPub, KexPub: h.alice.KexPub}, "owner-ok"); code != http.StatusCreated { t.Fatalf("self-owned create should be 201, got %d", code) } } // TestAudit_NonceCachePoisonPreAuth ports the auditor's H7 finding: the replay // cache was populated BEFORE the allowlist check, so any unregistered identity // (Ed25519 keys are free) could seed nonces into it. Now IsAuthorized runs first, // so an unauthorized identity's nonce is never cached: a repeat of the same nonce // still fails as "not authorized", not "replayed nonce". func TestAudit_NonceCachePoisonPreAuth(t *testing.T) { h := newAuthHarness(t, AuthEnforce) eve, _ := cs.GenerateIdentity() // valid signatures, NOT on the allowlist now := time.Now().Unix() code1, body1 := do(t, signedReq(t, h.ts.URL, "GET", "/rooms/x", nil, eve, now, "poison-nonce")) if code1 != http.StatusUnauthorized || !strings.Contains(body1, "not authorized") { t.Fatalf("unregistered first request should be 401 not-authorized, got %d (%s)", code1, body1) } // Same nonce again: if the nonce had been cached, this would report "replayed // nonce". It must still be "not authorized" — proving the nonce was NOT cached. code2, body2 := do(t, signedReq(t, h.ts.URL, "GET", "/rooms/x", nil, eve, now, "poison-nonce")) if code2 != http.StatusUnauthorized { t.Fatalf("unregistered replay should still be 401, got %d", code2) } if strings.Contains(body2, "replayed") { t.Fatalf("an unauthorized identity's nonce was cached pre-auth: %s", body2) } if !strings.Contains(body2, "not authorized") { t.Fatalf("second unregistered request should still be not-authorized, got: %s", body2) } // Positive control: an AUTHORIZED identity's replay IS still rejected, so the // reorder did not weaken anti-replay for legitimate traffic. if code, _ := do(t, signedReq(t, h.ts.URL, "GET", aliceRoomsPath(h), nil, h.alice, now, "alice-live")); code != http.StatusOK { t.Fatalf("alice's first request should be 200, got %d", code) } if code, body := do(t, signedReq(t, h.ts.URL, "GET", aliceRoomsPath(h), nil, h.alice, now, "alice-live")); code != http.StatusUnauthorized || !strings.Contains(body, "replayed") { t.Fatalf("alice's replay should be 401 replayed nonce, got %d (%s)", code, body) } }