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 }