feat: initial scaffold of unibus message bus (membership service + client lib + demo peers)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
cs "fn-registry/functions/cybersecurity"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
// newULID returns a fresh, lexicographically-sortable message id with
|
||||
// crypto/rand entropy.
|
||||
func newULID() string {
|
||||
return ulid.MustNew(ulid.Now(), rand.Reader).String()
|
||||
}
|
||||
|
||||
// identityFile is the on-disk JSON representation of an Identity. The four key
|
||||
// fields are base64-encoded.
|
||||
//
|
||||
// SECURITY: this file contains the peer's long-term PRIVATE keys (SignPriv and
|
||||
// KexPriv). It is written 0600. Losing it means losing the ability to decrypt
|
||||
// any message addressed to this endpoint — there is no recovery. Treat it like
|
||||
// an SSH private key. (Hardening with OS keyrings/HSM is a later phase.)
|
||||
type identityFile struct {
|
||||
SignPub string `json:"sign_pub"`
|
||||
SignPriv string `json:"sign_priv"`
|
||||
KexPub string `json:"kex_pub"`
|
||||
KexPriv string `json:"kex_priv"`
|
||||
}
|
||||
|
||||
// LoadOrCreateIdentity loads the identity at path, or generates and persists a
|
||||
// new one if the file does not exist. The file is written with 0600
|
||||
// permissions because it holds private keys.
|
||||
func LoadOrCreateIdentity(path string) (cs.Identity, error) {
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
var f identityFile
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("client: parse identity %q: %w", path, err)
|
||||
}
|
||||
id, err := f.toIdentity()
|
||||
if err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("client: decode identity %q: %w", path, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
id, err := cs.GenerateIdentity()
|
||||
if err != nil {
|
||||
return cs.Identity{}, fmt.Errorf("client: generate identity: %w", err)
|
||||
}
|
||||
if err := saveIdentity(path, id); err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func saveIdentity(path string, id cs.Identity) error {
|
||||
if dir := filepath.Dir(path); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("client: mkdir for identity: %w", err)
|
||||
}
|
||||
}
|
||||
f := identityFile{
|
||||
SignPub: base64.StdEncoding.EncodeToString(id.SignPub),
|
||||
SignPriv: base64.StdEncoding.EncodeToString(id.SignPriv),
|
||||
KexPub: base64.StdEncoding.EncodeToString(id.KexPub),
|
||||
KexPriv: base64.StdEncoding.EncodeToString(id.KexPriv),
|
||||
}
|
||||
data, err := json.MarshalIndent(f, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client: marshal identity: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
return fmt.Errorf("client: write identity %q: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f identityFile) toIdentity() (cs.Identity, error) {
|
||||
dec := func(s string) ([]byte, error) { return base64.StdEncoding.DecodeString(s) }
|
||||
signPub, err := dec(f.SignPub)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
signPriv, err := dec(f.SignPriv)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
kexPub, err := dec(f.KexPub)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
kexPriv, err := dec(f.KexPriv)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
return cs.Identity{SignPub: signPub, SignPriv: signPriv, KexPub: kexPub, KexPriv: kexPriv}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user