8d893d216b
Single Go binary: serves an embedded Mantine SPA and a small REST API over the unibus control plane. Holds the operator ADMIN identity, signs every control-plane request, never exposes a private key to the browser. - internal/admin: Repo interface + mock + bus implementations, REST server - repo_bus: rooms via pkg/client, members via signed GET (CanonicalRequest + SignEd25519), cluster via /healthz (CA-pinned), users via membership.Store - identity loaded from pass entry or 0600 file (operator-identity JSON) - go build CGO_ENABLED=0 green; go vet clean Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
package admin
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
|
|
cs "fn-registry/functions/cybersecurity"
|
|
)
|
|
|
|
// identityJSON mirrors the on-disk / pass-stored identity format shared across
|
|
// the unibus tooling: the four keypair halves, each std-base64. It is the SAME
|
|
// shape the bus client persists (pkg/client identityFile) and the operator's
|
|
// `pass` entry unibus/operator-identity, so the admin panel loads the operator's
|
|
// identity without a divergent serialization.
|
|
type identityJSON struct {
|
|
SignPub string `json:"sign_pub"`
|
|
SignPriv string `json:"sign_priv"`
|
|
KexPub string `json:"kex_pub"`
|
|
KexPriv string `json:"kex_priv"`
|
|
}
|
|
|
|
// decodeIdentity turns the JSON identity bytes into a cs.Identity. The private
|
|
// halves stay only in memory; this never writes them anywhere.
|
|
func decodeIdentity(raw []byte) (cs.Identity, error) {
|
|
var f identityJSON
|
|
if err := json.Unmarshal(raw, &f); err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: parse identity json: %w", err)
|
|
}
|
|
dec := base64.StdEncoding.DecodeString
|
|
signPub, err := dec(f.SignPub)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: decode sign_pub: %w", err)
|
|
}
|
|
signPriv, err := dec(f.SignPriv)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: decode sign_priv: %w", err)
|
|
}
|
|
kexPub, err := dec(f.KexPub)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: decode kex_pub: %w", err)
|
|
}
|
|
kexPriv, err := dec(f.KexPriv)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: decode kex_priv: %w", err)
|
|
}
|
|
if len(signPub) != 32 || len(signPriv) != 64 || len(kexPub) != 32 || len(kexPriv) != 32 {
|
|
return cs.Identity{}, fmt.Errorf("admin: identity has wrong key sizes (sign_pub=%d sign_priv=%d kex_pub=%d kex_priv=%d)",
|
|
len(signPub), len(signPriv), len(kexPub), len(kexPriv))
|
|
}
|
|
return cs.Identity{SignPub: signPub, SignPriv: signPriv, KexPub: kexPub, KexPriv: kexPriv}, nil
|
|
}
|
|
|
|
// LoadIdentityFromFile reads a 0600 identity JSON file (the same format the bus
|
|
// client writes) and decodes it. Used in production on the deploy host, where
|
|
// `pass` is not available and the operator identity is delivered as a protected
|
|
// file under the service's local_files directory.
|
|
func LoadIdentityFromFile(path string) (cs.Identity, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: read identity file %q: %w", path, err)
|
|
}
|
|
return decodeIdentity(raw)
|
|
}
|
|
|
|
// LoadIdentityFromPass shells out to `pass show <entry>` and decodes the JSON
|
|
// identity it returns. The secret is held only in memory; this process never
|
|
// writes it to disk or argv. Used in local operator workflows where the GNU
|
|
// password store holds unibus/operator-identity.
|
|
func LoadIdentityFromPass(entry string) (cs.Identity, error) {
|
|
out, err := exec.Command("pass", "show", entry).Output()
|
|
if err != nil {
|
|
return cs.Identity{}, fmt.Errorf("admin: pass show %q: %w", entry, err)
|
|
}
|
|
return decodeIdentity(out)
|
|
}
|