package cli import ( "bytes" "os" "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/paths" "git.cloudinit.dev/coreci/orca/internal/seal" "git.cloudinit.dev/coreci/orca/internal/secrets" ) // setupSealTestEnv prepares a temp ORCA_HOME with a CA (via runInit) and // a raw master key, so that `cluster seal` has something to seal. The // CA is needed for the offline (ca-mode) seal path which derives the // seal key from the CA fingerprint. func setupSealTestEnv(t *testing.T) { t.Helper() _, cleanup := initTestEnv(t) t.Cleanup(cleanup) if err := runInit(discardWriter{}); err != nil { t.Fatalf("init: %v", err) } // runInit does not create a master key; create one. mk, err := secrets.GenerateMasterKey() if err != nil { t.Fatalf("GenerateMasterKey: %v", err) } if err := secrets.SaveMasterKey(paths.MasterKeyPath(), mk); err != nil { t.Fatalf("SaveMasterKey: %v", err) } } // TestClusterSealUnsealCARoundTrip (T7) verifies that sealing the // master key (CA/offline mode) and then unsealing it allows secrets to // be read. This exercises the full seal → unseal → secrets get // round-trip. func TestClusterSealUnsealCARoundTrip(t *testing.T) { ns := "sealrt" setupSealTestEnv(t) mkPath := paths.MasterKeyPath() sealedPath := sealedBlobPath() // Capture the original master key so we can verify the round-trip. origMK, err := secrets.LoadMasterKey(mkPath) if err != nil { t.Fatalf("load orig master key: %v", err) } // Set a secret BEFORE sealing (under the raw key). if err := os.MkdirAll(paths.NamespaceDir(ns), 0o755); err != nil { t.Fatalf("mkdir ns: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"secrets", "set", ns, "TOKEN=roundtrip-secret"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("secrets set before seal: %v", err) } // Seal the cluster (CA mode — no OIDC creds present). buf.Reset() resetRootFlags(t) rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"cluster", "seal"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("cluster seal: %v", err) } sealOut := buf.String() if !strings.Contains(sealOut, "sealed") { t.Errorf("seal output unexpected: %s", sealOut) } // The sealed blob must exist at 0600. info, err := os.Stat(sealedPath) if err != nil { t.Fatalf("sealed blob missing after seal: %v", err) } if info.Mode().Perm() != 0o600 { t.Errorf("sealed blob mode = %04o, want 0600", info.Mode().Perm()) } // The raw master key MUST be deleted. if _, err := os.Stat(mkPath); !os.IsNotExist(err) { t.Errorf("raw master key still exists after seal (expected deleted): %v", err) } // The seal output must print 5 shards. if !strings.Contains(sealOut, "shard 1:") || !strings.Contains(sealOut, "shard 5:") { t.Errorf("seal output missing shards: %s", sealOut) } // Unseal the cluster (CA mode — derives key from CA fingerprint). buf.Reset() resetRootFlags(t) rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"cluster", "unseal"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("cluster unseal: %v", err) } unsealOut := buf.String() if !strings.Contains(unsealOut, "unsealed") { t.Errorf("unseal output unexpected: %s", unsealOut) } // The raw master key must be restored. restoredMK, err := secrets.LoadMasterKey(mkPath) if err != nil { t.Fatalf("load restored master key: %v", err) } if !bytes.Equal(restoredMK, origMK) { t.Error("restored master key != original (round-trip failed)") } // secrets get MUST work after unseal (the round-trip assertion). buf.Reset() resetRootFlags(t) rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"secrets", "get", ns, "TOKEN"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("secrets get after unseal: %v", err) } if buf.String() != "roundtrip-secret" { t.Errorf("secrets get after unseal = %q, want %q", buf.String(), "roundtrip-secret") } } // TestClusterSealShamirRecovery (T7 recovery path) verifies the // --recovery unseal path: seal, collect 3 shards, recover via stdin. func TestClusterSealShamirRecovery(t *testing.T) { setupSealTestEnv(t) mkPath := paths.MasterKeyPath() origMK, err := secrets.LoadMasterKey(mkPath) if err != nil { t.Fatalf("load orig master key: %v", err) } // Seal and capture the shards from stdout. resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"cluster", "seal"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("cluster seal: %v", err) } // Parse the 5 shards from the output. shards := parseShardsFromOutput(t, buf.String()) if len(shards) != 5 { t.Fatalf("expected 5 shards, got %d", len(shards)) } // Unseal via recovery using the first 3 shards via stdin. // Build the stdin input: 3 shard lines. var stdin bytes.Buffer for i := 0; i < 3; i++ { stdin.WriteString(shards[i]) stdin.WriteString("\n") } resetRootFlags(t) buf.Reset() rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetIn(&stdin) rootCmd.SetArgs([]string{"cluster", "unseal", "--recovery"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("cluster unseal --recovery: %v", err) } restoredMK, err := secrets.LoadMasterKey(mkPath) if err != nil { t.Fatalf("load restored master key: %v", err) } if !bytes.Equal(restoredMK, origMK) { t.Error("recovered master key != original (Shamir recovery failed)") } } // parseShardsFromOutput extracts the 5 base64 shard strings from the // `cluster seal` stdout (lines like " shard 1: "). func parseShardsFromOutput(t *testing.T, out string) []string { t.Helper() var shards []string for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "shard ") { idx := strings.IndexByte(line, ':') if idx < 0 { continue } s := strings.TrimSpace(line[idx+1:]) if s != "" { shards = append(shards, s) } } } return shards } // TestClusterSealIdempotencyRefuse verifies that sealing twice (without // unsealing) is refused — the operator must unseal first. func TestClusterSealIdempotencyRefuse(t *testing.T) { setupSealTestEnv(t) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"cluster", "seal"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("first seal: %v", err) } buf.Reset() resetRootFlags(t) rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"cluster", "seal"}) if err := rootCmd.Execute(); err == nil { t.Error("second seal should fail (sealed blob already exists)") } } // TestSealPackageShamirRecoveryRoundTrip verifies the seal-package // Shamir recovery path directly (UnsealWithShamir) as a unit-level // backstop for the CLI integration test above. func TestSealPackageShamirRecoveryRoundTrip(t *testing.T) { masterKey := make([]byte, 32) for i := range masterKey { masterKey[i] = byte(i + 7) } blob, shards, err := seal.Seal(masterKey, "test-sub", "https://idp.test") if err != nil { t.Fatalf("Seal: %v", err) } recovered, err := seal.UnsealWithShamir(blob, shards[:3]) if err != nil { t.Fatalf("UnsealWithShamir: %v", err) } if !bytes.Equal(recovered, masterKey) { t.Error("Shamir-recovered key != original") } }