package bootstrap import ( "context" "crypto/sha256" "encoding/hex" "fmt" "os" "path/filepath" "strings" "github.com/novox/mesh-host/internal/identity" ) // The operator's sealing key: made at genesis, before the mesh is told any secret. // // Every secret a module holds for itself is sealed to the node that uses it; from here on it is // sealed to this key as well, and the vault keeps those copies (novox/hq ADR 0085, amended). The // private half is written once, beside the produced bundle, and given to nothing: the mesh // records the public half and can open nothing it seals to it. The operator copies the file off // the machine and keeps it — it is what recovers the mesh's root secrets when a node cannot. // // **Before enrolment's first `secret accept`**, or the credentials genesis made would be sealed // to the node alone and be exactly as unrecoverable as the constants they replaced. // OperatorKeyFile is where the private half is written, beside the bundle. func OperatorKeyFile(o Options) string { return filepath.Join(filepath.Dir(o.Out), "operator.key") } // RootExportFile is where the export of every operator-sealed secret is written at the end. func RootExportFile(o Options) string { return filepath.Join(filepath.Dir(o.Out), "root-secrets.export.json") } type OperatorKey struct { Path string Fingerprint string Made bool } // MakeOperatorKey makes the key if this machine has none, and tells the mesh its public half. func MakeOperatorKey(ctx context.Context, o Options, control controlPlane, say func(string)) (OperatorKey, error) { out := OperatorKey{Path: OperatorKeyFile(o)} key, err := identity.LoadSealingKey(out.Path) switch { case err == nil: say(" operator key already at " + out.Path + " — kept") case os.IsNotExist(underlying(err)) || strings.Contains(err.Error(), "no sealing key at"): key, err = identity.GenerateSealingKey() if err != nil { return out, err } if err := os.MkdirAll(filepath.Dir(out.Path), 0o755); err != nil { return out, err } if err := os.WriteFile(out.Path, []byte(key.Private+"\n"), 0o600); err != nil { return out, err } out.Made = true default: return out, err } sum := sha256.Sum256([]byte(key.Public)) out.Fingerprint = "sha256:" + hex.EncodeToString(sum[:8]) if _, err := control.tell(ctx, "operator", "key", "set", key.Public); err != nil { return out, err } if out.Made { say(" operator key " + out.Fingerprint + " — private half at " + out.Path + " (0600)") say(" COPY IT OFF THIS MACHINE AND KEEP IT: it opens the mesh's root secrets, and") say(" nothing else does. The mesh holds only the public half.") } else { say(" operator key " + out.Fingerprint + " — the mesh seals its root secrets to it") } return out, nil } func underlying(err error) error { for { next, ok := err.(interface{ Unwrap() error }) if !ok || next.Unwrap() == nil { return err } err = next.Unwrap() } } // ExportRootSecrets writes the export beside the operator key: every secret sealed to it, as // ciphertext, and the honest list of what is not. What the vault keeps on its disk, kept once // more by the person who holds the key. func ExportRootSecrets(ctx context.Context, o Options, control controlPlane, say func(string)) (string, error) { path := RootExportFile(o) body, err := control.tell(ctx, "secret", "export") if err != nil { return path, err } if !strings.Contains(body, `"kept"`) { return path, fmt.Errorf("`secret export` did not produce an export:\n%s", body) } if err := os.WriteFile(path, []byte(body), 0o600); err != nil { return path, err } say(" exported " + path + " (0600) — ciphertext, sealed to the operator key; keep it with the key") return path, nil }