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>
147 lines
4.7 KiB
Go
147 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
cs "fn-registry/functions/cybersecurity"
|
|
)
|
|
|
|
// session is one logged-in browser. In the wallet model each session carries the
|
|
// user's OWN bus identity: the browser unlocks its locally-encrypted private key
|
|
// and hands the full keypair to the gateway over TLS, and the gateway spins up a
|
|
// dedicated bus client (a *gateway) that acts AS that user. The private key lives
|
|
// only in this process's memory for the life of the session — it is never written
|
|
// to disk and is dropped when the session ends.
|
|
//
|
|
// A session may instead point at the shared operator gateway (the legacy
|
|
// passphrase login); `owned` distinguishes the two so logout only closes the bus
|
|
// client it created.
|
|
type session struct {
|
|
gw *gateway
|
|
owned bool // true => gw was built for this session and must be Closed on logout
|
|
handle string
|
|
issuedAt time.Time
|
|
}
|
|
|
|
// sessionStore is the gateway's set of live browser sessions, keyed by the opaque
|
|
// cookie token. It is independent of any single bus identity.
|
|
type sessionStore struct {
|
|
mu sync.Mutex
|
|
m map[string]*session
|
|
}
|
|
|
|
func newSessionStore() *sessionStore { return &sessionStore{m: map[string]*session{}} }
|
|
|
|
func (st *sessionStore) put(token string, s *session) {
|
|
st.mu.Lock()
|
|
st.m[token] = s
|
|
st.mu.Unlock()
|
|
}
|
|
|
|
func (st *sessionStore) get(token string) (*session, bool) {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
s, ok := st.m[token]
|
|
return s, ok
|
|
}
|
|
|
|
// drop removes a session and returns it so the caller can close an owned gateway.
|
|
func (st *sessionStore) drop(token string) (*session, bool) {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
s, ok := st.m[token]
|
|
if ok {
|
|
delete(st.m, token)
|
|
}
|
|
return s, ok
|
|
}
|
|
|
|
// closeAll closes every owned per-user gateway (used at shutdown). The shared
|
|
// operator gateway is owned by main and closed separately.
|
|
func (st *sessionStore) closeAll() {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
for tok, s := range st.m {
|
|
if s.owned && s.gw != nil {
|
|
_ = s.gw.Close()
|
|
}
|
|
delete(st.m, tok)
|
|
}
|
|
}
|
|
|
|
// identityFromHex builds a cs.Identity from the four hex halves the browser sends
|
|
// on POST /api/session. It enforces the exact key sizes (sign_pub 32, sign_priv
|
|
// 64, kex_pub 32, kex_priv 32) so a malformed body cannot produce a half-built
|
|
// identity that fails opaquely deep in the bus client.
|
|
func identityFromHex(signPub, signPriv, kexPub, kexPriv string) (cs.Identity, error) {
|
|
sp, err := hex.DecodeString(signPub)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("sign_pub: %w", err)
|
|
}
|
|
spriv, err := hex.DecodeString(signPriv)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("sign_priv: %w", err)
|
|
}
|
|
kp, err := hex.DecodeString(kexPub)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("kex_pub: %w", err)
|
|
}
|
|
kpriv, err := hex.DecodeString(kexPriv)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("kex_priv: %w", err)
|
|
}
|
|
if len(sp) != 32 || len(spriv) != 64 || len(kp) != 32 || len(kpriv) != 32 {
|
|
return cs.Identity{}, fmt.Errorf("wrong key sizes (sign_pub=%d sign_priv=%d kex_pub=%d kex_priv=%d; want 32/64/32/32)",
|
|
len(sp), len(spriv), len(kp), len(kpriv))
|
|
}
|
|
return cs.Identity{SignPub: sp, SignPriv: spriv, KexPub: kp, KexPriv: kpriv}, nil
|
|
}
|
|
|
|
// sessionReq is the POST /api/session body: the user's full wallet identity (hex)
|
|
// plus a display handle. The private halves arrive only over TLS and are held in
|
|
// memory for the session; they are never persisted server-side.
|
|
type sessionReq struct {
|
|
Handle string `json:"handle"`
|
|
SignPub string `json:"sign_pub"`
|
|
SignPriv string `json:"sign_priv"`
|
|
KexPub string `json:"kex_pub"`
|
|
KexPriv string `json:"kex_priv"`
|
|
}
|
|
|
|
// handleSession opens a per-user session. It builds the user's bus identity from
|
|
// the posted keypair, connects a dedicated bus client as that user, and issues a
|
|
// session cookie bound to it. This is the wallet-model replacement for the
|
|
// operator passphrase login.
|
|
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
|
|
var req sessionReq
|
|
if !decode(w, r, &req) {
|
|
return
|
|
}
|
|
id, err := identityFromHex(req.SignPub, req.SignPriv, req.KexPub, req.KexPriv)
|
|
if err != nil {
|
|
writeErr(w, http.StatusBadRequest, "bad identity: "+err.Error())
|
|
return
|
|
}
|
|
cfg := s.busTemplate
|
|
cfg.Identity = id
|
|
gw, err := newGateway(cfg)
|
|
if err != nil {
|
|
writeErr(w, http.StatusBadGateway, "connect bus as user: "+err.Error())
|
|
return
|
|
}
|
|
tok := newToken()
|
|
s.sessions.put(tok, &session{gw: gw, owned: true, handle: req.Handle, issuedAt: time.Now()})
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: sessionCookie,
|
|
Value: tok,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
writeJSON(w, http.StatusOK, meResp{Endpoint: gw.endpoint, SignPub: req.SignPub, Handle: req.Handle})
|
|
}
|