package escrow import ( "bytes" "context" "os" "os/exec" "path/filepath" "runtime" "testing" ) // ---- pure-unit tests (no proxmox-backup-client) ---------------------------------------------- func TestConsume_InputValidation(t *testing.T) { ctx := context.Background() dest := filepath.Join(t.TempDir(), "k.json") cases := []struct { name string blob []byte r, fp, dst string }{ {"empty blob", nil, "R", "fp", dest}, {"empty R", []byte("x"), "", "fp", dest}, {"empty fingerprint", []byte("x"), "R", "", dest}, {"empty dest", []byte("x"), "R", "fp", ""}, } for _, c := range cases { if err := Consume(ctx, c.blob, c.r, c.fp, c.dst); err == nil { t.Errorf("%s: expected an error", c.name) } } // no key written on a validation failure if _, err := os.Stat(dest); !os.IsNotExist(err) { t.Error("a validation failure left a key behind") } } func TestFingerprintsEqual_NormalizesFormatting(t *testing.T) { if !fingerprintsEqual("01:36:E9:FE", "0136e9fe") { t.Error("case/colon-insensitive compare failed") } if fingerprintsEqual("", "") { t.Error("two empty fingerprints must NOT compare equal (no gate-bypass on empty)") } if fingerprintsEqual("01:36", "ff:ee") { t.Error("distinct fingerprints compared equal") } } func TestInstallKey_AtomicAnd0600(t *testing.T) { dir := t.TempDir() src := filepath.Join(dir, "src") if err := os.WriteFile(src, []byte("keymaterial"), 0o600); err != nil { t.Fatal(err) } dest := filepath.Join(dir, "sub", "installed.json") // parent dir must be created if err := installKey(src, dest); err != nil { t.Fatalf("installKey: %v", err) } b, _ := os.ReadFile(dest) if string(b) != "keymaterial" { t.Errorf("installed content = %q", b) } if runtime.GOOS != "windows" { // Windows does not enforce Unix file modes if info, _ := os.Stat(dest); info.Mode().Perm() != 0o600 { t.Errorf("installed key mode = %v, want 0600", info.Mode().Perm()) } } // no .tmp- siblings left behind entries, _ := os.ReadDir(filepath.Dir(dest)) for _, e := range entries { if filepath.Ext(e.Name()) != ".json" && len(e.Name()) > 4 && e.Name()[:4] == "inst" { t.Errorf("temp sibling left behind: %s", e.Name()) } } } // ---- integration tests (real proxmox-backup-client wrap/unwrap round-trip) -------------------- func ensurePbc(t *testing.T) { t.Helper() if !pbcAvailable() { t.Skip("skipping: proxmox-backup-client + linux required (runs on the demo/build host)") } if _, err := os.Stat(pbcBinary); err != nil { if p, e := exec.LookPath("proxmox-backup-client"); e == nil { pbcBinary = p } } } // makeBlob creates a throwaway kdf=none key Kt, wraps it under R, and returns (blobBytes, Kt-fp). func makeBlob(t *testing.T, dir, R string) ([]byte, string) { t.Helper() ctx := context.Background() Kt := filepath.Join(dir, "Kt.json") if out, err := exec.Command(pbcBinary, "key", "create", Kt, "--kdf", "none").CombinedOutput(); err != nil { t.Fatalf("key create: %v: %s", err, out) } fp, err := KeyFingerprint(ctx, Kt) if err != nil { t.Fatalf("fingerprint: %v", err) } blobPath := filepath.Join(dir, "escrow.blob") if err := Wrap(ctx, Kt, blobPath, R); err != nil { t.Fatalf("Wrap: %v", err) } blob, err := os.ReadFile(blobPath) if err != nil { t.Fatal(err) } return blob, fp } // VALID: Consume installs a key whose fingerprint matches the expected; blob is byte-unchanged. func TestConsume_ValidInstallsGatedKey(t *testing.T) { ensurePbc(t) ctx := context.Background() dir := t.TempDir() const R = "throwaway-correct-horse-battery-staple" blob, wantFP := makeBlob(t, dir, R) blobBefore := append([]byte(nil), blob...) dest := filepath.Join(dir, "freshbox", "encryption-key.json") if err := Consume(ctx, blob, R, wantFP, dest); err != nil { t.Fatalf("Consume(valid): %v", err) } // key installed + fingerprint matches the expected gate target. gotFP, err := KeyFingerprint(ctx, dest) if err != nil { t.Fatalf("fingerprint(dest): %v", err) } if !fingerprintsEqual(gotFP, wantFP) { t.Errorf("installed key fingerprint %q != expected %q", gotFP, wantFP) } // 0600. if info, _ := os.Stat(dest); info.Mode().Perm() != 0o600 { t.Errorf("installed key mode = %v, want 0600", info.Mode().Perm()) } // blob read-only / unchanged (retryable). if !bytes.Equal(blob, blobBefore) { t.Error("Consume mutated the input blob — must be read-only/retryable") } } // WRONG R: Unwrap fails closed → clear error, NO file at dest, blob unchanged. func TestConsume_WrongRNoInstall(t *testing.T) { ensurePbc(t) ctx := context.Background() dir := t.TempDir() blob, wantFP := makeBlob(t, dir, "the-correct-code") blobBefore := append([]byte(nil), blob...) dest := filepath.Join(dir, "freshbox", "encryption-key.json") err := Consume(ctx, blob, "DEFINITELY-the-wrong-code", wantFP, dest) if err == nil { t.Fatal("a wrong recovery code must fail") } if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) { t.Errorf("a wrong-R Consume left a key at dest (%v) — must install nothing", statErr) } if !bytes.Equal(blob, blobBefore) { t.Error("wrong-R Consume mutated the blob — must be retryable") } } // FINGERPRINT MISMATCH: right unwrap, wrong expected fingerprint → fail fast, NO install (proves // the gate runs BEFORE any restore). func TestConsume_FingerprintMismatchNoInstall(t *testing.T) { ensurePbc(t) ctx := context.Background() dir := t.TempDir() const R = "the-correct-code" blob, _ := makeBlob(t, dir, R) dest := filepath.Join(dir, "freshbox", "encryption-key.json") // Right R, but an expected fingerprint for a DIFFERENT datastore/key. err := Consume(ctx, blob, R, "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff", dest) if err == nil { t.Fatal("a fingerprint mismatch must fail") } if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) { t.Errorf("a fingerprint-mismatch Consume left a key at dest — the gate must run before install") } }