7d93d550d1
Add the gateway backend for the wallet onboarding flow so each browser session carries its OWN bus identity instead of sharing the single operator client. - POST /api/session (session.go): the browser hands its full wallet keypair (unlocked from the local encrypted key, over TLS) and the gateway spins up a dedicated bus client that acts AS that user. The private key lives only in process memory for the life of the session and is dropped on logout/shutdown. identityFromHex enforces the exact key sizes (sign_pub 32, sign_priv 64, kex_pub 32, kex_priv 32) that match cs.Identity on the Go side. - POST /api/register (register.go): unauthenticated onboarding gated by a one-shot invite token. Validates the two PUBLIC key halves, then either consumes a configured --mock-tokens invite (local testing) or proxies to the bus POST /register (--register-url, bus >= 0.12.0). The handle/role come from the invite, never from the client. - server.go: sessions move from a token->time map to a sessionStore of per-user *session records; auth() now resolves the session and passes its gateway to each handler. The legacy operator passphrase login (POST /api/login) is kept, bound to the shared operator gateway. - main.go: build a busTemplate config that wallet sessions clone with their own Identity; wire --register-url / --mock-tokens. - webgw_test.go: identity-size validation, hex-key validation, mock token parsing, and single-use register (201 then 409) using a fixed browser-derived wallet vector. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.3 KiB
Go
115 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fixed wallet vector derived in the browser from the mnemonic
|
|
// "legal winner thank year wave sausage worth useful legal winner thank yellow"
|
|
// using the unibus-sign-v1 / unibus-kex-v1 HKDF scheme. Used to assert the Go
|
|
// side accepts the browser-derived key sizes.
|
|
const (
|
|
fixSignPub = "3d594317212e53a3685b305539f6789eb8c538579e350ca795278b180ebb53db"
|
|
fixSignPriv = "94485d66ac958e23546be2e3b7575a47e1264bdf082e09abb7ad02ab32fcd55e3d594317212e53a3685b305539f6789eb8c538579e350ca795278b180ebb53db"
|
|
fixKexPub = "f3561ca116e4444b8880b8c0a35f2c9e85804d8628006facd84b1a6146208257"
|
|
fixKexPriv = "f6ffdf15e5ee2af0494897ff43e61a06d632af425a0372cb53a7c3e0f84c2bb2"
|
|
)
|
|
|
|
func TestIdentityFromHex(t *testing.T) {
|
|
id, err := identityFromHex(fixSignPub, fixSignPriv, fixKexPub, fixKexPriv)
|
|
if err != nil {
|
|
t.Fatalf("identityFromHex valid vector: %v", err)
|
|
}
|
|
if len(id.SignPub) != 32 || len(id.SignPriv) != 64 || len(id.KexPub) != 32 || len(id.KexPriv) != 32 {
|
|
t.Fatalf("wrong sizes: %d/%d/%d/%d", len(id.SignPub), len(id.SignPriv), len(id.KexPub), len(id.KexPriv))
|
|
}
|
|
|
|
// Wrong sign_priv size (32 instead of 64) must be rejected.
|
|
if _, err := identityFromHex(fixSignPub, fixSignPub, fixKexPub, fixKexPriv); err == nil {
|
|
t.Fatalf("expected error for short sign_priv")
|
|
}
|
|
// Non-hex must be rejected.
|
|
if _, err := identityFromHex("zz", fixSignPriv, fixKexPub, fixKexPriv); err == nil {
|
|
t.Fatalf("expected error for non-hex sign_pub")
|
|
}
|
|
}
|
|
|
|
func TestValidHexKey(t *testing.T) {
|
|
if !validHexKey(fixSignPub) {
|
|
t.Fatalf("fixSignPub should be a valid 32-byte hex key")
|
|
}
|
|
if validHexKey("abcd") {
|
|
t.Fatalf("short key should be invalid")
|
|
}
|
|
if validHexKey(strings.Repeat("z", 64)) {
|
|
t.Fatalf("non-hex key should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestNewRegistrarParsesMockTokens(t *testing.T) {
|
|
r := newRegistrar("", "demo=demo:member, bob=bob, alice=alice:admin")
|
|
if len(r.mockTokens) != 3 {
|
|
t.Fatalf("want 3 mock tokens, got %d", len(r.mockTokens))
|
|
}
|
|
if r.mockTokens["demo"].role != "member" || r.mockTokens["demo"].handle != "demo" {
|
|
t.Fatalf("demo token parsed wrong: %+v", r.mockTokens["demo"])
|
|
}
|
|
if r.mockTokens["bob"].role != "member" {
|
|
t.Fatalf("bob should default to role member, got %q", r.mockTokens["bob"].role)
|
|
}
|
|
if r.mockTokens["alice"].role != "admin" {
|
|
t.Fatalf("alice should be admin, got %q", r.mockTokens["alice"].role)
|
|
}
|
|
}
|
|
|
|
// post builds a server with only a registrar (the register path does not touch a
|
|
// gateway) and runs one POST /api/register, returning status + decoded body.
|
|
func postRegister(t *testing.T, s *server, body string) (int, map[string]string) {
|
|
t.Helper()
|
|
req := httptest.NewRequest("POST", "/api/register", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
s.handleRegister(w, req)
|
|
var m map[string]string
|
|
_ = json.Unmarshal(w.Body.Bytes(), &m)
|
|
return w.Code, m
|
|
}
|
|
|
|
func TestHandleRegisterMockSingleUse(t *testing.T) {
|
|
s := &server{registrar: newRegistrar("", "demo=demo:member")}
|
|
|
|
// 1) valid token + valid keys => 201 with the invite's handle/role.
|
|
code, body := postRegister(t, s, `{"token":"demo","sign_pub":"`+fixSignPub+`","kex_pub":"`+fixKexPub+`"}`)
|
|
if code != 201 {
|
|
t.Fatalf("first register: want 201, got %d (%v)", code, body)
|
|
}
|
|
if body["handle"] != "demo" || body["role"] != "member" {
|
|
t.Fatalf("first register body: %v", body)
|
|
}
|
|
|
|
// 2) same token again => 409 (single-use consumed).
|
|
code, _ = postRegister(t, s, `{"token":"demo","sign_pub":"`+fixSignPub+`","kex_pub":"`+fixKexPub+`"}`)
|
|
if code != 409 {
|
|
t.Fatalf("reused token: want 409, got %d", code)
|
|
}
|
|
}
|
|
|
|
func TestHandleRegisterValidation(t *testing.T) {
|
|
s := &server{registrar: newRegistrar("", "demo=demo:member")}
|
|
|
|
// bad sign_pub (too short) => 400
|
|
if code, _ := postRegister(t, s, `{"token":"demo","sign_pub":"abcd","kex_pub":"`+fixKexPub+`"}`); code != 400 {
|
|
t.Fatalf("short sign_pub: want 400, got %d", code)
|
|
}
|
|
// missing token => 400
|
|
if code, _ := postRegister(t, s, `{"sign_pub":"`+fixSignPub+`","kex_pub":"`+fixKexPub+`"}`); code != 400 {
|
|
t.Fatalf("missing token: want 400, got %d", code)
|
|
}
|
|
// unknown token with no mock match and no register-url => 400
|
|
if code, _ := postRegister(t, s, `{"token":"nope","sign_pub":"`+fixSignPub+`","kex_pub":"`+fixKexPub+`"}`); code != 400 {
|
|
t.Fatalf("unknown token: want 400, got %d", code)
|
|
}
|
|
}
|