Enchantrix/cmd/trix/keyserver.go
Claude 447f3ccaca
feat: add Keyserver Secure Environment (SE) for key isolation
Introduces an in-process keyserver that holds cryptographic key material
and exposes operations by opaque key ID — callers (including AI agents)
never see raw key bytes.

New packages:
- pkg/keystore: Trix-based encrypted key store with Argon2id master key
- pkg/keyserver: KeyServer interface, composite crypto ops, session/ACL,
  audit logging

New CLI commands:
- trix keystore init/import/generate/list/delete
- trix keyserver start, trix keyserver session create

Specification: RFC-0005-Keyserver-Secure-Environment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:30:31 +00:00

144 lines
4 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/Snider/Enchantrix/pkg/keyserver"
"github.com/Snider/Enchantrix/pkg/keystore"
"github.com/spf13/cobra"
)
var (
keyserverCmd = &cobra.Command{
Use: "keyserver",
Short: "Manage and run the keyserver",
Long: "Start the keyserver process and manage sessions for capability-based access to crypto operations.",
}
keyserverStartCmd = &cobra.Command{
Use: "start",
Short: "Start the keyserver (blocks until interrupted)",
Long: "Opens the key store and keeps the keyserver running. Reads master password from stdin or ENCHANTRIX_MASTER env.",
RunE: runKeyserverStart,
}
keyserverSessionCmd = &cobra.Command{
Use: "session",
Short: "Manage keyserver sessions",
}
keyserverSessionCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a new session token",
RunE: runKeyserverSessionCreate,
}
)
func init() {
keyserverStartCmd.Flags().StringP("store", "s", "", "Key store file path (required)")
keyserverStartCmd.Flags().StringP("addr", "a", ":9100", "Listen address (for future network use)")
keyserverStartCmd.MarkFlagRequired("store")
keyserverSessionCreateCmd.Flags().StringP("store", "s", "", "Key store file path (required)")
keyserverSessionCreateCmd.Flags().StringP("caps", "c", "", "Capabilities: comma-separated op:keyID pairs (required)")
keyserverSessionCreateCmd.Flags().StringP("ttl", "", "1h", "Session time-to-live (e.g. 1h, 30m, 24h)")
keyserverSessionCreateCmd.Flags().StringP("client", "", "", "Client identifier")
keyserverSessionCreateCmd.MarkFlagRequired("store")
keyserverSessionCreateCmd.MarkFlagRequired("caps")
keyserverSessionCmd.AddCommand(keyserverSessionCreateCmd)
keyserverCmd.AddCommand(keyserverStartCmd, keyserverSessionCmd)
rootCmd.AddCommand(keyserverCmd)
}
func runKeyserverStart(cmd *cobra.Command, args []string) error {
storePath, _ := cmd.Flags().GetString("store")
addr, _ := cmd.Flags().GetString("addr")
r := stdinReader(cmd)
master, err := readMasterPassword(cmd, r)
if err != nil {
return err
}
store, err := keystore.Open(storePath, master)
if err != nil {
return err
}
ks := keyserver.NewServer(store)
_ = ks // server is ready for in-process callers
fmt.Fprintf(cmd.OutOrStdout(), "Keyserver started (store: %s, addr: %s)\n", storePath, addr)
fmt.Fprintf(cmd.OutOrStdout(), "Keys loaded: %d\n", len(store.List()))
fmt.Fprintln(cmd.OutOrStdout(), "Press Ctrl+C to stop.")
// Block until signal
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-sigCh:
fmt.Fprintf(cmd.OutOrStdout(), "\nReceived %s, shutting down...\n", sig)
case <-ctx.Done():
}
store.Close()
fmt.Fprintln(cmd.OutOrStdout(), "Keyserver stopped. Key material zeroed.")
return nil
}
func runKeyserverSessionCreate(cmd *cobra.Command, args []string) error {
storePath, _ := cmd.Flags().GetString("store")
capsStr, _ := cmd.Flags().GetString("caps")
ttlStr, _ := cmd.Flags().GetString("ttl")
clientID, _ := cmd.Flags().GetString("client")
ttl, err := time.ParseDuration(ttlStr)
if err != nil {
return fmt.Errorf("invalid TTL %q: %w", ttlStr, err)
}
caps, err := keyserver.ParseCapabilities(capsStr)
if err != nil {
return err
}
r := stdinReader(cmd)
master, err := readMasterPassword(cmd, r)
if err != nil {
return err
}
store, err := keystore.Open(storePath, master)
if err != nil {
return err
}
defer store.Close()
sm := keyserver.NewSessionManager()
ctx := context.Background()
session, err := sm.CreateSession(ctx, clientID, caps, ttl)
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Session ID: %s\n", session.ID)
fmt.Fprintf(cmd.OutOrStdout(), "Client: %s\n", session.ClientID)
fmt.Fprintf(cmd.OutOrStdout(), "Expires: %s\n", session.ExpiresAt.Format(time.RFC3339))
fmt.Fprintln(cmd.OutOrStdout(), "Capabilities:")
for _, c := range session.Capabilities {
fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", c)
}
return nil
}